import { binToHex } from "@bitauth/libauth"; import { deserializeInvitation, serializeInvitation } from "@xo-cash/engine"; import type { XOInvitation, XOInvitationCommit } from "@xo-cash/types"; import { beforeEach, describe, expect, it, vi } from "vitest"; const client = vi.hoisted(() => ({ disconnect: vi.fn<() => Promise>(), onError: undefined as ((error: Error) => void) | undefined, onMessage: undefined as ((message: string) => void) | undefined, read: vi.fn<(resourceId: string) => Promise>(), subscribe: vi.fn<(resourceId: string) => Promise>(), write: vi.fn< (resourceId: string, value: Record) => Promise >(), })); vi.mock("../../src/utils/syncing/index.js", () => ({ SSEClient: class { constructor( _url: string, _privateKey: unknown, options: { onError?: (error: Error) => void; onMessage?: (message: string) => void; }, ) { client.onError = options.onError; client.onMessage = options.onMessage; } disconnect = client.disconnect; read = client.read; subscribe = client.subscribe; write = client.write; }, })); import { SyncServer } from "../../src/utils/sync-server.js"; const invitationIdentifier = "01".repeat(16); const templateIdentifier = "02".repeat(32); function makeCommit(commitIdentifier: string): XOInvitationCommit { return { commitIdentifier, createdAtTimestamp: 1, data: {}, } as XOInvitationCommit; } function makeInvitation( commits: XOInvitationCommit[], createdAtTimestamp = 1, ): XOInvitation { return { actionIdentifier: "send", commits, createdAtTimestamp, invitationIdentifier, templateIdentifier, }; } function invitationBlob(invitation: XOInvitation): Uint8Array { return new TextEncoder().encode(serializeInvitation(invitation)); } function encodeProtocolBlob(invitation: XOInvitation): string { return ``; } describe("SyncServer v2 adapter", () => { beforeEach(() => { vi.clearAllMocks(); client.onError = undefined; client.onMessage = undefined; client.disconnect.mockResolvedValue(); client.subscribe.mockResolvedValue(); client.write.mockResolvedValue({}); }); it("scopes connection and writes to one invitation resource", async () => { const sync = new SyncServer( "https://v2.sync.xo.harvmaster.com", invitationIdentifier, new Uint8Array(32).fill(1), ); const invitation = makeInvitation([makeCommit("commit-a")]); await sync.connect(); await sync.publishInvitation(invitation); await sync.disconnect(); expect(client.subscribe).toHaveBeenCalledWith(invitationIdentifier); expect(client.write).toHaveBeenCalledWith(invitationIdentifier, invitation); expect(client.disconnect).toHaveBeenCalledOnce(); }); it("merges signer instances returned by a v2 resource read", async () => { const older = makeInvitation([makeCommit("commit-a")], 1); const newer = makeInvitation([makeCommit("commit-b")], 2); client.read.mockResolvedValue([ { blob: invitationBlob(older), publicKey: "02aa", resourceId: invitationIdentifier, timestamp: 1, }, { blob: invitationBlob(newer), publicKey: "02bb", resourceId: invitationIdentifier, timestamp: 2, }, ]); const sync = new SyncServer( "https://v2.sync.xo.harvmaster.com", invitationIdentifier, new Uint8Array(32).fill(1), ); const invitation = await sync.getInvitation(invitationIdentifier); expect( invitation?.commits.map((commit) => commit.commitIdentifier).sort(), ).toEqual(["commit-a", "commit-b"]); expect(invitation?.createdAtTimestamp).toBe(2); }); it("translates live instance changes to the existing invitation event", () => { const updated = makeInvitation([makeCommit("commit-live")]); const sync = new SyncServer( "https://v2.sync.xo.harvmaster.com", invitationIdentifier, new Uint8Array(32).fill(1), ); const messages: Array<{ event?: string; data: XOInvitation }> = []; sync.on("message", (message) => messages.push(message)); client.onMessage?.( JSON.stringify({ blob: encodeProtocolBlob(updated), publicKey: "02aa", resourceId: invitationIdentifier, timestamp: 2, }), ); expect(messages).toHaveLength(1); expect(messages[0]?.event).toBe("invitation-updated"); expect(messages[0]?.data).toEqual(deserializeInvitation(serializeInvitation(updated))); }); });