Persist traffic settings and support multi-device traffic views
This commit is contained in:
@@ -206,3 +206,48 @@ test('route is unavailable when no traffic collector exists', async () => {
|
||||
(error) => error.code === 'ENDPOINT_NOT_FOUND',
|
||||
);
|
||||
});
|
||||
|
||||
test('PUT persists validated traffic settings with revision protection and returns canonical state', async () => {
|
||||
let state = { revision: 4, traffic: { grouping: 'site', sort: 'popular', retentionSeconds: 10 } };
|
||||
let sent = false;
|
||||
const route = createLiveTrafficRoute({
|
||||
traffic: null,
|
||||
settingsState: {
|
||||
read: () => state,
|
||||
update: (mutator) => { state = { ...mutator(state), revision: state.revision + 1 }; },
|
||||
},
|
||||
readBody: async () => ({
|
||||
settings: { grouping: 'device', sort: 'recent', retentionSeconds: 30 },
|
||||
expectedRevision: 4,
|
||||
}),
|
||||
sendState: async (res) => { sent = true; res.end(JSON.stringify(state)); },
|
||||
});
|
||||
const res = response();
|
||||
|
||||
assert.equal(await route.handle({ method: 'PUT', url: '/api/traffic/settings' }, res), true);
|
||||
assert.equal(sent, true);
|
||||
assert.deepEqual(state.traffic, { grouping: 'device', sort: 'recent', retentionSeconds: 30 });
|
||||
assert.equal(state.revision, 5);
|
||||
|
||||
const conflict = createLiveTrafficRoute({
|
||||
traffic: null,
|
||||
settingsState: { read: () => state, update: () => { throw new Error('must not update'); } },
|
||||
readBody: async () => ({ settings: state.traffic, expectedRevision: 4 }),
|
||||
sendState: async () => {},
|
||||
});
|
||||
await assert.rejects(
|
||||
conflict.handle({ method: 'PUT', url: '/api/traffic/settings' }, response()),
|
||||
(error) => error.code === 'STATE_CONFLICT',
|
||||
);
|
||||
|
||||
const invalid = createLiveTrafficRoute({
|
||||
traffic: null,
|
||||
settingsState: { read: () => state, update: () => { throw new Error('must not update'); } },
|
||||
readBody: async () => ({ settings: { grouping: 'guess' }, expectedRevision: 5 }),
|
||||
sendState: async () => {},
|
||||
});
|
||||
await assert.rejects(
|
||||
invalid.handle({ method: 'PUT', url: '/api/traffic/settings' }, response()),
|
||||
(error) => error.code === 'REQUEST_INVALID',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -339,6 +339,7 @@ setInterval(() => {}, 60_000);
|
||||
'singboxStartedAt',
|
||||
'subscription',
|
||||
'subscriptionHost',
|
||||
'traffic',
|
||||
'userInfo',
|
||||
]);
|
||||
let revision = initial.revision;
|
||||
|
||||
@@ -137,13 +137,14 @@ test('schema v7 migrates failover disabled without losing canonical state', (t)
|
||||
});
|
||||
const migrated = store.read();
|
||||
|
||||
assert.equal(migrated.schemaVersion, 8);
|
||||
assert.equal(migrated.schemaVersion, STATE_SCHEMA_VERSION);
|
||||
assert.equal(migrated.revision, 19);
|
||||
assert.equal(migrated.routeRulesRevision, 4);
|
||||
assert.equal(migrated.failoverPolicy.enabled, false);
|
||||
assert.equal(migrated.failoverRuntimeState.lastSwitchAt, null);
|
||||
assert.equal(migrated.appliedFailoverPolicy, null);
|
||||
assert.deepEqual(migrated.diagnostics.hiddenServiceIds, ['google']);
|
||||
assert.deepEqual(migrated.traffic, { grouping: 'site', sort: 'popular', retentionSeconds: 10 });
|
||||
assert.equal(store.migration.fromVersion, 7);
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(store.migration.backupPath, 'utf8')), legacy);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user