test: remove infrastructure-dependent integration suites (#1573)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-12 16:56:25 +02:00
1 parent f88b0f69b2
commit 10fc79ab37
199 files changed
+688 -46963

No files matched your search

@@ -1,249 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {randomUUID} from 'node:crypto';
import {existsSync} from 'node:fs';
import {getDefaultPostgresClient, initPostgres, quoteIdentifier, shutdownPostgres} from '@pkgs/postgres/src/Client';
import cassandra from 'cassandra-driver';
import {afterAll, beforeAll, describe, expect, test} from 'vitest';
import {defineTable} from './CassandraTableDsl';
import {Db} from './CassandraTypes';
import {ensurePostgresKvSchema, PostgresKvQueryExecutor, pruneExpiredPostgresKvRows} from './PostgresKvQueryExecutor';
interface TestRow {
tenant_id: string;
item_id: bigint;
created_at: Date;
payload: Buffer | null;
tags: Set<string> | null;
counts: Map<string, bigint> | null;
local_day: cassandra.types.LocalDate | null;
note: string | null;
}
function postgresHost(): string {
return process.env.FLUXER_POSTGRES_TEST_HOST ?? (existsSync('/.dockerenv') ? 'host.docker.internal' : '127.0.0.1');
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const kvTable = `fluxer_kv_test_${randomUUID().replaceAll('-', '_')}`;
const logicalTableName = `pg_kv_test_${randomUUID().replaceAll('-', '_')}`;
const TestRows = defineTable<TestRow, 'tenant_id' | 'item_id', 'tenant_id'>({
name: logicalTableName,
columns: ['tenant_id', 'item_id', 'created_at', 'payload', 'tags', 'counts', 'local_day', 'note'],
primaryKey: ['tenant_id', 'item_id'],
partitionKey: ['tenant_id'],
});
describe('PostgresKvQueryExecutor', () => {
let executor: PostgresKvQueryExecutor;
let initialized = false;
beforeAll(async () => {
await initPostgres({
host: postgresHost(),
port: Number(process.env.FLUXER_POSTGRES_PORT ?? 5432),
database: process.env.FLUXER_POSTGRES_DATABASE ?? 'fluxer',
username: process.env.FLUXER_POSTGRES_USERNAME ?? 'fluxer',
password: process.env.FLUXER_POSTGRES_PASSWORD ?? 'fluxer',
kvTable,
maxConnections: 4,
});
initialized = true;
const client = getDefaultPostgresClient();
await ensurePostgresKvSchema(client);
executor = new PostgresKvQueryExecutor(client);
});
afterAll(async () => {
if (!initialized) return;
try {
await getDefaultPostgresClient().query(`DROP TABLE IF EXISTS ${quoteIdentifier(kvTable)}`);
} finally {
await shutdownPostgres();
}
});
test('round trips typed values and conditional writes through a real Postgres KV table', async () => {
const row: TestRow = {
tenant_id: 'tenant-a',
item_id: 10n,
created_at: new Date('2026-01-02T03:04:05.006Z'),
payload: Buffer.from('hello postgres kv'),
tags: new Set(['alpha', 'beta']),
counts: new Map([['seen', 5n]]),
local_day: cassandra.types.LocalDate.fromString('2026-01-02'),
note: 'first',
};
await expect(executor.executeQuery<{['[applied]']: boolean}>(TestRows.insertIfNotExists(row))).resolves.toEqual([
{'[applied]': true},
]);
await expect(
executor.executeQuery<{['[applied]']: boolean}>(TestRows.insertIfNotExists({...row, note: 'second'})),
).resolves.toEqual([{'[applied]': false}]);
const fetched = await executor.executeQuery<TestRow>(
TestRows.select({
where: [TestRows.where.eq('tenant_id'), TestRows.where.eq('item_id')],
limit: 1,
}).bind({tenant_id: 'tenant-a', item_id: 10n}),
);
expect(fetched).toHaveLength(1);
expect(fetched[0]!.item_id).toBe(10n);
expect(fetched[0]!.created_at.getTime()).toBe(row.created_at.getTime());
expect(fetched[0]!.payload?.equals(row.payload!)).toBe(true);
expect([...fetched[0]!.tags!.values()].sort()).toEqual(['alpha', 'beta']);
expect(fetched[0]!.counts!.get('seen')).toBe(5n);
expect(fetched[0]!.local_day?.toString()).toBe('2026-01-02');
expect(fetched[0]!.note).toBe('first');
});
test('filters, orders, patches, pages, and deletes logical rows', async () => {
await executor.executeBatch(
[
TestRows.upsertAll({
tenant_id: 'tenant-b',
item_id: 1n,
created_at: new Date('2026-02-01T00:00:00.000Z'),
payload: null,
tags: null,
counts: null,
local_day: null,
note: 'one',
}),
TestRows.upsertAll({
tenant_id: 'tenant-b',
item_id: 2n,
created_at: new Date('2026-02-02T00:00:00.000Z'),
payload: null,
tags: null,
counts: null,
local_day: null,
note: 'two',
}),
TestRows.upsertAll({
tenant_id: 'tenant-b',
item_id: 3n,
created_at: new Date('2026-02-03T00:00:00.000Z'),
payload: null,
tags: null,
counts: null,
local_day: null,
note: 'three',
}),
].map((query) => ({query: query.cql, params: query.params, meta: query.kvMeta})),
);
const firstPage = await executor.executePagedQuery<TestRow>(
TestRows.select({
where: TestRows.where.eq('tenant_id'),
orderBy: {col: 'item_id', direction: 'DESC'},
}).bind({tenant_id: 'tenant-b'}),
{pageSize: 2},
);
expect(firstPage.rows.map((row) => row.item_id)).toEqual([3n, 2n]);
expect(firstPage.pageState).not.toBeNull();
const secondPage = await executor.executePagedQuery<TestRow>(
TestRows.select({
where: TestRows.where.eq('tenant_id'),
orderBy: {col: 'item_id', direction: 'DESC'},
}).bind({tenant_id: 'tenant-b'}),
{pageSize: 2, pageState: firstPage.pageState},
);
expect(secondPage.rows.map((row) => row.item_id)).toEqual([1n]);
expect(secondPage.pageState).toBeNull();
await executor.executeQuery(TestRows.patchByPk({tenant_id: 'tenant-b', item_id: 2n}, {note: Db.clear()}));
const patched = await executor.executeQuery<TestRow>(
TestRows.select({where: [TestRows.where.eq('tenant_id'), TestRows.where.eq('item_id')], limit: 1}).bind({
tenant_id: 'tenant-b',
item_id: 2n,
}),
);
expect(patched[0]!.note).toBeNull();
await executor.executeQuery(TestRows.delete({where: TestRows.where.eq('tenant_id')}).bind({tenant_id: 'tenant-b'}));
const remaining = await executor.executeQuery<TestRow>(
TestRows.select({where: TestRows.where.eq('tenant_id')}).bind({tenant_id: 'tenant-b'}),
);
expect(remaining).toEqual([]);
});
test('filters expired rows and prunes them physically', async () => {
const client = getDefaultPostgresClient();
await executor.executeQuery(
TestRows.upsertAllWithTtl(
{
tenant_id: 'tenant-expired',
item_id: 1n,
created_at: new Date('2026-03-01T00:00:00.000Z'),
payload: null,
tags: null,
counts: null,
local_day: null,
note: 'temporary',
},
1,
),
);
await sleep(1100);
const visibleRows = await executor.executeQuery<TestRow>(
TestRows.select({where: TestRows.where.eq('tenant_id')}).bind({tenant_id: 'tenant-expired'}),
);
expect(visibleRows).toEqual([]);
const physicalBefore = await client.query<{count: string}>(
`SELECT count(*)::text AS count FROM ${quoteIdentifier(kvTable)} WHERE table_name = $1 AND row_data ->> 'tenant_id' = $2`,
[logicalTableName, 'tenant-expired'],
);
expect(Number(physicalBefore.rows[0]!.count)).toBe(1);
const pruned = await pruneExpiredPostgresKvRows(client, 10);
expect(pruned).toBeGreaterThanOrEqual(1);
const physicalAfter = await client.query<{count: string}>(
`SELECT count(*)::text AS count FROM ${quoteIdentifier(kvTable)} WHERE table_name = $1 AND row_data ->> 'tenant_id' = $2`,
[logicalTableName, 'tenant-expired'],
);
expect(Number(physicalAfter.rows[0]!.count)).toBe(0);
});
test('patching a logically expired row makes it visible again without keeping stale TTL', async () => {
await executor.executeQuery(
TestRows.upsertAllWithTtl(
{
tenant_id: 'tenant-resurrect',
item_id: 1n,
created_at: new Date('2026-04-01T00:00:00.000Z'),
payload: null,
tags: null,
counts: null,
local_day: null,
note: 'temporary',
},
1,
),
);
await sleep(1100);
await executor.executeQuery(
TestRows.patchByPk({tenant_id: 'tenant-resurrect', item_id: 1n}, {note: Db.set('resurrected')}),
);
const rows = await executor.executeQuery<TestRow>(
TestRows.select({
where: [TestRows.where.eq('tenant_id'), TestRows.where.eq('item_id')],
limit: 1,
}).bind({tenant_id: 'tenant-resurrect', item_id: 1n}),
);
expect(rows).toHaveLength(1);
expect(rows[0]!.note).toBe('resurrected');
});
});
-127
View File
@@ -1,127 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import http from 'node:http';
import type {AddressInfo} from 'node:net';
import {type FakeNcmecFailure, FakeNcmecServer} from './FakeNcmecServer';
const host = process.env.FAKE_NCMEC_HOST ?? '127.0.0.1';
const port = readPort('FAKE_NCMEC_PORT', 9090);
const controlHost = process.env.FAKE_NCMEC_CONTROL_HOST ?? '127.0.0.1';
const controlPort = readPort('FAKE_NCMEC_CONTROL_PORT', port + 1);
const publicHost = process.env.FAKE_NCMEC_PUBLIC_HOST ?? (host === '0.0.0.0' ? '<reachable-host-or-ip>' : host);
const username = process.env.FAKE_NCMEC_USERNAME ?? 'usr123';
const password = process.env.FAKE_NCMEC_PASSWORD ?? 'pswd123';
if (host === controlHost && port === controlPort) {
throw new Error('FAKE_NCMEC_CONTROL_PORT must differ from FAKE_NCMEC_PORT when both servers bind the same host');
}
const fake = new FakeNcmecServer({username, password});
function log(...parts: Array<unknown>): void {
const ts = new Date().toISOString();
console.log(`[fake-ncmec ${ts}]`, ...parts);
}
await fake.start({
host,
port,
onRequest: (entry) => {
log(
`${entry.remoteAddress ?? '?'} ${entry.method} ${entry.path} -> ${entry.statusCode} ${entry.durationMs}ms (${entry.requestId})`,
);
},
});
const controlServer = http.createServer(async (req, res) => {
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
log(`control ${req.method} ${url.pathname}`);
if (req.method === 'GET' && url.pathname === '/_state') {
res.writeHead(200, {'content-type': 'application/json'});
res.end(JSON.stringify(fake.getReports(), null, 2));
return;
}
if (req.method === 'GET' && url.pathname.startsWith('/_report/')) {
const reportId = decodeURIComponent(url.pathname.slice('/_report/'.length));
const report = fake.getReport(reportId);
if (!report) {
res.writeHead(404, {'content-type': 'application/json'});
res.end(JSON.stringify({error: `Unknown report ${reportId}`}));
return;
}
res.writeHead(200, {'content-type': 'application/json'});
res.end(JSON.stringify(report, null, 2));
return;
}
if (req.method === 'POST' && url.pathname === '/_reset') {
fake.reset();
res.writeHead(204);
res.end();
return;
}
if (req.method === 'POST' && url.pathname === '/_fail') {
const mode = (url.searchParams.get('mode') ?? 'none') as FakeNcmecFailure;
if (!VALID_FAILURE_MODES.has(mode)) {
res.writeHead(400, {'content-type': 'application/json'});
res.end(JSON.stringify({error: `Invalid failure mode ${mode}`}));
return;
}
fake.setFailure(mode);
res.writeHead(200, {'content-type': 'application/json'});
res.end(JSON.stringify({failure: mode}));
return;
}
res.writeHead(404);
res.end('Not found. Try GET /_state, POST /_reset, POST /_fail?mode=...');
});
await new Promise<void>((resolve) => controlServer.listen(controlPort, controlHost, () => resolve()));
const address = controlServer.address() as AddressInfo;
log(`NCMEC-compatible fake server ready`);
log(` NCMEC bind → http://${fake.host}:${fake.port}/ispws`);
log(` NCMEC base URL → http://${publicHost}:${fake.port}/ispws`);
log(` control endpoint → http://${address.address}:${address.port}/_state`);
log(` credentials → ${username}:${password}`);
if (publicHost.startsWith('<')) {
log(' set FAKE_NCMEC_PUBLIC_HOST to the hostname or IP your prod API can reach');
}
log(' point prod Config.ncmec.baseUrl at the NCMEC base URL above');
function shutdown(signal: string): void {
log(`received ${signal}, shutting down`);
Promise.allSettled([fake.stop(), new Promise<void>((resolve) => controlServer.close(() => resolve()))]).then(() =>
process.exit(0),
);
}
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
const VALID_FAILURE_MODES = new Set<FakeNcmecFailure>([
'none',
'submit',
'upload',
'fileinfo',
'finish',
'retract',
'unauthorized',
]);
function readPort(name: string, fallback: number): number {
const raw = process.env[name];
if (!raw) return fallback;
const value = Number.parseInt(raw, 10);
if (!Number.isInteger(value) || value < 0 || value > 65535) {
throw new Error(`${name} must be an integer between 0 and 65535`);
}
return value;
}
@@ -1,430 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import crypto from 'node:crypto';
import http from 'node:http';
import type {AddressInfo} from 'node:net';
import {Readable} from 'node:stream';
import {XMLParser, XMLValidator} from 'fast-xml-parser';
export type FakeNcmecFailure = 'none' | 'submit' | 'upload' | 'fileinfo' | 'finish' | 'retract' | 'unauthorized';
export interface FakeNcmecFileRecord {
fileId: string;
filename: string;
size: number;
md5: string;
submittedDetails: boolean;
detailsXml: string | null;
}
export interface FakeNcmecReportRecord {
reportId: string;
reportXml: string;
files: Array<FakeNcmecFileRecord>;
finished: boolean;
retracted: boolean;
authUsername: string;
}
interface FakeNcmecServerOptions {
username: string;
password: string;
}
interface FakeNcmecServerStartOptions {
host?: string;
port?: number;
onRequest?: (entry: FakeNcmecRequestLogEntry) => void;
}
interface FakeNcmecRequestLogEntry {
method: string;
path: string;
statusCode: number;
requestId: string;
durationMs: number;
remoteAddress: string | null;
}
const xmlParser = new XMLParser({ignoreAttributes: false, parseTagValue: false, trimValues: true});
export class FakeNcmecServer {
private server: http.Server | null = null;
private listenPort = 0;
private listenHost = '127.0.0.1';
private onRequest: ((entry: FakeNcmecRequestLogEntry) => void) | null = null;
private reportCounter = 1000000;
private fileCounter = 1;
private requestCounter = 1;
private readonly reports = new Map<string, FakeNcmecReportRecord>();
private failureMode: FakeNcmecFailure = 'none';
constructor(private readonly options: FakeNcmecServerOptions) {}
async start(startOptions: FakeNcmecServerStartOptions = {}): Promise<void> {
if (this.server) {
throw new Error('FakeNcmecServer already started');
}
this.listenHost = startOptions.host ?? '127.0.0.1';
this.onRequest = startOptions.onRequest ?? null;
this.server = http.createServer((req, res) => this.handle(req, res));
await new Promise<void>((resolve) => this.server!.listen(startOptions.port ?? 0, this.listenHost, () => resolve()));
this.listenPort = (this.server!.address() as AddressInfo).port;
}
async stop(): Promise<void> {
if (!this.server) return;
await new Promise<void>((resolve, reject) => {
this.server!.close((error) => (error ? reject(error) : resolve()));
});
this.server = null;
this.listenPort = 0;
this.onRequest = null;
}
get baseUrl(): string {
if (!this.server) throw new Error('FakeNcmecServer not started');
return `http://${this.listenHost}:${this.listenPort}/ispws`;
}
get host(): string {
if (!this.server) throw new Error('FakeNcmecServer not started');
return this.listenHost;
}
get port(): number {
if (!this.server) throw new Error('FakeNcmecServer not started');
return this.listenPort;
}
reset(): void {
this.reports.clear();
this.reportCounter = 1000000;
this.fileCounter = 1;
this.requestCounter = 1;
this.failureMode = 'none';
}
setFailure(mode: FakeNcmecFailure): void {
this.failureMode = mode;
}
getReports(): Array<FakeNcmecReportRecord> {
return [...this.reports.values()];
}
getReport(reportId: string): FakeNcmecReportRecord | null {
return this.reports.get(reportId) ?? null;
}
private async handle(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
try {
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
const pathname = url.pathname.replace(/^\/ispws/, '') || '/';
const requestId = `req-${this.requestCounter++}`;
const startedAt = Date.now();
res.setHeader('Request-ID', requestId);
res.on('finish', () => {
this.onRequest?.({
method: req.method ?? 'GET',
path: pathname,
statusCode: res.statusCode,
requestId,
durationMs: Date.now() - startedAt,
remoteAddress: req.socket.remoteAddress ?? null,
});
});
if (this.failureMode === 'unauthorized') {
return this.sendStatus(res, 401, '');
}
if (!this.checkBasicAuth(req)) {
return this.sendStatus(res, 401, '');
}
const injectedHeader = (req.headers['x-test-fail'] ?? '').toString() as FakeNcmecFailure;
const effectiveFailure: FakeNcmecFailure = injectedHeader || this.failureMode;
if (req.method === 'GET' && pathname === '/status') {
const remoteIp = req.socket.remoteAddress ?? 'unknown';
return this.sendXml(
res,
200,
ok(
`<responseDescription>Remote User : ${this.options.username}, Remote Ip : ${remoteIp}</responseDescription>`,
),
);
}
if (req.method === 'GET' && pathname === '/xsd') {
return this.sendText(res, 200, '<schema></schema>');
}
if (req.method !== 'POST') {
return this.sendStatus(res, 405, '');
}
const request = await this.toWebRequest(req, url);
switch (pathname) {
case '/submit':
return this.handleSubmit(request, res, effectiveFailure);
case '/upload':
return this.handleUpload(request, res, effectiveFailure);
case '/fileinfo':
return this.handleFileInfo(request, res, effectiveFailure);
case '/finish':
return this.handleFinish(request, res, effectiveFailure);
case '/retract':
return this.handleRetract(request, res, effectiveFailure);
default:
return this.sendStatus(res, 404, '');
}
} catch (error) {
this.sendStatus(res, 500, String(error));
}
}
private async handleSubmit(request: Request, res: http.ServerResponse, failure: FakeNcmecFailure): Promise<void> {
if (failure === 'submit') {
return this.sendXml(res, 200, errorXml(1100, 'Save failed'));
}
if (!isXmlContentType(request)) {
return this.sendXml(res, 200, errorXml(4000, 'Invalid request'));
}
const body = await request.text();
if (XMLValidator.validate(body) !== true) {
return this.sendXml(res, 200, errorXml(4110, 'Malformed XML submittal'));
}
const report = parseReportDocument(body);
if (!hasRequiredReportFields(report)) {
return this.sendXml(res, 200, errorXml(4100, 'Validation failed'));
}
const reportId = String(this.reportCounter++);
this.reports.set(reportId, {
reportId,
reportXml: body,
files: [],
finished: false,
retracted: false,
authUsername: this.options.username,
});
return this.sendXml(res, 200, ok(`<reportId>${reportId}</reportId>`));
}
private async handleUpload(request: Request, res: http.ServerResponse, failure: FakeNcmecFailure): Promise<void> {
if (failure === 'upload') {
return this.sendXml(res, 200, errorXml(1111, 'File upload failed'));
}
const form = await request.formData();
const reportId = String(form.get('id') ?? '');
const file = form.get('file');
const record = this.reports.get(reportId);
if (!reportId || !record) {
return this.sendXml(res, 200, errorXml(5001, 'Report does not exist'));
}
if (record.retracted) {
return this.sendXml(res, 200, errorXml(5101, 'Report already retracted'));
}
if (record.finished) {
return this.sendXml(res, 200, errorXml(5102, 'Report already finished'));
}
if (!(file instanceof Blob)) {
return this.sendXml(res, 200, errorXml(4200, 'Malformed file submittal'));
}
const buffer = Buffer.from(await file.arrayBuffer());
const md5 = crypto.createHash('md5').update(buffer).digest('hex');
const fileId = `file-${this.fileCounter++}`;
record.files.push({
fileId,
filename: (file as File).name || 'upload.bin',
size: buffer.byteLength,
md5,
submittedDetails: false,
detailsXml: null,
});
return this.sendXml(res, 200, ok(`<reportId>${reportId}</reportId><fileId>${fileId}</fileId><hash>${md5}</hash>`));
}
private async handleFileInfo(request: Request, res: http.ServerResponse, failure: FakeNcmecFailure): Promise<void> {
if (failure === 'fileinfo') {
return this.sendXml(res, 200, errorXml(1300, 'Update failed'));
}
if (!isXmlContentType(request)) {
return this.sendXml(res, 200, errorXml(4000, 'Invalid request'));
}
const body = await request.text();
if (XMLValidator.validate(body) !== true) {
return this.sendXml(res, 200, errorXml(4110, 'Malformed XML submittal'));
}
const fileDetails = parseFileDetailsDocument(body);
if (!hasFileDetailsFields(fileDetails)) {
return this.sendXml(res, 200, errorXml(4100, 'Validation failed'));
}
const reportId = nonBlankString(fileDetails?.reportId);
const fileId = nonBlankString(fileDetails?.fileId);
const record = reportId ? this.reports.get(reportId) : null;
if (!record) {
return this.sendXml(res, 200, errorXml(5001, 'Report does not exist'));
}
if (record.retracted) {
return this.sendXml(res, 200, errorXml(5101, 'Report already retracted'));
}
if (record.finished) {
return this.sendXml(res, 200, errorXml(5102, 'Report already finished'));
}
const file = record.files.find((f) => f.fileId === fileId);
if (!file) {
return this.sendXml(res, 200, errorXml(5002, 'File does not exist'));
}
file.submittedDetails = true;
file.detailsXml = body;
return this.sendXml(res, 200, ok(`<reportId>${reportId}</reportId>`));
}
private async handleFinish(request: Request, res: http.ServerResponse, failure: FakeNcmecFailure): Promise<void> {
if (failure === 'finish') {
return this.sendXml(res, 200, errorXml(1300, 'Update failed'));
}
const form = await request.formData();
const reportId = String(form.get('id') ?? '');
const record = this.reports.get(reportId);
if (!record) {
return this.sendXml(res, 200, errorXml(5001, 'Report does not exist'));
}
if (record.retracted) {
return this.sendXml(res, 200, errorXml(5101, 'Report already retracted'));
}
if (record.finished) {
return this.sendXml(res, 200, errorXml(5102, 'Report already finished'));
}
record.finished = true;
const files = record.files.map((f) => ` <fileId>${f.fileId}</fileId>`).join('\n');
const body = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<reportDoneResponse>
<responseCode>0</responseCode>
<reportId>${reportId}</reportId>
<files>
${files}
</files>
</reportDoneResponse>`;
return this.sendXml(res, 200, body);
}
private async handleRetract(request: Request, res: http.ServerResponse, failure: FakeNcmecFailure): Promise<void> {
if (failure === 'retract') {
return this.sendXml(res, 200, errorXml(1300, 'Update failed'));
}
const form = await request.formData();
const reportId = String(form.get('id') ?? '');
const record = this.reports.get(reportId);
if (!record) {
return this.sendXml(res, 200, errorXml(5001, 'Report does not exist'));
}
if (record.retracted) {
return this.sendXml(res, 200, errorXml(5101, 'Report already retracted'));
}
if (record.finished) {
return this.sendXml(res, 200, errorXml(5102, 'Report already finished'));
}
record.retracted = true;
return this.sendXml(res, 200, ok(`<reportId>${reportId}</reportId>`));
}
private checkBasicAuth(req: http.IncomingMessage): boolean {
const header = req.headers.authorization ?? '';
if (!header.toLowerCase().startsWith('basic ')) return false;
const decoded = Buffer.from(header.slice(6), 'base64').toString('utf-8');
const idx = decoded.indexOf(':');
if (idx === -1) return false;
const user = decoded.slice(0, idx);
const pass = decoded.slice(idx + 1);
return user === this.options.username && pass === this.options.password;
}
private async toWebRequest(req: http.IncomingMessage, url: URL): Promise<Request> {
const headers = new Headers();
for (const [k, v] of Object.entries(req.headers)) {
if (v === undefined) continue;
if (Array.isArray(v)) headers.set(k, v.join(','));
else headers.set(k, String(v));
}
const method = req.method ?? 'GET';
const body = method === 'GET' || method === 'HEAD' ? undefined : (Readable.toWeb(req) as ReadableStream);
return new Request(url, {method, headers, body, duplex: 'half'} as RequestInit & {
duplex: 'half';
});
}
private sendXml(res: http.ServerResponse, status: number, body: string): void {
res.writeHead(status, {'content-type': 'application/xml; charset=utf-8'});
res.end(body);
}
private sendText(res: http.ServerResponse, status: number, body: string): void {
res.writeHead(status, {'content-type': 'text/plain; charset=utf-8'});
res.end(body);
}
private sendStatus(res: http.ServerResponse, status: number, body: string): void {
res.writeHead(status);
res.end(body);
}
}
function ok(extra: string): string {
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<reportResponse>
<responseCode>0</responseCode>
<responseDescription>Success</responseDescription>
${extra}
</reportResponse>`;
}
function errorXml(code: number, description: string): string {
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<reportResponse>
<responseCode>${code}</responseCode>
<responseDescription>${description}</responseDescription>
</reportResponse>`;
}
function isXmlContentType(request: Request): boolean {
return (request.headers.get('content-type') ?? '').toLowerCase().includes('text/xml');
}
function parseReportDocument(xml: string): Record<string, unknown> | null {
const parsed = xmlParser.parse(xml) as Record<string, unknown>;
return objectField(parsed, 'report');
}
function parseFileDetailsDocument(xml: string): Record<string, unknown> | null {
const parsed = xmlParser.parse(xml) as Record<string, unknown>;
return objectField(parsed, 'fileDetails');
}
function hasRequiredReportFields(report: Record<string, unknown> | null): boolean {
const incidentSummary = objectField(report, 'incidentSummary');
const reporter = objectField(report, 'reporter');
const reportingPerson = objectField(reporter, 'reportingPerson');
return Boolean(
nonBlankString(incidentSummary?.incidentType) &&
nonBlankString(incidentSummary?.incidentDateTime) &&
nonBlankString(reportingPerson?.email),
);
}
function hasFileDetailsFields(fileDetails: Record<string, unknown> | null): boolean {
return Boolean(nonBlankString(fileDetails?.reportId) && nonBlankString(fileDetails?.fileId));
}
function objectField(value: unknown, key: string): Record<string, unknown> | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
const field = (value as Record<string, unknown>)[key];
if (!field || typeof field !== 'object' || Array.isArray(field)) return null;
return field as Record<string, unknown>;
}
function nonBlankString(value: unknown): string | null {
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed || null;
}
if (typeof value === 'number' || typeof value === 'bigint') {
return String(value);
}
return null;
}
@@ -3,7 +3,26 @@
import crypto from 'node:crypto';
import {XMLParser, XMLValidator} from 'fast-xml-parser';
import {HttpResponse, http, type RequestHandler} from 'msw';
import type {FakeNcmecFailure, FakeNcmecFileRecord, FakeNcmecReportRecord} from '../../fake-ncmec/FakeNcmecServer';
type FakeNcmecFailure = 'none' | 'submit' | 'upload' | 'fileinfo' | 'finish' | 'retract' | 'unauthorized';
interface FakeNcmecFileRecord {
fileId: string;
filename: string;
size: number;
md5: string;
submittedDetails: boolean;
detailsXml: string | null;
}
interface FakeNcmecReportRecord {
reportId: string;
reportXml: string;
files: Array<FakeNcmecFileRecord>;
finished: boolean;
retracted: boolean;
authUsername: string;
}
const BASE_URL = 'https://ncmec.test/ispws';
const USERNAME = 'usr123';