100 lines
4.5 KiB
TypeScript
100 lines
4.5 KiB
TypeScript
import { describe, it, expect, afterEach } from 'bun:test';
|
|
import { groupSnapshotIdsByDestination, bulkDeleteSnapshots, bulkDeleteToast } from '../src/lib/utils/backup';
|
|
|
|
const snap = (id: string, dest: number) => ({ id, _destinationId: dest });
|
|
|
|
describe('groupSnapshotIdsByDestination', () => {
|
|
it('groups snapshot ids by their destination (one batch per repo)', () => {
|
|
const g = groupSnapshotIdsByDestination([
|
|
snap('a', 1), snap('b', 2), snap('c', 1), snap('d', 2), snap('e', 3),
|
|
]);
|
|
const byId = Object.fromEntries(g.map((x) => [x.destinationId, x.snapshotIds.sort()]));
|
|
expect(byId[1]).toEqual(['a', 'c']);
|
|
expect(byId[2]).toEqual(['b', 'd']);
|
|
expect(byId[3]).toEqual(['e']);
|
|
expect(g.length).toBe(3);
|
|
});
|
|
|
|
it('a single-destination selection is one batch', () => {
|
|
const g = groupSnapshotIdsByDestination([snap('a', 5), snap('b', 5)]);
|
|
expect(g).toEqual([{ destinationId: 5, snapshotIds: ['a', 'b'] }]);
|
|
});
|
|
|
|
it('empty input yields no batches', () => {
|
|
expect(groupSnapshotIdsByDestination([])).toEqual([]);
|
|
});
|
|
|
|
it('preserves per-destination order (restic forget order is stable)', () => {
|
|
const g = groupSnapshotIdsByDestination([snap('z', 1), snap('a', 1), snap('m', 1)]);
|
|
expect(g[0].snapshotIds).toEqual(['z', 'a', 'm']);
|
|
});
|
|
});
|
|
|
|
describe('bulkDeleteToast', () => {
|
|
it('failed with some deleted -> error naming the count', () => {
|
|
expect(bulkDeleteToast({ deleted: 2, skipped: 0, failed: true }))
|
|
.toEqual({ type: 'error', message: 'Deleted 2, but some snapshots could not be deleted' });
|
|
});
|
|
it('failed with nothing deleted -> plain error', () => {
|
|
expect(bulkDeleteToast({ deleted: 0, skipped: 0, failed: true }))
|
|
.toEqual({ type: 'error', message: 'Snapshots could not be deleted' });
|
|
});
|
|
it('nothing deleted but skipped -> error (not owned / no access), pluralized', () => {
|
|
expect(bulkDeleteToast({ deleted: 0, skipped: 1, failed: false }))
|
|
.toEqual({ type: 'error', message: '1 snapshot could not be deleted (not owned or no access)' });
|
|
expect(bulkDeleteToast({ deleted: 0, skipped: 3, failed: false }).message)
|
|
.toBe('3 snapshots could not be deleted (not owned or no access)');
|
|
});
|
|
it('success -> singular vs plural and optional skipped suffix', () => {
|
|
expect(bulkDeleteToast({ deleted: 1, skipped: 0, failed: false }))
|
|
.toEqual({ type: 'success', message: 'Deleted 1 snapshot' });
|
|
expect(bulkDeleteToast({ deleted: 3, skipped: 0, failed: false }).message).toBe('Deleted 3 snapshots');
|
|
expect(bulkDeleteToast({ deleted: 3, skipped: 2, failed: false }).message).toBe('Deleted 3 snapshots (2 skipped)');
|
|
});
|
|
});
|
|
|
|
describe('bulkDeleteSnapshots', () => {
|
|
const realFetch = globalThis.fetch;
|
|
afterEach(() => { globalThis.fetch = realFetch; });
|
|
|
|
it('one batch call per destination; sums deleted and skipped across repos', async () => {
|
|
const calls: any[] = [];
|
|
// dest 1 -> deletes a,c; dest 2 -> deletes b, skips d
|
|
globalThis.fetch = (async (_url: string, init: any) => {
|
|
const body = JSON.parse(init.body);
|
|
calls.push(body);
|
|
const per: Record<number, any> = {
|
|
1: { deleted: ['a', 'c'], skipped: [] },
|
|
2: { deleted: ['b'], skipped: ['d'] }
|
|
};
|
|
return { ok: true, json: async () => per[body.destinationId] } as any;
|
|
}) as any;
|
|
|
|
const r = await bulkDeleteSnapshots([snap('a', 1), snap('b', 2), snap('c', 1), snap('d', 2)]);
|
|
expect(calls.length).toBe(2);
|
|
expect(r).toEqual({ deleted: 3, skipped: 1, failed: false });
|
|
});
|
|
|
|
it('a non-ok response marks failed and does not count that repo\'s skipped', async () => {
|
|
globalThis.fetch = (async (_url: string, init: any) => {
|
|
const body = JSON.parse(init.body);
|
|
if (body.destinationId === 2) return { ok: false, json: async () => ({ deleted: ['b'], skipped: ['x'] }) } as any;
|
|
return { ok: true, json: async () => ({ deleted: ['a'], skipped: [] }) } as any;
|
|
}) as any;
|
|
const r = await bulkDeleteSnapshots([snap('a', 1), snap('b', 2)]);
|
|
// deleted still counts what the failed repo reported forgotten; skipped from a failed repo is not added
|
|
expect(r).toEqual({ deleted: 2, skipped: 0, failed: true });
|
|
});
|
|
|
|
it('a thrown fetch marks failed without aborting the other repos', async () => {
|
|
globalThis.fetch = (async (_url: string, init: any) => {
|
|
const body = JSON.parse(init.body);
|
|
if (body.destinationId === 1) throw new Error('network');
|
|
return { ok: true, json: async () => ({ deleted: ['b'], skipped: [] }) } as any;
|
|
}) as any;
|
|
const r = await bulkDeleteSnapshots([snap('a', 1), snap('b', 2)]);
|
|
expect(r.failed).toBe(true);
|
|
expect(r.deleted).toBe(1);
|
|
});
|
|
});
|