added base test coverage

This commit is contained in:
Dmitri 2026-07-02 13:13:36 +02:00
parent 6085443549
commit e1711b42c2
5 changed files with 461 additions and 0 deletions

View File

@ -9,6 +9,8 @@
"build": "tsc -p tsconfig.json && node scripts/copy-runtime-assets.ts",
"test": "node --test tests/*.test.ts",
"test:integration": "node --test tests/integration/*.test.ts",
"test:e2e": "node --test tests/e2e/*.test.ts",
"test:all": "npm run test && npm run test:integration && npm run test:e2e",
"verify": "npm run typecheck && npm run lint && npm run check:esm-boundaries && npm run test",
"check:esm-boundaries": "node scripts/check-esm-boundaries.ts",
"check:public-access": "node scripts/check-public-access-hardening.ts",

View File

@ -0,0 +1,72 @@
import assert from 'node:assert/strict';
import express from 'express';
import test from 'node:test';
import { runtimeContextMiddleware } from '../../src/middlewares/runtime-context.ts';
import runtimeContextRoutes from '../../src/routes/runtime-context.ts';
import { startTestServer } from '../http-test-utils.ts';
function buildRuntimeContextApp() {
const app = express();
app.use(runtimeContextMiddleware);
app.use('/api/runtime-context', runtimeContextRoutes);
return app;
}
void test('GET /api/runtime-context returns default admin runtime context', async () => {
const server = await startTestServer(buildRuntimeContextApp());
try {
const response = await fetch(`${server.baseUrl}/api/runtime-context`);
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), {
mode: 'admin',
projectSlug: null,
});
} finally {
await server.close();
}
});
void test('GET /api/runtime-context exposes valid runtime headers over HTTP', async () => {
const server = await startTestServer(buildRuntimeContextApp());
try {
const response = await fetch(`${server.baseUrl}/api/runtime-context`, {
headers: {
'x-runtime-environment': 'production',
'x-runtime-project-slug': 'museum-tour',
},
});
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), {
mode: 'admin',
projectSlug: null,
headerEnvironment: 'production',
headerProjectSlug: 'museum-tour',
});
} finally {
await server.close();
}
});
void test('GET /api/runtime-context ignores unsupported runtime environment headers', async () => {
const server = await startTestServer(buildRuntimeContextApp());
try {
const response = await fetch(`${server.baseUrl}/api/runtime-context`, {
headers: {
'x-runtime-environment': 'qa',
'x-runtime-project-slug': 'museum-tour',
},
});
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), {
mode: 'admin',
projectSlug: null,
headerProjectSlug: 'museum-tour',
});
} finally {
await server.close();
}
});

View File

@ -0,0 +1,39 @@
import http from 'node:http';
import type { Express } from 'express';
export interface RunningTestServer {
baseUrl: string;
close: () => Promise<void>;
}
export function startTestServer(app: Express): Promise<RunningTestServer> {
return new Promise((resolve, reject) => {
const server = http.createServer(app);
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
server.off('error', reject);
const address = server.address();
if (typeof address !== 'object' || address === null) {
server.close();
reject(new Error('Test server did not expose a listening address.'));
return;
}
resolve({
baseUrl: `http://127.0.0.1:${address.port}`,
close: () =>
new Promise((closeResolve, closeReject) => {
server.close((error) => {
if (error) {
closeReject(error);
return;
}
closeResolve();
});
}),
});
});
});
}

View File

@ -0,0 +1,246 @@
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",',
);
});

View File

@ -0,0 +1,102 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import UploadSessionManager from '../src/services/file/UploadSessionManager.ts';
function makeSessionManager(ttlMs = 60_000): {
manager: UploadSessionManager;
rootDir: string;
} {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'upload-sessions-'));
return {
manager: new UploadSessionManager({ sessionDir: rootDir, ttlMs }),
rootDir,
};
}
void test('UploadSessionManager creates metadata and tracks uploaded chunks', async () => {
const { manager, rootDir } = makeSessionManager();
try {
const sessionId = manager.createSession({
filename: 'tour.mp4',
folder: 'assets',
totalChunks: 2,
totalSize: 11,
userId: 'user-1',
contentType: 'video/mp4',
});
let meta = manager.readMeta(sessionId);
assert.equal(meta?.filename, 'tour.mp4');
assert.equal(meta?.uploadedChunks !== undefined, true);
assert.equal(manager.isComplete(sessionId), false);
await manager.saveChunk(sessionId, 0, Buffer.from('hello '));
await manager.saveChunk(sessionId, 1, Buffer.from('world'));
meta = manager.readMeta(sessionId);
assert.equal(meta?.uploadedChunks[0]?.size, 6);
assert.equal(meta?.uploadedChunks[1]?.size, 5);
assert.equal(manager.chunkExists(sessionId, 0), true);
assert.equal(manager.isComplete(sessionId), true);
} finally {
fs.rmSync(rootDir, { recursive: true, force: true });
}
});
void test('UploadSessionManager assembles chunks in index order', async () => {
const { manager, rootDir } = makeSessionManager();
try {
const sessionId = manager.createSession({
filename: 'image.bin',
folder: 'assets',
totalChunks: 3,
totalSize: 9,
});
await manager.saveChunk(sessionId, 2, Buffer.from('ghi'));
await manager.saveChunk(sessionId, 0, Buffer.from('abc'));
await manager.saveChunk(sessionId, 1, Buffer.from('def'));
const targetPath = path.join(rootDir, 'assembled', 'image.bin');
await manager.assembleChunks(sessionId, targetPath);
assert.equal(fs.readFileSync(targetPath, 'utf8'), 'abcdefghi');
} finally {
fs.rmSync(rootDir, { recursive: true, force: true });
}
});
void test('UploadSessionManager removes expired and invalid sessions during cleanup', () => {
const { manager, rootDir } = makeSessionManager(1);
try {
const expiredSessionId = manager.createSession({
filename: 'expired.txt',
folder: 'assets',
totalChunks: 1,
totalSize: 1,
});
const meta = manager.readMeta(expiredSessionId);
assert.ok(meta);
meta.updatedAt = new Date(Date.now() - 10_000).toISOString();
manager.writeMeta(expiredSessionId, meta);
const invalidSessionId = 'invalid-session';
const invalidSessionDir = manager.getSessionDir(invalidSessionId);
fs.mkdirSync(invalidSessionDir, { recursive: true });
fs.writeFileSync(
manager.getMetaPath(invalidSessionId),
JSON.stringify({ sessionId: invalidSessionId }),
'utf8',
);
manager.cleanupExpiredSessions();
assert.equal(fs.existsSync(manager.getSessionDir(expiredSessionId)), false);
assert.equal(fs.existsSync(invalidSessionDir), false);
} finally {
fs.rmSync(rootDir, { recursive: true, force: true });
}
});