540 lines
21 KiB
JavaScript
540 lines
21 KiB
JavaScript
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['"]/);
|
|
});
|