← back to Wine Finder Next

__tests__/unit/inputValidation.test.ts

251 lines

// Unit Tests for Input Validation
import {
  validateBottleId,
  validateVoteId,
  validateMembershipUnitId,
  validateUserId,
  validateVoteChoice,
  validateEmail,
  validatePrice,
  validateQuantity,
  sanitizeString,
  validateShippingAddress,
  ValidationError,
  validateRequest,
} from '@/lib/inputValidation';

describe('Input Validation', () => {
  describe('validateBottleId', () => {
    it('should accept valid bottle IDs', () => {
      expect(validateBottleId('bottle_1700000000000_abc123def')).toBe(true);
      expect(validateBottleId('bottle_1234567890123_xyz987poi')).toBe(true);
    });

    it('should reject invalid bottle IDs', () => {
      expect(validateBottleId('invalid_id')).toBe(false);
      expect(validateBottleId('bottle_123')).toBe(false);
      expect(validateBottleId('bottle_abc_123')).toBe(false);
      expect(validateBottleId('')).toBe(false);
      expect(validateBottleId('bottle_1700000000000_ABC')).toBe(false); // uppercase not allowed
    });

    it('should reject SQL injection attempts', () => {
      expect(validateBottleId("bottle_1700000000000_abc'; DROP TABLE--")).toBe(false);
      expect(validateBottleId("bottle_1700000000000_abc' OR '1'='1")).toBe(false);
    });
  });

  describe('validateVoteId', () => {
    it('should accept valid vote IDs', () => {
      expect(validateVoteId('vote_1700000000000_abc123def')).toBe(true);
    });

    it('should reject invalid vote IDs', () => {
      expect(validateVoteId('invalid_id')).toBe(false);
      expect(validateVoteId('vote_123')).toBe(false);
    });
  });

  describe('validateMembershipUnitId', () => {
    it('should accept valid unit IDs', () => {
      expect(validateMembershipUnitId('unit_1700000000000_abc123def')).toBe(true);
    });

    it('should reject invalid unit IDs', () => {
      expect(validateMembershipUnitId('invalid_id')).toBe(false);
      expect(validateMembershipUnitId('unit_123')).toBe(false);
    });
  });

  describe('validateUserId', () => {
    it('should accept valid user IDs', () => {
      expect(validateUserId('user_1')).toBe(true);
      expect(validateUserId('user_12345')).toBe(true);
      expect(validateUserId('a'.repeat(40))).toBe(true); // long hash format
    });

    it('should reject invalid user IDs', () => {
      expect(validateUserId('user')).toBe(false);
      expect(validateUserId('invalid')).toBe(false);
      expect(validateUserId('')).toBe(false);
    });
  });

  describe('validateVoteChoice', () => {
    it('should accept valid vote choices', () => {
      expect(validateVoteChoice('for')).toBe(true);
      expect(validateVoteChoice('against')).toBe(true);
      expect(validateVoteChoice('abstain')).toBe(true);
    });

    it('should reject invalid vote choices', () => {
      expect(validateVoteChoice('yes')).toBe(false);
      expect(validateVoteChoice('no')).toBe(false);
      expect(validateVoteChoice('')).toBe(false);
      expect(validateVoteChoice('FOR')).toBe(false); // case sensitive
    });
  });

  describe('validateEmail', () => {
    it('should accept valid email addresses', () => {
      expect(validateEmail('test@example.com')).toBe(true);
      expect(validateEmail('user.name+tag@example.co.uk')).toBe(true);
      expect(validateEmail('test_user@domain.org')).toBe(true);
    });

    it('should reject invalid email addresses', () => {
      expect(validateEmail('invalid')).toBe(false);
      expect(validateEmail('test@')).toBe(false);
      expect(validateEmail('@example.com')).toBe(false);
      expect(validateEmail('test@example')).toBe(false);
      expect(validateEmail('')).toBe(false);
    });

    it('should reject emails longer than 254 characters', () => {
      const longEmail = 'a'.repeat(250) + '@example.com';
      expect(validateEmail(longEmail)).toBe(false);
    });
  });

  describe('validatePrice', () => {
    it('should accept valid prices', () => {
      expect(validatePrice(10.50)).toBe(true);
      expect(validatePrice(0.01)).toBe(true);
      expect(validatePrice(999999.99)).toBe(true);
    });

    it('should reject invalid prices', () => {
      expect(validatePrice(0)).toBe(false);
      expect(validatePrice(-10)).toBe(false);
      expect(validatePrice(1000000)).toBe(false);
      expect(validatePrice(Infinity)).toBe(false);
      expect(validatePrice(NaN)).toBe(false);
    });
  });

  describe('validateQuantity', () => {
    it('should accept valid quantities', () => {
      expect(validateQuantity(1)).toBe(true);
      expect(validateQuantity(100)).toBe(true);
      expect(validateQuantity(10000)).toBe(true);
    });

    it('should reject invalid quantities', () => {
      expect(validateQuantity(0)).toBe(false);
      expect(validateQuantity(-1)).toBe(false);
      expect(validateQuantity(10001)).toBe(false);
      expect(validateQuantity(1.5)).toBe(false); // must be integer
    });
  });

  describe('sanitizeString', () => {
    it('should remove dangerous HTML tags', () => {
      expect(sanitizeString('<script>alert("XSS")</script>')).toBe('scriptalert("XSS")/script');
      expect(sanitizeString('<img src=x onerror=alert(1)>')).toBe('img src=x alert(1)');
    });

    it('should remove javascript: protocol', () => {
      expect(sanitizeString('javascript:alert(1)')).toBe('alert(1)');
      expect(sanitizeString('JAVASCRIPT:alert(1)')).toBe('alert(1)');
    });

    it('should remove event handlers', () => {
      expect(sanitizeString('onclick=alert(1)')).toBe('alert(1)');
      expect(sanitizeString('onload=malicious()')).toBe('malicious()');
    });

    it('should trim and limit length', () => {
      const longString = 'a'.repeat(2000);
      expect(sanitizeString(longString, 100)).toHaveLength(100);
      expect(sanitizeString('  test  ')).toBe('test');
    });

    it('should preserve safe content', () => {
      expect(sanitizeString('Hello World')).toBe('Hello World');
      expect(sanitizeString('Test 123 !@#$%')).toBe('Test 123 !@#$%');
    });
  });

  describe('validateShippingAddress', () => {
    const validAddress = {
      name: 'John Doe',
      addressLine1: '123 Wine Street',
      city: 'Napa',
      state: 'CA',
      zipCode: '94558',
      country: 'USA',
      phone: '+1-555-123-4567',
    };

    it('should accept valid shipping addresses', () => {
      expect(validateShippingAddress(validAddress)).toBe(true);
    });

    it('should reject missing required fields', () => {
      expect(validateShippingAddress({ ...validAddress, name: '' })).toBe(false);
      expect(validateShippingAddress({ ...validAddress, addressLine1: '' })).toBe(false);
      expect(validateShippingAddress({ ...validAddress, city: '' })).toBe(false);
    });

    it('should validate ZIP code format', () => {
      expect(validateShippingAddress({ ...validAddress, zipCode: '12345' })).toBe(true);
      expect(validateShippingAddress({ ...validAddress, zipCode: '12345-6789' })).toBe(true);
      expect(validateShippingAddress({ ...validAddress, zipCode: 'ABCDE' })).toBe(false);
      expect(validateShippingAddress({ ...validAddress, zipCode: '123' })).toBe(false);
    });

    it('should validate phone format', () => {
      expect(validateShippingAddress({ ...validAddress, phone: '+1-555-123-4567' })).toBe(true);
      expect(validateShippingAddress({ ...validAddress, phone: '(555) 123-4567' })).toBe(true);
      expect(validateShippingAddress({ ...validAddress, phone: '123' })).toBe(false);
    });
  });

  describe('validateRequest', () => {
    it('should validate data against schema', () => {
      const schema = {
        email: validateEmail,
        price: validatePrice,
      };

      const validData = {
        email: 'test@example.com',
        price: 10.50,
      };

      expect(() => validateRequest(validData, schema)).not.toThrow();
    });

    it('should throw ValidationError for invalid data', () => {
      const schema = {
        email: validateEmail,
        price: validatePrice,
      };

      const invalidData = {
        email: 'invalid-email',
        price: -10,
      };

      expect(() => validateRequest(invalidData, schema)).toThrow(ValidationError);
    });

    it('should include field names in error message', () => {
      const schema = {
        email: validateEmail,
      };

      const invalidData = {
        email: 'invalid',
      };

      try {
        validateRequest(invalidData, schema);
      } catch (error) {
        expect(error).toBeInstanceOf(ValidationError);
        expect((error as Error).message).toContain('email');
      }
    });
  });
});