|
|
|
|
@@ -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');
|
|
|
|
|
|