39948-vm/backend/tests/integration/router-factory.test.ts
2026-07-02 13:13:36 +02:00

247 lines
7.1 KiB
TypeScript

import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import express from 'express';
import test from 'node:test';
import type { RequestHandler } from 'express';
import { createRequest, createResponse } from 'node-mocks-http';
import type { Body, Headers, RequestMethod, RequestOptions } from 'node-mocks-http';
import { createEntityRouter } from '../../src/factories/router.factory.ts';
import { commonErrorHandler } from '../../src/helpers.ts';
import type {
EntityRouterDbApi,
EntityRouterService,
} from '../../src/types/index.ts';
import {
setCurrentUser,
setRuntimeContext,
} from '../../src/utils/request-context.ts';
const USER_ID = '0f5cb907-73d3-45da-9e42-bf3e4b499efb';
const ENTITY_ID = 'aa0b89ab-b0db-4d6c-a59c-c7785fc3eae5';
interface RecordedCall {
name: string;
payload: unknown;
}
function contextMiddleware(): RequestHandler {
return (req, _res, next) => {
setCurrentUser(req, {
id: USER_ID,
app_role: {
name: 'Test Role',
permissions: [
{ name: 'READ_TEST_ENTITIES' },
{ name: 'CREATE_TEST_ENTITIES' },
{ name: 'UPDATE_TEST_ENTITIES' },
{ name: 'DELETE_TEST_ENTITIES' },
],
},
});
setRuntimeContext(req, {
mode: 'admin',
projectSlug: null,
headerEnvironment: 'stage',
headerProjectSlug: 'demo-tour',
});
next();
};
}
function buildHarness() {
const calls: RecordedCall[] = [];
const service: EntityRouterService<Record<string, unknown>, Record<string, unknown>> = {
create: (options) => {
calls.push({ name: 'create', payload: options });
return Promise.resolve({ id: ENTITY_ID, ...options.data });
},
update: (options) => {
calls.push({ name: 'update', payload: options });
return Promise.resolve(true);
},
remove: (options) => {
calls.push({ name: 'remove', payload: options });
return Promise.resolve(true);
},
deleteByIds: (options) => {
calls.push({ name: 'deleteByIds', payload: options });
return Promise.resolve(true);
},
bulkImport: () => {
calls.push({ name: 'bulkImport', payload: null });
return Promise.resolve();
},
};
const dbApi: EntityRouterDbApi = {
MODEL: {
rawAttributes: {
id: {},
name: {},
createdAt: {},
},
},
findAll: (query, options) => {
calls.push({ name: 'findAll', payload: { query, options } });
return Promise.resolve({
rows: [{ id: ENTITY_ID, name: 'Lobby' }],
count: 1,
});
},
findAllAutocomplete: (options) => {
calls.push({ name: 'findAllAutocomplete', payload: options });
return Promise.resolve([{ id: ENTITY_ID, label: 'Lobby' }]);
},
findBy: (options) => {
calls.push({ name: 'findBy', payload: options });
return Promise.resolve({ id: ENTITY_ID, name: 'Lobby' });
},
};
const app = express();
app.use(contextMiddleware());
app.use('/entities', createEntityRouter('test_entities', service, dbApi));
app.use(commonErrorHandler);
return { app, calls };
}
interface AppRequestOptions {
method: RequestMethod;
url: string;
headers?: Headers;
body?: Body;
}
function dispatchApp(app: express.Express, options: AppRequestOptions) {
const requestOptions: RequestOptions = {
method: options.method,
url: options.url,
};
if (options.headers !== undefined) {
requestOptions.headers = options.headers;
}
if (options.body !== undefined) {
requestOptions.body = options.body;
}
const req = createRequest(requestOptions);
const res = createResponse({ eventEmitter: EventEmitter });
return new Promise<typeof res>((resolve, reject) => {
res.on('end', () => resolve(res));
app(req, res, (error?: unknown) => {
if (error) {
reject(
error instanceof Error
? error
: new Error('Express app returned a non-Error callback value.'),
);
return;
}
resolve(res);
});
});
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function asRecord(value: unknown): Record<string, unknown> {
if (isRecord(value)) return value;
throw new TypeError('Expected value to be an object record.');
}
void test('createEntityRouter passes current user, runtime context, and host into create service calls', async () => {
const { app, calls } = buildHarness();
const response = await dispatchApp(app, {
method: 'POST',
url: '/entities',
headers: {
referer: 'https://builder.example.test/projects',
},
body: { data: { name: 'Lobby' } },
});
assert.equal(response.statusCode, 200);
assert.deepEqual(response._getData(), { id: ENTITY_ID, name: 'Lobby' });
const call = calls.find((item) => item.name === 'create');
assert.ok(call);
const payload = asRecord(call.payload);
assert.equal(payload.host, 'https://builder.example.test');
assert.equal(asRecord(payload.currentUser).id, USER_ID);
assert.equal(
asRecord(payload.runtimeContext).headerProjectSlug,
'demo-tour',
);
});
void test('createEntityRouter normalizes list query controls and preserves filters', async () => {
const { app, calls } = buildHarness();
const response = await dispatchApp(app, {
method: 'GET',
url: '/entities?limit=1000&page=2&sort=asc&field=name&unknown=keep',
});
assert.equal(response.statusCode, 200);
assert.deepEqual(response._getData(), {
rows: [{ id: ENTITY_ID, name: 'Lobby' }],
count: 1,
});
const call = calls.find((item) => item.name === 'findAll');
assert.ok(call);
const payload = asRecord(call.payload);
assert.deepEqual(payload.query, {
limit: 1000,
page: 2,
unknown: 'keep',
sort: 'ASC',
field: 'name',
});
assert.equal(asRecord(payload.options).currentUser !== undefined, true);
});
void test('createEntityRouter rejects mismatched body ids before update service call', async () => {
const { app, calls } = buildHarness();
const response = await dispatchApp(app, {
method: 'PUT',
url: `/entities/${ENTITY_ID}`,
body: {
data: {
id: '6a373ead-7ff4-4f1c-9a69-29ff1e97420d',
name: 'Mismatch',
},
},
});
assert.equal(response.statusCode, 400);
assert.equal(response._getData(), 'Request body id does not match route id');
assert.equal(calls.some((item) => item.name === 'update'), false);
});
void test('createEntityRouter emits CSV exports with configured fields', async () => {
const { app } = buildHarness();
const response = await dispatchApp(app, {
method: 'GET',
url: '/entities?filetype=csv',
});
assert.equal(response.statusCode, 200);
const disposition: unknown = response.getHeader('content-disposition');
if (typeof disposition !== 'string') {
throw new TypeError('Expected content-disposition response header.');
}
assert.match(disposition, /export\.csv/);
const csvPayload: unknown = response._getData();
if (typeof csvPayload !== 'string') {
throw new TypeError('Expected CSV response body.');
}
assert.equal(
csvPayload.trim(),
'"id","createdAt"\n"aa0b89ab-b0db-4d6c-a59c-c7785fc3eae5",',
);
});