157 lines
5.9 KiB
JavaScript
157 lines
5.9 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { spawn } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import http from 'node:http';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
const root = path.resolve(import.meta.dirname, '../..');
|
|
function listen(server) {
|
|
return new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve(server.address().port)));
|
|
}
|
|
|
|
async function freePort() {
|
|
const server = http.createServer();
|
|
const port = await listen(server);
|
|
await new Promise((resolve) => server.close(resolve));
|
|
return port;
|
|
}
|
|
|
|
function socketRequest(socketPath, pathname) {
|
|
return new Promise((resolve, reject) => {
|
|
const request = http.request({ socketPath, path: pathname }, (response) => {
|
|
const chunks = [];
|
|
response.on('data', (chunk) => chunks.push(chunk));
|
|
response.on('end', () => {
|
|
try {
|
|
resolve({ status: response.statusCode, body: JSON.parse(Buffer.concat(chunks).toString('utf8')) });
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
});
|
|
request.on('error', reject);
|
|
request.end();
|
|
});
|
|
}
|
|
|
|
async function httpText(url) {
|
|
const response = await fetch(url);
|
|
return { status: response.status, type: response.headers.get('content-type'), body: await response.text() };
|
|
}
|
|
|
|
async function waitFor(probe, child, stderr) {
|
|
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
if (child.exitCode !== null) throw new Error(`Compiled Harbor exited early: ${stderr()}`);
|
|
try {
|
|
const result = await probe();
|
|
if (result) return result;
|
|
} catch {
|
|
// The disposable listener is still starting.
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
}
|
|
throw new Error(`Compiled Harbor did not become ready: ${stderr()}`);
|
|
}
|
|
|
|
async function stop(child) {
|
|
child.kill('SIGTERM');
|
|
if (child.exitCode === null) await new Promise((resolve) => child.once('exit', resolve));
|
|
assert.equal(child.exitCode, 0);
|
|
}
|
|
|
|
test('compiled dispatcher starts and stops control and dataplane contracts', async (t) => {
|
|
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-compiled-'));
|
|
const controlData = path.join(directory, 'control');
|
|
const dataplaneData = path.join(directory, 'dataplane');
|
|
const socketPath = path.join(directory, 'dataplane.sock');
|
|
const port = await freePort();
|
|
fs.mkdirSync(controlData);
|
|
fs.mkdirSync(dataplaneData);
|
|
const children = [];
|
|
t.after(async () => {
|
|
for (const child of children) {
|
|
if (child.exitCode === null) await stop(child);
|
|
}
|
|
fs.rmSync(directory, { recursive: true, force: true });
|
|
});
|
|
|
|
const start = (env) => {
|
|
const child = spawn(process.execPath, ['dist/server/main.js'], {
|
|
cwd: root,
|
|
env: { ...process.env, ...env },
|
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
});
|
|
let stderr = '';
|
|
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
children.push(child);
|
|
return { child, stderr: () => stderr };
|
|
};
|
|
|
|
const control = start({
|
|
APP_COMPONENT: 'control',
|
|
APP_MODE: 'client',
|
|
DATA_DIR: controlData,
|
|
DIST_DIR: '',
|
|
HARBOR_HOST_NETWORK_STATE: path.join(directory, 'missing-network.json'),
|
|
PORT: String(port),
|
|
SING_BOX_CACHE: path.join(controlData, 'cache.db'),
|
|
});
|
|
const state = await waitFor(async () => {
|
|
const response = await fetch(`http://127.0.0.1:${port}/api/state`);
|
|
return response.ok ? response.json() : null;
|
|
}, control.child, control.stderr);
|
|
assert.equal(state.apiVersion, 1);
|
|
assert.equal(state.mode, 'client');
|
|
const page = await httpText(`http://127.0.0.1:${port}/`);
|
|
assert.equal(page.status, 200);
|
|
assert.match(page.type, /^text\/html/);
|
|
assert.match(page.body, /<div id="root"><\/div>/);
|
|
await stop(control.child);
|
|
|
|
const dataplane = start({
|
|
APP_COMPONENT: 'dataplane',
|
|
APP_MODE: 'gateway',
|
|
DATA_DIR: dataplaneData,
|
|
DATAPLANE_SOCKET: socketPath,
|
|
SING_BOX_CACHE: path.join(dataplaneData, 'cache.db'),
|
|
SING_BOX_CONFIG: path.join(dataplaneData, 'missing-config.json'),
|
|
});
|
|
const status = await waitFor(async () => {
|
|
const response = await socketRequest(socketPath, '/status');
|
|
return response.status === 200 ? response.body : null;
|
|
}, dataplane.child, dataplane.stderr);
|
|
assert.equal(status.ready, true);
|
|
await stop(dataplane.child);
|
|
});
|
|
|
|
test('production paths use only the compiled dispatcher', () => {
|
|
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
|
const packageJson = JSON.parse(read('package.json'));
|
|
const main = read('src/server/main.ts');
|
|
const gatewayEntrypoint = read('entrypoint.sh');
|
|
const clientEntrypoint = read('entrypoint.client.sh');
|
|
const workflow = read('.gitea/workflows/gateway-build.yml');
|
|
const legacyBuild = read('scripts/build-on-107-deploy-111.sh');
|
|
const dockerignore = read('.dockerignore');
|
|
|
|
assert.equal(packageJson.scripts['build:production'], 'npm run build && npm run build:server');
|
|
assert.equal(packageJson.scripts.prestart, 'npm run build:production');
|
|
assert.equal(packageJson.scripts.start, 'node dist/server/main.js');
|
|
assert.match(main, /APP_COMPONENT === 'dataplane'/);
|
|
assert.match(main, /process\.env\.DIST_DIR \|\|= path\.resolve\('dist'\)/);
|
|
assert.match(main, /import\('\.\/dataplane\.js'\)/);
|
|
assert.match(main, /import\('\.\/index\.js'\)/);
|
|
for (const entrypoint of [gatewayEntrypoint, clientEntrypoint]) {
|
|
assert.match(entrypoint, /node \/app\/dist\/server\/main\.js/);
|
|
assert.doesNotMatch(entrypoint, /\/app\/src/);
|
|
}
|
|
assert.match(workflow, /npm run build:production/);
|
|
assert.match(workflow, /NODE_BUILD_IMAGE: mirror\.gcr\.io\/library\/node:20\.19-alpine/);
|
|
assert.match(workflow, /docker run --rm "\$\{\{ env\.NODE_BUILD_IMAGE \}\}" sh -lc 'command -v npm/);
|
|
assert.doesNotMatch(workflow, /docker image inspect "\$\{\{ env\.NODE_BUILD_IMAGE \}\}"/);
|
|
assert.match(legacyBuild, /npm run build:production && docker build/);
|
|
assert.match(dockerignore, /^dist$/m);
|
|
});
|