Files
creator-hub/scripts/dev-backend.test.mjs
T

129 lines
5.0 KiB
JavaScript

import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { once } from 'node:events';
import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
const script = fileURLToPath(new URL('./dev-backend.mjs', import.meta.url));
function fixture(t, source, options = {}) {
const root = mkdtempSync(path.join(tmpdir(), 'creator-backend-test-'));
mkdirSync(path.join(root, 'scripts'));
mkdirSync(path.join(root, 'bin'));
copyFileSync(script, path.join(root, 'scripts/dev-backend.mjs'));
if (options.envFile) writeFileSync(path.join(root, '.env'), options.envFile);
writeFileSync(path.join(root, 'bin/air'), `#!${process.execPath}\n${source}`, { mode: 0o755 });
const children = [];
t.after(async () => {
for (const child of children) {
if (child.backendPID) {
try { process.kill(child.backendPID, 'SIGTERM'); } catch (error) {
if (error.code !== 'ESRCH') throw error;
}
}
if (child.exitCode === null && child.signalCode === null) {
const closed = once(child, 'close');
child.kill('SIGTERM');
await closed;
}
}
rmSync(root, { recursive: true, force: true });
});
return () => {
const child = spawn(process.execPath, [path.join(root, 'scripts/dev-backend.mjs')], {
env: { ...process.env, PATH: options.noFlock ? path.join(root, 'bin') : `${path.join(root, 'bin')}:${process.env.PATH}` },
stdio: ['ignore', 'pipe', 'pipe'],
});
children.push(child);
child.errors = '';
child.output = '';
child.stdout.on('data', (chunk) => {
child.output += chunk;
const pid = /ready:(\d+)/.exec(chunk.toString());
if (pid) child.backendPID = Number(pid[1]);
});
child.stderr.on('data', (chunk) => { child.errors += chunk; });
return child;
};
}
async function ready(child) {
const [chunk] = await once(child.stdout, 'data', { signal: AbortSignal.timeout(1500) });
assert.match(chunk.toString(), /ready:/);
}
function exited(child) {
return once(child, 'close', { signal: AbortSignal.timeout(1500) });
}
const server = `
process.on('SIGTERM', () => process.exit(0));
process.on('SIGINT', () => process.exit(0));
console.log('ready:' + process.pid);
setInterval(() => {}, 1000);
`;
test('rejects a second backend without disturbing the first', { timeout: 5000 }, async (t) => {
const start = fixture(t, server);
const first = start();
await ready(first);
const second = start();
const [code] = await exited(second);
assert.equal(code, 73);
assert.match(second.errors, /后台已在运行/);
assert.equal(first.exitCode, null);
});
test('forwards termination and releases the lock for the next launch', { timeout: 5000 }, async (t) => {
const start = fixture(t, server);
const first = start();
await ready(first);
const stopped = exited(first);
first.kill('SIGTERM');
const [code] = await stopped;
assert.equal(code, 0);
const second = start();
await ready(second);
const closed = exited(second);
second.kill('SIGINT');
assert.equal((await closed)[0], 0);
});
test('reports backend startup failure and allows a retry', { timeout: 5000 }, async (t) => {
const start = fixture(t, "console.error('build failed'); process.exit(17);");
const first = start();
assert.equal((await exited(first))[0], 17);
assert.match(first.errors, /build failed/);
const second = start();
assert.equal((await exited(second))[0], 17);
});
test('preserves environment configuration when launching through the lock', { timeout: 5000 }, async (t) => {
const start = fixture(t, "console.log(process.env.LISTEN_ADDR); console.log(process.env.CREATORHUB_CREDENTIAL_MASTER_KEY);", {
envFile: `\n# ignored comment\ninvalid line\nLISTEN_ADDR=:19091\nLISTEN_ADDR=:19092\nCREATORHUB_CREDENTIAL_MASTER_KEY=${Buffer.alloc(32, 1).toString('base64')}\nCONTROL_PLANE_USERNAME=test\nCONTROL_PLANE_PASSWORD=test\nDATABASE_URL=postgres://test\nWEB_DIR=/tmp/web\nCREATOR_COVER_DIR=/tmp/covers\nCREATORHUB_CREDENTIAL_STORE_DIR=/tmp/credentials\nLOG_LEVEL=info\nNATIVE_GATEWAY_ENDPOINT=http://127.0.0.1:28187\n`,
});
const child = start();
assert.equal((await exited(child))[0], 0);
assert.match(child.output, /:19091/);
assert.match(child.output, /AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=/);
});
test('preserves a signal-caused backend failure instead of reporting success', { timeout: 5000 }, async (t) => {
const start = fixture(t, server);
const child = start();
await ready(child);
const closed = exited(child);
process.kill(child.backendPID, 'SIGKILL');
assert.equal((await closed)[0], 137);
});
test('reports a missing lock command instead of silently exiting successfully', { timeout: 5000 }, async (t) => {
const start = fixture(t, server, { noFlock: true });
const child = start();
assert.equal((await exited(child))[0], 1);
assert.match(child.errors, /启动失败.*ENOENT/);
});