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>; }; 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((resolve) => { child.once('exit', () => resolve()); }); } function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } function availablePort() { return new Promise((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)); }); }); }