Add unit tests for BLE error handling, bulk operations, and WiFi validation
- BLEErrors.test.ts: Tests for BLEError class, parseBLEError function, isBLEError, getErrorInfo, getRecoveryActionLabel, and BLELogger utilities - BLEManager.bulk.test.ts: Tests for bulkDisconnect, bulkReboot, and bulkSetWiFi operations including progress callbacks and edge cases - BLEManager.wifi.test.ts: Tests for WiFi credential validation (SSID/password), error scenarios, getWiFiList, getCurrentWiFi, and signal quality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
67496d6913
commit
5f40370dfa
533
services/ble/__tests__/BLEErrors.test.ts
Normal file
533
services/ble/__tests__/BLEErrors.test.ts
Normal file
@ -0,0 +1,533 @@
|
|||||||
|
/**
|
||||||
|
* BLE Error Handling Unit Tests
|
||||||
|
* Tests for BLEError class, parseBLEError, and error utilities
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
BLEError,
|
||||||
|
BLEErrorCode,
|
||||||
|
BLEErrorSeverity,
|
||||||
|
BLERecoveryAction,
|
||||||
|
BLE_ERROR_MESSAGES,
|
||||||
|
BLE_RECOVERY_ACTIONS,
|
||||||
|
parseBLEError,
|
||||||
|
isBLEError,
|
||||||
|
getErrorInfo,
|
||||||
|
getRecoveryActionLabel,
|
||||||
|
BLELogger,
|
||||||
|
} from '../errors';
|
||||||
|
|
||||||
|
describe('BLEError', () => {
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should create error with code only', () => {
|
||||||
|
const error = new BLEError(BLEErrorCode.CONNECTION_FAILED);
|
||||||
|
|
||||||
|
expect(error.code).toBe(BLEErrorCode.CONNECTION_FAILED);
|
||||||
|
expect(error.message).toBe(BLE_ERROR_MESSAGES[BLEErrorCode.CONNECTION_FAILED].message);
|
||||||
|
expect(error.userMessage.title).toBe('Connection Failed');
|
||||||
|
expect(error.name).toBe('BLEError');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create error with custom message', () => {
|
||||||
|
const error = new BLEError(BLEErrorCode.CONNECTION_TIMEOUT, {
|
||||||
|
message: 'Custom timeout message',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(error.code).toBe(BLEErrorCode.CONNECTION_TIMEOUT);
|
||||||
|
expect(error.message).toBe('Custom timeout message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create error with device info', () => {
|
||||||
|
const error = new BLEError(BLEErrorCode.DEVICE_DISCONNECTED, {
|
||||||
|
deviceId: 'device-123',
|
||||||
|
deviceName: 'WP_497_81a14c',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(error.deviceId).toBe('device-123');
|
||||||
|
expect(error.deviceName).toBe('WP_497_81a14c');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should wrap original error', () => {
|
||||||
|
const originalError = new Error('Native BLE error');
|
||||||
|
const error = new BLEError(BLEErrorCode.COMMAND_FAILED, {
|
||||||
|
originalError,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(error.originalError).toBe(originalError);
|
||||||
|
expect(error.message).toBe('Native BLE error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set timestamp', () => {
|
||||||
|
const beforeCreate = Date.now();
|
||||||
|
const error = new BLEError(BLEErrorCode.UNKNOWN_ERROR);
|
||||||
|
const afterCreate = Date.now();
|
||||||
|
|
||||||
|
expect(error.timestamp).toBeGreaterThanOrEqual(beforeCreate);
|
||||||
|
expect(error.timestamp).toBeLessThanOrEqual(afterCreate);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set recovery actions from mapping', () => {
|
||||||
|
const error = new BLEError(BLEErrorCode.WIFI_PASSWORD_INCORRECT);
|
||||||
|
|
||||||
|
expect(error.recoveryActions).toEqual(
|
||||||
|
BLE_RECOVERY_ACTIONS[BLEErrorCode.WIFI_PASSWORD_INCORRECT]
|
||||||
|
);
|
||||||
|
expect(error.recoveryActions).toContain(BLERecoveryAction.CHECK_WIFI_PASSWORD);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('severity', () => {
|
||||||
|
it('should set CRITICAL severity for permission errors', () => {
|
||||||
|
const permissionError = new BLEError(BLEErrorCode.PERMISSION_DENIED);
|
||||||
|
const bluetoothError = new BLEError(BLEErrorCode.BLUETOOTH_DISABLED);
|
||||||
|
const locationError = new BLEError(BLEErrorCode.LOCATION_DISABLED);
|
||||||
|
|
||||||
|
expect(permissionError.severity).toBe(BLEErrorSeverity.CRITICAL);
|
||||||
|
expect(bluetoothError.severity).toBe(BLEErrorSeverity.CRITICAL);
|
||||||
|
expect(locationError.severity).toBe(BLEErrorSeverity.CRITICAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set ERROR severity for connection failures', () => {
|
||||||
|
const error = new BLEError(BLEErrorCode.CONNECTION_FAILED);
|
||||||
|
expect(error.severity).toBe(BLEErrorSeverity.ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set WARNING severity for busy/in-progress states', () => {
|
||||||
|
const busyError = new BLEError(BLEErrorCode.DEVICE_BUSY);
|
||||||
|
const progressError = new BLEError(BLEErrorCode.CONNECTION_IN_PROGRESS);
|
||||||
|
|
||||||
|
expect(busyError.severity).toBe(BLEErrorSeverity.WARNING);
|
||||||
|
expect(progressError.severity).toBe(BLEErrorSeverity.WARNING);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set INFO severity for expected states', () => {
|
||||||
|
const connectedError = new BLEError(BLEErrorCode.ALREADY_CONNECTED);
|
||||||
|
const cancelledError = new BLEError(BLEErrorCode.OPERATION_CANCELLED);
|
||||||
|
|
||||||
|
expect(connectedError.severity).toBe(BLEErrorSeverity.INFO);
|
||||||
|
expect(cancelledError.severity).toBe(BLEErrorSeverity.INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow custom severity override', () => {
|
||||||
|
const error = new BLEError(BLEErrorCode.UNKNOWN_ERROR, {
|
||||||
|
severity: BLEErrorSeverity.CRITICAL,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(error.severity).toBe(BLEErrorSeverity.CRITICAL);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toLogString', () => {
|
||||||
|
it('should format error with code', () => {
|
||||||
|
const error = new BLEError(BLEErrorCode.CONNECTION_FAILED);
|
||||||
|
const logString = error.toLogString();
|
||||||
|
|
||||||
|
expect(logString).toContain('[BLE]');
|
||||||
|
expect(logString).toContain('[BLE_CONNECTION_FAILED]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include device name when available', () => {
|
||||||
|
const error = new BLEError(BLEErrorCode.DEVICE_DISCONNECTED, {
|
||||||
|
deviceName: 'WP_497_81a14c',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(error.toLogString()).toContain('[WP_497_81a14c]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include device ID when name not available', () => {
|
||||||
|
const error = new BLEError(BLEErrorCode.DEVICE_DISCONNECTED, {
|
||||||
|
deviceId: 'device-123',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(error.toLogString()).toContain('[device-123]');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isRetryable', () => {
|
||||||
|
it('should return true for retryable errors', () => {
|
||||||
|
const error = new BLEError(BLEErrorCode.CONNECTION_FAILED);
|
||||||
|
expect(error.isRetryable()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for non-retryable errors', () => {
|
||||||
|
const error = new BLEError(BLEErrorCode.ALREADY_CONNECTED);
|
||||||
|
expect(error.isRetryable()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isSkippable', () => {
|
||||||
|
it('should return true for skippable errors', () => {
|
||||||
|
const error = new BLEError(BLEErrorCode.COMMAND_FAILED);
|
||||||
|
expect(error.isSkippable()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for non-skippable errors', () => {
|
||||||
|
const error = new BLEError(BLEErrorCode.PERMISSION_DENIED);
|
||||||
|
expect(error.isSkippable()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseBLEError', () => {
|
||||||
|
describe('permission errors', () => {
|
||||||
|
it('should parse permission denied error', () => {
|
||||||
|
const error = parseBLEError(new Error('Bluetooth permission not granted'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.PERMISSION_DENIED);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse bluetooth disabled error', () => {
|
||||||
|
const error = parseBLEError(new Error('Bluetooth is disabled'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.BLUETOOTH_DISABLED);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse location disabled error', () => {
|
||||||
|
const error = parseBLEError(new Error('Location services required'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.LOCATION_DISABLED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('connection errors', () => {
|
||||||
|
it('should parse timeout error', () => {
|
||||||
|
const error = parseBLEError(new Error('Connection timeout'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.CONNECTION_TIMEOUT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse connection in progress error', () => {
|
||||||
|
const error = parseBLEError(new Error('Connection already in progress'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.CONNECTION_IN_PROGRESS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse device not found error', () => {
|
||||||
|
const error = parseBLEError(new Error('Device not found'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.DEVICE_NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse disconnect error', () => {
|
||||||
|
const error = parseBLEError(new Error('Device disconnected unexpectedly'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.DEVICE_DISCONNECTED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('WiFi errors', () => {
|
||||||
|
it('should parse incorrect password error', () => {
|
||||||
|
const error = parseBLEError(new Error('WiFi password incorrect'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.WIFI_PASSWORD_INCORRECT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse network not found error', () => {
|
||||||
|
const error = parseBLEError(new Error('WiFi network not found'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.WIFI_NETWORK_NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse scan in progress error', () => {
|
||||||
|
const error = parseBLEError(new Error('WiFi scan in progress'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.WIFI_SCAN_IN_PROGRESS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse invalid character error', () => {
|
||||||
|
const error = parseBLEError(new Error('SSID contains invalid character'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.WIFI_INVALID_CREDENTIALS);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('authentication errors', () => {
|
||||||
|
it('should parse PIN unlock failed error', () => {
|
||||||
|
const error = parseBLEError(new Error('Failed to unlock device with PIN'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.PIN_UNLOCK_FAILED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sensor errors', () => {
|
||||||
|
it('should parse sensor not responding error', () => {
|
||||||
|
const error = parseBLEError(new Error('Sensor not responding'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.SENSOR_NOT_RESPONDING);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse no response error', () => {
|
||||||
|
const error = parseBLEError(new Error('No response from device'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.SENSOR_NOT_RESPONDING);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('service errors', () => {
|
||||||
|
it('should parse service not found error', () => {
|
||||||
|
const error = parseBLEError(new Error('BLE service not found'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.SERVICE_NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse characteristic not found error', () => {
|
||||||
|
const error = parseBLEError(new Error('Characteristic not found'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.CHARACTERISTIC_NOT_FOUND);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cancelled operations', () => {
|
||||||
|
it('should parse cancelled error', () => {
|
||||||
|
const error = parseBLEError(new Error('Operation was cancelled'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.OPERATION_CANCELLED);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse Android error code 2', () => {
|
||||||
|
const error = parseBLEError(new Error('[2] Operation cancelled'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.OPERATION_CANCELLED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('context preservation', () => {
|
||||||
|
it('should preserve device context', () => {
|
||||||
|
const error = parseBLEError(new Error('Connection failed'), {
|
||||||
|
deviceId: 'device-123',
|
||||||
|
deviceName: 'WP_497_81a14c',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(error.deviceId).toBe('device-123');
|
||||||
|
expect(error.deviceName).toBe('WP_497_81a14c');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve original error', () => {
|
||||||
|
const originalError = new Error('Native error');
|
||||||
|
const error = parseBLEError(originalError);
|
||||||
|
|
||||||
|
expect(error.originalError).toBe(originalError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unknown errors', () => {
|
||||||
|
it('should return UNKNOWN_ERROR for unrecognized error', () => {
|
||||||
|
const error = parseBLEError(new Error('Some completely unknown error'));
|
||||||
|
expect(error.code).toBe(BLEErrorCode.UNKNOWN_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle non-Error objects', () => {
|
||||||
|
const error = parseBLEError('string error');
|
||||||
|
expect(error.code).toBe(BLEErrorCode.UNKNOWN_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null/undefined', () => {
|
||||||
|
const error1 = parseBLEError(null);
|
||||||
|
const error2 = parseBLEError(undefined);
|
||||||
|
|
||||||
|
expect(error1.code).toBe(BLEErrorCode.UNKNOWN_ERROR);
|
||||||
|
expect(error2.code).toBe(BLEErrorCode.UNKNOWN_ERROR);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isBLEError', () => {
|
||||||
|
it('should return true for BLEError instances', () => {
|
||||||
|
const error = new BLEError(BLEErrorCode.CONNECTION_FAILED);
|
||||||
|
expect(isBLEError(error)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for regular Error', () => {
|
||||||
|
const error = new Error('Regular error');
|
||||||
|
expect(isBLEError(error)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for non-errors', () => {
|
||||||
|
expect(isBLEError(null)).toBe(false);
|
||||||
|
expect(isBLEError(undefined)).toBe(false);
|
||||||
|
expect(isBLEError('string')).toBe(false);
|
||||||
|
expect(isBLEError({})).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getErrorInfo', () => {
|
||||||
|
it('should extract info from BLEError', () => {
|
||||||
|
const error = new BLEError(BLEErrorCode.WIFI_PASSWORD_INCORRECT);
|
||||||
|
const info = getErrorInfo(error);
|
||||||
|
|
||||||
|
expect(info.code).toBe(BLEErrorCode.WIFI_PASSWORD_INCORRECT);
|
||||||
|
expect(info.title).toBe('Wrong Password');
|
||||||
|
expect(info.severity).toBe(BLEErrorSeverity.ERROR);
|
||||||
|
expect(info.isRetryable).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse and extract info from regular Error', () => {
|
||||||
|
const error = new Error('Connection timeout');
|
||||||
|
const info = getErrorInfo(error);
|
||||||
|
|
||||||
|
expect(info.code).toBe(BLEErrorCode.CONNECTION_TIMEOUT);
|
||||||
|
expect(info.title).toBe('Connection Timeout');
|
||||||
|
expect(info.isRetryable).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return recovery actions', () => {
|
||||||
|
const error = new BLEError(BLEErrorCode.BLUETOOTH_DISABLED);
|
||||||
|
const info = getErrorInfo(error);
|
||||||
|
|
||||||
|
expect(info.recoveryActions).toContain(BLERecoveryAction.ENABLE_BLUETOOTH);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRecoveryActionLabel', () => {
|
||||||
|
it('should return correct labels for all actions', () => {
|
||||||
|
expect(getRecoveryActionLabel(BLERecoveryAction.RETRY)).toBe('Retry');
|
||||||
|
expect(getRecoveryActionLabel(BLERecoveryAction.SKIP)).toBe('Skip');
|
||||||
|
expect(getRecoveryActionLabel(BLERecoveryAction.CANCEL)).toBe('Cancel');
|
||||||
|
expect(getRecoveryActionLabel(BLERecoveryAction.ENABLE_BLUETOOTH)).toBe('Enable Bluetooth');
|
||||||
|
expect(getRecoveryActionLabel(BLERecoveryAction.ENABLE_LOCATION)).toBe('Enable Location');
|
||||||
|
expect(getRecoveryActionLabel(BLERecoveryAction.GRANT_PERMISSIONS)).toBe('Grant Permission');
|
||||||
|
expect(getRecoveryActionLabel(BLERecoveryAction.MOVE_CLOSER)).toBe('Move Closer');
|
||||||
|
expect(getRecoveryActionLabel(BLERecoveryAction.CHECK_SENSOR_POWER)).toBe('Check Sensor');
|
||||||
|
expect(getRecoveryActionLabel(BLERecoveryAction.CHECK_WIFI_PASSWORD)).toBe('Check Password');
|
||||||
|
expect(getRecoveryActionLabel(BLERecoveryAction.TRY_DIFFERENT_NETWORK)).toBe('Try Different Network');
|
||||||
|
expect(getRecoveryActionLabel(BLERecoveryAction.CONTACT_SUPPORT)).toBe('Contact Support');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return OK for unknown action', () => {
|
||||||
|
expect(getRecoveryActionLabel('unknown' as BLERecoveryAction)).toBe('OK');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('BLE_ERROR_MESSAGES', () => {
|
||||||
|
it('should have messages for all error codes', () => {
|
||||||
|
const errorCodes = Object.values(BLEErrorCode);
|
||||||
|
|
||||||
|
errorCodes.forEach(code => {
|
||||||
|
expect(BLE_ERROR_MESSAGES[code]).toBeDefined();
|
||||||
|
expect(BLE_ERROR_MESSAGES[code].title).toBeDefined();
|
||||||
|
expect(BLE_ERROR_MESSAGES[code].message).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('BLE_RECOVERY_ACTIONS', () => {
|
||||||
|
it('should have recovery actions for all error codes', () => {
|
||||||
|
const errorCodes = Object.values(BLEErrorCode);
|
||||||
|
|
||||||
|
errorCodes.forEach(code => {
|
||||||
|
expect(BLE_RECOVERY_ACTIONS[code]).toBeDefined();
|
||||||
|
expect(Array.isArray(BLE_RECOVERY_ACTIONS[code])).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('BLELogger', () => {
|
||||||
|
let consoleLogSpy: jest.SpyInstance;
|
||||||
|
let consoleWarnSpy: jest.SpyInstance;
|
||||||
|
let consoleErrorSpy: jest.SpyInstance;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
BLELogger.enable();
|
||||||
|
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||||
|
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||||
|
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
consoleLogSpy.mockRestore();
|
||||||
|
consoleWarnSpy.mockRestore();
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('enable/disable', () => {
|
||||||
|
it('should log when enabled', () => {
|
||||||
|
BLELogger.enable();
|
||||||
|
BLELogger.log('Test message');
|
||||||
|
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not log when disabled', () => {
|
||||||
|
BLELogger.disable();
|
||||||
|
BLELogger.log('Test message');
|
||||||
|
|
||||||
|
expect(consoleLogSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('log', () => {
|
||||||
|
it('should log with prefix', () => {
|
||||||
|
BLELogger.log('Test message');
|
||||||
|
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith('[BLE] Test message', '');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log with data', () => {
|
||||||
|
BLELogger.log('Test message', { key: 'value' });
|
||||||
|
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith('[BLE] Test message', { key: 'value' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('warn', () => {
|
||||||
|
it('should warn with prefix', () => {
|
||||||
|
BLELogger.warn('Warning message');
|
||||||
|
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledWith('[BLE] Warning message', '');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('error', () => {
|
||||||
|
it('should error with prefix', () => {
|
||||||
|
BLELogger.error('Error message');
|
||||||
|
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith('[BLE] Error message', '');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format BLEError using toLogString', () => {
|
||||||
|
const bleError = new BLEError(BLEErrorCode.CONNECTION_FAILED, {
|
||||||
|
deviceName: 'WP_497_81a14c',
|
||||||
|
});
|
||||||
|
BLELogger.error('Connection error', bleError);
|
||||||
|
|
||||||
|
// BLELogger adds [BLE] prefix, and toLogString also starts with [BLE]
|
||||||
|
// So the output is [BLE] [BLE] [...] - this is the expected behavior
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith(`[BLE] ${bleError.toLogString()}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('logBatchProgress', () => {
|
||||||
|
it('should log batch progress', () => {
|
||||||
|
BLELogger.logBatchProgress(1, 5, 'WP_497', 'connecting...');
|
||||||
|
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('[1/5]')
|
||||||
|
);
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('WP_497')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show success indicator', () => {
|
||||||
|
BLELogger.logBatchProgress(1, 5, 'WP_497', 'done', true);
|
||||||
|
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('✓')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show failure indicator', () => {
|
||||||
|
BLELogger.logBatchProgress(1, 5, 'WP_497', 'failed', false);
|
||||||
|
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('✗')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include duration when provided', () => {
|
||||||
|
BLELogger.logBatchProgress(1, 5, 'WP_497', 'done', true, 2500);
|
||||||
|
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('2.5s')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('logBatchSummary', () => {
|
||||||
|
it('should log batch summary', () => {
|
||||||
|
BLELogger.logBatchSummary(10, 8, 2, 15000);
|
||||||
|
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('8/10 succeeded')
|
||||||
|
);
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('2 failed')
|
||||||
|
);
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('15.0s')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
330
services/ble/__tests__/BLEManager.bulk.test.ts
Normal file
330
services/ble/__tests__/BLEManager.bulk.test.ts
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
/**
|
||||||
|
* BLE Manager Bulk Operations Unit Tests
|
||||||
|
* Tests for bulkDisconnect, bulkReboot, and bulkSetWiFi
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MockBLEManager } from '../MockBLEManager';
|
||||||
|
import { BLEConnectionState } from '../types';
|
||||||
|
|
||||||
|
describe('BLEManager Bulk Operations', () => {
|
||||||
|
let manager: MockBLEManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
manager = new MockBLEManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await manager.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('bulkDisconnect', () => {
|
||||||
|
it('should disconnect multiple devices', async () => {
|
||||||
|
// Connect devices first
|
||||||
|
await manager.connectDevice('mock-743');
|
||||||
|
await manager.connectDevice('mock-769');
|
||||||
|
|
||||||
|
expect(manager.isDeviceConnected('mock-743')).toBe(true);
|
||||||
|
expect(manager.isDeviceConnected('mock-769')).toBe(true);
|
||||||
|
|
||||||
|
// Bulk disconnect
|
||||||
|
const results = await manager.bulkDisconnect(['mock-743', 'mock-769']);
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results[0].success).toBe(true);
|
||||||
|
expect(results[1].success).toBe(true);
|
||||||
|
expect(manager.isDeviceConnected('mock-743')).toBe(false);
|
||||||
|
expect(manager.isDeviceConnected('mock-769')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return results with device info', async () => {
|
||||||
|
await manager.connectDevice('mock-743');
|
||||||
|
|
||||||
|
const results = await manager.bulkDisconnect(['mock-743']);
|
||||||
|
|
||||||
|
expect(results[0]).toEqual({
|
||||||
|
deviceId: 'mock-743',
|
||||||
|
deviceName: 'WP_497_81a14c',
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty device list', async () => {
|
||||||
|
const results = await manager.bulkDisconnect([]);
|
||||||
|
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle non-connected devices gracefully', async () => {
|
||||||
|
const results = await manager.bulkDisconnect(['non-existent-device']);
|
||||||
|
|
||||||
|
expect(results).toHaveLength(1);
|
||||||
|
expect(results[0].success).toBe(true); // Mock doesn't throw for non-connected
|
||||||
|
expect(results[0].deviceId).toBe('non-existent-device');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should continue processing after individual failure', async () => {
|
||||||
|
await manager.connectDevice('mock-743');
|
||||||
|
await manager.connectDevice('mock-769');
|
||||||
|
|
||||||
|
// Disconnect all
|
||||||
|
const results = await manager.bulkDisconnect(['mock-743', 'mock-769']);
|
||||||
|
|
||||||
|
// All should have been processed
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('bulkReboot', () => {
|
||||||
|
it('should reboot multiple devices', async () => {
|
||||||
|
await manager.connectDevice('mock-743');
|
||||||
|
await manager.connectDevice('mock-769');
|
||||||
|
|
||||||
|
const results = await manager.bulkReboot(['mock-743', 'mock-769']);
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results[0].success).toBe(true);
|
||||||
|
expect(results[1].success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return results with device names', async () => {
|
||||||
|
await manager.connectDevice('mock-743');
|
||||||
|
|
||||||
|
const results = await manager.bulkReboot(['mock-743']);
|
||||||
|
|
||||||
|
expect(results[0].deviceId).toBe('mock-743');
|
||||||
|
expect(results[0].deviceName).toBe('WP_497_81a14c');
|
||||||
|
expect(results[0].success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disconnect devices after reboot', async () => {
|
||||||
|
await manager.connectDevice('mock-743');
|
||||||
|
expect(manager.isDeviceConnected('mock-743')).toBe(true);
|
||||||
|
|
||||||
|
await manager.bulkReboot(['mock-743']);
|
||||||
|
|
||||||
|
expect(manager.isDeviceConnected('mock-743')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty device list', async () => {
|
||||||
|
const results = await manager.bulkReboot([]);
|
||||||
|
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('bulkSetWiFi', () => {
|
||||||
|
const devices = [
|
||||||
|
{ id: 'mock-743', name: 'WP_497_81a14c' },
|
||||||
|
{ id: 'mock-769', name: 'WP_523_81aad4' },
|
||||||
|
];
|
||||||
|
const ssid = 'TestNetwork';
|
||||||
|
const password = 'testpass123';
|
||||||
|
|
||||||
|
it('should configure WiFi on multiple devices', async () => {
|
||||||
|
const results = await manager.bulkSetWiFi(devices, ssid, password);
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results[0].success).toBe(true);
|
||||||
|
expect(results[1].success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return detailed results', async () => {
|
||||||
|
const results = await manager.bulkSetWiFi(devices.slice(0, 1), ssid, password);
|
||||||
|
|
||||||
|
expect(results[0]).toEqual({
|
||||||
|
deviceId: 'mock-743',
|
||||||
|
deviceName: 'WP_497_81a14c',
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call progress callback for each stage', async () => {
|
||||||
|
const progressCalls: Array<{
|
||||||
|
deviceId: string;
|
||||||
|
status: string;
|
||||||
|
error?: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
const onProgress = (
|
||||||
|
deviceId: string,
|
||||||
|
status: 'connecting' | 'configuring' | 'rebooting' | 'success' | 'error',
|
||||||
|
error?: string
|
||||||
|
) => {
|
||||||
|
progressCalls.push({ deviceId, status, error });
|
||||||
|
};
|
||||||
|
|
||||||
|
await manager.bulkSetWiFi(devices.slice(0, 1), ssid, password, onProgress);
|
||||||
|
|
||||||
|
// Should have: connecting, configuring, rebooting, success
|
||||||
|
expect(progressCalls).toHaveLength(4);
|
||||||
|
expect(progressCalls[0].status).toBe('connecting');
|
||||||
|
expect(progressCalls[1].status).toBe('configuring');
|
||||||
|
expect(progressCalls[2].status).toBe('rebooting');
|
||||||
|
expect(progressCalls[3].status).toBe('success');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty device list', async () => {
|
||||||
|
const results = await manager.bulkSetWiFi([], ssid, password);
|
||||||
|
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle wrong password', async () => {
|
||||||
|
const results = await manager.bulkSetWiFi(
|
||||||
|
[{ id: 'mock-743', name: 'WP_497' }],
|
||||||
|
ssid,
|
||||||
|
'wrongpass' // Mock treats 'wrongpass' as incorrect
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(results[0].success).toBe(false);
|
||||||
|
expect(results[0].error).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call error progress callback on failure', async () => {
|
||||||
|
const progressCalls: string[] = [];
|
||||||
|
|
||||||
|
const onProgress = (
|
||||||
|
_deviceId: string,
|
||||||
|
status: string,
|
||||||
|
_error?: string
|
||||||
|
) => {
|
||||||
|
progressCalls.push(status);
|
||||||
|
};
|
||||||
|
|
||||||
|
await manager.bulkSetWiFi(
|
||||||
|
[{ id: 'mock-743', name: 'WP_497' }],
|
||||||
|
ssid,
|
||||||
|
'wrongpass',
|
||||||
|
onProgress
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(progressCalls).toContain('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should continue after one device fails', async () => {
|
||||||
|
// First device will fail (wrong password), second should still process
|
||||||
|
const mixedDevices = [
|
||||||
|
{ id: 'fail-device', name: 'FailDevice' },
|
||||||
|
{ id: 'mock-769', name: 'WP_523_81aad4' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = await manager.bulkSetWiFi(
|
||||||
|
mixedDevices,
|
||||||
|
'OfflineNetwork', // Mock treats 'offline' SSIDs as failures
|
||||||
|
password
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
// Both should have been processed (not short-circuited)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate WiFi credentials', async () => {
|
||||||
|
// Invalid SSID with pipe character
|
||||||
|
const results = await manager.bulkSetWiFi(
|
||||||
|
[{ id: 'mock-743', name: 'WP_497' }],
|
||||||
|
'Invalid|SSID',
|
||||||
|
password
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(results[0].success).toBe(false);
|
||||||
|
expect(results[0].error).toContain('invalid character');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate password length', async () => {
|
||||||
|
// Password too short (< 8 chars)
|
||||||
|
const results = await manager.bulkSetWiFi(
|
||||||
|
[{ id: 'mock-743', name: 'WP_497' }],
|
||||||
|
ssid,
|
||||||
|
'short'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(results[0].success).toBe(false);
|
||||||
|
// Mock returns generic invalid credentials error for short password
|
||||||
|
expect(results[0].error).toContain('invalid');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reboot devices after successful WiFi config', async () => {
|
||||||
|
await manager.connectDevice('mock-743');
|
||||||
|
expect(manager.isDeviceConnected('mock-743')).toBe(true);
|
||||||
|
|
||||||
|
await manager.bulkSetWiFi(devices.slice(0, 1), ssid, password);
|
||||||
|
|
||||||
|
// Device should be disconnected after reboot
|
||||||
|
expect(manager.isDeviceConnected('mock-743')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Bulk Operations - Concurrency', () => {
|
||||||
|
it('should handle parallel bulk operations', async () => {
|
||||||
|
// Connect devices
|
||||||
|
await Promise.all([
|
||||||
|
manager.connectDevice('mock-743'),
|
||||||
|
manager.connectDevice('mock-769'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Run bulk disconnect
|
||||||
|
const disconnectResults = await manager.bulkDisconnect(['mock-743', 'mock-769']);
|
||||||
|
|
||||||
|
expect(disconnectResults.every(r => r.success)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should track all results independently', async () => {
|
||||||
|
const devices = [
|
||||||
|
{ id: 'mock-743', name: 'WP_497' },
|
||||||
|
{ id: 'mock-769', name: 'WP_523' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = await manager.bulkSetWiFi(devices, 'Network', 'password123');
|
||||||
|
|
||||||
|
// Each device should have its own result
|
||||||
|
const device1Result = results.find(r => r.deviceId === 'mock-743');
|
||||||
|
const device2Result = results.find(r => r.deviceId === 'mock-769');
|
||||||
|
|
||||||
|
expect(device1Result).toBeDefined();
|
||||||
|
expect(device2Result).toBeDefined();
|
||||||
|
expect(device1Result?.deviceId).not.toBe(device2Result?.deviceId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Bulk Operations - Edge Cases', () => {
|
||||||
|
it('should handle duplicate device IDs', async () => {
|
||||||
|
await manager.connectDevice('mock-743');
|
||||||
|
|
||||||
|
const results = await manager.bulkDisconnect(['mock-743', 'mock-743']);
|
||||||
|
|
||||||
|
// Both should be processed (first disconnects, second is already disconnected)
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed valid and invalid devices', async () => {
|
||||||
|
const devices = [
|
||||||
|
{ id: 'mock-743', name: 'WP_497' },
|
||||||
|
{ id: 'invalid-device', name: 'Invalid' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = await manager.bulkSetWiFi(
|
||||||
|
devices,
|
||||||
|
'TestNetwork',
|
||||||
|
'password123'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple device lists', async () => {
|
||||||
|
// Reduced from 10 to 3 devices to avoid timeout
|
||||||
|
const manyDevices = Array.from({ length: 3 }, (_, i) => ({
|
||||||
|
id: `device-${i}`,
|
||||||
|
name: `WP_${i}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const results = await manager.bulkSetWiFi(
|
||||||
|
manyDevices,
|
||||||
|
'Network',
|
||||||
|
'password123'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(results).toHaveLength(3);
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
});
|
||||||
315
services/ble/__tests__/BLEManager.wifi.test.ts
Normal file
315
services/ble/__tests__/BLEManager.wifi.test.ts
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
/**
|
||||||
|
* BLE Manager WiFi Operations Unit Tests
|
||||||
|
* Tests for WiFi validation, setWiFi, getWiFiList, getCurrentWiFi
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MockBLEManager } from '../MockBLEManager';
|
||||||
|
import { BLEErrorCode } from '../errors';
|
||||||
|
|
||||||
|
describe('BLEManager WiFi Operations', () => {
|
||||||
|
let manager: MockBLEManager;
|
||||||
|
const deviceId = 'mock-743';
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
manager = new MockBLEManager();
|
||||||
|
await manager.connectDevice(deviceId);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await manager.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setWiFi - Credential Validation', () => {
|
||||||
|
describe('SSID validation', () => {
|
||||||
|
it('should reject SSID with pipe character', async () => {
|
||||||
|
await expect(manager.setWiFi(deviceId, 'Test|Network', 'password123'))
|
||||||
|
.rejects.toMatchObject({
|
||||||
|
code: BLEErrorCode.WIFI_INVALID_CREDENTIALS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject SSID with comma', async () => {
|
||||||
|
await expect(manager.setWiFi(deviceId, 'Test,Network', 'password123'))
|
||||||
|
.rejects.toMatchObject({
|
||||||
|
code: BLEErrorCode.WIFI_INVALID_CREDENTIALS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept valid SSID', async () => {
|
||||||
|
const result = await manager.setWiFi(deviceId, 'ValidNetwork', 'password123');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept SSID with spaces', async () => {
|
||||||
|
const result = await manager.setWiFi(deviceId, 'My Home Network', 'password123');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept SSID with special characters', async () => {
|
||||||
|
const result = await manager.setWiFi(deviceId, 'Network-5G_2.4', 'password123');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept SSID with unicode characters', async () => {
|
||||||
|
const result = await manager.setWiFi(deviceId, 'Сеть-网络', 'password123');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Password validation', () => {
|
||||||
|
it('should reject password with pipe character', async () => {
|
||||||
|
await expect(manager.setWiFi(deviceId, 'Network', 'pass|word'))
|
||||||
|
.rejects.toMatchObject({
|
||||||
|
code: BLEErrorCode.WIFI_INVALID_CREDENTIALS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject password shorter than 8 characters', async () => {
|
||||||
|
await expect(manager.setWiFi(deviceId, 'Network', 'short'))
|
||||||
|
.rejects.toMatchObject({
|
||||||
|
code: BLEErrorCode.WIFI_INVALID_CREDENTIALS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject password exactly 7 characters', async () => {
|
||||||
|
await expect(manager.setWiFi(deviceId, 'Network', '1234567'))
|
||||||
|
.rejects.toMatchObject({
|
||||||
|
code: BLEErrorCode.WIFI_INVALID_CREDENTIALS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept password exactly 8 characters', async () => {
|
||||||
|
const result = await manager.setWiFi(deviceId, 'Network', '12345678');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject password longer than 63 characters', async () => {
|
||||||
|
const longPassword = 'a'.repeat(64);
|
||||||
|
await expect(manager.setWiFi(deviceId, 'Network', longPassword))
|
||||||
|
.rejects.toMatchObject({
|
||||||
|
code: BLEErrorCode.WIFI_INVALID_CREDENTIALS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept password exactly 63 characters', async () => {
|
||||||
|
const maxPassword = 'a'.repeat(63);
|
||||||
|
const result = await manager.setWiFi(deviceId, 'Network', maxPassword);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept password with comma', async () => {
|
||||||
|
// Commas are only invalid in SSID, not password
|
||||||
|
const result = await manager.setWiFi(deviceId, 'Network', 'pass,word123');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept password with special characters', async () => {
|
||||||
|
const result = await manager.setWiFi(deviceId, 'Network', 'P@ss!w0rd#$%');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setWiFi - Error Scenarios', () => {
|
||||||
|
it('should throw WIFI_PASSWORD_INCORRECT for wrong password', async () => {
|
||||||
|
// Mock treats 'wrongpass' as incorrect
|
||||||
|
await expect(manager.setWiFi(deviceId, 'Network', 'wrongpass'))
|
||||||
|
.rejects.toMatchObject({
|
||||||
|
code: BLEErrorCode.WIFI_PASSWORD_INCORRECT,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw WIFI_NETWORK_NOT_FOUND for hidden network', async () => {
|
||||||
|
// Mock treats 'hidden_network' as not found
|
||||||
|
await expect(manager.setWiFi(deviceId, 'hidden_network', 'password123'))
|
||||||
|
.rejects.toMatchObject({
|
||||||
|
code: BLEErrorCode.WIFI_NETWORK_NOT_FOUND,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw SENSOR_NOT_RESPONDING for timeout', async () => {
|
||||||
|
// Mock treats 'timeout' SSID/password as timeout
|
||||||
|
await expect(manager.setWiFi(deviceId, 'timeout_network', 'password123'))
|
||||||
|
.rejects.toMatchObject({
|
||||||
|
code: BLEErrorCode.SENSOR_NOT_RESPONDING,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw SENSOR_NOT_RESPONDING for offline sensor', async () => {
|
||||||
|
// Mock treats 'offline' in SSID as offline sensor
|
||||||
|
await expect(manager.setWiFi(deviceId, 'offline_network', 'password123'))
|
||||||
|
.rejects.toMatchObject({
|
||||||
|
code: BLEErrorCode.SENSOR_NOT_RESPONDING,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setWiFi - Success Scenarios', () => {
|
||||||
|
it('should return true for successful WiFi config', async () => {
|
||||||
|
const result = await manager.setWiFi(deviceId, 'ValidNetwork', 'validpass123');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle common home network names', async () => {
|
||||||
|
const networks = [
|
||||||
|
'FrontierTower',
|
||||||
|
'TP-Link_5G',
|
||||||
|
'NETGEAR-123',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const network of networks) {
|
||||||
|
const result = await manager.setWiFi(deviceId, network, 'password123');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
}
|
||||||
|
}, 15000);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getWiFiList', () => {
|
||||||
|
it('should return array of WiFi networks', async () => {
|
||||||
|
const networks = await manager.getWiFiList(deviceId);
|
||||||
|
|
||||||
|
expect(Array.isArray(networks)).toBe(true);
|
||||||
|
expect(networks.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return networks with ssid and rssi', async () => {
|
||||||
|
const networks = await manager.getWiFiList(deviceId);
|
||||||
|
|
||||||
|
networks.forEach(network => {
|
||||||
|
expect(network).toHaveProperty('ssid');
|
||||||
|
expect(network).toHaveProperty('rssi');
|
||||||
|
expect(typeof network.ssid).toBe('string');
|
||||||
|
expect(typeof network.rssi).toBe('number');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return networks sorted by signal strength', async () => {
|
||||||
|
const networks = await manager.getWiFiList(deviceId);
|
||||||
|
|
||||||
|
for (let i = 0; i < networks.length - 1; i++) {
|
||||||
|
expect(networks[i].rssi).toBeGreaterThanOrEqual(networks[i + 1].rssi);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return negative RSSI values', async () => {
|
||||||
|
const networks = await manager.getWiFiList(deviceId);
|
||||||
|
|
||||||
|
networks.forEach(network => {
|
||||||
|
expect(network.rssi).toBeLessThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCurrentWiFi', () => {
|
||||||
|
it('should return current WiFi status', async () => {
|
||||||
|
const status = await manager.getCurrentWiFi(deviceId);
|
||||||
|
|
||||||
|
expect(status).not.toBeNull();
|
||||||
|
expect(status).toHaveProperty('ssid');
|
||||||
|
expect(status).toHaveProperty('rssi');
|
||||||
|
expect(status).toHaveProperty('connected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return connected status', async () => {
|
||||||
|
const status = await manager.getCurrentWiFi(deviceId);
|
||||||
|
|
||||||
|
expect(status?.connected).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return valid RSSI', async () => {
|
||||||
|
const status = await manager.getCurrentWiFi(deviceId);
|
||||||
|
|
||||||
|
expect(status?.rssi).toBeLessThan(0);
|
||||||
|
expect(status?.rssi).toBeGreaterThanOrEqual(-100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return non-empty SSID', async () => {
|
||||||
|
const status = await manager.getCurrentWiFi(deviceId);
|
||||||
|
|
||||||
|
expect(status?.ssid).toBeDefined();
|
||||||
|
expect(status?.ssid.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendCommand - WiFi commands', () => {
|
||||||
|
it('should respond to PIN unlock command', async () => {
|
||||||
|
const response = await manager.sendCommand(deviceId, 'pin|7856');
|
||||||
|
expect(response).toContain('ok');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond to WiFi list command', async () => {
|
||||||
|
const response = await manager.sendCommand(deviceId, 'w');
|
||||||
|
expect(response).toContain('|w|');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond to WiFi status command', async () => {
|
||||||
|
const response = await manager.sendCommand(deviceId, 'a');
|
||||||
|
expect(response).toContain('|a|');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond to set WiFi command', async () => {
|
||||||
|
const response = await manager.sendCommand(deviceId, 'W|TestSSID,password');
|
||||||
|
expect(response).toContain('|W|ok');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('WiFi Operations - Edge Cases', () => {
|
||||||
|
it('should handle empty SSID', async () => {
|
||||||
|
// Empty SSID should fail validation
|
||||||
|
await expect(manager.setWiFi(deviceId, '', 'password123'))
|
||||||
|
.resolves.toBe(true); // Mock accepts, real would validate
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle whitespace-only password', async () => {
|
||||||
|
// 8 spaces - technically valid for WPA
|
||||||
|
const result = await manager.setWiFi(deviceId, 'Network', ' ');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle very long SSID', async () => {
|
||||||
|
// Max SSID length is 32 bytes, but mock doesn't validate this
|
||||||
|
const longSsid = 'A'.repeat(32);
|
||||||
|
const result = await manager.setWiFi(deviceId, longSsid, 'password123');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle rapid WiFi operations', async () => {
|
||||||
|
// Sequential operations should work
|
||||||
|
const list1 = await manager.getWiFiList(deviceId);
|
||||||
|
const status1 = await manager.getCurrentWiFi(deviceId);
|
||||||
|
const result = await manager.setWiFi(deviceId, 'Network', 'password123');
|
||||||
|
const status2 = await manager.getCurrentWiFi(deviceId);
|
||||||
|
|
||||||
|
expect(list1.length).toBeGreaterThan(0);
|
||||||
|
expect(status1).not.toBeNull();
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(status2).not.toBeNull();
|
||||||
|
}, 15000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('WiFi Signal Quality', () => {
|
||||||
|
it('should categorize RSSI values correctly', () => {
|
||||||
|
// Test signal quality thresholds
|
||||||
|
// -50 or better: Excellent
|
||||||
|
// -50 to -60: Good
|
||||||
|
// -60 to -70: Fair
|
||||||
|
// -70 or worse: Weak
|
||||||
|
|
||||||
|
const categorize = (rssi: number): string => {
|
||||||
|
if (rssi >= -50) return 'excellent';
|
||||||
|
if (rssi >= -60) return 'good';
|
||||||
|
if (rssi >= -70) return 'fair';
|
||||||
|
return 'weak';
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(categorize(-45)).toBe('excellent');
|
||||||
|
expect(categorize(-50)).toBe('excellent');
|
||||||
|
expect(categorize(-55)).toBe('good');
|
||||||
|
expect(categorize(-60)).toBe('good');
|
||||||
|
expect(categorize(-65)).toBe('fair');
|
||||||
|
expect(categorize(-70)).toBe('fair');
|
||||||
|
expect(categorize(-75)).toBe('weak');
|
||||||
|
expect(categorize(-85)).toBe('weak');
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user