Files
multi-simadmin/apps/api/src/interface/http/secure-operation-routes.test.ts
T

122 lines
4.5 KiB
TypeScript

import Database from 'better-sqlite3';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { buildApp } from '../../app.js';
import { migrateDatabase } from '../../infrastructure/database/migrations.js';
import {
SecureOperationExecution,
secureOperationRegistry,
} from '../../application/operations/secure-operation-execution.js';
import { operationCatalogRegistry } from '../../application/operations/operation-catalog-data.js';
import { registerOperationRoutes } from './operation-routes.js';
const resources: Array<{ close(): unknown }> = [];
afterEach(async () => {
for (const resource of resources.splice(0)) await resource.close();
});
function fixture() {
const db = new Database(':memory:');
resources.push(db);
db.pragma('foreign_keys=ON');
migrateDatabase(db);
db.prepare(
`INSERT INTO instances (id,name,base_url,auth_mode,enabled,config_revision,created_at,updated_at)
VALUES ('i','one','http://192.168.1.10','none',1,1,'2026-07-17T00:00:00.000Z','2026-07-17T00:00:00.000Z')`,
).run();
const request = vi.fn(async () => ({ status: 200 }));
const execution = new SecureOperationExecution({
db,
registry: secureOperationRegistry,
transport: { request },
idFactory: (() => {
let n = 0;
return () => `id-${++n}`;
})(),
tokenFactory: () => Buffer.alloc(32, 4).toString('base64url'),
});
const app = buildApp({
registerRoutes: (server) =>
registerOperationRoutes(server, operationCatalogRegistry, execution),
});
resources.push(app);
return { app, db, request };
}
const prepareBody = {
operationId: 'postNetworkRegisterAuto',
targets: [{ instanceId: 'i', revision: 1 }],
parameters: {
parameterSchemaId: 'simadmin.58e2204.postNetworkRegisterAuto.parameters.v1',
fields: [],
},
};
describe('secure operation routes', () => {
it('returns 200 Preparation then 202 terminal Job with request IDs', async () => {
const { app, db, request } = fixture();
const prepared = await app.inject({
method: 'POST',
url: '/api/v1/operations/prepare',
payload: prepareBody,
});
expect(prepared.statusCode).toBe(200);
expect(prepared.headers['x-request-id']).toBeTruthy();
const preparation = prepared.json();
expect(db.prepare('SELECT count(*) count FROM jobs').get()).toEqual({ count: 0 });
const executed = await app.inject({
method: 'POST',
url: '/api/v1/operations/execute',
payload: {
preparationId: preparation.id,
confirmationToken: preparation.confirmationToken,
},
});
expect(executed.statusCode).toBe(202);
expect(executed.headers['x-request-id']).toBeTruthy();
expect(executed.json()).toMatchObject({
operationId: 'postNetworkRegisterAuto',
status: 'succeeded',
});
expect(request).toHaveBeenCalledOnce();
});
it.each([
['/api/v1/operations/prepare', { ...prepareBody, extra: true }],
[
'/api/v1/operations/prepare',
{ ...prepareBody, targets: [{ instanceId: 'i', revision: 1, extra: true }] },
],
[
'/api/v1/operations/prepare',
{ ...prepareBody, parameters: { ...prepareBody.parameters, extra: true } },
],
['/api/v1/operations/execute', { preparationId: 'x', confirmationToken: 'x', extra: true }],
])('strictly rejects additional properties at %s', async (url, payload) => {
const { app } = fixture();
const response = await app.inject({ method: 'POST', url, payload });
expect(response.statusCode).toBe(400);
expect(response.headers['content-type']).toContain('application/problem+json');
expect(response.headers['x-request-id']).toBeTruthy();
expect(response.json()).toMatchObject({
status: 400,
code: 'VALIDATION_FAILED',
requestId: response.headers['x-request-id'],
});
});
it('returns Problem Details and does not consume a wrong confirmation', async () => {
const { app, db } = fixture();
const preparation = (
await app.inject({ method: 'POST', url: '/api/v1/operations/prepare', payload: prepareBody })
).json();
const response = await app.inject({
method: 'POST',
url: '/api/v1/operations/execute',
payload: { preparationId: preparation.id, confirmationToken: 'wrong' },
});
expect(response.statusCode).toBe(400);
expect(response.json()).toMatchObject({ status: 400, code: 'CONFIRMATION_INVALID' });
expect(
db.prepare('SELECT status FROM operation_preparations WHERE id=?').get(preparation.id),
).toEqual({ status: 'prepared' });
});
});