Refine subscription import and refresh flow
This commit is contained in:
@@ -114,11 +114,32 @@ setInterval(() => {}, 60_000);
|
||||
fs.writeFileSync(singboxPath, workingSingbox);
|
||||
fs.chmodSync(singboxPath, 0o755);
|
||||
|
||||
const subscriptionServer = http.createServer((req, res) => {
|
||||
let providerFetchCount = 0;
|
||||
let delayedPath = '';
|
||||
let invalidNextPath = '';
|
||||
let delayedRequestStarted = null;
|
||||
let releaseDelayedRequest = null;
|
||||
const subscriptionServer = http.createServer(async (req, res) => {
|
||||
providerFetchCount += 1;
|
||||
if (req.url === '/timeout') return;
|
||||
if (req.url === '/unavailable') {
|
||||
res.writeHead(503);
|
||||
return res.end('unavailable');
|
||||
}
|
||||
if (req.url === '/invalid') {
|
||||
res.writeHead(200, { 'content-type': 'text/plain' });
|
||||
return res.end('not a subscription');
|
||||
}
|
||||
if (req.url === invalidNextPath) {
|
||||
invalidNextPath = '';
|
||||
res.writeHead(200, { 'content-type': 'text/plain' });
|
||||
return res.end('not a subscription');
|
||||
}
|
||||
if (req.url === delayedPath) {
|
||||
delayedRequestStarted?.();
|
||||
await new Promise((resolve) => { releaseDelayedRequest = resolve; });
|
||||
delayedPath = '';
|
||||
}
|
||||
res.writeHead(200, {
|
||||
'content-type': 'application/json',
|
||||
'subscription-userinfo': 'upload=10; download=20; total=100',
|
||||
@@ -147,6 +168,7 @@ setInterval(() => {}, 60_000);
|
||||
PORT: String(port),
|
||||
PATH: `${binDir}:${process.env.PATH}`,
|
||||
HARBOR_HOST_NETWORK_STATE: path.join(dir, 'missing-network.json'),
|
||||
SUBSCRIPTION_TIMEOUT_MS: '50',
|
||||
},
|
||||
stdio: ['ignore', 'ignore', 'pipe'],
|
||||
});
|
||||
@@ -206,6 +228,36 @@ setInterval(() => {}, 60_000);
|
||||
assert.equal(providerUnavailable.payload.error.code, 'PROVIDER_UNAVAILABLE');
|
||||
assert.equal(providerUnavailable.payload.error.retryable, true);
|
||||
|
||||
const preservedSubscription = {
|
||||
state: JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8')),
|
||||
cache: fs.readFileSync(path.join(dir, 'subscription-cache.json'), 'utf8'),
|
||||
config: fs.readFileSync(path.join(dir, 'sing-box-config.json'), 'utf8'),
|
||||
};
|
||||
for (const [pathname, expectedCode] of [
|
||||
['/timeout', 'PROVIDER_UNAVAILABLE'],
|
||||
['/invalid', 'SUBSCRIPTION_INVALID'],
|
||||
]) {
|
||||
const failedImport = await rawRequest(
|
||||
port,
|
||||
'/api/subscription/fetch',
|
||||
'POST',
|
||||
{ url: `http://127.0.0.1:${subscriptionPort}${pathname}` },
|
||||
);
|
||||
assert.equal(failedImport.payload.error.code, expectedCode);
|
||||
const storedAfterFailure = JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8'));
|
||||
assert.equal(storedAfterFailure.subscriptionUrl, preservedSubscription.state.subscriptionUrl);
|
||||
assert.equal(storedAfterFailure.selectedTag, preservedSubscription.state.selectedTag);
|
||||
assert.deepEqual(storedAfterFailure.servers, preservedSubscription.state.servers);
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(dir, 'subscription-cache.json'), 'utf8'),
|
||||
preservedSubscription.cache,
|
||||
);
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(dir, 'sing-box-config.json'), 'utf8'),
|
||||
preservedSubscription.config,
|
||||
);
|
||||
}
|
||||
|
||||
const missingServer = await rawRequest(
|
||||
port,
|
||||
'/api/apply',
|
||||
@@ -230,14 +282,25 @@ setInterval(() => {}, 60_000);
|
||||
return result;
|
||||
}
|
||||
|
||||
await stateResponse('/api/subscription/validate', 'POST', { url: subscriptionUrl });
|
||||
const fetchesBeforeImport = providerFetchCount;
|
||||
await mutation('/api/subscription/fetch', 'POST', { url: subscriptionUrl });
|
||||
assert.equal(providerFetchCount, fetchesBeforeImport + 1);
|
||||
const applied = await mutation('/api/apply', 'POST', { selectedTag: 'test-vpn' });
|
||||
assert.deepEqual(applied.state.selection, {
|
||||
desiredServerId: 'test-vpn',
|
||||
appliedServerId: 'test-vpn',
|
||||
});
|
||||
assert.equal(applied.state.connection.process, 'running');
|
||||
const cacheBeforeFailedRefresh = fs.readFileSync(path.join(dir, 'subscription-cache.json'), 'utf8');
|
||||
const configBeforeFailedRefresh = fs.readFileSync(path.join(dir, 'sing-box-config.json'), 'utf8');
|
||||
invalidNextPath = '/subscription/test';
|
||||
const failedRefresh = await rawRequest(port, '/api/subscription/refresh', 'POST');
|
||||
assert.equal(failedRefresh.response.status, 400);
|
||||
assert.equal(failedRefresh.payload.error.code, 'SUBSCRIPTION_INVALID');
|
||||
assert.equal(JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8')).selectedTag, 'test-vpn');
|
||||
assert.equal(fs.readFileSync(path.join(dir, 'subscription-cache.json'), 'utf8'), cacheBeforeFailedRefresh);
|
||||
assert.equal(fs.readFileSync(path.join(dir, 'sing-box-config.json'), 'utf8'), configBeforeFailedRefresh);
|
||||
revision = (await request(port, '/api/state')).revision;
|
||||
assert.equal((await mutation('/api/singbox/stop')).state.connection.desired, 'stopped');
|
||||
assert.equal((await mutation('/api/singbox/restart')).state.connection.desired, 'running');
|
||||
|
||||
@@ -341,7 +404,19 @@ if (process.argv[2] === 'check') {
|
||||
fs.writeFileSync(singboxPath, workingSingbox);
|
||||
fs.chmodSync(singboxPath, 0o755);
|
||||
|
||||
await mutation('/api/subscription/refresh');
|
||||
const delayedRequest = new Promise((resolve) => { delayedRequestStarted = resolve; });
|
||||
delayedPath = '/subscription/test';
|
||||
const staleRefresh = rawRequest(port, '/api/subscription/refresh', 'POST');
|
||||
await delayedRequest;
|
||||
const replacementUrl = `http://127.0.0.1:${subscriptionPort}/subscription/replacement`;
|
||||
await mutation('/api/subscription/fetch', 'POST', { url: replacementUrl });
|
||||
releaseDelayedRequest();
|
||||
const staleRefreshResult = await staleRefresh;
|
||||
assert.equal(staleRefreshResult.response.status, 409);
|
||||
assert.equal(staleRefreshResult.payload.error.code, 'STATE_CONFLICT');
|
||||
assert.equal(JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8')).subscriptionUrl, replacementUrl);
|
||||
assert.equal(JSON.parse(fs.readFileSync(path.join(dir, 'subscription-cache.json'), 'utf8')).url, replacementUrl);
|
||||
revision = (await request(port, '/api/state')).revision;
|
||||
assert.equal((await mutation('/api/gateway-auto', 'POST', { enabled: false })).state.gatewayAuto.enabled, false);
|
||||
const forgotten = await mutation('/api/subscription', 'DELETE');
|
||||
assert.equal(forgotten.state.subscription.status, 'missing');
|
||||
|
||||
Reference in New Issue
Block a user