import assert from 'node:assert/strict'; import { buildApp } from '../dist/app.js'; import { signAccessToken } from '../dist/auth/jwt.js'; import { DeviceError, DeviceRepository } from '../dist/devices/device-repository.js'; const storeActor = { tenantId: '7', userId: '22', access: { roles: ['STORE_ADMIN'], capabilities: ['device.read', 'device.write'], storeIds: ['11'] }, traceId: 'trace', ip: '127.0.0.1', userAgent: 'test' }; const repository = new DeviceRepository({ async execute(sql) { if (sql.includes('FROM qipai_devices d')) { return [[{ id: 51, storeId: 11, roomId: 31, deviceId: 'BOX_001', imei: '123', iccid: null, deviceType: 'CONTROL_BOX', model: 'BOX', firmwareVersion: '1.0', status: 'ONLINE', signalStrength: 20, capabilities: '["LOCK","POWER"]', stateSnapshot: '{"power":true}', lastSeenAt: null, lastHeartbeatAt: null, maintenanceStatus: 'NORMAL' }], []]; } return [[], []]; } }); const assets = await repository.listAssets(storeActor, '11'); assert.equal(assets[0].id, '51'); assert.deepEqual(assets[0].capabilities, ['LOCK', 'POWER']); assert.deepEqual(assets[0].stateSnapshot, { power: true }); await assert.rejects( () => repository.listAssets(storeActor, '12'), (error) => error instanceof DeviceError && 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 assetInput; let channelInput; const app = await buildApp({ devices: { 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 storeActor.access; } }, repository: { async listAssets() { return []; }, async getTopology() { return { assets: [], channels: [], links: [], openAlerts: [], maintenance: [] }; }, async createAsset(_actor, input) { assetInput = input; return { assetId: '51' }; }, async bindChannel(_actor, input) { channelInput = input; return { assetId: '51' }; }, async bindSubDevice() { return { childAssetId: '52', parentAssetId: '51' }; }, async recordStatus() { return { assetId: '51' }; }, async addMaintenance() { return { maintenanceId: '61' }; } } } }); const created = await app.inject({ method: 'POST', url: '/admin-api/devices', headers: { authorization: `Bearer ${token}` }, payload: { storeId: '11', roomId: '31', deviceId: 'BOX_001', imei: '123456789012345', deviceType: 'CONTROL_BOX', model: 'JL-BOX', firmwareVersion: '1.0.0', signalStrength: 18, capabilities: ['POWER', 'LOCK', 'POWER'] } }); assert.equal(created.statusCode, 201); assert.equal(created.json().data.assetId, '51'); assert.equal(assetInput.deviceType, 'CONTROL_BOX'); const channel = await app.inject({ method: 'POST', url: '/admin-api/device-channels', headers: { authorization: `Bearer ${token}` }, payload: { assetId: '51', storeId: '11', roomId: '31', channelCode: 'SLOT1', purpose: 'ROOM_POWER' } }); assert.equal(channel.statusCode, 201); assert.equal(channelInput.purpose, 'ROOM_POWER'); const invalid = await app.inject({ method: 'POST', url: '/admin-api/device-channels', headers: { authorization: `Bearer ${token}` }, payload: { assetId: '51', storeId: '11', roomId: '31', channelCode: 'SLOT9', purpose: 'ROOM_POWER' } }); assert.equal(invalid.statusCode, 400); await app.close(); console.log('PASS: M06-B device assets, scope, topology routes and target validation work.');