115 lines
3.5 KiB
TypeScript
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));
|
|
});
|
|
});
|
|
}
|