Files
qipai/backend/tests/device-control.test.mjs
T
2026-08-10 19:10:25 +08:00

403 lines
15 KiB
JavaScript

import assert from 'node:assert/strict';
import { buildApp } from '../dist/app.js';
import { signAccessToken } from '../dist/auth/jwt.js';
import {
DeviceControlError,
DeviceControlService
} from '../dist/devices/device-control-service.js';
const managerAccess = {
roles: ['STORE_ADMIN'],
capabilities: ['device.read', 'device.write'],
storeIds: ['11']
};
const issued = [];
const audits = [];
const service = new DeviceControlService({
async execute(sql) {
if (sql.includes('FROM qipai_devices')) {
if (sql.includes("device_type = 'SMART_SOCKET'")) {
return [[{
id: 52, deviceId: 'SOCKET_001', storeId: 11, roomId: 31,
deviceType: 'SMART_SOCKET', status: 'ONLINE', model: 'JL-SOCKET-16A',
capabilities: ['POWER', 'METERING', 'RATED_16A']
}], []];
}
return [[{
id: 51, deviceId: 'BOX_001', storeId: 11, roomId: 31,
deviceType: 'CONTROL_BOX', status: 'ONLINE'
}], []];
}
if (sql.includes('DEVICE_COMMAND_REQUESTED')) audits.push(sql);
return [[], []];
}
}, {
async issue(input) {
const payload = input.payloadFactory('1782120000001');
issued.push({ ...input, payload });
return { commandId: '1782120000001', status: 'PUBLISHED' };
}
});
const context = {
tenantId: '7', storeId: '11', roomId: '31', traceId: 'trace',
actorType: 'ADMIN', actorId: '22', ip: '127.0.0.1', userAgent: 'test',
access: managerAccess
};
await service.controlPower(context, { slot1: 'on', slot3: 'off' });
assert.equal(issued.at(-1).payload.action, 'ConctolPower');
await service.controlDoor(context, { order: 'open', holdopen: 1, delayTime: 4 });
assert.equal(issued.at(-1).payload.action, 'Crldoor');
await service.playTts(context, { content: '欢迎光临' });
assert.deepEqual(issued.at(-1).payload, {
action: 'PlayTTS', content: '欢迎光临', vol: 8, firstPlay: 0,
loop: 1, speaker: 0, style: 0, speed: 5, intona: 5,
id: '1782120000001'
});
await service.stopTts(context);
assert.equal(issued.at(-1).payload.action, 'stopTTS');
await service.controlLed(context, 30);
assert.equal(issued.at(-1).payload.action, 'CrlLED');
await service.startTask(context, { minute: 120, type: 2, subID: 'SUB001' });
assert.equal(issued.at(-1).payload.action, 'task');
await service.extendTask(context, 30);
assert.equal(issued.at(-1).payload.action, 'addtask');
await service.cancelTask(context);
assert.equal(issued.at(-1).payload.action, 'canceltask');
await service.pairSubLock(context, 60);
assert.deepEqual(issued.at(-1).payload, {
action: 'AddDevice', time: 60, id: '1782120000001'
});
await service.controlSubLock(context, {
subtype: '14', subID: 'SUB001', order: 'open', delayTime: 4
});
assert.deepEqual(issued.at(-1).payload, {
action: 'CtrlDevice', subtype: '14', subID: 'SUB001',
order: 'open', delayTime: 4, id: '1782120000001'
});
await service.controlSubLock(context, {
subtype: '14', subID: 'SUB001', order: 'setkey', value: '123456654321'
});
assert.equal(issued.at(-1).payload.value, '123456654321');
assert.equal('content' in issued.at(-1).payload, false);
await service.readSmartSocket(context, 'workInfo');
assert.deepEqual(issued.at(-1).payload, { read: 'workInfo', slotNum: 1 });
await service.readSmartSocket(context, {
target: 'localtask', slotNum: 2, taskNum: 0
});
assert.deepEqual(issued.at(-1).payload, {
read: 'localtask', slotNum: 2, taskNum: 0
});
await service.readSmartSocket(context, { target: 'mqttConfig' });
assert.deepEqual(issued.at(-1).payload, { read: 'mqttConfig' });
await service.switchSmartSocket(context, { on: false, slotNum: 2 });
assert.deepEqual(issued.at(-1).payload, {
action: 'off', id: '1782120000001', slotNum: 2
});
await service.scheduleSmartSocket(context, {
taskNum: 1, action: 'on', mode: 'weekly', time: '18:30', weekdays: [5, 6]
});
assert.equal(issued.at(-1).payload.action, 'localtask');
assert.deepEqual(issued.at(-1).payload, {
action: 'localtask', slotNum: 1, taskNum: 1, tasktype: 'on',
cyctype: 'weekcyc', weekdata: '0000110', actiontime: '1830',
id: '1782120000001'
});
await service.clearSmartSocketTask(context, 1);
assert.deepEqual(issued.at(-1).payload, {
action: 'clearTask', slotNum: 1, taskNum: 1, id: '1782120000001'
});
await service.configureSmartSocketProtection(context, {
maxPower: 3500, maxCurrent: 15, maxTemperature: 80,
pullOutStop: 1, pullOutPower: 5, pullOutSec: 10,
chargeFullStop: 1, chargeFullPower: 8, chargeFullSec: 30
});
assert.equal(issued.at(-1).payload.setting, 'protectOff');
assert.equal('ratedAmps' in issued.at(-1).payload, false);
await assert.rejects(
() => service.configureSmartSocketProtection(context, {
maxPower: 3500, maxCurrent: 17, maxTemperature: 80,
pullOutStop: 1, pullOutPower: 5, pullOutSec: 10,
chargeFullStop: 1, chargeFullPower: 8, chargeFullSec: 30
}),
/16A capability/
);
await service.configureSmartSocketParameter(context, {
resetHold: 1, keyLock: 0, keyOff: 1
});
assert.deepEqual(issued.at(-1).payload, {
setting: 'parameter', resetHold: 1, keyLock: 0, KeyOff: 1,
id: '1782120000001'
});
assert.equal(audits.length, 20);
assert.match(audits[0], /DEVICE_COMMAND_REQUESTED/);
await assert.rejects(
() => service.controlSubLock(context, {
subtype: '14', subID: 'SUB001', order: 'factoryreset',
dangerConfirmation: 'CONFIRM_FACTORY_RESET'
}),
(error) => error instanceof DeviceControlError
&& error.code === 'DEVICE_DANGEROUS_ACTION_FORBIDDEN'
);
const platformContext = {
...context,
access: { roles: ['PLATFORM_ADMIN'], capabilities: [], storeIds: [] }
};
await service.controlSubLock(platformContext, {
subtype: '14', subID: 'SUB001', order: 'factoryreset',
dangerConfirmation: 'CONFIRM_FACTORY_RESET'
});
assert.equal(issued.at(-1).payload.order, 'factoryreset');
assert.equal('dangerConfirmation' in issued.at(-1).payload, false);
await assert.rejects(
() => service.controlSubLock(context, {
subtype: '14', subID: 'SUB001', order: 'delkey', value: 'all'
}),
(error) => error instanceof DeviceControlError
&& error.code === 'DEVICE_DANGEROUS_ACTION_FORBIDDEN'
);
await service.controlSubLock(platformContext, {
subtype: '15', subID: 'SUB002', order: 'delcard', value: 'all',
dangerConfirmation: 'CONFIRM_CLEAR_CREDENTIALS'
});
assert.equal(issued.at(-1).payload.value, 'all');
assert.equal(issued.at(-1).payload.subtype, '15');
await assert.rejects(
() => service.emptySmartSocketEnergy(context, 'CONFIRM_EMPTY_SOCKET_ENERGY'),
(error) => error instanceof DeviceControlError
&& error.code === 'DEVICE_DANGEROUS_ACTION_FORBIDDEN'
);
await service.emptySmartSocketEnergy(platformContext, 'CONFIRM_EMPTY_SOCKET_ENERGY');
assert.deepEqual(issued.at(-1).payload, {
action: 'emptyPower', id: '1782120000001'
});
await assert.rejects(
() => service.resetSmartSocket(platformContext, {
password: 'return-secret', dangerConfirmation: 'WRONG'
}),
(error) => error instanceof DeviceControlError
&& error.code === 'DEVICE_DANGEROUS_ACTION_FORBIDDEN'
);
await service.resetSmartSocket(platformContext, {
password: 'return-secret', dangerConfirmation: 'CONFIRM_SOCKET_FACTORY_RESET'
});
assert.equal(issued.at(-1).payload.action, 'returnFactory');
assert.equal(issued.at(-1).payload.password, 'return-secret');
assert.equal('dangerConfirmation' in issued.at(-1).payload, false);
await service.changeSmartSocketReturnKey(platformContext, {
old: 'old-secret', new: 'new-secret',
dangerConfirmation: 'CONFIRM_CHANGE_SOCKET_RETURN_KEY'
});
assert.deepEqual(issued.at(-1).payload, {
action: 'setReturnKey', old: 'old-secret', new: 'new-secret',
id: '1782120000001'
});
await assert.rejects(
() => service.clearSmartSocketTask(context, {
taskNum: 0, dangerConfirmation: 'CONFIRM_CLEAR_ALL_SOCKET_TASKS'
}),
(error) => error instanceof DeviceControlError
&& error.code === 'DEVICE_DANGEROUS_ACTION_FORBIDDEN'
);
await service.clearSmartSocketTask(platformContext, {
slotNum: 2, taskNum: 0, dangerConfirmation: 'CONFIRM_CLEAR_ALL_SOCKET_TASKS'
});
assert.deepEqual(issued.at(-1).payload, {
action: 'clearTask', slotNum: 2, taskNum: 0, id: '1782120000001'
});
await assert.rejects(
() => service.controlPower({
...context,
access: { roles: ['STAFF'], capabilities: ['device.write'], storeIds: ['12'] }
}, { slot1: 'on' }),
(error) => error.code === 'DEVICE_SCOPE_FORBIDDEN'
);
const secret = 'test-only-jwt-secret-with-at-least-32-characters';
const token = signAccessToken({
sub: '22', sid: '5c4d3af8-c63c-4edb-bf95-b84127bb3f6e',
tid: '7', aid: '9', rv: 1
}, secret, 900);
let routed;
let routedContext;
const app = await buildApp({
deviceControl: {
jwtSecret: secret,
authRepository: {
async validateSession() {
return {
id: '5c4d3af8-c63c-4edb-bf95-b84127bb3f6e',
tenantId: '7', platformAppId: '9', expiresAt: new Date(Date.now() + 60000),
user: {
id: '22', tenantId: '7', userType: 'STAFF', status: 'ACTIVE',
roleVersion: 1, nickname: '', avatarUrl: '', phone: ''
}
};
}
},
accessControl: { async getAccessProfile() { return managerAccess; } },
customerAccess: {
async getDoorContext(input) {
assert.deepEqual(input, { tenantId: '7', userId: '22', orderId: '31' });
return { orderId: '31', storeId: '11', roomId: '31', status: 'IN_PROGRESS' };
}
},
service: {
async controlPower(_context, input) { routed = input; return { status: 'PUBLISHED' }; },
async controlDoor(context, input) {
routedContext = context;
routed = input;
return { commandId: 'cmd-door', status: 'PUBLISHED' };
},
async playTts() { return {}; },
async stopTts() { return {}; },
async controlLed() { return {}; },
async startTask() { return {}; },
async extendTask() { return {}; },
async cancelTask() { return {}; },
async pairSubLock() { return {}; },
async controlSubLock() { return {}; },
async readSmartSocket(_context, input) { routed = input; return {}; },
async switchSmartSocket(_context, input) { routed = input; return { status: 'PUBLISHED' }; },
async scheduleSmartSocket() { return {}; },
async clearSmartSocketTask() { return {}; },
async rebootSmartSocket() { return {}; },
async emptySmartSocketEnergy() { return {}; },
async resetSmartSocket(_context, input) { routed = input; return {}; },
async changeSmartSocketReturnKey() { return {}; },
async configureSmartSocketProtection(_context, input) { routed = input; return {}; },
async configureSmartSocketParameter() { return {}; }
}
}
});
const response = await app.inject({
method: 'POST',
url: '/admin-api/device-control/power',
headers: { authorization: `Bearer ${token}` },
payload: { storeId: '11', roomId: '31', slot1: 'on' }
});
assert.equal(response.statusCode, 200);
assert.equal(routed.slot1, 'on');
const appPowerResponse = await app.inject({
method: 'POST',
url: '/app-api/management/device-control/power',
headers: { authorization: `Bearer ${token}` },
payload: { storeId: '11', roomId: '31', slotall: 'off' }
});
assert.equal(appPowerResponse.statusCode, 200);
assert.equal(routed.slotall, 'off');
const appDoorResponse = await app.inject({
method: 'POST',
url: '/app-api/management/device-control/door',
headers: { authorization: `Bearer ${token}` },
payload: { storeId: '11', roomId: '31', order: 'open', holdopen: 0, delayTime: 4 }
});
assert.equal(appDoorResponse.statusCode, 200);
assert.equal(routed.order, 'open');
assert.equal(routedContext.storeId, '11');
assert.equal(routedContext.actorType, 'ADMIN');
assert.equal(routedContext.actorId, '22');
const socketResponse = await app.inject({
method: 'POST',
url: '/admin-api/device-control/socket/switch',
headers: { authorization: `Bearer ${token}` },
payload: { storeId: '11', roomId: '31', on: true, slotNum: 1 }
});
assert.equal(socketResponse.statusCode, 200);
assert.equal(routed.on, true);
const socketTaskRead = await app.inject({
method: 'POST',
url: '/admin-api/device-control/socket/read',
headers: { authorization: `Bearer ${token}` },
payload: { storeId: '11', roomId: '31', target: 'localtask', slotNum: 1, taskNum: 0 }
});
assert.equal(socketTaskRead.statusCode, 200);
assert.equal(routed.target, 'localtask');
assert.equal(routed.taskNum, 0);
const invalidSocketTaskRead = await app.inject({
method: 'POST',
url: '/admin-api/device-control/socket/read',
headers: { authorization: `Bearer ${token}` },
payload: { storeId: '11', roomId: '31', target: 'localtask' }
});
assert.equal(invalidSocketTaskRead.statusCode, 400);
const invalidSocketTask = await app.inject({
method: 'POST',
url: '/admin-api/device-control/socket/task',
headers: { authorization: `Bearer ${token}` },
payload: {
storeId: '11', roomId: '31', taskNum: 1, action: 'on',
mode: 'weekly', time: '24:00', weekdays: [1]
}
});
assert.equal(invalidSocketTask.statusCode, 400);
const socketProtection = await app.inject({
method: 'POST',
url: '/admin-api/device-control/socket/protection',
headers: { authorization: `Bearer ${token}` },
payload: {
storeId: '11', roomId: '31', maxPower: 3500, maxCurrent: 15,
maxTemperature: 80, pullOutStop: 1, pullOutPower: 5, pullOutSec: 10,
chargeFullStop: 1, chargeFullPower: 8, chargeFullSec: 30
}
});
assert.equal(socketProtection.statusCode, 200);
assert.equal(routed.maxCurrent, 15);
const subLockResponse = await app.inject({
method: 'POST',
url: '/admin-api/device-control/sub-lock/action',
headers: { authorization: `Bearer ${token}` },
payload: {
storeId: '11', roomId: '31', subtype: '14', subID: 'SUB001',
order: 'open', delayTime: 4
}
});
assert.equal(subLockResponse.statusCode, 200);
const invalidSubLock = await app.inject({
method: 'POST',
url: '/admin-api/device-control/sub-lock/action',
headers: { authorization: `Bearer ${token}` },
payload: { storeId: '11', roomId: '31', subID: 'SUB001', order: 'open' }
});
assert.equal(invalidSubLock.statusCode, 400);
const invalidTts = await app.inject({
method: 'POST',
url: '/admin-api/device-control/tts',
headers: { authorization: `Bearer ${token}` },
payload: { storeId: '11', roomId: '31', content: 'test', volume: 11 }
});
assert.equal(invalidTts.statusCode, 400);
const invalid = await app.inject({
method: 'POST',
url: '/admin-api/device-control/door',
headers: { authorization: `Bearer ${token}` },
payload: { storeId: '11', roomId: '31', order: 'open', delayTime: 15 }
});
assert.equal(invalid.statusCode, 400);
const appOpenDoor = await app.inject({
method: 'POST',
url: '/app-api/orders/31/open-door',
headers: { authorization: `Bearer ${token}`, 'x-trace-id': 'm08a-open-door' },
payload: { delayTime: 4, holdopen: 1 }
});
assert.equal(appOpenDoor.statusCode, 400);
const appOpenDoorOk = await app.inject({
method: 'POST',
url: '/app-api/orders/31/open-door',
headers: { authorization: `Bearer ${token}`, 'x-trace-id': 'm08a-open-door' },
payload: { delayTime: 4 }
});
assert.equal(appOpenDoorOk.statusCode, 200);
assert.equal(routed.order, 'open');
assert.equal(routed.holdopen, 0);
assert.equal(routedContext.orderId, '31');
assert.equal(routedContext.traceId, 'm08a-open-door');
assert.deepEqual(routedContext.access.capabilities, ['device.write']);
await app.close();
console.log('PASS: M06-E-R1 socket controls, capability limits and dangerous gates work.');