diff --git a/package.json b/package.json index dcf6de4c8..4ed06e25f 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "test": "jest", "scripts": { "test-unit": "jest ./src/tests/unit_tests", - "test": "ava", + "test": "ts-node ./testing/check-integration-env.ts && ava", "docker-dev": "nodemon ./app.js", "build": "tsc", "start-ts": "ts-node ./app.ts", diff --git a/src/tests/types/index.ts b/src/tests/types/index.ts index c493c3bc1..bb7baf8be 100644 --- a/src/tests/types/index.ts +++ b/src/tests/types/index.ts @@ -25,4 +25,5 @@ export interface Headers { export interface RequestArgs { headers: Headers body: RequestBody + timeout?: number } diff --git a/src/tests/utils/helpers.ts b/src/tests/utils/helpers.ts index 9c53ef051..488af5608 100644 --- a/src/tests/utils/helpers.ts +++ b/src/tests/utils/helpers.ts @@ -5,6 +5,10 @@ import { NodeConfig, RequestArgs, RequestBody } from '../types' import { config } from '../config' import * as hmac from '../../crypto/hmac' +export const TEST_HTTP_TIMEOUT_MS = Number( + process.env.SPHINX_TEST_HTTP_TIMEOUT_MS || 10000 +) + export const makeArgs = ( node: NodeConfig, body: RequestBody = {}, @@ -27,7 +31,7 @@ export const makeArgs = ( } else { headers['x-user-token'] = node.authToken } - return { body, headers } + return { body, headers, timeout: TEST_HTTP_TIMEOUT_MS } } export const makeRelayRequest = async ( @@ -100,7 +104,9 @@ export async function getToken(t, node) { const protocol = memeProtocol(config.memeHost) //get authentication challenge from meme server - const r = await http.get(`${protocol}://${config.memeHost}/ask`) + const r = await http.get(`${protocol}://${config.memeHost}/ask`, { + timeout: TEST_HTTP_TIMEOUT_MS, + }) t.truthy(r, 'r should exist') t.truthy(r.challenge, 'r.challenge should exist') @@ -115,6 +121,7 @@ export async function getToken(t, node) { //get server token const r3 = await http.post(`${protocol}://${config.memeHost}/verify`, { form: { id: r.id, sig: r2.response.sig, pubkey: node.pubkey }, + timeout: TEST_HTTP_TIMEOUT_MS, }) t.truthy(r3, 'r3 should exist') t.truthy(r3.token, 'r3.token should exist') @@ -141,6 +148,7 @@ export function makeJwtArgs(jwt, body) { return { headers: { 'x-jwt': jwt }, body, + timeout: TEST_HTTP_TIMEOUT_MS, } } diff --git a/testing/README.md b/testing/README.md index 7da362e9e..21d4cd93d 100644 --- a/testing/README.md +++ b/testing/README.md @@ -48,5 +48,11 @@ Once you've done the above, you can run the tests with [ava](https://github.com/ npm run test ``` +`npm run test` first checks that every relay in `src/tests/configs/nodes.json` +responds to `/contacts`. If the stack is not ready, it fails before starting +AVA and prints the unreachable node aliases. Use `SPHINX_TEST_ENV_TIMEOUT_MS` +to adjust the readiness timeout and `SPHINX_TEST_HTTP_TIMEOUT_MS` to adjust +the per-request timeout used by test helpers. + The tests expect both the relay server and sphinx-stack from the setup steps above to be running. diff --git a/testing/check-integration-env.ts b/testing/check-integration-env.ts new file mode 100644 index 000000000..6dbb51076 --- /dev/null +++ b/testing/check-integration-env.ts @@ -0,0 +1,122 @@ +import * as fs from 'fs' +import * as http from 'http' +import * as https from 'https' +import * as path from 'path' + +interface TestNode { + alias?: string + external_ip?: string + authToken?: string +} + +const nodesPath = path.join(process.cwd(), 'src/tests/configs/nodes.json') +const timeoutMs = Number(process.env.SPHINX_TEST_ENV_TIMEOUT_MS || 5000) + +function readNodes(): TestNode[] { + if (!fs.existsSync(nodesPath)) { + throw new Error( + `Missing ${nodesPath}. Start the sphinx-stack test environment and copy relay/NODES.json into src/tests/configs/nodes.json before running integration tests.` + ) + } + + const nodes = JSON.parse(fs.readFileSync(nodesPath, 'utf8')) + if (!Array.isArray(nodes) || nodes.length === 0) { + throw new Error(`${nodesPath} must contain at least one relay node config.`) + } + + return nodes +} + +function checkNode(node: TestNode): Promise { + if (!node.external_ip) { + return Promise.reject(new Error('missing external_ip')) + } + + const url = new URL('/contacts', node.external_ip) + const client = url.protocol === 'https:' ? https : http + + return new Promise((resolve, reject) => { + const req = client.request( + url, + { + method: 'GET', + headers: { + 'x-user-token': node.authToken || '', + }, + timeout: timeoutMs, + }, + (res) => { + res.resume() + res.on('end', () => { + if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { + resolve() + } else { + reject(new Error(`HTTP ${res.statusCode}`)) + } + }) + } + ) + + req.on('timeout', () => { + req.destroy(new Error(`timed out after ${timeoutMs}ms`)) + }) + req.on('error', reject) + req.end() + }) +} + +function formatError(error: unknown): string { + if (error && typeof error === 'object') { + const err = error as { + address?: string + code?: string + message?: string + port?: number + } + const details = [err.code, err.message, err.address, err.port] + .filter((detail) => detail !== undefined && detail !== '') + .join(' ') + + if (details) return details + } + + return error instanceof Error ? error.message : String(error) +} + +async function main() { + const nodes = readNodes() + const failures: string[] = [] + + await Promise.all( + nodes.map(async (node, index) => { + const label = node.alias || `node ${index + 1}` + try { + await checkNode(node) + } catch (error) { + const message = formatError(error) + failures.push( + `${label} (${node.external_ip || 'missing external_ip'}): ${message}` + ) + } + }) + ) + + if (failures.length > 0) { + throw new Error( + [ + 'Sphinx integration test environment is not ready.', + ...failures.map((failure) => `- ${failure}`), + 'Start sphinx-stack/relay test services or refresh src/tests/configs/nodes.json, then rerun npm test.', + ].join('\n') + ) + } + + console.log( + `Sphinx integration test environment ready: ${nodes.length} relay node(s) responded within ${timeoutMs}ms.` + ) +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : error) + process.exit(1) +})