import { describe, expect, it, vi } from "vitest"; import type { MediaIpcEvent, MediaIpcOptions } from "./media-ipc.js"; import { MEDIA_PHOTOS_CHANNELS, outcomeForError, parseSyncRequest, registerMediaIpc, } from "./media-ipc.js"; import type { PhotosHelper } from "./media-photos-helper.js"; import { PhotosHelperError } from "./media-photos-helper.js"; import type { PhotosSyncResult } from "./media-photos-sync.js"; import { PhotosSyncError } from "./media-photos-sync.js"; type Listener = (event: MediaIpcEvent, ...args: unknown[]) => unknown; const result: PhotosSyncResult = { indexed: 2, created: 2, updated: 0, unchanged: 0, uploaded: 0, fullListing: true, }; function setup(overrides: Partial = {}) { const handlers = new Map(); const helper = { authorize: vi.fn(async () => "authorized" as const), } as unknown as PhotosHelper; const options: MediaIpcOptions = { ipcMain: { handle: (channel, listener) => handlers.set(channel, listener) }, platform: "darwin", helper, isTrusted: () => true, runSync: vi.fn(async () => result), ...overrides, }; const registration = registerMediaIpc(options); const sent: [string, unknown][] = []; const event: MediaIpcEvent = { sender: { send: (channel, payload) => sent.push([channel, payload]), isDestroyed: () => false }, }; const call = (channel: string, ...args: unknown[]) => handlers.get(channel)!(event, ...args); return { handlers, helper, options, registration, call, sent }; } describe("media IPC on platforms without Apple Photos", () => { it.each(["linux", "win32"])( "reports %s as unavailable and rejects sync and cancel", async (platform) => { const { call, options, handlers } = setup({ platform }); expect([...handlers.keys()].sort()).toEqual( [ MEDIA_PHOTOS_CHANNELS.cancel, MEDIA_PHOTOS_CHANNELS.status, MEDIA_PHOTOS_CHANNELS.sync, ].sort(), ); await expect(call(MEDIA_PHOTOS_CHANNELS.status)).resolves.toEqual({ available: false }); expect(() => call(MEDIA_PHOTOS_CHANNELS.sync, { spaceId: null })).toThrow(/macOS/); expect(() => call(MEDIA_PHOTOS_CHANNELS.cancel)).toThrow(/macOS/); expect(options.runSync).not.toHaveBeenCalled(); expect(options.helper!.authorize).not.toHaveBeenCalled(); }, ); it("treats a macOS build without the helper as unavailable", async () => { const { call } = setup({ helper: null }); await expect(call(MEDIA_PHOTOS_CHANNELS.status)).resolves.toEqual({ available: false }); expect(() => call(MEDIA_PHOTOS_CHANNELS.sync, {})).toThrow(/macOS/); }); it("treats a helper that cannot start as unavailable", async () => { const { call, helper } = setup(); vi.mocked(helper.authorize).mockRejectedValueOnce(new PhotosHelperError("unavailable", "x")); await expect(call(MEDIA_PHOTOS_CHANNELS.status)).resolves.toEqual({ available: false }); }); }); describe("media IPC on macOS", () => { it("reads status without prompting", async () => { const { call, helper } = setup(); await expect(call(MEDIA_PHOTOS_CHANNELS.status)).resolves.toEqual({ available: true, authorization: "authorized", syncing: false, }); expect(helper.authorize).toHaveBeenCalledWith(false); }); it("runs one sync at a time, forwards progress and resolves with the outcome", async () => { let finish: (value: PhotosSyncResult) => void = () => {}; const runSync = vi.fn( ({ onProgress }: Parameters[0]) => new Promise((resolve) => { onProgress({ phase: "indexing", indexed: 1, uploadsDone: 0, uploadsTotal: 0 }); finish = resolve; }), ); const { call, sent } = setup({ runSync }); const first = call(MEDIA_PHOTOS_CHANNELS.sync, { spaceId: "space_1" }); const second = call(MEDIA_PHOTOS_CHANNELS.sync, { spaceId: "space_1" }); expect(second).toBe(first); expect(runSync).toHaveBeenCalledTimes(1); expect(runSync.mock.calls[0]?.[0].spaceId).toBe("space_1"); await expect(call(MEDIA_PHOTOS_CHANNELS.status)).resolves.toMatchObject({ syncing: true }); expect(sent).toEqual([ [ MEDIA_PHOTOS_CHANNELS.progress, { phase: "indexing", indexed: 1, uploadsDone: 0, uploadsTotal: 0 }, ], ]); finish(result); await expect(first).resolves.toEqual({ ok: true, result }); await expect(call(MEDIA_PHOTOS_CHANNELS.status)).resolves.toMatchObject({ syncing: false }); }); it("cancels the running sync", async () => { const runSync = vi.fn( ({ signal }: Parameters[0]) => new Promise((_resolve, reject) => { signal.addEventListener("abort", () => reject(new PhotosSyncError("cancelled", "Sync cancelled.")), ); }), ); const { call } = setup({ runSync }); const running = call(MEDIA_PHOTOS_CHANNELS.sync, {}); await call(MEDIA_PHOTOS_CHANNELS.cancel); await expect(running).resolves.toEqual({ ok: false, code: "cancelled", message: "Sync cancelled.", }); }); it("rejects untrusted senders and malformed requests", async () => { const { call } = setup({ isTrusted: () => false }); expect(() => call(MEDIA_PHOTOS_CHANNELS.sync, {})).toThrow("Not allowed."); await expect(call(MEDIA_PHOTOS_CHANNELS.status)).rejects.toThrow("Not allowed."); expect(() => parseSyncRequest(null)).toThrow(); expect(() => parseSyncRequest({ spaceId: "../x" })).toThrow(); expect(() => parseSyncRequest({ spaceId: 4 })).toThrow(); expect(parseSyncRequest({})).toEqual({ spaceId: null }); }); it("never forwards unexpected error text to the renderer", () => { expect(outcomeForError(new Error("/Users/example/secret path"))).toEqual({ ok: false, code: "failed", message: "Apple Photos sync failed.", }); expect( outcomeForError( new PhotosHelperError("not_authorized", "Photos access has not been granted."), ), ).toMatchObject({ code: "not_authorized", }); }); });