165 lines
5.1 KiB
JavaScript
165 lines
5.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import test from 'node:test';
|
|
|
|
import { buildGatewayVersionInfo, buildVersionInfo } from '../../dist/server/version.js';
|
|
import { createVersionRoute } from '../../dist/server/http/routes/versionRoute.js';
|
|
import { HARBOR_VERSIONS, versionCompatibility } from '../../dist/shared/versions.js';
|
|
|
|
function response() {
|
|
return {
|
|
writeHead(status, headers) {
|
|
this.status = status;
|
|
this.headers = headers;
|
|
},
|
|
end(payload) {
|
|
this.rawPayload = payload;
|
|
this.payload = JSON.parse(payload);
|
|
},
|
|
};
|
|
}
|
|
|
|
const controlVersionInfo = {
|
|
apiVersion: 1,
|
|
location: 'gateway',
|
|
components: { gatewayBackend: '0.21.17' },
|
|
runtime: { singBox: '1.12.13' },
|
|
};
|
|
|
|
test('component versions enforce one Harbor major and one Gateway major.minor', () => {
|
|
assert.deepEqual(versionCompatibility(HARBOR_VERSIONS), {
|
|
compatible: true,
|
|
major: true,
|
|
gateway: true,
|
|
});
|
|
assert.equal(versionCompatibility({
|
|
macClient: '1.9.4',
|
|
gatewayClient: '1.3.1',
|
|
gatewayBackend: '1.3.8',
|
|
}).compatible, true);
|
|
assert.equal(versionCompatibility({
|
|
macClient: '1.0.0',
|
|
gatewayClient: '1.3.0',
|
|
gatewayBackend: '1.4.0',
|
|
}).compatible, false);
|
|
});
|
|
|
|
test('gateway reports control backend and deployed dataplane separately', () => {
|
|
const control = {
|
|
apiVersion: 1,
|
|
location: 'gateway',
|
|
components: { gatewayBackend: '0.4.0' },
|
|
runtime: { singBox: '1.12.13' },
|
|
};
|
|
assert.deepEqual(buildGatewayVersionInfo(control, {
|
|
gatewayBackendVersion: '0.3.0',
|
|
singBoxVersion: '1.12.13',
|
|
}), {
|
|
apiVersion: 1,
|
|
location: 'gateway',
|
|
components: { gatewayBackend: '0.4.0' },
|
|
runtime: { dataplaneVersion: '0.3.0', singBox: '1.12.13' },
|
|
});
|
|
});
|
|
|
|
test('runtime version info reports the installed sing-box binary', () => {
|
|
const run = () => ({ stdout: 'sing-box version 1.12.13\n', stderr: '' });
|
|
assert.deepEqual(buildVersionInfo('gateway', run), {
|
|
apiVersion: 1,
|
|
location: 'gateway',
|
|
components: { gatewayBackend: HARBOR_VERSIONS.gatewayBackend },
|
|
runtime: { singBox: '1.12.13' },
|
|
});
|
|
});
|
|
|
|
test('version route returns prebuilt local info without a dataplane refresh', async () => {
|
|
const res = response();
|
|
const route = createVersionRoute({
|
|
versionInfo: controlVersionInfo,
|
|
refreshDataplaneRuntime: null,
|
|
});
|
|
|
|
assert.equal(await route.handle({ method: 'GET', url: '/api/version' }, res), true);
|
|
assert.equal(res.status, 200);
|
|
assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
|
|
assert.equal(res.rawPayload.endsWith('\n'), false);
|
|
assert.deepEqual(res.payload, controlVersionInfo);
|
|
});
|
|
|
|
test('version route refreshes split Gateway once and preserves remote runtime overlay', async () => {
|
|
let refreshes = 0;
|
|
const route = createVersionRoute({
|
|
versionInfo: controlVersionInfo,
|
|
refreshDataplaneRuntime: async () => {
|
|
refreshes += 1;
|
|
return {
|
|
gatewayBackendVersion: '0.21.16',
|
|
singBoxVersion: '1.12.14',
|
|
};
|
|
},
|
|
});
|
|
const res = response();
|
|
|
|
assert.equal(await route.handle({ method: 'GET', url: '/api/version' }, res), true);
|
|
assert.equal(refreshes, 1);
|
|
assert.deepEqual(res.payload, {
|
|
apiVersion: 1,
|
|
location: 'gateway',
|
|
components: { gatewayBackend: '0.21.17' },
|
|
runtime: { dataplaneVersion: '0.21.16', singBox: '1.12.14' },
|
|
});
|
|
});
|
|
|
|
test('version route keeps remote null fallbacks', async () => {
|
|
const route = createVersionRoute({
|
|
versionInfo: controlVersionInfo,
|
|
refreshDataplaneRuntime: async () => ({}),
|
|
});
|
|
const res = response();
|
|
await route.handle({ method: 'GET', url: '/api/version' }, res);
|
|
assert.deepEqual(res.payload.runtime, { dataplaneVersion: null, singBox: null });
|
|
});
|
|
|
|
test('version route exact guard avoids refresh and refresh errors propagate unchanged', async () => {
|
|
let refreshes = 0;
|
|
const refreshError = new Error('status failed');
|
|
const route = createVersionRoute({
|
|
versionInfo: controlVersionInfo,
|
|
refreshDataplaneRuntime: async () => {
|
|
refreshes += 1;
|
|
throw refreshError;
|
|
},
|
|
});
|
|
|
|
for (const [method, url] of [
|
|
['POST', '/api/version'],
|
|
['GET', '/api/version?source=client'],
|
|
['GET', '/api/version/'],
|
|
['GET', '/api/other'],
|
|
]) {
|
|
assert.equal(await route.handle({ method, url }, response()), false);
|
|
}
|
|
assert.equal(refreshes, 0);
|
|
|
|
const res = response();
|
|
await assert.rejects(
|
|
route.handle({ method: 'GET', url: '/api/version' }, res),
|
|
(error) => error === refreshError,
|
|
);
|
|
assert.equal(refreshes, 1);
|
|
assert.equal(res.status, undefined);
|
|
});
|
|
|
|
test('version route is the sole HTTP owner', () => {
|
|
const index = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8');
|
|
const route = readFileSync(
|
|
new URL('../../src/server/http/routes/versionRoute.ts', import.meta.url),
|
|
'utf8',
|
|
);
|
|
assert.match(index, /createVersionRoute\(\{/);
|
|
assert.match(index, /versionRoute\.handle\(req, res\)/);
|
|
assert.doesNotMatch(index, /['"]\/api\/version['"]|buildGatewayVersionInfo|sendJson/);
|
|
assert.match(route, /req\.url !== '\/api\/version'/);
|
|
assert.match(route, /buildGatewayVersionInfo\(/);
|
|
});
|