- 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>
316 lines
11 KiB
TypeScript
316 lines
11 KiB
TypeScript
/**
|
|
* 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');
|
|
});
|
|
});
|