feat(api): absorb the Hub control plane into the local instance model
Add central notification channels, rules, queue and delivery logs, fleet organization groups and tags, device discovery, the device action catalog, instance module reads, the log centre, connection settings and system maintenance as native /api/v1 routes backed by the existing secret store, audit trail and pinned upstream transport.
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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<string, unknown> {
|
||||
const params: Record<string, unknown> = {};
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user