diff --git a/test/cli/backup-undecryptable.test.ts b/test/cli/backup-undecryptable.test.ts index 9500dc0..f7d2573 100644 --- a/test/cli/backup-undecryptable.test.ts +++ b/test/cli/backup-undecryptable.test.ts @@ -23,7 +23,13 @@ * key replaced by bytes no key opens, or its metadata text that is not JSON. */ -import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { + existsSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, +} from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import sodium from "libsodium-wrappers-sumo"; @@ -253,6 +259,18 @@ const brokenAccount = (): FakeEnte => { return ente; }; +// Album 1, "Trip", holds file 10, and album 2, "Family", holds file 20. Both +// files decrypt, but album 2's own key does not, so the album is the only +// record left out. All of it as of time 1. +const brokenAlbumAccount = (): FakeEnte => { + const ente = new FakeEnte(); + ente.putAlbum(1, "Trip", 1); + ente.putFile(1, 10, 1); + ente.putAlbum(2, "Family", 1, true); + ente.putFile(2, 20, 1); + return ente; +}; + // Collects everything written to it. class Output { text = ""; @@ -321,6 +339,12 @@ const failures = (): Record => { const stored = (id: number): boolean => existsSync(join(backupDir(), DAY, `2026-03-01.${id}.jpg`)); +// The IDs of the files that album `name`'s JSON lists. +const albumFiles = (name: string): number[] => + JSON.parse( + readFileSync(join(backupDir(), "collections", `${name}.json`), "utf-8"), + ).files.map((f: { id: number }) => f.id); + describe("quak backup with records it cannot decrypt", () => { it("backs up every other file, records each file it cannot decrypt in failures.json, names each such album, and exits 1", async () => { expect(await backup(brokenAccount())).toBe(1); @@ -398,6 +422,29 @@ describe("quak backup with records it cannot decrypt", () => { expect(stderr.text).not.toContain("FAILED"); }); + it("exits 1 when the only record it cannot decrypt is an album, and 0 once the album decrypts", async () => { + const ente = brokenAlbumAccount(); + + // No file is in `failures.json`: the album alone keeps the exit code + // at 1. + expect(await backup(ente)).toBe(1); + expect(failures()).toEqual({}); + + ente.putAlbum(2, "Family", 2); + expect(await backup(ente)).toBe(0); + expect(stored(20)).toBe(true); + }); + + it("exits 1 when the only record it cannot decrypt is an album, and 0 once the album is deleted", async () => { + const ente = brokenAlbumAccount(); + + expect(await backup(ente)).toBe(1); + expect(failures()).toEqual({}); + + ente.deleteAlbum(2, 2); + expect(await backup(ente)).toBe(0); + }); + it("backs up a file that does not decrypt in one album but does in another, and does not report it", async () => { // File 30 is in albums 1 and 3, each holding its key sealed under the // album's own key. In album 1 that is corrupt; in album 3 it is not, @@ -412,6 +459,13 @@ describe("quak backup with records it cannot decrypt", () => { expect(stored(30)).toBe(true); expect(failures()).toEqual({}); + // Album 1 has no copy of file 30 that decrypts, so it neither links + // the original nor lists the file; album 3 lists it. + expect(readdirSync(join(backupDir(), "collections", "Trip"))).toEqual( + [], + ); + expect(albumFiles("Trip")).toEqual([]); + expect(albumFiles("Favorites")).toEqual([30]); }); it("backs up that file in a run limited to the album where it does not decrypt", async () => {