feat: simplify agent permissions and require current ZIP backups
This commit is contained in:
1 parent
efc2c7c734
commit
91c489513a
51 files changed
+1680
-1270
No files matched your search
+50
-99
@@ -7,14 +7,24 @@ import { packBackup, readBackupZip } from '../src/zip';
|
||||
const empty = () =>
|
||||
validateBackup({
|
||||
format: 'worthpath',
|
||||
version: 2,
|
||||
version: 9,
|
||||
exportedAt: new Date().toISOString(),
|
||||
baseCurrency: 'CNY',
|
||||
preferences: { hiddenMenus: ['asset'], showNotes: false, idleMinutes: 9 },
|
||||
preferences: {
|
||||
hiddenMenus: ['asset'],
|
||||
showNotes: false,
|
||||
idleMinutes: 9,
|
||||
accountGroupOrder: [],
|
||||
sessionHours: 168,
|
||||
requireHiddenPassword: true,
|
||||
overviewCards: ['net'],
|
||||
includeIndependentAssets: true,
|
||||
},
|
||||
currencies: ['CNY'],
|
||||
icons: [],
|
||||
transfers: [],
|
||||
schedules: [],
|
||||
metalPrices: [],
|
||||
positions: [],
|
||||
rates: [],
|
||||
links: [],
|
||||
@@ -48,7 +58,10 @@ test('ZIP contains separate JSON files and restores settings without authenticat
|
||||
'settings.json',
|
||||
'transfers.json',
|
||||
]);
|
||||
assert.doesNotMatch(JSON.stringify(contents), /password|token|session|userId/i);
|
||||
assert.doesNotMatch(
|
||||
Object.values(contents).join('\n'),
|
||||
/"(?:passwordHash|password|token|sessionId|userId|accessDigest|refreshDigest)"/i,
|
||||
);
|
||||
assert.deepEqual(validateBackup(await readBackupZip(await archive(contents))), b);
|
||||
});
|
||||
test('ZIP rejects incomplete files, tampering, unknown entries and invalid entry names', async () => {
|
||||
@@ -69,6 +82,15 @@ test('backup accepts over 1000 positions, 10000 revisions per position and 20000
|
||||
const position = (count: number) => ({
|
||||
id: randomUUID(),
|
||||
name: 'count acceptance',
|
||||
groupName: '',
|
||||
iconId: null,
|
||||
included: true,
|
||||
importedFromId: null,
|
||||
metalType: null,
|
||||
metalGrams: null,
|
||||
metalCostPerGram: null,
|
||||
metalPurity: '1',
|
||||
autoValuation: false,
|
||||
kind: 'asset',
|
||||
side: 'asset',
|
||||
category: 'other',
|
||||
@@ -100,102 +122,31 @@ test('backup accepts over 1000 positions, 10000 revisions per position and 20000
|
||||
);
|
||||
});
|
||||
|
||||
test('legacy v3 ZIP remains readable without icons', async () => {
|
||||
const contents = packBackup(empty());
|
||||
delete contents['icons.json'];
|
||||
delete contents['transfers.json'];
|
||||
delete contents['schedules.json'];
|
||||
const manifest = JSON.parse(contents['manifest.json']);
|
||||
manifest.version = 3;
|
||||
manifest.files = manifest.files.filter(
|
||||
(f: { name: string }) => !['icons.json', 'transfers.json', 'schedules.json'].includes(f.name),
|
||||
);
|
||||
contents['manifest.json'] = JSON.stringify(manifest);
|
||||
const restored = validateBackup(await readBackupZip(await archive(contents)));
|
||||
assert.equal(restored.icons, undefined);
|
||||
assert.deepEqual(restored.positions, []);
|
||||
test('all historical ZIP versions and JSON formats are rejected', async () => {
|
||||
for (const version of [3, 4, 5, 6, 7, 8]) {
|
||||
const contents = packBackup(empty());
|
||||
const manifest = JSON.parse(contents['manifest.json']);
|
||||
manifest.version = version;
|
||||
contents['manifest.json'] = JSON.stringify(manifest);
|
||||
await assert.rejects(async () => readBackupZip(await archive(contents)));
|
||||
}
|
||||
for (const version of [1, 2, 3]) assert.throws(() => validateBackup({ ...empty(), version }));
|
||||
});
|
||||
|
||||
test('legacy v4 ZIP remains readable without transfers', async () => {
|
||||
const contents = packBackup(empty());
|
||||
delete contents['transfers.json'];
|
||||
delete contents['schedules.json'];
|
||||
const manifest = JSON.parse(contents['manifest.json']);
|
||||
manifest.version = 4;
|
||||
manifest.files = manifest.files.filter(
|
||||
(f: { name: string }) => !['transfers.json', 'schedules.json'].includes(f.name),
|
||||
);
|
||||
contents['manifest.json'] = JSON.stringify(manifest);
|
||||
const restored = validateBackup(await readBackupZip(await archive(contents)));
|
||||
assert.deepEqual(restored.icons, []);
|
||||
assert.equal(restored.transfers, undefined);
|
||||
assert.equal(restored.preferences?.showNotes, false);
|
||||
assert.deepEqual(restored.preferences?.hiddenMenus, ['asset']);
|
||||
});
|
||||
|
||||
test('ZIP v7 restores groups and schedules while v6 remains readable', async () => {
|
||||
const id = randomUUID(),
|
||||
stamp = new Date().toISOString();
|
||||
const b = validateBackup({
|
||||
...empty(),
|
||||
positions: [
|
||||
{
|
||||
id,
|
||||
name: '账户',
|
||||
groupName: '日常',
|
||||
kind: 'account',
|
||||
side: 'asset',
|
||||
category: 'bank',
|
||||
currency: 'CNY',
|
||||
notes: '',
|
||||
archived: false,
|
||||
hidden: false,
|
||||
createdAt: stamp,
|
||||
updatedAt: stamp,
|
||||
revisions: [
|
||||
{
|
||||
id: randomUUID(),
|
||||
amount: '-10',
|
||||
date: '2026-09-01T10:00',
|
||||
notes: '',
|
||||
reason: 'balance',
|
||||
createdAt: stamp,
|
||||
updatedAt: stamp,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
schedules: [
|
||||
{
|
||||
id: randomUUID(),
|
||||
name: '租金',
|
||||
operation: 'expense',
|
||||
sourceId: id,
|
||||
targetId: null,
|
||||
amount: '100',
|
||||
received: '0',
|
||||
nextAt: '2026-11-01T10:00',
|
||||
intervalDays: 30,
|
||||
enabled: true,
|
||||
notes: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
const contents = packBackup(b);
|
||||
assert.deepEqual(validateBackup(await readBackupZip(await archive(contents))), b);
|
||||
delete contents['schedules.json'];
|
||||
const manifest = JSON.parse(contents['manifest.json']);
|
||||
manifest.version = 6;
|
||||
manifest.files = manifest.files.filter((f: { name: string }) => f.name !== 'schedules.json');
|
||||
contents['manifest.json'] = JSON.stringify(manifest);
|
||||
assert.equal(validateBackup(await readBackupZip(await archive(contents))).schedules, undefined);
|
||||
});
|
||||
|
||||
test('ZIP settings retain group order while missing legacy order remains optional', async () => {
|
||||
test('current backups require complete settings and metadata, with no legacy defaults', async () => {
|
||||
const b = empty();
|
||||
b.preferences!.accountGroupOrder = ['理财', '', '未分组', '日常'];
|
||||
const restored = validateBackup(await readBackupZip(await archive(packBackup(b))));
|
||||
assert.deepEqual(restored.preferences!.accountGroupOrder, b.preferences!.accountGroupOrder);
|
||||
const legacy = validateBackup(await readBackupZip(await archive(packBackup(empty()))));
|
||||
assert.equal(legacy.preferences!.accountGroupOrder, undefined);
|
||||
b.preferences.accountGroupOrder = ['日常', ''];
|
||||
assert.deepEqual(validateBackup(await readBackupZip(await archive(packBackup(b)))), b);
|
||||
for (const key of Object.keys(b.preferences)) {
|
||||
const incomplete = structuredClone(b);
|
||||
delete (incomplete.preferences as any)[key];
|
||||
assert.throws(() => validateBackup(incomplete));
|
||||
}
|
||||
for (const key of ['icons', 'transfers', 'schedules', 'metalPrices']) {
|
||||
const incomplete = structuredClone(b);
|
||||
delete (incomplete as any)[key];
|
||||
assert.throws(() => validateBackup(incomplete));
|
||||
}
|
||||
assert.throws(() =>
|
||||
validateBackup({ ...b, preferences: { ...b.preferences, showSidebar: false } }),
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user