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'; const oldServer = { id: 'shared', label: 'Old', host: 'old.example', port: 443, protocol: 'vless' }; const nextServer = { id: 'next', label: 'Next', host: 'next.example', port: 443, protocol: 'vless' }; const routeRules = [{ type: 'domain_suffix', value: 'example', enabled: true }]; const profile = (id, label, server = oldServer) => ({ id, label, subscriptionUrl: `https://${id}.example/sub`, subscriptionConfig: { profile: id }, servers: [server], userInfo: { total: 100 }, fetchedAt: '2026-08-08T10:00:00.000Z', desiredServerId: server.id, lastRefreshAttemptAt: null, lastRefreshErrorCode: null, }); const parsed = (id, servers = [nextServer]) => ({ config: { profile: id, refreshed: true }, servers, userInfo: { total: 200 }, fetchedAt: '2026-08-08T12:00:00.000Z', }); function defaultState() { return { revision: 3, profiles: [profile('personal', 'Личный'), profile('work', 'Работа')], desiredProfileId: 'personal', appliedProfileId: 'personal', appliedServerId: 'shared', appliedServerSnapshot: oldServer, routeRules, appliedRouteRules: routeRules, routeRulesRevision: 1, connectionDesired: 'running', }; } function createHarness(overrides = {}) { let state = structuredClone(overrides.state ?? defaultState()); let config = Object.hasOwn(overrides, 'config') ? overrides.config : 'old-config'; let gatewayAuto = { mode: 'gateway-direct', gatewayId: 'gateway' }; let running = overrides.running ?? true; let tail = Promise.resolve(); let updateCount = 0; const calls = []; const timers = []; const providerCalls = []; const serialize = (operation) => { const result = tail.then(operation, operation); tail = result.then(() => undefined, () => undefined); return result; }; const service = createSubscriptionService({ provider: { fetchSubscription: async (url) => { providerCalls.push(url); if (overrides.fetchSubscription) return overrides.fetchSubscription(url); return parsed(url.includes('work') ? 'work' : 'new'); }, selectRefreshedServer: (current, _before, after) => ( after.some((server) => server.id === current) ? current : '' ), }, state: { read: () => structuredClone(state), update: (mutator) => { updateCount += 1; const next = structuredClone(mutator(structuredClone(state))); state = { ...next, revision: state.revision + 1 }; calls.push('state.update'); if (overrides.failStateAt === updateCount) throw new Error('state failed'); return structuredClone(state); }, }, config: { build: (_value, selectedServerId, rules) => ({ selectedServerId, rules }), read: () => config, write: (value) => { config = JSON.stringify(value); calls.push('config.write'); }, restore: (value) => { config = value; calls.push('config.restore'); }, remove: () => { config = null; calls.push('config.remove'); }, }, runtime: { isRunning: async () => running, stop: async () => { calls.push('runtime.stop'); if (overrides.failStop) throw overrides.failStop; running = false; }, start: async () => { calls.push('runtime.start'); if (overrides.failStart) throw overrides.failStart; running = true; }, }, gatewayAuto: { read: () => structuredClone(gatewayAuto), set: (value) => { gatewayAuto = structuredClone(value); calls.push('gateway.set'); }, createInitial: () => ({ mode: 'local-vpn' }), }, serialize, 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 || (() => {}), now: () => new Date('2026-08-08T12:30:00.000Z'), }); return { service, calls, timers, providerCalls, advanceBackgroundRevision: (patch = {}) => { state = { ...state, ...structuredClone(patch), revision: state.revision + 1 }; }, snapshot: () => structuredClone({ state, config, gatewayAuto, running }), }; } test('add fetches before commit, keeps the runtime, and only the first profile becomes desired', async () => { const empty = defaultState(); empty.profiles = []; empty.desiredProfileId = ''; empty.appliedProfileId = ''; empty.appliedServerId = ''; empty.appliedServerSnapshot = null; empty.connectionDesired = 'stopped'; const harness = createHarness({ state: empty, running: false }); const result = await harness.service.addProfile(' Личный ', 'https://new.example/sub', 3); const after = harness.snapshot(); assert.equal(result.label, 'Личный'); assert.equal(after.state.profiles.length, 1); assert.equal(after.state.desiredProfileId, result.profileId); assert.equal(after.state.profiles[0].subscriptionUrl, 'https://new.example/sub'); assert.equal(after.state.profiles[0].desiredServerId, ''); assert.equal(after.running, false); assert.equal(after.config, 'old-config'); await harness.service.addProfile('Работа', 'https://work.example/sub'); assert.equal(harness.snapshot().state.desiredProfileId, result.profileId); }); test('rename and server selection are profile-scoped and reject stale or duplicate writes', async () => { const harness = createHarness(); await harness.service.renameProfile('work', 'Офис', 3); assert.equal(harness.snapshot().state.profiles.find(({ id }) => id === 'work').label, 'Офис'); const duplicateRevision = harness.snapshot().state.revision; await assert.rejects(harness.service.renameProfile('work', 'личный'), (error) => error.code === 'PROFILE_NAME_CONFLICT'); assert.equal(harness.snapshot().state.revision, duplicateRevision); await assert.rejects(harness.service.selectProfileServer('work', 'missing'), (error) => error.code === 'SERVER_NOT_FOUND'); const state = harness.snapshot().state; state.profiles.find(({ id }) => id === 'work').servers = [oldServer, nextServer]; state.profiles.find(({ id }) => id === 'work').desiredServerId = 'shared'; const scoped = createHarness({ state }); await scoped.service.selectProfileServer('work', 'next', state.revision); assert.equal(scoped.snapshot().state.profiles.find(({ id }) => id === 'work').desiredServerId, 'next'); assert.equal(scoped.snapshot().state.profiles.find(({ id }) => id === 'personal').desiredServerId, 'shared'); }); test('refreshing an inactive profile does not mutate config, runtime, or the applied pair', async () => { const harness = createHarness({ fetchSubscription: async () => parsed('work', [oldServer, nextServer]) }); await harness.service.refreshProfile('work', 3); const after = harness.snapshot(); assert.equal(after.state.profiles.find(({ id }) => id === 'work').servers.length, 2); assert.equal(after.state.appliedProfileId, 'personal'); assert.equal(after.state.appliedServerId, 'shared'); assert.equal(after.config, 'old-config'); assert.equal(harness.calls.includes('runtime.start'), false); }); test('provider failure retains the last successful list and records a scoped stale marker', async () => { const failure = Object.assign(new Error('provider down'), { code: 'PROVIDER_UNAVAILABLE' }); const harness = createHarness({ fetchSubscription: async () => { throw failure; } }); await assert.rejects(harness.service.refreshProfile('work', 3), (error) => error === failure); const work = harness.snapshot().state.profiles.find(({ id }) => id === 'work'); assert.deepEqual(work.servers, [oldServer]); assert.equal(work.lastRefreshErrorCode, 'PROVIDER_UNAVAILABLE'); assert.equal(harness.snapshot().state.appliedProfileId, 'personal'); }); test('provider mutations keep admission CAS while tolerating background-only revision bumps', async () => { let releaseAdd; let addStarted; const addFetchStarted = new Promise((resolve) => { addStarted = resolve; }); const adding = createHarness({ fetchSubscription: async () => { addStarted(); return new Promise((resolve) => { releaseAdd = resolve; }); }, }); const pendingAdd = adding.service.addProfile('Резерв', 'https://reserve.example/sub', 3); await addFetchStarted; adding.advanceBackgroundRevision({ gatewayFreshness: 'verified' }); releaseAdd(parsed('reserve')); const added = await pendingAdd; assert.equal(adding.snapshot().state.profiles.some(({ id }) => id === added.profileId), true); assert.equal(adding.snapshot().state.gatewayFreshness, 'verified'); let rejectRefresh; let refreshStarted; const refreshFetchStarted = new Promise((resolve) => { refreshStarted = resolve; }); const refreshing = createHarness({ fetchSubscription: async () => { refreshStarted(); return new Promise((_resolve, reject) => { rejectRefresh = reject; }); }, }); const providerFailure = Object.assign(new Error('provider down'), { code: 'PROVIDER_UNAVAILABLE' }); const pendingRefresh = refreshing.service.refreshProfile('work', 3); await refreshFetchStarted; refreshing.advanceBackgroundRevision({ gatewayFreshness: 'verified' }); rejectRefresh(providerFailure); await assert.rejects(pendingRefresh, (error) => error === providerFailure); const refreshedState = refreshing.snapshot().state; assert.equal(refreshedState.gatewayFreshness, 'verified'); assert.equal( refreshedState.profiles.find(({ id }) => id === 'work').lastRefreshErrorCode, 'PROVIDER_UNAVAILABLE', ); }); test('active refresh applies a retained server and keeps last-applied snapshot if it disappears', async () => { const retained = createHarness({ fetchSubscription: async () => parsed('personal', [oldServer, nextServer]) }); await retained.service.refreshProfile('personal', 3); assert.ok(retained.calls.includes('config.write')); assert.ok(retained.calls.includes('runtime.start')); assert.equal(retained.snapshot().state.appliedServerId, 'shared'); const removed = createHarness({ fetchSubscription: async () => parsed('personal', [nextServer]) }); await removed.service.refreshProfile('personal', 3); const after = removed.snapshot(); assert.equal(after.state.profiles[0].desiredServerId, ''); assert.equal(after.state.appliedServerId, 'shared'); assert.deepEqual(after.state.appliedServerSnapshot, oldServer); assert.equal(removed.calls.includes('config.write'), false); assert.equal(after.running, true); }); test('inactive delete is state-only; applied delete requires one stop-and-delete transaction', async () => { const inactive = createHarness(); await inactive.service.deleteProfile('work', 'delete', 3); assert.deepEqual(inactive.snapshot().state.profiles.map(({ id }) => id), ['personal']); assert.equal(inactive.snapshot().config, 'old-config'); assert.equal(inactive.snapshot().running, true); const active = createHarness(); await assert.rejects(active.service.deleteProfile('personal', 'delete', 3), (error) => error.code === 'PROFILE_IN_USE'); await active.service.deleteProfile('personal', 'stop-and-delete', 3); const after = active.snapshot(); assert.deepEqual(after.state.profiles.map(({ id }) => id), ['work']); assert.equal(after.state.appliedProfileId, ''); assert.equal(after.state.appliedServerSnapshot, null); assert.equal(after.state.connectionDesired, 'stopped'); assert.equal(after.running, false); assert.equal(after.config, null); }); test('deleting a pending desired profile leaves the running applied route untouched', async () => { const state = defaultState(); state.desiredProfileId = 'work'; const harness = createHarness({ state }); await harness.service.deleteProfile('work', 'delete', 3); const after = harness.snapshot(); assert.equal(after.config, 'old-config'); assert.equal(after.running, true); assert.deepEqual(after.gatewayAuto, { mode: 'gateway-direct', gatewayId: 'gateway' }); assert.equal(harness.calls.includes('gateway.set'), false); }); test('auto refresh iterates profiles once, reports scoped failures, and stops idempotently', async () => { const errors = []; const failure = Object.assign(new Error('down'), { code: 'PROVIDER_UNAVAILABLE' }); const harness = createHarness({ fetchSubscription: async (url) => { if (url.includes('work')) throw failure; return parsed('personal', [oldServer]); }, 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].unrefCalls, 1); harness.timers[0].callback(); await new Promise(setImmediate); await new Promise(setImmediate); assert.equal(harness.providerCalls.length, 2); assert.deepEqual(errors, ['PROVIDER_UNAVAILABLE']); harness.service.stopAutoRefresh(); harness.service.stopAutoRefresh(); assert.equal(harness.timers[0].clearCalls, 1); }); test('profile mutation route scopes every target and keeps bounded legacy shims', async () => { const calls = []; const sent = []; const service = { preflightAddProfile: () => {}, preflightRenameProfile: () => {}, addProfile: async (...args) => ({ success: true, args }), renameProfile: async (...args) => ({ success: true, args }), selectProfileServer: async (...args) => ({ success: true, args }), refreshProfile: async (...args) => ({ success: true, args }), deleteProfile: async (...args) => ({ success: true, args }), importSubscription: async (...args) => ({ success: true, args }), refreshSavedSubscription: async (...args) => ({ success: true, args }), resetSavedSubscription: async (...args) => ({ success: true, args }), }; const route = createSubscriptionMutationRoute({ subscriptionService: service, connection: { activate: async (...args) => ({ success: true, args }) }, readBody: async () => ({ label: 'Работа', url: 'https://work.example/sub', serverId: 'next', mode: 'delete', expectedRevision: 7, }), withOperation: async (kind, operation, options = {}) => { calls.push([kind, options]); return operation(8); }, sendState: async (_res, extra = {}) => { sent.push(extra); }, }); const response = {}; for (const [method, url] of [ ['POST', '/api/profiles'], ['PATCH', '/api/profiles/work'], ['PUT', '/api/profiles/work/server'], ['POST', '/api/profiles/work/activate'], ['POST', '/api/profiles/work/refresh'], ['DELETE', '/api/profiles/work'], ]) assert.equal(await route.handle({ method, url }, response), true); assert.deepEqual(calls.map(([kind]) => kind), [ 'profile-add', 'profile-rename', 'profile-select-server', 'profile-activate', 'profile-refresh', 'profile-delete', ]); assert.equal(sent.length, 6); assert.deepEqual(calls[2][1], { expectedRevision: 7, profileId: 'work', serverId: 'next' }); const source = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8'); assert.match(source, /createSubscriptionMutationRoute\(\{/); assert.doesNotMatch(source, /req\.url === ['"]\/api\/subscription\/\(?:fetch\|refresh\)['"]/); });