/** * Integration between `Library` and the content cache (issue #46). * * The cache itself is covered in `content.test.ts`; this file locks the wiring: * `Library.open` builds the cache from a content source, `lib.photos` hands out * `Photo` objects that fetch through it, `lib.thumbnails.ensure` drives it, and * a cached path shows up on the projected record. A library opened without a * content source leaves those methods throwing rather than silently doing * nothing. */ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdtempSync, rmSync, existsSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Library } from "../../src/library/index.js"; import type { ContentSource } from "../../src/library/content.js"; import type { CollectionsPage, FilesPage } from "../../src/client.js"; import type { Collection, EnteFile } from "../../src/model/types.js"; const USER_ID = 7; const collection = (id: number): Collection => ({ id, ownerID: USER_ID, key: new Uint8Array([id & 0xff]), name: `album-${id}`, type: "album", updationTime: 1, isShared: false, }); const file = (id: number, collectionID: number): EnteFile => ({ id, collectionID, ownerID: USER_ID, key: new Uint8Array([id & 0xff]), metadata: { title: `file-${id}.jpg`, fileType: "image", creationTime: 1, modificationTime: 1, }, file: { decryptionHeader: "aGVhZGVy" }, thumbnail: { decryptionHeader: "dGh1bWI=" }, updationTime: 1, }); // A metadata-only client serving one album with one file, once. class MockClient { served = false; whoami(): { email: string; userID: number } { return { email: "u@example.com", userID: USER_ID }; } async collectionsSince(): Promise { if (this.served) return { collections: [], deleted: [], cursor: 1 }; this.served = true; return { collections: [collection(1)], deleted: [], cursor: 1 }; } async filesSince(): Promise { return { files: [file(1, 1)], deleted: [], cursor: 1 }; } } // A content source that writes a marker file and counts thumbnail fetches. const stubSource = (): ContentSource & { thumbCalls: () => number } => { let thumbCalls = 0; return { thumbCalls: () => thumbCalls, original: async ({ destination }) => { writeFileSync(destination, "orig-bytes"); return { bytesWritten: 10 }; }, thumbnail: async ({ destination }) => { thumbCalls++; writeFileSync(destination, "thumb"); return { bytesWritten: 5 }; }, }; }; let root: string; beforeEach(() => { root = mkdtempSync(join(tmpdir(), "quak-content-lib-")); }); afterEach(() => { if (root && existsSync(root)) rmSync(root, { recursive: true, force: true }); }); describe("Library content wiring", () => { it("fetches a thumbnail through a Photo and records its cache path", async () => { const source = stubSource(); const lib = await Library.open({ client: new MockClient(), cacheDirectory: join(root, "cache"), contentSource: source, refreshIntervalSeconds: 3600, // On-demand wiring only; the background precache (#48) is covered // in precache.test.ts and would race the exact-count assertions. precacheThumbnails: false, precacheOriginals: false, }); const photo = lib.photos.byID({ fileID: 1 }); expect(photo).toBeDefined(); const result = await photo!.thumbnail(); expect(source.thumbCalls()).toBe(1); expect(result.path).toBe(join(root, "cache", "thumbnails", "1.jpg")); expect(existsSync(result.path)).toBe(true); // The cached path is now on the projected record. expect(lib.photos.byID({ fileID: 1 })!.record().thumbnailPath).toBe( result.path, ); lib.close(); }); it("drives thumbnails.ensure through the cache", async () => { const source = stubSource(); const lib = await Library.open({ client: new MockClient(), cacheDirectory: join(root, "cache"), contentSource: source, refreshIntervalSeconds: 3600, // On-demand wiring only; the background precache (#48) is covered // in precache.test.ts and would race the exact-count assertions. precacheThumbnails: false, precacheOriginals: false, }); const results = await lib.thumbnails.ensure({ fileIDs: [1], priority: "visible", }); expect(results).toEqual([ { fileID: 1, path: join(root, "cache", "thumbnails", "1.jpg") }, ]); lib.close(); }); it("throws from content methods when opened without a content source", async () => { const lib = await Library.open({ client: new MockClient(), cacheDirectory: join(root, "cache"), refreshIntervalSeconds: 3600, }); await expect( lib.photos.byID({ fileID: 1 })!.thumbnail(), ).rejects.toThrow(/content cache/i); await expect( lib.thumbnails.ensure({ fileIDs: [1], priority: "visible" }), ).rejects.toThrow(/content cache/i); lib.close(); }); });