Files
harbor-net/test/server/compiled-entrypoint.test.js
T
dokril 3204ecf4a5
Build and Deploy Gateway / build-and-push (push) Successful in 36s
Build and Deploy Gateway / deploy (push) Successful in 18s
Retry sing-box downloads in runtime builds
2026-08-09 13:33:28 +03:00

182 lines
6.7 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-bookworm/);
assert.match(workflow, /command -v npm[^']+command -v git[^']+test -x \/bin\/bash/);
assert.doesNotMatch(workflow, /docker image inspect "\$\{\{ env\.NODE_BUILD_IMAGE \}\}"/);
assert.match(legacyBuild, /npm run build:production && docker build/);
assert.match(dockerignore, /^dist$/m);
});
test('every shipped build defaults to sing-box 1.13.18', () => {
for (const file of [
'.env.example',
'.gitea/workflows/gateway-build.yml',
'Dockerfile',
'Dockerfile.client',
'Dockerfile.runtime-base',
'docker-compose.client.yml',
'docker-compose.gateway.yml',
'scripts/build-on-107-deploy-111.sh',
'scripts/build-runtime-base.sh',
]) {
assert.match(fs.readFileSync(path.join(root, file), 'utf8'), /SINGBOX_VERSION[^\n]*1\.13\.18/);
}
});
test('runtime builds retry transient sing-box release download failures', () => {
for (const file of ['Dockerfile', 'Dockerfile.client', 'Dockerfile.runtime-base']) {
assert.match(
fs.readFileSync(path.join(root, file), 'utf8'),
/curl --retry 5 --retry-all-errors --retry-delay 2 -fsSL .*SagerNet\/sing-box/,
);
}
});