import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import test from 'node:test'; import { createSubscriptionService } from '../../dist/server/features/subscription/index.js'; import { createSubscriptionMutationRoute } from '../../dist/server/http/routes/subscriptionMutationRoute.js'; import { HarborError } from '../../dist/shared/errors.js'; const oldServer = { id: 'srv_old', label: 'Old', host: 'old.example', port: 443, protocol: 'vless', }; const nextServer = { id: 'srv_next', label: 'Next', host: 'next.example', port: 443, protocol: 'vless', }; const routeRules = [{ type: 'domain_suffix', value: 'example', enabled: true }]; function parsed(servers = [nextServer]) { return { config: { normalized: true }, sourceConfig: { source: true }, servers, userInfo: { total: 100 }, fetchedAt: '2026-08-08T10:00:00.000Z', }; } function createHarness(overrides = {}) { let state = structuredClone(overrides.state ?? { revision: 3, subscriptionUrl: 'https://old.example/sub', servers: [oldServer], selectedServerId: oldServer.id, appliedServerId: oldServer.id, routeRules, gatewayAutoEnabled: false, connectionDesired: 'running', }); let cache = structuredClone(overrides.cache ?? { url: 'old', config: { old: true } }); let config = overrides.config ?? 'old-config-bytes'; let gatewayAuto = structuredClone(overrides.gatewayAuto ?? { mode: 'gateway-direct', gatewayId: 'old' }); let running = overrides.running ?? true; let serialized = Promise.resolve(); const calls = []; const failures = { ...(overrides.failures || {}) }; const timers = []; const failOnce = (name) => { const failure = failures[name]; if (!failure) return; delete failures[name]; throw failure; }; const dependencies = { provider: { fetchSubscription: overrides.fetchSubscription || (async () => parsed()), selectRefreshedServer: overrides.selectRefreshedServer || ((current, _before, after) => ( after.some((server) => server.id === current) ? current : '' )), }, state: { read: () => structuredClone(state), update: (mutator) => { failOnce('stateUpdate'); const revision = state.revision + 1; state = { ...structuredClone(mutator(structuredClone(state))), revision }; calls.push('state.update'); failOnce('stateUpdateAfter'); return structuredClone(state); }, }, cache: { read: () => structuredClone(cache), write: (value) => { failOnce('cacheWrite'); cache = structuredClone(value); calls.push('cache.write'); }, remove: () => { failOnce('cacheRemove'); cache = null; calls.push('cache.remove'); }, }, config: { build: (_value, selectedServerId, rules) => ({ selectedServerId, rules }), read: () => config, write: (value) => { failOnce('configWrite'); config = JSON.stringify(value); calls.push('config.write'); }, restore: (value) => { failOnce('configRestore'); config = value; calls.push('config.restore'); }, remove: () => { failOnce('configRemove'); config = null; calls.push('config.remove'); }, }, runtime: { isRunning: async () => running, stop: async () => { calls.push('runtime.stop'); failOnce('runtimeStop'); running = false; }, start: async () => { calls.push('runtime.start'); const failure = Array.isArray(failures.runtimeStart) ? failures.runtimeStart.shift() : failures.runtimeStart; if (failure) { if (!Array.isArray(failures.runtimeStart)) delete failures.runtimeStart; running = false; throw failure; } running = true; }, }, gatewayAuto: { read: () => structuredClone(gatewayAuto), set: (value) => { gatewayAuto = structuredClone(value); calls.push('gateway.set'); }, createInitial: () => ({ mode: 'local-vpn' }), }, serialize: (operation) => { const result = serialized.then(operation, operation); serialized = result.then(() => undefined, () => undefined); return result; }, scheduler: { setInterval: (callback, intervalMs) => { const timer = { callback, intervalMs, unrefCalls: 0, clearCalls: 0, unref() { this.unrefCalls += 1; }, }; timers.push(timer); return timer; }, clearInterval: (timer) => { timer.clearCalls += 1; }, }, onRefreshError: overrides.onRefreshError || (() => {}), }; return { service: createSubscriptionService(dependencies), calls, timers, snapshot: () => structuredClone({ state, cache, config, gatewayAuto, running }), }; } function assertRestoredSnapshot(actual, expected, label) { const actualRevision = actual.state.revision; const expectedRevision = expected.state.revision; const actualDomain = structuredClone(actual); const expectedDomain = structuredClone(expected); delete actualDomain.state.revision; delete expectedDomain.state.revision; assert.deepEqual(actualDomain, expectedDomain, label); assert.ok(actualRevision >= expectedRevision, `${label}: revision moved backwards`); } test('subscription import commits source config, clears selection, and leaves runtime stopped', async () => { const harness = createHarness(); const result = await harness.service.importSubscription('https://new.example/sub'); const snapshot = harness.snapshot(); assert.equal(result.success, true); assert.equal(result.selectedServerId, ''); assert.equal(snapshot.state.subscriptionUrl, 'https://new.example/sub'); assert.deepEqual(snapshot.state.routeRules, routeRules); assert.equal(snapshot.state.gatewayAutoEnabled, false); assert.equal(snapshot.state.connectionDesired, 'stopped'); assert.deepEqual(snapshot.cache.config, { source: true }); assert.equal(snapshot.config, null); assert.deepEqual(snapshot.gatewayAuto, { mode: 'local-vpn' }); assert.equal(snapshot.running, false); assert.equal(harness.calls.filter((call) => call === 'runtime.stop').length, 1); assert.equal(harness.calls.filter((call) => call === 'runtime.start').length, 0); }); test('subscription refresh retains selection and restarts only a previously running runtime', async () => { const retained = { ...oldServer, label: 'Renamed' }; const harness = createHarness({ fetchSubscription: async () => parsed([retained, nextServer]) }); const result = await harness.service.refreshSavedSubscription(); const snapshot = harness.snapshot(); assert.equal(result.selectedServerId, oldServer.id); assert.equal(snapshot.state.selectedServerId, oldServer.id); assert.match(snapshot.config, /srv_old/); assert.equal(snapshot.running, true); assert.equal(harness.calls.filter((call) => call === 'runtime.stop').length, 0); assert.equal(harness.calls.filter((call) => call === 'runtime.start').length, 1); }); test('subscription import restores every snapshot on pre-commit failures', async () => { for (const failurePoint of ['runtimeStop', 'configRemove', 'cacheWrite', 'stateUpdate', 'stateUpdateAfter']) { const failure = new Error(failurePoint); const harness = createHarness({ failures: { [failurePoint]: failure } }); const before = harness.snapshot(); await assert.rejects( harness.service.importSubscription('https://new.example/sub'), (error) => error === failure, failurePoint, ); assertRestoredSnapshot(harness.snapshot(), before, failurePoint); } }); test('subscription refresh restores snapshots and reapplies the old config on pre-commit failures', async () => { for (const failurePoint of ['configWrite', 'cacheWrite', 'stateUpdate', 'stateUpdateAfter']) { const failure = new Error(failurePoint); const harness = createHarness({ fetchSubscription: async () => parsed([oldServer]), failures: { [failurePoint]: failure }, }); const before = harness.snapshot(); await assert.rejects( harness.service.refreshSavedSubscription(), (error) => error === failure, failurePoint, ); assertRestoredSnapshot(harness.snapshot(), before, failurePoint); assert.equal( harness.calls.filter((call) => call === 'runtime.start').length, failurePoint === 'stateUpdate' || failurePoint === 'stateUpdateAfter' ? 2 : 0, failurePoint, ); } }); test('subscription reset restores cache, config, gateway, state, and runtime on removal or commit failure', async () => { for (const failurePoint of ['configRemove', 'cacheRemove', 'stateUpdate', 'stateUpdateAfter']) { const failure = new Error(failurePoint); const harness = createHarness({ failures: { [failurePoint]: failure } }); const before = harness.snapshot(); await assert.rejects( harness.service.resetSavedSubscription(), (error) => error === failure, failurePoint, ); assertRestoredSnapshot(harness.snapshot(), before, failurePoint); } const noRuntime = createHarness({ failures: { stateUpdate: new Error('commit') } }); await assert.rejects(noRuntime.service.resetSavedSubscription({ stopRuntime: false })); assert.equal(noRuntime.calls.includes('runtime.stop'), false); assert.equal(noRuntime.calls.includes('runtime.start'), false); const stoppedCleanup = createHarness({ running: false }); await stoppedCleanup.service.resetSavedSubscription(); assert.equal(stoppedCleanup.calls.filter((call) => call === 'runtime.stop').length, 1); assert.equal(stoppedCleanup.calls.includes('runtime.start'), false); }); test('subscription rollback attempts remaining restores and aggregates restoration failures', async () => { const commitFailure = new Error('commit failed'); const configRestoreFailure = new Error('config restore failed'); const configBroken = createHarness({ fetchSubscription: async () => parsed([oldServer]), failures: { stateUpdate: commitFailure, configRestore: configRestoreFailure }, }); const beforeConfigBroken = configBroken.snapshot(); await assert.rejects(configBroken.service.refreshSavedSubscription(), (error) => { assert.ok(error instanceof AggregateError); assert.deepEqual(error.errors, [commitFailure, configRestoreFailure]); return true; }); assert.equal(configBroken.snapshot().cache.url, beforeConfigBroken.cache.url); assert.deepEqual(configBroken.snapshot().gatewayAuto, beforeConfigBroken.gatewayAuto); assert.equal(configBroken.snapshot().running, true); assert.equal(configBroken.calls.filter((call) => call === 'runtime.start').length, 2); const removeFailure = new Error('remove failed'); const cacheRestoreFailure = new Error('cache restore failed'); const cacheBroken = createHarness({ failures: { configRemove: removeFailure, cacheWrite: cacheRestoreFailure }, }); const beforeCacheBroken = cacheBroken.snapshot(); await assert.rejects(cacheBroken.service.importSubscription('https://new.example/sub'), (error) => { assert.ok(error instanceof AggregateError); assert.deepEqual(error.errors, [removeFailure, cacheRestoreFailure]); return true; }); assert.equal(cacheBroken.snapshot().config, beforeCacheBroken.config); assert.deepEqual(cacheBroken.snapshot().gatewayAuto, beforeCacheBroken.gatewayAuto); assert.equal(cacheBroken.snapshot().running, true); assert.equal(cacheBroken.calls.filter((call) => call === 'runtime.start').length, 1); }); test('failed refresh start rolls back, and failed rollback start reports both causes', async () => { const applyFailure = new Error('apply failed'); const recovered = createHarness({ fetchSubscription: async () => parsed([oldServer]), failures: { runtimeStart: [applyFailure] }, }); const before = recovered.snapshot(); await assert.rejects( recovered.service.refreshSavedSubscription(), (error) => error === applyFailure, ); assert.deepEqual(recovered.snapshot(), before); assert.equal(recovered.calls.filter((call) => call === 'runtime.start').length, 2); const rollbackFailure = new Error('rollback failed'); const broken = createHarness({ fetchSubscription: async () => parsed([oldServer]), failures: { runtimeStart: [applyFailure, rollbackFailure] }, }); await assert.rejects(broken.service.refreshSavedSubscription(), (error) => { assert.equal(error.code, 'PROCESS_START_FAILED'); assert.ok(error.cause instanceof AggregateError); assert.deepEqual(error.cause.errors, [applyFailure, rollbackFailure]); return true; }); }); test('subscription refresh coalesces provider work and rejects a stale late result', async () => { let releaseOld; const oldResult = new Promise((resolve) => { releaseOld = resolve; }); let providerCalls = 0; const harness = createHarness({ fetchSubscription: async (url) => { providerCalls += 1; return url.includes('old') ? oldResult : parsed(); }, }); const first = harness.service.refreshSavedSubscription(); const second = harness.service.refreshSavedSubscription(); assert.equal(first, second); assert.equal(providerCalls, 1); await harness.service.importSubscription('https://new.example/sub'); releaseOld(parsed([oldServer])); await assert.rejects(first, (error) => error.code === 'STATE_CONFLICT'); assert.equal(harness.snapshot().state.subscriptionUrl, 'https://new.example/sub'); assert.equal(providerCalls, 2); }); test('subscription generation rejects late success after a newer import of the same URL', async () => { let releaseRefresh; const delayed = new Promise((resolve) => { releaseRefresh = resolve; }); let providerCalls = 0; const harness = createHarness({ fetchSubscription: async () => { providerCalls += 1; return providerCalls === 1 ? delayed : parsed([nextServer]); }, }); const stale = harness.service.refreshSavedSubscription(); await harness.service.importSubscription('https://old.example/sub'); releaseRefresh(parsed([oldServer])); await assert.rejects(stale, (error) => error.code === 'STATE_CONFLICT'); assert.deepEqual(harness.snapshot().state.servers, [nextServer]); assert.equal(harness.snapshot().state.subscriptionUrl, 'https://old.example/sub'); }); test('subscription generation prevents a late terminal refresh from forgetting a newer import', async () => { let rejectRefresh; const delayed = new Promise((_resolve, reject) => { rejectRefresh = reject; }); let providerCalls = 0; const harness = createHarness({ fetchSubscription: async () => { providerCalls += 1; return providerCalls === 1 ? delayed : parsed([nextServer]); }, }); const stale = harness.service.refreshSavedSubscription(); await harness.service.importSubscription('https://old.example/sub'); rejectRefresh(new HarborError('SUBSCRIPTION_EXPIRED')); await assert.rejects(stale, (error) => error.code === 'STATE_CONFLICT'); assert.equal(harness.snapshot().state.subscriptionUrl, 'https://old.example/sub'); assert.deepEqual(harness.snapshot().state.servers, [nextServer]); assert.ok(harness.snapshot().cache); }); test('subscription generation prevents a delayed import from resurrecting a forgotten subscription', async () => { let releaseImport; const delayed = new Promise((resolve) => { releaseImport = resolve; }); const harness = createHarness({ fetchSubscription: async () => delayed }); const staleImport = harness.service.importSubscription('https://new.example/sub'); await harness.service.resetSavedSubscription(); releaseImport(parsed()); await assert.rejects(staleImport, (error) => error.code === 'STATE_CONFLICT'); assert.equal(Boolean(harness.snapshot().state.subscriptionUrl), false); assert.equal(harness.snapshot().cache, null); assert.equal(harness.snapshot().config, null); }); test('subscription generation lets only the first completed concurrent import commit', async () => { let releaseFirst; let releaseSecond; const firstResult = new Promise((resolve) => { releaseFirst = resolve; }); const secondResult = new Promise((resolve) => { releaseSecond = resolve; }); const harness = createHarness({ fetchSubscription: async (url) => url.includes('first') ? firstResult : secondResult, }); const first = harness.service.importSubscription('https://first.example/sub'); const second = harness.service.importSubscription('https://second.example/sub'); releaseSecond(parsed([nextServer])); await second; releaseFirst(parsed([oldServer])); await assert.rejects(first, (error) => error.code === 'STATE_CONFLICT'); assert.equal(harness.snapshot().state.subscriptionUrl, 'https://second.example/sub'); assert.deepEqual(harness.snapshot().state.servers, [nextServer]); }); test('only terminal subscription refresh failures forget saved data', async () => { const cases = [ ['SUBSCRIPTION_EXPIRED', true], ['SUBSCRIPTION_DISABLED', true], ['SUBSCRIPTION_REJECTED', true], ['SUBSCRIPTION_TRAFFIC_EXHAUSTED', false], ['SUBSCRIPTION_INVALID', false], ['PROVIDER_UNAVAILABLE', false], ]; for (const [code, resets] of cases) { const harness = createHarness({ running: false, fetchSubscription: async () => { throw new HarborError(code); }, }); await assert.rejects(harness.service.refreshSavedSubscription(), (error) => error.code === code); assert.equal(Boolean(harness.snapshot().state.subscriptionUrl), !resets, code); assert.equal(Boolean(harness.snapshot().cache), !resets, code); } }); test('subscription scheduler is unrefed, skips empty state, reports errors, and stops idempotently', async () => { const errors = []; let providerCalls = 0; const harness = createHarness({ state: { revision: 0, servers: [], selectedServerId: '', appliedServerId: '', routeRules: [] }, fetchSubscription: async () => { providerCalls += 1; throw new HarborError('PROVIDER_UNAVAILABLE'); }, onRefreshError: (error) => errors.push(error.code), }); harness.service.startAutoRefresh(1234); harness.service.startAutoRefresh(1234); assert.equal(harness.timers.length, 1); assert.equal(harness.timers[0].intervalMs, 1234); assert.equal(harness.timers[0].unrefCalls, 1); harness.timers[0].callback(); await new Promise(setImmediate); assert.equal(providerCalls, 0); const stateful = createHarness({ fetchSubscription: async () => { providerCalls += 1; throw new HarborError('PROVIDER_UNAVAILABLE'); }, onRefreshError: (error) => errors.push(error.code), }); stateful.service.startAutoRefresh(10); stateful.timers[0].callback(); await new Promise(setImmediate); await new Promise(setImmediate); assert.equal(providerCalls, 1); assert.deepEqual(errors, ['PROVIDER_UNAVAILABLE']); stateful.service.stopAutoRefresh(); stateful.service.stopAutoRefresh(); assert.equal(stateful.timers[0].clearCalls, 1); }); test('subscription mutation route preserves methods, operation kinds, and response fields', async () => { const operations = []; const sent = []; const service = { importSubscription: async (url) => ({ success: true, imported: url }), refreshSavedSubscription: async () => ({ success: true, refreshed: true }), resetSavedSubscription: async () => { operations.push('reset'); }, }; const route = createSubscriptionMutationRoute({ subscriptionService: service, readBody: async () => ({ url: ' https://new.example/sub ' }), withOperation: async (kind, operation) => { operations.push(kind); return operation(); }, sendState: async (_res, extra = {}) => { sent.push(extra); }, }); const response = {}; assert.equal(await route.handle({ method: 'GET', url: '/api/subscription/fetch' }, response), false); assert.equal(await route.handle({ method: 'POST', url: '/api/subscription/fetch' }, response), true); assert.equal(await route.handle({ method: 'POST', url: '/api/subscription/refresh' }, response), true); assert.equal(await route.handle({ method: 'DELETE', url: '/api/subscription' }, response), true); assert.deepEqual(operations, ['subscription-import', 'subscription-refresh', 'subscription-forget', 'reset']); assert.deepEqual(sent, [ { success: true, imported: 'https://new.example/sub' }, { refreshed: true }, {}, ]); }); test('composition root contains no displaced subscription mutation owner', () => { const source = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8'); assert.match(source, /createSubscriptionMutationRoute\(\{/); assert.match(source, /subscriptionService\.startAutoRefresh\(SUBSCRIPTION_REFRESH_INTERVAL_MS\)/); assert.doesNotMatch(source, /subscriptionRefreshPromise|subscriptionRefreshTimer|TERMINAL_SUBSCRIPTION_CODES/); assert.doesNotMatch(source, /req\.url === ['"]\/api\/subscription\/(?:fetch|refresh)['"]/); assert.doesNotMatch(source, /req\.method === ['"]DELETE['"] && req\.url === ['"]\/api\/subscription['"]/); });