WellNuo/utils/serialValidation.ts
Sergei 54336986ad Improve serial number validation with comprehensive testing
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
2026-01-29 11:33:54 -08:00

147 lines
3.5 KiB
TypeScript

/**
* Serial Number Validation Utilities
*
* WellNuo device serial numbers follow formats:
* - Production: WELLNUO-XXXX-XXXX (16 chars with hyphens)
* - Demo: DEMO-00000 or DEMO-1234-5678
* - Legacy: 8+ alphanumeric characters
*/
export interface SerialValidationResult {
isValid: boolean;
format: 'production' | 'demo' | 'legacy' | 'invalid';
normalized: string; // Uppercase, trimmed
error?: string;
}
// Demo serial numbers
const DEMO_SERIALS = new Set([
'DEMO-00000',
'DEMO-1234-5678',
]);
/**
* Validates a serial number and returns detailed result
*/
export function validateSerial(serial: string | null | undefined): SerialValidationResult {
// Handle empty input
if (!serial || typeof serial !== 'string') {
return {
isValid: false,
format: 'invalid',
normalized: '',
error: 'Serial number is required',
};
}
// Normalize: trim and uppercase
const normalized = serial.trim().toUpperCase();
if (normalized.length === 0) {
return {
isValid: false,
format: 'invalid',
normalized: '',
error: 'Serial number is required',
};
}
// Check for demo serial
if (DEMO_SERIALS.has(normalized)) {
return {
isValid: true,
format: 'demo',
normalized,
};
}
// Production format: WELLNUO-XXXX-XXXX
// Must be exactly 16 characters with hyphens at positions 7 and 12
if (/^WELLNUO-[A-Z0-9]{4}-[A-Z0-9]{4}$/.test(normalized)) {
return {
isValid: true,
format: 'production',
normalized,
};
}
// Legacy format: minimum 8 alphanumeric characters (no special characters except hyphens)
if (/^[A-Z0-9\-]{8,}$/.test(normalized)) {
return {
isValid: true,
format: 'legacy',
normalized,
};
}
// Invalid format
let error = 'Invalid serial number format';
if (normalized.length < 8) {
error = 'Serial number must be at least 8 characters';
} else if (!/^[A-Z0-9\-]+$/.test(normalized)) {
error = 'Serial number can only contain letters, numbers, and hyphens';
}
return {
isValid: false,
format: 'invalid',
normalized,
error,
};
}
/**
* Quick validation - returns true/false only
*/
export function isValidSerial(serial: string | null | undefined): boolean {
return validateSerial(serial).isValid;
}
/**
* Check if serial is a demo serial
*/
export function isDemoSerial(serial: string | null | undefined): boolean {
if (!serial) return false;
const normalized = serial.trim().toUpperCase();
return DEMO_SERIALS.has(normalized);
}
/**
* Get user-friendly error message for invalid serial
*/
export function getSerialErrorMessage(serial: string | null | undefined): string {
const result = validateSerial(serial);
if (result.isValid) {
return '';
}
if (result.error) {
return result.error;
}
return 'Please enter a valid serial number from your WellNuo Starter Kit.\n\nFor testing, use: DEMO-00000';
}
/**
* Format serial for display (add hyphens if missing for production format)
*/
export function formatSerialForDisplay(serial: string): string {
const normalized = serial.trim().toUpperCase();
// If it looks like a production serial without hyphens, add them
if (/^WELLNUO[A-Z0-9]{8}$/.test(normalized)) {
return `${normalized.slice(0, 7)}-${normalized.slice(7, 11)}-${normalized.slice(11)}`;
}
return normalized;
}
/**
* Get placeholder text based on format preference
*/
export function getSerialPlaceholder(preferProduction: boolean = true): string {
return preferProduction ? 'WELLNUO-XXXX-XXXX' : 'Enter serial number';
}