Update Harbor client and gateway functionality
This commit is contained in:
@@ -5,8 +5,8 @@ import test from 'node:test';
|
||||
import { createRouteRulesService } from '../../dist/server/features/routing/index.js';
|
||||
import { createRouteRulesRoute } from '../../dist/server/http/routes/routeRulesRoute.js';
|
||||
|
||||
const oldRules = [{ type: 'domain_suffix', value: 'old.example', enabled: true }];
|
||||
const newRules = [{ type: 'domain_suffix', value: 'new.example', enabled: true }];
|
||||
const oldRules = [{ type: 'domain_suffix', value: 'old.example', enabled: true, outbound: 'direct' }];
|
||||
const newRules = [{ type: 'domain_suffix', value: 'new.example', enabled: true, outbound: 'vpn' }];
|
||||
const server = { id: 'server', label: 'Server', host: 'server.example', port: 443, protocol: 'vless' };
|
||||
const storedProfile = (servers = [server]) => ({
|
||||
id: 'primary',
|
||||
@@ -78,7 +78,7 @@ function createHarness(overrides = {}) {
|
||||
write: (value) => {
|
||||
events.push('config.write');
|
||||
if (failures.configWrite) throw failures.configWrite;
|
||||
config = JSON.stringify(value);
|
||||
config = JSON.stringify(value, null, 2);
|
||||
if (failures.configWriteAfter) throw failures.configWriteAfter;
|
||||
},
|
||||
restore: (value) => {
|
||||
@@ -103,6 +103,7 @@ function createHarness(overrides = {}) {
|
||||
if (failures.runtimeRestore) throw failures.runtimeRestore;
|
||||
},
|
||||
},
|
||||
route: { isGatewayDirect: () => overrides.gatewayDirect === true },
|
||||
serialize,
|
||||
runOperation: async (operation) => {
|
||||
events.push('operation');
|
||||
@@ -130,27 +131,28 @@ function assertDomainRestored(actual, expected) {
|
||||
|
||||
test('route rules validate strictly, conflict before no-op, and preserve no-op revisions', async () => {
|
||||
const harness = createHarness();
|
||||
assert.throws(() => harness.service.update('bad', 2, undefined), (error) => error.code === 'REQUEST_INVALID');
|
||||
assert.throws(() => harness.service.update(newRules, -1, undefined), (error) => error.code === 'REQUEST_INVALID');
|
||||
await assert.rejects(harness.service.update(oldRules, 1, undefined), (error) => error.code === 'STATE_CONFLICT');
|
||||
assert.throws(() => harness.service.update(newRules, 2, undefined), (error) => error.code === 'REQUEST_INVALID');
|
||||
assert.throws(() => harness.service.update('bad', 2, 2), (error) => error.code === 'REQUEST_INVALID');
|
||||
assert.throws(() => harness.service.update(newRules, -1, 2), (error) => error.code === 'REQUEST_INVALID');
|
||||
assert.throws(
|
||||
() => harness.service.update([{ type: 'domain', value: 'example.com', enabled: true }], 2, 2),
|
||||
(error) => error.code === 'REQUEST_INVALID',
|
||||
);
|
||||
await assert.rejects(harness.service.update(oldRules, 1, 2), (error) => error.code === 'STATE_CONFLICT');
|
||||
const before = harness.snapshot();
|
||||
await harness.service.update(oldRules, 2, undefined);
|
||||
await harness.service.update(oldRules, 2, 2);
|
||||
assert.deepEqual(harness.snapshot(), before);
|
||||
assert.deepEqual(harness.events, []);
|
||||
});
|
||||
|
||||
test('route rules support explicit domain revision and legacy global revision with normalization', async () => {
|
||||
test('route rules use the explicit domain revision and normalize without losing target', async () => {
|
||||
const explicit = createHarness();
|
||||
await explicit.service.update([
|
||||
{ type: 'domain_suffix', value: 'NEW.EXAMPLE', enabled: true },
|
||||
{ type: 'domain_suffix', value: 'new.example', enabled: true },
|
||||
], 2, undefined);
|
||||
{ type: 'domain_suffix', value: 'NEW.EXAMPLE', enabled: true, outbound: 'vpn' },
|
||||
{ type: 'domain_suffix', value: 'new.example', enabled: true, outbound: 'direct' },
|
||||
], 2, 2);
|
||||
assert.deepEqual(explicit.snapshot().state.routeRules, newRules);
|
||||
assert.equal(explicit.snapshot().state.routeRulesRevision, 3);
|
||||
|
||||
const legacy = createHarness();
|
||||
await legacy.service.update(newRules, undefined, 10);
|
||||
assert.deepEqual(legacy.snapshot().state.routeRules, newRules);
|
||||
});
|
||||
|
||||
test('route rules state-only path leaves config and applied rules unchanged', async () => {
|
||||
@@ -159,7 +161,7 @@ test('route rules state-only path leaves config and applied rules unchanged', as
|
||||
{ ...canonicalState(), revision: 1, routeRulesRevision: 0 },
|
||||
]) {
|
||||
const harness = createHarness({ state, missingSubscription: Boolean(state.profiles.length) });
|
||||
await harness.service.update(newRules, 0, undefined);
|
||||
await harness.service.update(newRules, 0, 2);
|
||||
assert.deepEqual(harness.snapshot().state.routeRules, newRules);
|
||||
assert.deepEqual(harness.snapshot().state.appliedRouteRules, oldRules);
|
||||
assert.equal(harness.snapshot().config, 'old-config');
|
||||
@@ -169,16 +171,35 @@ test('route rules state-only path leaves config and applied rules unchanged', as
|
||||
|
||||
test('route rules running apply updates active rules while stopped leaves them pending', async () => {
|
||||
const running = createHarness({ running: true });
|
||||
await running.service.update(newRules, 2, undefined);
|
||||
await running.service.update(newRules, 2, 2);
|
||||
assert.deepEqual(running.snapshot().state.appliedRouteRules, newRules);
|
||||
assert.deepEqual(running.events, ['operation', 'config.write', 'runtime.apply', 'state.update']);
|
||||
|
||||
const stopped = createHarness({ running: false });
|
||||
await stopped.service.update(newRules, 2, undefined);
|
||||
await stopped.service.update(newRules, 2, 2);
|
||||
assert.deepEqual(stopped.snapshot().state.appliedRouteRules, oldRules);
|
||||
assert.equal(stopped.events.includes('runtime.apply'), false);
|
||||
});
|
||||
|
||||
test('route rules in gateway-direct update desired order without touching config or runtime', async () => {
|
||||
const state = { ...canonicalState(), appliedRouteRules: [] };
|
||||
const harness = createHarness({ state, gatewayDirect: true, running: true });
|
||||
await harness.service.update(newRules, 2, 2);
|
||||
assert.deepEqual(harness.snapshot().state.routeRules, newRules);
|
||||
assert.deepEqual(harness.snapshot().state.appliedRouteRules, []);
|
||||
assert.equal(harness.snapshot().state.routeRulesRevision, 3);
|
||||
assert.equal(harness.snapshot().config, 'old-config');
|
||||
assert.deepEqual(harness.events, ['operation', 'state.update']);
|
||||
});
|
||||
|
||||
test('route rules skip runtime apply when the generated config bytes are unchanged', async () => {
|
||||
const config = JSON.stringify({ selectedServerId: 'server', routeRules: newRules }, null, 2);
|
||||
const harness = createHarness({ running: true, config });
|
||||
await harness.service.update(newRules, 2, 2);
|
||||
assert.deepEqual(harness.snapshot().state.appliedRouteRules, newRules);
|
||||
assert.deepEqual(harness.events, ['operation', 'state.update']);
|
||||
});
|
||||
|
||||
test('route rules rollback restores config/domain and honors runtime mutation phase', async () => {
|
||||
for (const failures of [
|
||||
{ configWriteAfter: new Error('config') },
|
||||
@@ -186,18 +207,18 @@ test('route rules rollback restores config/domain and honors runtime mutation ph
|
||||
]) {
|
||||
const harness = createHarness({ failures });
|
||||
const before = harness.snapshot();
|
||||
await assert.rejects(harness.service.update(newRules, 2, undefined));
|
||||
await assert.rejects(harness.service.update(newRules, 2, 2));
|
||||
assertDomainRestored(harness.snapshot(), before);
|
||||
}
|
||||
|
||||
const preMutation = new Error('invalid config');
|
||||
const local = createHarness({ commandResult: { ok: false, mutationStarted: false, error: preMutation } });
|
||||
await assert.rejects(local.service.update(newRules, 2, undefined), (error) => error === preMutation);
|
||||
await assert.rejects(local.service.update(newRules, 2, 2), (error) => error === preMutation);
|
||||
assert.equal(local.events.includes('runtime.restore'), false);
|
||||
|
||||
const postMutation = new Error('remote failed');
|
||||
const remote = createHarness({ commandResult: { ok: false, mutationStarted: true, error: postMutation } });
|
||||
await assert.rejects(remote.service.update(newRules, 2, undefined), (error) => error === postMutation);
|
||||
await assert.rejects(remote.service.update(newRules, 2, 2), (error) => error === postMutation);
|
||||
assert.equal(remote.events.includes('runtime.restore'), true);
|
||||
});
|
||||
|
||||
@@ -207,7 +228,7 @@ test('route rules rollback continues and classifies runtime restore failure', as
|
||||
const aggregate = createHarness({
|
||||
failures: { stateUpdates: [original], configRestore },
|
||||
});
|
||||
await assert.rejects(aggregate.service.update(newRules, 2, undefined), (error) => {
|
||||
await assert.rejects(aggregate.service.update(newRules, 2, 2), (error) => {
|
||||
assert.ok(error instanceof AggregateError);
|
||||
assert.deepEqual(error.errors, [original, configRestore]);
|
||||
return true;
|
||||
@@ -217,7 +238,7 @@ test('route rules rollback continues and classifies runtime restore failure', as
|
||||
const broken = createHarness({
|
||||
failures: { stateUpdates: [original], runtimeRestore },
|
||||
});
|
||||
await assert.rejects(broken.service.update(newRules, 2, undefined), (error) => {
|
||||
await assert.rejects(broken.service.update(newRules, 2, 2), (error) => {
|
||||
assert.equal(error.code, 'PROCESS_START_FAILED');
|
||||
assert.deepEqual(error.cause.errors, [original, runtimeRestore]);
|
||||
return true;
|
||||
@@ -229,13 +250,13 @@ test('route rules route preserves one adapter and state-only response', async ()
|
||||
const calls = [];
|
||||
const route = createRouteRulesRoute({
|
||||
routeRules: { update: async (...args) => { calls.push(args); } },
|
||||
readBody: async () => ({ rules: newRules, expectedRulesRevision: 3, expectedRevision: 99 }),
|
||||
readBody: async () => ({ rules: newRules, expectedRulesRevision: 3, rulesContractVersion: 2 }),
|
||||
sendState: async () => { calls.push('sent'); },
|
||||
});
|
||||
const response = {};
|
||||
assert.equal(await route.handle({ method: 'POST', url: '/api/route-rules' }, response), false);
|
||||
assert.equal(await route.handle({ method: 'PUT', url: '/api/route-rules' }, response), true);
|
||||
assert.deepEqual(calls, [[newRules, 3, 99], 'sent']);
|
||||
assert.equal(await route.handle({ method: 'PUT', url: '/api/route-rules/v2' }, response), true);
|
||||
assert.deepEqual(calls, [[newRules, 3, 2], 'sent']);
|
||||
|
||||
const source = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8');
|
||||
assert.match(source, /createRouteRulesRoute\(\{/);
|
||||
|
||||
Reference in New Issue
Block a user