122 lines
4.5 KiB
TypeScript
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' });
|
|
});
|
|
});
|