Files
xo-cli/tests/utils/sync-server.test.ts
T
2026-09-07 10:56:08 +00:00

155 lines
4.6 KiB
TypeScript

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<void>>(),
onError: undefined as ((error: Error) => void) | undefined,
onMessage: undefined as ((message: string) => void) | undefined,
read: vi.fn<(resourceId: string) => Promise<unknown>>(),
subscribe: vi.fn<(resourceId: string) => Promise<void>>(),
write:
vi.fn<
(resourceId: string, value: Record<string, unknown>) => Promise<unknown>
>(),
}));
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 `<Uint8Array: ${binToHex(invitationBlob(invitation))}>`;
}
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)));
});
});