Files
dockhand-upstream/tests/group-snapshots-by-destination.test.ts
T

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);
});
});