ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
126 lines
6.1 KiB
JavaScript
126 lines
6.1 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import ts from 'typescript';
|
|
|
|
const source = readFileSync(new URL('../src/ble/client.ts', import.meta.url), 'utf8');
|
|
const compiled = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } }).outputText;
|
|
const { BLEClient, BLE_SERVICE, BLEDeviceSelectionCancelledError } = await import('data:text/javascript;base64,' + Buffer.from(compiled).toString('base64'));
|
|
globalThis.window = { isSecureContext: true };
|
|
|
|
for (const name of ['NotFoundError', 'AbortError']) test(`localizes ${name} from the device chooser and allows retry`, async () => {
|
|
Object.defineProperty(globalThis.navigator, 'bluetooth', { configurable: true, value: {
|
|
async requestDevice() { throw new DOMException('User cancelled the requestDevice() chooser.', name); },
|
|
} });
|
|
const client = new BLEClient();
|
|
await assert.rejects(client.connect(), error => error instanceof BLEDeviceSelectionCancelledError && error.message === '已取消选择设备,可重新搜索。');
|
|
const { gatt } = fake();
|
|
assert.equal((await client.connect()).protocol_version, 2);
|
|
client.disconnect();
|
|
assert.equal(gatt.connected, false);
|
|
});
|
|
|
|
test('does not mistake a missing GATT service for cancelling the chooser', async () => {
|
|
const { gatt } = fake();
|
|
const missing = new DOMException('Missing GATT service', 'NotFoundError');
|
|
gatt.getPrimaryService = async () => { throw missing; };
|
|
await assert.rejects(new BLEClient().connect(), error => error === missing && !(error instanceof BLEDeviceSelectionCancelledError));
|
|
assert.equal(gatt.connected, false);
|
|
});
|
|
|
|
function fake(mode = 'normal') {
|
|
const tx = new EventTarget();
|
|
tx.startNotifications = async () => tx;
|
|
const writes = [];
|
|
const commands = [];
|
|
let input = Buffer.alloc(0);
|
|
const gatt = {
|
|
connected: false,
|
|
async connect() { this.connected = true; return this; },
|
|
disconnect() { this.connected = false; },
|
|
async getPrimaryService(id) {
|
|
assert.equal(id, BLE_SERVICE);
|
|
return { getCharacteristic: async id => id.includes('2001-') ? rx : tx };
|
|
},
|
|
};
|
|
const rx = { async writeValueWithResponse(bytes) {
|
|
assert(bytes.length <= 20);
|
|
writes.push(bytes);
|
|
input = Buffer.concat([input, bytes]);
|
|
if (input.length < 2 || input.length < 2 + input.readUInt16LE(0)) return;
|
|
assert.equal(input.length, 2 + input.readUInt16LE(0));
|
|
const command = JSON.parse(input.subarray(2).toString('utf8'));
|
|
commands.push(command);
|
|
let response = command.cmd === 'get_info'
|
|
? { device_id: 'KP01-B43A451634A8', model: 'ECHOEAR-S3', protocol_version: mode === 'bad-protocol' ? 1 : 2 }
|
|
: { status: 'received' };
|
|
const body = Buffer.from(JSON.stringify(response));
|
|
const frame = Buffer.concat([Buffer.from([body.length & 255, body.length >> 8]), body]);
|
|
input = Buffer.alloc(0);
|
|
if (mode === 'oversize') {
|
|
tx.value = new DataView(new Uint8Array(21).buffer);
|
|
tx.dispatchEvent(new Event('characteristicvaluechanged'));
|
|
return;
|
|
}
|
|
for (let i = 0; i < frame.length; i += 7) {
|
|
const chunk = Uint8Array.from(frame.subarray(i, i + 7));
|
|
tx.value = new DataView(chunk.buffer);
|
|
tx.dispatchEvent(new Event('characteristicvaluechanged'));
|
|
}
|
|
} };
|
|
const device = Object.assign(new EventTarget(), { gatt });
|
|
Object.defineProperty(globalThis.navigator, 'bluetooth', { configurable: true, value: {
|
|
async requestDevice({ filters }) {
|
|
assert.deepEqual(filters, [{ services: [BLE_SERVICE] }]);
|
|
return device;
|
|
},
|
|
} });
|
|
return { writes, commands, gatt };
|
|
}
|
|
|
|
test('discovers by service and uses the same plaintext v2 frames as the mini program', async () => {
|
|
const { writes, commands, gatt } = fake();
|
|
const client = new BLEClient();
|
|
const info = await client.connect();
|
|
assert.deepEqual(info, { device_id: 'KP01-B43A451634A8', model: 'ECHOEAR-S3', protocol_version: 2 });
|
|
assert.deepEqual(commands[0], { cmd: 'get_info' });
|
|
assert.equal(writes[0][0] | writes[0][1] << 8, Buffer.byteLength(JSON.stringify({ cmd: 'get_info' })));
|
|
assert.deepEqual(await client.configure({ ssid: 'HomeWiFi', password: 'test-only-password', token: 'x'.repeat(43) }), { status: 'received' });
|
|
assert.deepEqual(commands[1], { cmd: 'configure', wifi_ssid: 'HomeWiFi', wifi_password: 'test-only-password', bind_token: 'x'.repeat(43) });
|
|
client.disconnect();
|
|
assert.equal(gatt.connected, false);
|
|
});
|
|
|
|
for (const mode of ['oversize', 'bad-protocol']) test(`rejects ${mode} response and disconnects`, async () => {
|
|
const { gatt } = fake(mode);
|
|
const client = new BLEClient();
|
|
await assert.rejects(client.connect());
|
|
assert.equal(gatt.connected, false);
|
|
});
|
|
|
|
test('rejects invalid Wi-Fi data before sending it to the device', async () => {
|
|
const { commands } = fake();
|
|
const client = new BLEClient();
|
|
await client.connect();
|
|
await assert.rejects(client.configure({ ssid: '', password: 'short', token: 'bad' }), /格式不正确/);
|
|
assert.equal(commands.length, 1);
|
|
});
|
|
|
|
test('Wi-Fi-only reconfiguration sends an empty token, never a new ownership claim', async () => {
|
|
const { commands } = fake();
|
|
const client = new BLEClient();
|
|
await client.connect();
|
|
await client.configure({ ssid: 'HomeWiFi', password: 'test-only-password', token: '' });
|
|
assert.deepEqual(commands[1], { cmd: 'configure', wifi_ssid: 'HomeWiFi', wifi_password: 'test-only-password', bind_token: '' });
|
|
await assert.rejects(client.configure({ ssid: 'HomeWiFi', password: 'test-only-password', token: 'malformed' }));
|
|
assert.equal(commands.length, 2);
|
|
client.disconnect();
|
|
});
|
|
|
|
test('Factory reset uses a distinct BLE command with no binding token and validates Wi-Fi', async () => {
|
|
const {commands}=fake();const client=new BLEClient();await client.connect();
|
|
await client.factoryReset({ssid:'Home',password:'TestOnly123'});
|
|
assert.deepEqual(commands[1],{cmd:'factory_reset',wifi_ssid:'Home',wifi_password:'TestOnly123',bind_token:''});
|
|
await assert.rejects(client.factoryReset({ssid:'',password:'short'}));assert.equal(commands.length,2);client.disconnect();
|
|
});
|