/** * Tests for the CLI read helpers (`src/cli-read.ts`, owner amendment to * issue #36, issue #52). * * The `collections`, `files`, `get`, and `get-thumb` commands must answer for * current server state, not the local cache, so each helper forces a * `Library.fresh()` round-trip before it reads. The stand-in library below * serves nothing until `fresh()` has been awaited, so a helper that read * without refreshing would come back empty and fail here. * * `collections` and `files` also list in the library's enumeration order * (`listCollections`/`listFiles`) — the order the pre-library CLI printed — not * the albums/photos projection's newest-first order. The fixtures are seeded in * an enumeration order that a newest-first sort would rearrange, so a * regression to the projection order would fail here too. Field values still * come from the raw metadata via `cli-output.ts`. */ import { describe, it, expect } from "vitest"; import { freshCollections, freshFiles, freshFile, type FreshReadLibrary, } from "../../src/cli-read.js"; import { fileListRow } from "../../src/cli-output.js"; import type { Photo } from "../../src/library/index.js"; import type { Collection, EnteFile } from "../../src/model/types.js"; const collection = (id: number, updationTime: number): Collection => ({ id, ownerID: 42, key: new Uint8Array(), name: `album-${id}`, type: "album", updationTime, isShared: false, }); // Microseconds, as Ente stores times. const file = ( id: number, collectionID: number, creationTime: number, ): EnteFile => ({ id, collectionID, ownerID: 42, key: new Uint8Array(), metadata: { title: `file-${id}.jpg`, fileType: "image", creationTime, modificationTime: creationTime, }, file: { decryptionHeader: "" }, thumbnail: { decryptionHeader: "" }, updationTime: creationTime, }); // A library that reveals its records only after `fresh()` has been awaited, and // serves them in the enumeration order it was given. `photos.byID` returns a // stand-in `Photo` carrying just the fileID the helper passes through. class FakeLibrary implements FreshReadLibrary { freshCalls = 0; private refreshed = false; constructor( private readonly collections: Collection[], private readonly files: EnteFile[], ) {} async fresh(): Promise { this.freshCalls++; this.refreshed = true; return {}; } listCollections(): Collection[] { return this.refreshed ? this.collections : []; } getCollection(id: number): Collection | undefined { return this.listCollections().find((c) => c.id === id); } listFiles(collectionID: number): EnteFile[] { return this.refreshed ? this.files.filter((f) => f.collectionID === collectionID) : []; } getFileByID(fileID: number): EnteFile | undefined { if (!this.refreshed) return undefined; return this.files.find((f) => f.id === fileID); } photos = { byID: ({ fileID }: { fileID: number }): Photo | undefined => { if (!this.refreshed) return undefined; if (!this.files.some((f) => f.id === fileID)) return undefined; return { fileID } as unknown as Photo; }, }; } describe("CLI read helpers (issue #36 amendment, issue #52)", () => { it("freshCollections refreshes first, then lists in enumeration order", async () => { // Enumeration order 2, 1, 3; a newest-first sort would be 3, 2, 1. const lib = new FakeLibrary( [collection(2, 200), collection(1, 300), collection(3, 100)], [], ); const rows = await freshCollections(lib); expect(lib.freshCalls).toBe(1); expect(rows.map((c) => c.id)).toEqual([2, 1, 3]); // The projection's newest-first order is a different sequence, so this // is not accidentally that order. const newestFirst = [...rows] .sort((a, b) => b.updationTime - a.updationTime) .map((c) => c.id); expect(newestFirst).toEqual([1, 2, 3]); expect(rows.map((c) => c.id)).not.toEqual(newestFirst); }); it("freshFiles refreshes first, lists in enumeration order, keeps raw fields", async () => { // Enumeration order by id 10, 11, 12; creationTimes ascending, so a // newest-first sort would reverse them. const files = [ file(10, 1, 1_700_000_000_000_000), file(11, 1, 1_700_000_000_000_001), file(12, 1, 1_700_000_000_000_002), ]; const lib = new FakeLibrary([collection(1, 100)], files); const rows = await freshFiles(lib, 1); expect(lib.freshCalls).toBe(1); expect(rows?.map((f) => f.id)).toEqual([10, 11, 12]); // Field values come from raw metadata: microsecond creationTime and the // raw title, unchanged. expect(rows?.map(fileListRow)).toEqual([ { id: 10, title: "file-10.jpg", fileType: "image", creationTime: 1_700_000_000_000_000, collectionID: 1, }, { id: 11, title: "file-11.jpg", fileType: "image", creationTime: 1_700_000_000_000_001, collectionID: 1, }, { id: 12, title: "file-12.jpg", fileType: "image", creationTime: 1_700_000_000_000_002, collectionID: 1, }, ]); }); it("freshFiles returns undefined for an unknown collection", async () => { const lib = new FakeLibrary([collection(1, 100)], []); const rows = await freshFiles(lib, 999); expect(lib.freshCalls).toBe(1); expect(rows).toBeUndefined(); }); it("freshFile refreshes first, then resolves the photo and its raw record", async () => { const f = file(10, 1, 1_700_000_000_000_000); const lib = new FakeLibrary([collection(1, 100)], [f]); const resolved = await freshFile(lib, 10); expect(lib.freshCalls).toBe(1); expect(resolved?.photo.fileID).toBe(10); expect(resolved?.file.metadata.title).toBe("file-10.jpg"); expect(resolved?.file.metadata.creationTime).toBe( 1_700_000_000_000_000, ); }); it("freshFile returns undefined for an unknown file", async () => { const lib = new FakeLibrary([collection(1, 100)], []); const resolved = await freshFile(lib, 404); expect(lib.freshCalls).toBe(1); expect(resolved).toBeUndefined(); }); });