clone

a self-hosted sync node for lofi apps: jazz sync over iroh, ticket-gated, with no static ip or cloud needed

feat/write-verdict-foundations
lofi-node / tests / pair_test.ts
pair_test.ts 4.9K
1// Integration: the full daemon topology — two real JazzServers, the leaf's2// upstream election `{ peer: ticket }` carried over the iroh tunnel to the3// root. Proves createSyncNode wiring end to end; whether Jazz's upstream WS4// client dials eagerly at start or lazily on first sync traffic is Jazz's5// business — here we assert the topology comes up and both servers stay6// healthy with the tunnel in the path.78import { assert, assertEquals } from "@std/assert";9import { createSyncNode } from "../src/node.ts";10import { resolveIrohLib } from "../src/native/loader.ts";1112const available = resolveIrohLib().status === "ok";1314async function healthy(wsUrl: string): Promise<boolean> {15 try {16 const res = await fetch(new URL("/health", wsUrl.replace(/^ws/, "http")));17 return res.status === 200 && (await res.json()).status === "healthy";18 } catch {19 return false;20 }21}2223Deno.test({24 name: "two nodes pair over iroh: leaf upstream tunnels to root",25 ignore: !available,26 sanitizeOps: false,27 sanitizeResources: false,28 fn: async () => {29 const shared = {30 appId: crypto.randomUUID(),31 backendSecret: "lofi_backend_pairtest",32 adminSecret: "lofi_admin_pairtest",33 };3435 const root = await createSyncNode({ ...shared, inMemory: true });36 const rootStatus = root.status();37 assertEquals(rootStatus.mesh.state, "up", "root mesh up");3839 const leaf = await createSyncNode({40 ...shared,41 inMemory: true,42 upstream: { peer: root.ticket() },43 });44 const leafStatus = leaf.status();45 assertEquals(leafStatus.mesh.state, "up", "leaf mesh up");46 assert("peer" in (leafStatus.upstream as object), "leaf elected peer upstream");4748 // Give Jazz's upstream client a moment to dial through the tunnel.49 await new Promise((r) => setTimeout(r, 3000));5051 assert(await healthy(root.url), "root healthy with tunnel in path");52 assert(await healthy(leaf.url), "leaf healthy with tunnel in path");5354 // Observability: the live upstream link shows up in both nodes' status.55 const leafMesh = leaf.status().mesh;56 const rootMesh = root.status().mesh;57 assert(leafMesh.state === "up" && rootMesh.state === "up", "both meshes up");58 assert(59 leafMesh.connections.some((c) => c.direction === "out" && c.paths >= 1),60 `leaf shows a live outbound tunnel conn: ${JSON.stringify(leafMesh.connections)}`,61 );62 assert(63 rootMesh.connections.some((c) => c.direction === "in"),64 `root shows a live inbound tunnel conn: ${JSON.stringify(rootMesh.connections)}`,65 );6667 await leaf.stop();68 await root.stop();69 },70});7172Deno.test({73 name: "runtime pair(): late upstream election without losing the port",74 ignore: !available,75 sanitizeOps: false,76 sanitizeResources: false,77 fn: async () => {78 const shared = {79 appId: crypto.randomUUID(),80 backendSecret: "lofi_backend_latepair",81 adminSecret: "lofi_admin_latepair",82 };83 const root = await createSyncNode({ ...shared, inMemory: true });84 const leaf = await createSyncNode({ ...shared, inMemory: true }); // unpaired85 const portBefore = leaf.port;86 assertEquals(leaf.status().upstream, "none");8788 await leaf.pair(root.ticket());8990 assertEquals(leaf.port, portBefore, "port survives re-pairing");91 assert("peer" in (leaf.status().upstream as object), "upstream re-elected");92 assert(await healthy(leaf.url), "leaf healthy on the same URL after pair");9394 // Jazz eagerly dials the new upstream through the tunnel.95 await new Promise((r) => setTimeout(r, 3000));96 const rootMesh = root.status().mesh;97 assert(98 rootMesh.state === "up" && rootMesh.connections.some((c) => c.direction === "in"),99 "root sees the newly paired leaf's tunnel",100 );101102 await leaf.stop();103 await root.stop();104 },105});106107Deno.test({108 name: "ticket mode: gate URL and port survive runtime pair()",109 ignore: !available,110 sanitizeOps: false,111 sanitizeResources: false,112 fn: async () => {113 const shared = {114 appId: crypto.randomUUID(),115 backendSecret: "lofi_backend_gatepair",116 adminSecret: "lofi_admin_gatepair",117 };118 const root = await createSyncNode({ ...shared, inMemory: true });119 const leaf = await createSyncNode({ ...shared, inMemory: true, access: "ticket" });120 const gateUrlBefore = leaf.url;121 const gatePortBefore = leaf.port;122 const internalBefore = leaf.status().jazz.port;123 assert(gatePortBefore !== internalBefore, "gate and internal jazz are distinct ports");124125 await leaf.pair(root.ticket());126127 assertEquals(leaf.url, gateUrlBefore, "public gate URL unchanged across pair()");128 assertEquals(leaf.port, gatePortBefore, "public gate port unchanged across pair()");129 // The gate still reaches the (restarted) internal Jazz.130 const res = await fetch(`${gateUrlBefore.replace(/^ws/, "http")}/health`);131 assertEquals((await res.json()).status, "healthy", "gate → restarted jazz healthy");132133 await leaf.stop();134 await root.stop();135 },136});