Migrate Harbor state and traffic history to SQLite
This commit is contained in:
@@ -5,6 +5,13 @@ import http from 'node:http';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
|
||||
function readState(directory) {
|
||||
const db = new DatabaseSync(path.join(directory, 'harbor.sqlite'), { readOnly: true });
|
||||
try { return JSON.parse(db.prepare("SELECT value FROM documents WHERE key = 'state'").get().value); }
|
||||
finally { db.close(); }
|
||||
}
|
||||
|
||||
import {
|
||||
assertStateSnapshot,
|
||||
@@ -99,7 +106,7 @@ test('state v1 projects legacy storage through the canonical profile snapshot',
|
||||
);
|
||||
});
|
||||
|
||||
test('startup discards a rejected cached subscription and returns to first-run', async (t) => {
|
||||
test('startup rejects an invalid legacy subscription without changing migration originals', async (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-rejected-cache-'));
|
||||
const port = await freePort();
|
||||
const subscriptionUrl = 'https://provider.example/disabled';
|
||||
@@ -141,16 +148,11 @@ test('startup discards a rejected cached subscription and returns to first-run',
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const state = await waitForState(port, child, () => stderr);
|
||||
assert.equal(state.subscription.status, 'missing');
|
||||
assert.equal(state.hasSubscription, false);
|
||||
assert.deepEqual(state.servers, []);
|
||||
assert.ok(state.route.localRules.some((rule) => (
|
||||
rule.type === 'domain_suffix' && rule.value === 'example.org' && rule.enabled
|
||||
)));
|
||||
assert.equal(fs.existsSync(path.join(dir, 'subscription-cache.json')), false);
|
||||
assert.equal(fs.existsSync(path.join(dir, 'sing-box-config.json')), false);
|
||||
assert.equal(child.exitCode, null);
|
||||
await assert.rejects(waitForState(port, child, () => stderr), /Harbor exited early/);
|
||||
assert.notEqual(child.exitCode, 0);
|
||||
assert.equal(fs.existsSync(path.join(dir, 'subscription-cache.json')), true);
|
||||
assert.equal(fs.readFileSync(path.join(dir, 'sing-box-config.json'), 'utf8'), '{}');
|
||||
assert.equal(JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8')).subscriptionUrl, subscriptionUrl);
|
||||
});
|
||||
|
||||
test('data invariant: canonical profile API mutates one snapshot and preserves migrated profile data', async (t) => {
|
||||
@@ -305,15 +307,15 @@ setInterval(() => {}, 60_000);
|
||||
assert.equal(initial.route.localRulesRevision, 0);
|
||||
assert.equal(initial.route.localRulesPendingRestart, false);
|
||||
assert.equal(JSON.stringify(initial).includes(subscriptionUrl), false);
|
||||
const migratedState = JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8'));
|
||||
const migratedState = readState(dir);
|
||||
assert.equal(migratedState.schemaVersion, STATE_SCHEMA_VERSION);
|
||||
assert.equal(migratedState.profiles.length, 1);
|
||||
assert.equal(migratedState.profiles[0].subscriptionUrl, subscriptionUrl);
|
||||
assert.equal(migratedState.profiles[0].desiredServerId, testServerId);
|
||||
assert.equal(migratedState.profiles[0].subscriptionConfig.outbounds[0].tag, testServerId);
|
||||
assert.equal(Object.hasOwn(migratedState, 'subscriptionUrl'), false);
|
||||
assert.equal(fs.existsSync(path.join(dir, 'subscription-cache.json')), false);
|
||||
assert.ok(fs.readdirSync(dir).some((name) => name.startsWith('subscription-cache.json.backup-v1-')));
|
||||
assert.equal(fs.existsSync(path.join(dir, 'subscription-cache.json')), true);
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(dir, 'subscription-cache.json'), 'utf8')), { url: subscriptionUrl, config });
|
||||
const stateKeys = Object.keys(initial).sort();
|
||||
assert.deepEqual(stateKeys, [
|
||||
'apiVersion',
|
||||
@@ -373,7 +375,7 @@ setInterval(() => {}, 60_000);
|
||||
|
||||
const primaryProfileId = initial.profiles[0].id;
|
||||
const preservedPrimary = structuredClone(
|
||||
JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8')).profiles[0],
|
||||
readState(dir).profiles[0],
|
||||
);
|
||||
const preservedConfig = fs.readFileSync(path.join(dir, 'sing-box-config.json'), 'utf8');
|
||||
for (const [pathname, expectedCode] of [
|
||||
@@ -394,9 +396,9 @@ setInterval(() => {}, 60_000);
|
||||
},
|
||||
);
|
||||
assert.equal(failedAdd.payload.error.code, expectedCode);
|
||||
const storedAfterFailure = JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8'));
|
||||
const storedAfterFailure = readState(dir);
|
||||
assert.deepEqual(storedAfterFailure.profiles, [preservedPrimary]);
|
||||
assert.equal(fs.existsSync(path.join(dir, 'subscription-cache.json')), false);
|
||||
assert.equal(fs.existsSync(path.join(dir, 'subscription-cache.json')), true);
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(dir, 'sing-box-config.json'), 'utf8'),
|
||||
preservedConfig,
|
||||
@@ -475,7 +477,7 @@ setInterval(() => {}, 60_000);
|
||||
assert.equal(added.state.selection.desiredProfileId, primaryProfileId);
|
||||
assert.equal(added.state.profiles.find(({ id }) => id === workProfileId).desiredServerId, '');
|
||||
assert.equal(
|
||||
JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8'))
|
||||
readState(dir)
|
||||
.profiles.find(({ id }) => id === workProfileId).subscriptionConfig.outbounds[0].tag,
|
||||
testServerId,
|
||||
);
|
||||
@@ -517,7 +519,7 @@ setInterval(() => {}, 60_000);
|
||||
);
|
||||
assert.equal(failedRefresh.response.status, 400);
|
||||
assert.equal(failedRefresh.payload.error.code, 'SUBSCRIPTION_INVALID');
|
||||
const storedAfterFailedRefresh = JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8'));
|
||||
const storedAfterFailedRefresh = readState(dir);
|
||||
const staleWorkProfile = storedAfterFailedRefresh.profiles.find(({ id }) => id === workProfileId);
|
||||
assert.equal(staleWorkProfile.desiredServerId, testServerId);
|
||||
assert.equal(staleWorkProfile.lastRefreshErrorCode, 'SUBSCRIPTION_INVALID');
|
||||
|
||||
Reference in New Issue
Block a user