WellNuo/services/ble/__tests__/BLEErrors.test.ts
Sergei 5f40370dfa 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>
2026-02-01 10:38:56 -08:00

534 lines
18 KiB
TypeScript

/**
* 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')
);
});
});
});