Files
2026-07-24 18:39:08 -07:00

115 lines
3.5 KiB
TypeScript

import assert from 'node:assert/strict';
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
import fs from 'node:fs/promises';
import net from 'node:net';
import os from 'node:os';
import path from 'node:path';
import { test } from 'node:test';
const requiredTradePaths = [
'/wallet/foreign',
'/wallet/foreign/derive',
'/trade/qortal/create-sell-offer',
'/trade/qortal/respond-sell-offer',
'/trade/qortal/respond-multiple-sell-offers',
'/trade/qortal/cancel-offer',
'/trade/foreign/balance',
'/trade/foreign/send',
];
test('OpenAPI advertises dedicated trade helper endpoints', { timeout: 15_000 }, async () => {
const port = await availablePort();
const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'qortal-auth-openapi-'));
const daemon = spawnDaemon(port, dataDir);
try {
await waitForDaemon(port, daemon);
const response = await fetch(`http://127.0.0.1:${port}/openapi.json`);
assert.equal(response.status, 200);
const document = (await response.json()) as {
paths?: Record<string, Record<string, unknown>>;
};
for (const routePath of requiredTradePaths) {
assert.ok(document.paths?.[routePath], `${routePath} is missing from OpenAPI paths`);
const operation = routePath === '/wallet/foreign' ? 'get' : 'post';
assert.ok(document.paths?.[routePath]?.[operation], `${routePath} is missing ${operation.toUpperCase()} operation`);
}
} finally {
daemon.kill('SIGTERM');
await onceExit(daemon);
await fs.rm(dataDir, { recursive: true, force: true });
}
});
function spawnDaemon(port: number, dataDir: string) {
const daemon = spawn(
process.execPath,
[path.resolve(process.cwd(), '../../node_modules/tsx/dist/cli.mjs'), 'src/index.ts'],
{
cwd: process.cwd(),
env: {
...process.env,
QORTAL_AUTH_HOST: '127.0.0.1',
QORTAL_AUTH_PORT: String(port),
QORTAL_AUTH_DATA_DIR: dataDir,
QORTAL_AUTH_BODY_LIMIT_BYTES: '1048576',
},
stdio: ['ignore', 'pipe', 'pipe'],
},
);
return daemon;
}
async function waitForDaemon(port: number, daemon: ChildProcessWithoutNullStreams) {
let output = '';
daemon.stdout.on('data', (chunk) => {
output += chunk.toString();
});
daemon.stderr.on('data', (chunk) => {
output += chunk.toString();
});
const deadline = Date.now() + 10_000;
while (Date.now() < deadline) {
if (daemon.exitCode !== null) {
throw new Error(`daemon exited before becoming ready: ${output}`);
}
try {
const response = await fetch(`http://127.0.0.1:${port}/health`);
if (response.ok) return;
} catch {
await delay(100);
}
}
throw new Error(`daemon did not become ready: ${output}`);
}
function onceExit(child: ChildProcessWithoutNullStreams) {
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
return new Promise<void>((resolve) => {
child.once('exit', () => resolve());
});
}
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function availablePort() {
return new Promise<number>((resolve, reject) => {
const server = net.createServer();
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
const address = server.address();
if (!address || typeof address === 'string') {
server.close(() => reject(new Error('unable to allocate test port')));
return;
}
server.close(() => resolve(address.port));
});
});
}