import { describe, expect, it, vi } from "vitest"; import { applySyncEntriesMessage, buildSyncEntriesMessage, buildLocalSyncSnapshot, cancelSyncIfVaultLocked, checkSyncPreflight, planManifestSync, } from "./sync-coordinator"; import { decodeSyncMessage } from "../shared/sync-wire"; import type { VaultEntry } from "../shared/types"; function entry(overrides: Partial = {}): VaultEntry { return { id: "entry-1", type: "login", title: "GitHub", username: "june", password: "secret", url: "https://github.com", createdAt: 1_000, modifiedAt: 1_000, ...overrides, }; } describe("sync coordinator", () => { it("blocks sync for free users before inspecting the vault", async () => { const getEntries = vi.fn(() => [entry()]); await expect( checkSyncPreflight({ isProUser: async () => false, isUnlocked: () => true, }) ).resolves.toEqual({ ok: false, error: "pro_required" }); expect(getEntries).not.toHaveBeenCalled(); }); it("blocks sync when the vault is locked", async () => { await expect( checkSyncPreflight({ isProUser: async () => true, isUnlocked: () => false, }) ).resolves.toEqual({ ok: false, error: "vault_locked" }); }); it("builds a local sync snapshot only after preflight passes", async () => { const snapshot = await buildLocalSyncSnapshot({ isProUser: async () => true, isUnlocked: () => true, getEntries: () => [entry({ id: "b" }), entry({ id: "a", modifiedAt: 2_000 })], }); expect(snapshot.entries).toHaveLength(2); expect(snapshot.manifest.map((item) => item.id)).toEqual(["a", "b"]); }); it("plans manifest sync without sending entry payloads", async () => { const plan = await planManifestSync( [{ id: "remote-only", modifiedAt: 1_500, hash: "remote" }], { isProUser: async () => true, isUnlocked: () => true, getEntries: () => [entry({ id: "local-only", modifiedAt: 2_000 })], } ); expect(plan.diff).toEqual({ requestFromRemote: ["remote-only"], offerToRemote: ["local-only"], conflicts: [], }); }); it("builds encrypted entry messages for requested ids", async () => { const rawEntryKey = new Uint8Array(32).fill(5); const message = await buildSyncEntriesMessage(["b"], rawEntryKey, { isProUser: async () => true, isUnlocked: () => true, getEntries: () => [ entry({ id: "a" }), entry({ id: "b", passkey: { rpId: "github.com", rpName: "GitHub", credentialId: "credential", publicKeyCose: "public", privateKeyJwk: { kty: "EC", d: "secret" }, userHandle: "user", userName: "june", userDisplayName: "June", algorithm: -7, signCount: 1, aaguid: "aaguid", backupEligible: true, backupState: true, createdAt: 1_000, }, }), ], }); expect(message.type).toBe("SYNC_ENTRIES"); expect(message.entries).toHaveLength(1); expect(message.entries[0]).toMatchObject({ id: "b" }); expect(message.entries[0]?.ciphertext).not.toContain("secret"); }); it("decrypts and applies incoming entry messages through vault merge", async () => { const rawEntryKey = new Uint8Array(32).fill(6); const message = await buildSyncEntriesMessage(["remote"], rawEntryKey, { isProUser: async () => true, isUnlocked: () => true, getEntries: () => [entry({ id: "remote", modifiedAt: 2_000 })], }); const restoreEntries = vi.fn(async () => ({ added: 1, updated: 0, skipped: 0 })); await expect( applySyncEntriesMessage(message, rawEntryKey, { isProUser: async () => true, isUnlocked: () => true, restoreEntries, }) ).resolves.toEqual({ added: 1, updated: 0, skipped: 0 }); expect(restoreEntries).toHaveBeenCalledWith( [expect.objectContaining({ id: "remote", modifiedAt: 2_000 })], "merge" ); }); it("sends a stable sync error when a peer asks while the vault is locked", async () => { const sendSyncData = vi.fn(); const cancelled = await cancelSyncIfVaultLocked("conn-1", { isUnlocked: () => false, sendSyncData, }); expect(cancelled).toBe(true); expect(sendSyncData).toHaveBeenCalledWith("conn-1", expect.any(String)); expect(decodeSyncMessage(sendSyncData.mock.calls[0]![1])).toEqual({ type: "SYNC_ERROR", code: "vault_locked", message: "Vault is locked; unlock Peach before syncing.", }); }); it("does not cancel an unlocked vault sync", async () => { const sendSyncData = vi.fn(); expect( await cancelSyncIfVaultLocked("conn-1", { isUnlocked: () => true, sendSyncData, }) ).toBe(false); expect(sendSyncData).not.toHaveBeenCalled(); }); });