Added robust serial validation with support for multiple formats: - Production format: WELLNUO-XXXX-XXXX (strict validation) - Demo serials: DEMO-00000 and DEMO-1234-5678 - Legacy format: 8+ alphanumeric characters with hyphens Frontend improvements (activate.tsx): - Real-time validation feedback with error messages - Visual error indicators (red border, error icon) - Proper normalization (uppercase, trimmed) - Better user experience with clear error messages Backend improvements (beneficiaries.js): - Enhanced serial validation on activation endpoint - Stores normalized serial in device_id field - Better logging for debugging - Consistent error responses with validation details Testing: - 52 frontend tests covering all validation scenarios - 40 backend tests ensuring consistency - Edge case handling (long serials, special chars, etc.) Code quality: - ESLint configuration for test files - All tests passing - Zero linting errors
319 lines
11 KiB
TypeScript
319 lines
11 KiB
TypeScript
import {
|
|
validateSerial,
|
|
isValidSerial,
|
|
isDemoSerial,
|
|
getSerialErrorMessage,
|
|
formatSerialForDisplay,
|
|
getSerialPlaceholder,
|
|
} from '../serialValidation';
|
|
|
|
describe('Serial Validation', () => {
|
|
describe('validateSerial', () => {
|
|
// Production format tests
|
|
it('should validate production format WELLNUO-XXXX-XXXX', () => {
|
|
const result = validateSerial('WELLNUO-1234-5678');
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.format).toBe('production');
|
|
expect(result.normalized).toBe('WELLNUO-1234-5678');
|
|
expect(result.error).toBeUndefined();
|
|
});
|
|
|
|
it('should validate production format with lowercase', () => {
|
|
const result = validateSerial('wellnuo-abcd-efgh');
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.format).toBe('production');
|
|
expect(result.normalized).toBe('WELLNUO-ABCD-EFGH');
|
|
});
|
|
|
|
it('should validate production format with mixed case and spaces', () => {
|
|
const result = validateSerial(' WeLlNuO-1a2B-3c4D ');
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.format).toBe('production');
|
|
expect(result.normalized).toBe('WELLNUO-1A2B-3C4D');
|
|
});
|
|
|
|
// Demo serial tests
|
|
it('should validate demo serial DEMO-00000', () => {
|
|
const result = validateSerial('DEMO-00000');
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.format).toBe('demo');
|
|
expect(result.normalized).toBe('DEMO-00000');
|
|
});
|
|
|
|
it('should validate demo serial DEMO-1234-5678', () => {
|
|
const result = validateSerial('DEMO-1234-5678');
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.format).toBe('demo');
|
|
expect(result.normalized).toBe('DEMO-1234-5678');
|
|
});
|
|
|
|
it('should validate demo serial with lowercase', () => {
|
|
const result = validateSerial('demo-00000');
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.format).toBe('demo');
|
|
expect(result.normalized).toBe('DEMO-00000');
|
|
});
|
|
|
|
// Legacy format tests
|
|
it('should validate legacy 8-character alphanumeric', () => {
|
|
const result = validateSerial('ABC12345');
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.format).toBe('legacy');
|
|
expect(result.normalized).toBe('ABC12345');
|
|
});
|
|
|
|
it('should validate legacy format with hyphens', () => {
|
|
const result = validateSerial('ABC-123-456');
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.format).toBe('legacy');
|
|
expect(result.normalized).toBe('ABC-123-456');
|
|
});
|
|
|
|
it('should validate legacy 16-character serial', () => {
|
|
const result = validateSerial('ABCDEFGH12345678');
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.format).toBe('legacy');
|
|
expect(result.normalized).toBe('ABCDEFGH12345678');
|
|
});
|
|
|
|
// Invalid format tests
|
|
it('should reject empty string', () => {
|
|
const result = validateSerial('');
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.format).toBe('invalid');
|
|
expect(result.error).toBe('Serial number is required');
|
|
});
|
|
|
|
it('should reject null', () => {
|
|
const result = validateSerial(null);
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.format).toBe('invalid');
|
|
expect(result.error).toBe('Serial number is required');
|
|
});
|
|
|
|
it('should reject undefined', () => {
|
|
const result = validateSerial(undefined);
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.format).toBe('invalid');
|
|
expect(result.error).toBe('Serial number is required');
|
|
});
|
|
|
|
it('should reject strings with only spaces', () => {
|
|
const result = validateSerial(' ');
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.format).toBe('invalid');
|
|
expect(result.error).toBe('Serial number is required');
|
|
});
|
|
|
|
it('should reject too short serial (less than 8 chars)', () => {
|
|
const result = validateSerial('ABC123');
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.format).toBe('invalid');
|
|
expect(result.error).toBe('Serial number must be at least 8 characters');
|
|
});
|
|
|
|
it('should reject serial with special characters', () => {
|
|
const result = validateSerial('ABC123!@#');
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.format).toBe('invalid');
|
|
expect(result.error).toBe('Serial number can only contain letters, numbers, and hyphens');
|
|
});
|
|
|
|
it('should accept serials with wrong prefix as legacy format', () => {
|
|
const result = validateSerial('BADNAME-1234-5678');
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.format).toBe('legacy'); // Valid as legacy, not production
|
|
});
|
|
|
|
it('should accept malformed production format as legacy', () => {
|
|
const result = validateSerial('WELLNUO-123-5678'); // only 3 chars in middle
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.format).toBe('legacy'); // Valid as legacy, not production
|
|
});
|
|
|
|
it('should accept serials with extra hyphens as legacy', () => {
|
|
const result = validateSerial('WELLNUO-12-34-5678');
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.format).toBe('legacy'); // Valid as legacy, not production
|
|
});
|
|
|
|
it('should reject production format without hyphens', () => {
|
|
const result = validateSerial('WELLNUO12345678');
|
|
expect(result.isValid).toBe(true); // Actually valid as legacy format
|
|
expect(result.format).toBe('legacy'); // Not production
|
|
});
|
|
});
|
|
|
|
describe('isValidSerial', () => {
|
|
it('should return true for valid production serial', () => {
|
|
expect(isValidSerial('WELLNUO-1234-5678')).toBe(true);
|
|
});
|
|
|
|
it('should return true for valid demo serial', () => {
|
|
expect(isValidSerial('DEMO-00000')).toBe(true);
|
|
});
|
|
|
|
it('should return true for valid legacy serial', () => {
|
|
expect(isValidSerial('ABC12345')).toBe(true);
|
|
});
|
|
|
|
it('should return false for invalid serial', () => {
|
|
expect(isValidSerial('ABC')).toBe(false);
|
|
});
|
|
|
|
it('should return false for null', () => {
|
|
expect(isValidSerial(null)).toBe(false);
|
|
});
|
|
|
|
it('should return false for undefined', () => {
|
|
expect(isValidSerial(undefined)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('isDemoSerial', () => {
|
|
it('should return true for DEMO-00000', () => {
|
|
expect(isDemoSerial('DEMO-00000')).toBe(true);
|
|
});
|
|
|
|
it('should return true for DEMO-1234-5678', () => {
|
|
expect(isDemoSerial('DEMO-1234-5678')).toBe(true);
|
|
});
|
|
|
|
it('should return true for lowercase demo serial', () => {
|
|
expect(isDemoSerial('demo-00000')).toBe(true);
|
|
});
|
|
|
|
it('should return true for demo serial with spaces', () => {
|
|
expect(isDemoSerial(' DEMO-00000 ')).toBe(true);
|
|
});
|
|
|
|
it('should return false for production serial', () => {
|
|
expect(isDemoSerial('WELLNUO-1234-5678')).toBe(false);
|
|
});
|
|
|
|
it('should return false for legacy serial', () => {
|
|
expect(isDemoSerial('ABC12345')).toBe(false);
|
|
});
|
|
|
|
it('should return false for null', () => {
|
|
expect(isDemoSerial(null)).toBe(false);
|
|
});
|
|
|
|
it('should return false for undefined', () => {
|
|
expect(isDemoSerial(undefined)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('getSerialErrorMessage', () => {
|
|
it('should return empty string for valid serial', () => {
|
|
expect(getSerialErrorMessage('WELLNUO-1234-5678')).toBe('');
|
|
});
|
|
|
|
it('should return error for empty string', () => {
|
|
const error = getSerialErrorMessage('');
|
|
expect(error).toBeTruthy();
|
|
expect(error).toContain('required');
|
|
});
|
|
|
|
it('should return error for null', () => {
|
|
const error = getSerialErrorMessage(null);
|
|
expect(error).toBeTruthy();
|
|
expect(error).toContain('required');
|
|
});
|
|
|
|
it('should return error for too short serial', () => {
|
|
const error = getSerialErrorMessage('ABC');
|
|
expect(error).toBeTruthy();
|
|
expect(error).toContain('8 characters');
|
|
});
|
|
|
|
it('should return error for serial with special characters', () => {
|
|
const error = getSerialErrorMessage('ABC123!@#');
|
|
expect(error).toBeTruthy();
|
|
expect(error).toContain('letters, numbers, and hyphens');
|
|
});
|
|
|
|
it('should return specific error for too short serial', () => {
|
|
const error = getSerialErrorMessage('ABC');
|
|
expect(error).toContain('8 characters');
|
|
});
|
|
});
|
|
|
|
describe('formatSerialForDisplay', () => {
|
|
it('should add hyphens to production serial without hyphens', () => {
|
|
const formatted = formatSerialForDisplay('WELLNUO12345678');
|
|
expect(formatted).toBe('WELLNUO-1234-5678');
|
|
});
|
|
|
|
it('should keep hyphens in production serial with hyphens', () => {
|
|
const formatted = formatSerialForDisplay('WELLNUO-1234-5678');
|
|
expect(formatted).toBe('WELLNUO-1234-5678');
|
|
});
|
|
|
|
it('should uppercase and trim', () => {
|
|
const formatted = formatSerialForDisplay(' wellnuo-abcd-efgh ');
|
|
expect(formatted).toBe('WELLNUO-ABCD-EFGH');
|
|
});
|
|
|
|
it('should not modify demo serial', () => {
|
|
const formatted = formatSerialForDisplay('DEMO-00000');
|
|
expect(formatted).toBe('DEMO-00000');
|
|
});
|
|
|
|
it('should not modify legacy serial', () => {
|
|
const formatted = formatSerialForDisplay('ABC12345');
|
|
expect(formatted).toBe('ABC12345');
|
|
});
|
|
|
|
it('should uppercase lowercase serial', () => {
|
|
const formatted = formatSerialForDisplay('abc12345');
|
|
expect(formatted).toBe('ABC12345');
|
|
});
|
|
});
|
|
|
|
describe('getSerialPlaceholder', () => {
|
|
it('should return production placeholder by default', () => {
|
|
const placeholder = getSerialPlaceholder();
|
|
expect(placeholder).toBe('WELLNUO-XXXX-XXXX');
|
|
});
|
|
|
|
it('should return production placeholder when preferProduction is true', () => {
|
|
const placeholder = getSerialPlaceholder(true);
|
|
expect(placeholder).toBe('WELLNUO-XXXX-XXXX');
|
|
});
|
|
|
|
it('should return generic placeholder when preferProduction is false', () => {
|
|
const placeholder = getSerialPlaceholder(false);
|
|
expect(placeholder).toBe('Enter serial number');
|
|
});
|
|
});
|
|
|
|
describe('Edge cases', () => {
|
|
it('should handle very long serials', () => {
|
|
const longSerial = 'A'.repeat(100);
|
|
const result = validateSerial(longSerial);
|
|
expect(result.isValid).toBe(true); // Valid as legacy
|
|
expect(result.format).toBe('legacy');
|
|
});
|
|
|
|
it('should handle serials with multiple hyphens', () => {
|
|
const result = validateSerial('ABC-123-456-789');
|
|
expect(result.isValid).toBe(true); // Valid as legacy
|
|
expect(result.format).toBe('legacy');
|
|
});
|
|
|
|
it('should handle numeric-only serials', () => {
|
|
const result = validateSerial('12345678');
|
|
expect(result.isValid).toBe(true); // Valid as legacy
|
|
expect(result.format).toBe('legacy');
|
|
});
|
|
|
|
it('should handle mixed case in production format', () => {
|
|
const result = validateSerial('WeLlNuO-AbCd-EfGh');
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.format).toBe('production');
|
|
expect(result.normalized).toBe('WELLNUO-ABCD-EFGH');
|
|
});
|
|
});
|
|
});
|