- Add real-time validation for WiFi credentials with password strength indicator - Add validateRealTime() for immediate UI feedback as user types - Add parseWiFiErrorResponse() for user-friendly error messages - Add prepareCredentialsForDevice() for pre-transmission validation - Block BLE protocol delimiter characters (| and ,) in credentials - Create WiFiPasswordInput component with strength bar and validation hints - Update BLEManager with improved error messages and pre-validation - Update MockBLEManager with simulation scenarios for testing - Add 62 comprehensive tests for all validation functions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
527 lines
18 KiB
TypeScript
527 lines
18 KiB
TypeScript
/**
|
|
* Tests for WiFi Credentials Validation
|
|
*/
|
|
|
|
import {
|
|
validateSSID,
|
|
validatePassword,
|
|
validateWiFiCredentials,
|
|
isStrongPassword,
|
|
sanitizeWiFiCredentials,
|
|
getValidationErrorMessage,
|
|
getValidationMessage,
|
|
validateRealTime,
|
|
parseWiFiErrorResponse,
|
|
prepareCredentialsForDevice,
|
|
} from '../wifiValidation';
|
|
|
|
describe('WiFi Validation', () => {
|
|
describe('validateSSID', () => {
|
|
it('should accept valid SSIDs', () => {
|
|
const validSSIDs = [
|
|
'MyHomeWiFi',
|
|
'Guest Network',
|
|
'Office-5G',
|
|
'Café WiFi',
|
|
'Network_2.4GHz',
|
|
'a',
|
|
'1234567890123456789012345678901', // 31 chars
|
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZ123456', // 32 chars
|
|
];
|
|
|
|
validSSIDs.forEach((ssid) => {
|
|
const result = validateSSID(ssid);
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
it('should reject empty SSID', () => {
|
|
const result = validateSSID('');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain('Network name (SSID) cannot be empty');
|
|
});
|
|
|
|
it('should reject whitespace-only SSID', () => {
|
|
const result = validateSSID(' ');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain('Network name (SSID) cannot be empty');
|
|
});
|
|
|
|
it('should reject SSID longer than 32 bytes', () => {
|
|
const longSSID = 'A'.repeat(33);
|
|
const result = validateSSID(longSSID);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain('Network name is too long (max 32 bytes)');
|
|
});
|
|
|
|
it('should reject SSID with control characters', () => {
|
|
const result = validateSSID('Network\x00Name');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain('Network name contains invalid control characters');
|
|
});
|
|
|
|
it('should warn about leading/trailing spaces', () => {
|
|
const result1 = validateSSID(' MyWiFi');
|
|
expect(result1.valid).toBe(true);
|
|
expect(result1.warnings).toContain(
|
|
'Network name has leading or trailing spaces - this may cause connection issues'
|
|
);
|
|
|
|
const result2 = validateSSID('MyWiFi ');
|
|
expect(result2.valid).toBe(true);
|
|
expect(result2.warnings).toContain(
|
|
'Network name has leading or trailing spaces - this may cause connection issues'
|
|
);
|
|
});
|
|
|
|
it('should warn about multiple consecutive spaces', () => {
|
|
const result = validateSSID('My WiFi');
|
|
expect(result.valid).toBe(true);
|
|
expect(result.warnings).toContain('Network name has multiple consecutive spaces');
|
|
});
|
|
});
|
|
|
|
describe('validatePassword', () => {
|
|
describe('WPA/WPA2/WPA3 networks', () => {
|
|
it('should accept valid WPA passwords', () => {
|
|
const validPasswords = [
|
|
'MySecurePass123',
|
|
'abcdefgh', // 8 chars minimum
|
|
'A'.repeat(63), // 63 chars maximum
|
|
'Pass with spaces!@#',
|
|
'ComplexP@ssw0rd!',
|
|
];
|
|
|
|
validPasswords.forEach((password) => {
|
|
const result = validatePassword(password, 'WPA2 PSK');
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
it('should reject passwords shorter than 8 characters', () => {
|
|
const result = validatePassword('1234567', 'WPA2 PSK');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain(
|
|
'Password must be at least 8 characters for WPA/WPA2/WPA3 networks'
|
|
);
|
|
});
|
|
|
|
it('should reject passwords longer than 63 characters', () => {
|
|
const longPassword = 'A'.repeat(64);
|
|
const result = validatePassword(longPassword, 'WPA2 PSK');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain(
|
|
'Password cannot exceed 63 characters for WPA/WPA2/WPA3 networks'
|
|
);
|
|
});
|
|
|
|
it('should warn about weak passwords', () => {
|
|
const result = validatePassword('12345678', 'WPA2 PSK');
|
|
expect(result.valid).toBe(true);
|
|
expect(result.warnings).toContain('Password is weak - consider using at least 12 characters');
|
|
});
|
|
|
|
it('should warn about commonly used weak passwords', () => {
|
|
const weakPasswords = ['password', '12345678', 'qwerty123'];
|
|
|
|
weakPasswords.forEach((password) => {
|
|
const result = validatePassword(password, 'WPA2 PSK');
|
|
expect(result.warnings).toContain('This is a commonly used weak password');
|
|
});
|
|
});
|
|
|
|
it('should warn about numeric-only passwords', () => {
|
|
const result = validatePassword('12345678', 'WPA2 PSK');
|
|
expect(result.warnings).toContain(
|
|
'Password contains only numbers - consider adding letters and symbols'
|
|
);
|
|
});
|
|
|
|
it('should warn about non-ASCII characters', () => {
|
|
const result = validatePassword('Password™123', 'WPA2 PSK');
|
|
expect(result.valid).toBe(true);
|
|
expect(result.warnings).toContain(
|
|
'Password contains non-ASCII characters - this may cause issues on some devices'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Open networks', () => {
|
|
it('should accept empty password for open networks', () => {
|
|
const result = validatePassword('', 'Open');
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
|
|
it('should accept any password for open networks (will be ignored)', () => {
|
|
const result = validatePassword('any password', 'Open');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('WEP networks', () => {
|
|
it('should accept valid 5-character WEP password', () => {
|
|
const result = validatePassword('abcde', 'WEP');
|
|
expect(result.valid).toBe(true);
|
|
expect(result.warnings).toContain(
|
|
'WEP is deprecated and insecure - consider upgrading your router to WPA2/WPA3'
|
|
);
|
|
});
|
|
|
|
it('should accept valid 13-character WEP password', () => {
|
|
const result = validatePassword('abcdefghijklm', 'WEP');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('should accept valid 10-digit hex WEP password', () => {
|
|
const result = validatePassword('1234567890', 'WEP');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('should accept valid 26-digit hex WEP password', () => {
|
|
const result = validatePassword('12345678901234567890123456', 'WEP');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('should reject invalid WEP password length', () => {
|
|
const result = validatePassword('abc', 'WEP');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain(
|
|
'WEP password must be 5 or 13 ASCII characters, or 10/26 hexadecimal digits'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Secured networks without auth type', () => {
|
|
it('should require password', () => {
|
|
const result = validatePassword('');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain('Password is required for secured networks');
|
|
});
|
|
|
|
it('should apply WPA rules by default', () => {
|
|
const result = validatePassword('1234567'); // 7 chars
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain(
|
|
'Password must be at least 8 characters for WPA/WPA2/WPA3 networks'
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should reject passwords with control characters', () => {
|
|
const result = validatePassword('Pass\x00word123', 'WPA2 PSK');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain('Password contains invalid control characters');
|
|
});
|
|
});
|
|
|
|
describe('validateWiFiCredentials', () => {
|
|
it('should validate both SSID and password', () => {
|
|
const result = validateWiFiCredentials('MyWiFi', 'SecurePass123', 'WPA2 PSK');
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
|
|
it('should combine errors from both validations', () => {
|
|
const result = validateWiFiCredentials('', '123', 'WPA2 PSK');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain('Network name (SSID) cannot be empty');
|
|
expect(result.errors).toContain(
|
|
'Password must be at least 8 characters for WPA/WPA2/WPA3 networks'
|
|
);
|
|
});
|
|
|
|
it('should combine warnings from both validations', () => {
|
|
const result = validateWiFiCredentials(' MyWiFi ', 'password123', 'WPA2 PSK');
|
|
expect(result.valid).toBe(true);
|
|
expect(result.warnings.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should validate open network correctly', () => {
|
|
const result = validateWiFiCredentials('GuestNetwork', '', 'Open');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('isStrongPassword', () => {
|
|
it('should accept strong passwords', () => {
|
|
const strongPasswords = [
|
|
'MyP@ssw0rd123!',
|
|
'Secure_Password_2024',
|
|
'Compl3x!P@ssw0rd',
|
|
'abCD1234!@#$',
|
|
];
|
|
|
|
strongPasswords.forEach((password) => {
|
|
expect(isStrongPassword(password)).toBe(true);
|
|
});
|
|
});
|
|
|
|
it('should reject passwords shorter than 12 characters', () => {
|
|
expect(isStrongPassword('P@ssw0rd')).toBe(false);
|
|
});
|
|
|
|
it('should reject passwords with less than 3 character types', () => {
|
|
expect(isStrongPassword('onlylowercase')).toBe(false);
|
|
expect(isStrongPassword('12345678901234')).toBe(false);
|
|
expect(isStrongPassword('ONLYUPPERCASE')).toBe(false);
|
|
});
|
|
|
|
it('should accept passwords with 3 or 4 character types', () => {
|
|
// 3 types: lowercase, uppercase, numbers
|
|
expect(isStrongPassword('Password1234')).toBe(true);
|
|
|
|
// 4 types: lowercase, uppercase, numbers, special
|
|
expect(isStrongPassword('P@ssword1234')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('sanitizeWiFiCredentials', () => {
|
|
it('should trim SSID whitespace', () => {
|
|
const result = sanitizeWiFiCredentials({
|
|
ssid: ' MyWiFi ',
|
|
password: 'password',
|
|
});
|
|
expect(result.ssid).toBe('MyWiFi');
|
|
});
|
|
|
|
it('should preserve password whitespace', () => {
|
|
const result = sanitizeWiFiCredentials({
|
|
ssid: 'MyWiFi',
|
|
password: ' password with spaces ',
|
|
});
|
|
expect(result.password).toBe(' password with spaces ');
|
|
});
|
|
|
|
it('should not modify valid credentials', () => {
|
|
const result = sanitizeWiFiCredentials({
|
|
ssid: 'MyWiFi',
|
|
password: 'SecurePass123',
|
|
});
|
|
expect(result.ssid).toBe('MyWiFi');
|
|
expect(result.password).toBe('SecurePass123');
|
|
});
|
|
});
|
|
|
|
describe('getValidationErrorMessage', () => {
|
|
it('should return empty string for valid credentials', () => {
|
|
const result = validateWiFiCredentials('MyWiFi', 'SecurePass123', 'WPA2 PSK');
|
|
expect(getValidationErrorMessage(result)).toBe('');
|
|
});
|
|
|
|
it('should return first error message', () => {
|
|
const result = validateWiFiCredentials('', '123', 'WPA2 PSK');
|
|
expect(getValidationErrorMessage(result)).toBe('Network name (SSID) cannot be empty');
|
|
});
|
|
|
|
it('should return default message if no specific error', () => {
|
|
const result = { valid: false, errors: [], warnings: [] };
|
|
expect(getValidationErrorMessage(result)).toBe('Invalid WiFi credentials');
|
|
});
|
|
});
|
|
|
|
describe('getValidationMessage', () => {
|
|
it('should return empty string for valid credentials with no warnings', () => {
|
|
const result = { valid: true, errors: [], warnings: [] };
|
|
expect(getValidationMessage(result)).toBe('');
|
|
});
|
|
|
|
it('should return errors only', () => {
|
|
const result = {
|
|
valid: false,
|
|
errors: ['Error 1', 'Error 2'],
|
|
warnings: [],
|
|
};
|
|
expect(getValidationMessage(result)).toBe('Error 1\nError 2');
|
|
});
|
|
|
|
it('should return warnings only', () => {
|
|
const result = {
|
|
valid: true,
|
|
errors: [],
|
|
warnings: ['Warning 1', 'Warning 2'],
|
|
};
|
|
expect(getValidationMessage(result)).toBe('Warning 1\nWarning 2');
|
|
});
|
|
|
|
it('should return both errors and warnings', () => {
|
|
const result = {
|
|
valid: false,
|
|
errors: ['Error 1'],
|
|
warnings: ['Warning 1'],
|
|
};
|
|
expect(getValidationMessage(result)).toBe('Error 1\nWarning 1');
|
|
});
|
|
});
|
|
|
|
describe('validateRealTime', () => {
|
|
it('should return valid state for valid credentials', () => {
|
|
const result = validateRealTime('MyWiFi', 'SecurePass123', 'WPA2 PSK');
|
|
expect(result.ssidValid).toBe(true);
|
|
expect(result.passwordValid).toBe(true);
|
|
expect(result.canSubmit).toBe(true);
|
|
expect(result.ssidError).toBeNull();
|
|
expect(result.passwordError).toBeNull();
|
|
});
|
|
|
|
it('should indicate password strength', () => {
|
|
// Weak password (8-9 chars)
|
|
const weak = validateRealTime('MyWiFi', 'password', 'WPA2 PSK');
|
|
expect(weak.passwordStrength).toBe('weak');
|
|
|
|
// Medium password (10+ chars but not complex)
|
|
const medium = validateRealTime('MyWiFi', 'mediumpassword', 'WPA2 PSK');
|
|
expect(medium.passwordStrength).toBe('medium');
|
|
|
|
// Strong password (12+ chars with variety)
|
|
const strong = validateRealTime('MyWiFi', 'Str0ng!Pass123', 'WPA2 PSK');
|
|
expect(strong.passwordStrength).toBe('strong');
|
|
});
|
|
|
|
it('should handle open networks', () => {
|
|
const result = validateRealTime('GuestNetwork', '', 'Open');
|
|
expect(result.passwordValid).toBe(true);
|
|
expect(result.passwordStrength).toBeNull();
|
|
expect(result.canSubmit).toBe(true);
|
|
});
|
|
|
|
it('should not allow submit with short password', () => {
|
|
const result = validateRealTime('MyWiFi', '1234567', 'WPA2 PSK');
|
|
expect(result.canSubmit).toBe(false);
|
|
expect(result.passwordValid).toBe(false);
|
|
});
|
|
|
|
it('should not allow submit with empty SSID', () => {
|
|
const result = validateRealTime('', 'password123', 'WPA2 PSK');
|
|
expect(result.canSubmit).toBe(false);
|
|
});
|
|
|
|
it('should collect warnings', () => {
|
|
const result = validateRealTime(' MyWiFi', 'password', 'WPA2 PSK');
|
|
expect(result.warnings.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should return null strength for empty password', () => {
|
|
const result = validateRealTime('MyWiFi', '', 'WPA2 PSK');
|
|
expect(result.passwordStrength).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('parseWiFiErrorResponse', () => {
|
|
it('should detect wrong password errors', () => {
|
|
const errors = [
|
|
'WiFi credentials rejected by sensor',
|
|
'wrong password',
|
|
'check password',
|
|
'W|fail response',
|
|
];
|
|
|
|
errors.forEach((error) => {
|
|
const result = parseWiFiErrorResponse(error);
|
|
expect(result.type).toBe('password_wrong');
|
|
expect(result.suggestion).toContain('check');
|
|
});
|
|
});
|
|
|
|
it('should detect network not found errors', () => {
|
|
const errors = [
|
|
'Network not found',
|
|
'no network available',
|
|
'SSID not available',
|
|
];
|
|
|
|
errors.forEach((error) => {
|
|
const result = parseWiFiErrorResponse(error);
|
|
expect(result.type).toBe('network_not_found');
|
|
expect(result.suggestion).toContain('range');
|
|
});
|
|
});
|
|
|
|
it('should detect timeout errors', () => {
|
|
const errors = [
|
|
'Command timeout',
|
|
'Request timed out',
|
|
'Sensor did not respond in time',
|
|
];
|
|
|
|
errors.forEach((error) => {
|
|
const result = parseWiFiErrorResponse(error);
|
|
expect(result.type).toBe('timeout');
|
|
expect(result.suggestion).toContain('closer');
|
|
});
|
|
});
|
|
|
|
it('should detect connection errors', () => {
|
|
const errors = [
|
|
'BLE connection failed',
|
|
'Bluetooth error',
|
|
'Could not connect to device',
|
|
];
|
|
|
|
errors.forEach((error) => {
|
|
const result = parseWiFiErrorResponse(error);
|
|
expect(result.type).toBe('connection_failed');
|
|
expect(result.suggestion).toContain('try again');
|
|
});
|
|
});
|
|
|
|
it('should return unknown for unrecognized errors', () => {
|
|
const result = parseWiFiErrorResponse('Some random error message');
|
|
expect(result.type).toBe('unknown');
|
|
expect(result.message).toBe('WiFi configuration failed');
|
|
});
|
|
});
|
|
|
|
describe('prepareCredentialsForDevice', () => {
|
|
it('should return sanitized credentials for valid input', () => {
|
|
const result = prepareCredentialsForDevice(' MyWiFi ', 'password123', 'WPA2 PSK');
|
|
expect(result.ssid).toBe('MyWiFi');
|
|
expect(result.password).toBe('password123');
|
|
});
|
|
|
|
it('should throw for invalid SSID with pipe character', () => {
|
|
expect(() => {
|
|
prepareCredentialsForDevice('My|WiFi', 'password123', 'WPA2 PSK');
|
|
}).toThrow('Network name contains invalid characters');
|
|
});
|
|
|
|
it('should throw for invalid SSID with comma character', () => {
|
|
expect(() => {
|
|
prepareCredentialsForDevice('My,WiFi', 'password123', 'WPA2 PSK');
|
|
}).toThrow('Network name contains invalid characters');
|
|
});
|
|
|
|
it('should throw for password with pipe character', () => {
|
|
expect(() => {
|
|
prepareCredentialsForDevice('MyWiFi', 'pass|word', 'WPA2 PSK');
|
|
}).toThrow('Password contains invalid character');
|
|
});
|
|
|
|
it('should allow password with comma character', () => {
|
|
// Comma is allowed in password (only pipe is delimiter between command and payload)
|
|
const result = prepareCredentialsForDevice('MyWiFi', 'pass,word123', 'WPA2 PSK');
|
|
expect(result.password).toBe('pass,word123');
|
|
});
|
|
|
|
it('should throw for empty SSID', () => {
|
|
expect(() => {
|
|
prepareCredentialsForDevice('', 'password123', 'WPA2 PSK');
|
|
}).toThrow('Network name (SSID) cannot be empty');
|
|
});
|
|
|
|
it('should throw for short password', () => {
|
|
expect(() => {
|
|
prepareCredentialsForDevice('MyWiFi', 'short', 'WPA2 PSK');
|
|
}).toThrow('at least 8 characters');
|
|
});
|
|
|
|
it('should allow empty password for open networks', () => {
|
|
const result = prepareCredentialsForDevice('GuestNetwork', '', 'Open');
|
|
expect(result.ssid).toBe('GuestNetwork');
|
|
expect(result.password).toBe('');
|
|
});
|
|
});
|
|
});
|