import Database from 'better-sqlite3'; import { describe, expect, it } from 'vitest'; import { migrateDatabase } from '../../infrastructure/database/migrations.js'; import { SafeUpstreamGateway } from '../../infrastructure/transport/safe-upstream-gateway.js'; import { InstanceSessionStore, type UpstreamRequest, } from '../connections/upstream-session-client.js'; import type { InstanceService } from '../instances/instance-service.js'; import { DEVICE_ACTIONS, type DeviceActionDefinition, type DeviceActionField, } from './device-action-catalog.js'; import { DeviceActionService } from './device-action-service.js'; const ORIGIN = 'http://192.168.1.20:8080'; /** A value that satisfies the catalog's own validator for every declared field kind. */ function sampleValue(field: DeviceActionField): unknown { if (field.id === 'phone_number' || field.id === 'sms_center') return '+15550001111'; if (field.id === 'mccmnc') return '00101'; if (field.id === 'iccid') return '89882020202220963176'; switch (field.kind) { case 'boolean': return true; case 'number': return field.min ?? 1; case 'string-list': return ['1']; case 'choice': return field.choices?.[0]?.value ?? 'auto'; default: return 'sample'; } } function paramsFor(action: DeviceActionDefinition): Record { const params: Record = {}; for (const field of action.fields ?? []) params[field.id] = sampleValue(field); return params; } function harness() { const db = new Database(':memory:'); db.pragma('foreign_keys = ON'); migrateDatabase(db); db.prepare( `INSERT INTO instances (id,name,base_url,config_revision,created_at,updated_at) VALUES ('node-a','Node A',?,1,?,?)`, ).run(ORIGIN, '2026-09-04T00:00:00.000Z', '2026-09-04T00:00:00.000Z'); const sessions = new InstanceSessionStore(); sessions.set('node-a', ORIGIN, 'simadmin_session=opaque'); const dispatched: { url: string; method: string; body: string }[] = []; const gateway = new SafeUpstreamGateway({ transport: { get: async (url, headers) => { void headers; dispatched.push({ url, method: 'GET', body: '' }); return { status: 200, headers: {}, body: '{}' }; }, post: async (url, headers, body) => { void headers; dispatched.push({ url, method: 'POST', body }); return { status: 200, headers: {}, body: '{"status":"success"}' }; }, delete: async (url, headers) => { void headers; dispatched.push({ url, method: 'DELETE', body: '' }); return { status: 200, headers: {}, body: '{"status":"success"}' }; }, }, }); const instances = { get: async (id: string) => id === 'node-a' ? { id: 'node-a', name: 'Node A', origin: ORIGIN } : undefined, } as unknown as InstanceService; const service = new DeviceActionService({ instances, sessions, db, request: (request: UpstreamRequest) => gateway.request(request), now: () => new Date('2026-09-04T00:00:00.000Z'), id: () => 'audit-row-1', }); return { service, db, dispatched }; } const context = { actor: 'loopback-control-plane', requestId: 'req-1', confirm: true }; describe('device action catalog transport parity', () => { it('covers every module the console can render', () => { expect(DEVICE_ACTIONS.length).toBeGreaterThan(40); }); for (const action of DEVICE_ACTIONS) { it(`dispatches ${action.id} through the pinned allowlist`, async () => { const { service, dispatched } = harness(); const result = await service.execute('node-a', action.id, paramsFor(action), context); expect(result.ok).toBe(true); expect(dispatched).toHaveLength(1); expect(dispatched[0]?.url.startsWith(`${ORIGIN}/api/`)).toBe(true); }); } });