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.');