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