clone

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

feat/browser-cors-sync-tickets
lofi-node / tests / activity_test.ts
activity_test.ts 3.2K
1// Connection observability: gate stats carry last-seen, the activity sink is2// notified on authenticated traffic, and the sidecar round-trips for the CLI.34import { assert, assertEquals } from "@std/assert";5import { AppTicketStore } from "../src/appticket.ts";6import { startGate } from "../src/gate.ts";7import { readTicketActivity, TicketActivity } from "../src/ticket-activity.ts";89function startStubUpstream() {10 const server = Deno.serve(11 { hostname: "127.0.0.1", port: 0, onListen: () => {} },12 () => new Response("{}", { headers: { "content-type": "application/json" } }),13 );14 return {15 url: `http://127.0.0.1:${(server.addr as Deno.NetAddr).port}`,16 close: () => server.shutdown(),17 };18}1920Deno.test("authenticated traffic stamps gate stats and the activity sink", async () => {21 const upstream = startStubUpstream();22 const store = await AppTicketStore.load();23 const noted: string[] = [];24 const gate = startGate({25 port: 0,26 hostname: "127.0.0.1",27 target: () => upstream.url,28 mode: "ticket",29 store,30 appId: "stub-app",31 adminSecret: "node-admin-secret",32 activity: { note: (ticketId) => noted.push(ticketId) },33 });34 try {35 const issued = await store.issue("phone");36 const before = gate.stats();37 assertEquals(before.length, 0);3839 const res = await fetch(`http://127.0.0.1:${gate.port}/t/${issued.secret}/echo`);40 assertEquals(res.status, 200);41 await res.body?.cancel();4243 const after = gate.stats();44 const entry = after.find((e) => e.ticketId === issued.record.id);45 assert(entry, "stats must include the seen ticket");46 assertEquals(entry.connections, 0);47 assert(typeof entry.lastSeenAt === "string", "lastSeenAt must be stamped");48 assert(noted.includes(issued.record.id), "the activity sink must be notified");4950 // An invalid secret stamps nothing.51 const bad = await fetch(`http://127.0.0.1:${gate.port}/t/${"A".repeat(43)}/echo`);52 assertEquals(bad.status, 401);53 await bad.body?.cancel();54 assertEquals(gate.stats().length, 1);55 } finally {56 await gate.close();57 await upstream.close();58 }59});6061Deno.test("the activity sidecar persists and reloads", async () => {62 const dir = await Deno.makeTempDir();63 try {64 const activity = await TicketActivity.load(dir);65 assertEquals(activity.lastSeen("t1"), undefined);66 activity.note("t1");67 const stamped = activity.lastSeen("t1");68 assert(typeof stamped === "string", "note must stamp in memory immediately");69 await activity.flush();7071 const viaCli = await readTicketActivity(dir);72 assertEquals(viaCli.get("t1"), stamped);7374 const reloaded = await TicketActivity.load(dir);75 assertEquals(reloaded.lastSeen("t1"), stamped, "restarts keep last-seen");76 } finally {77 await Deno.remove(dir, { recursive: true });78 }79});8081Deno.test("a missing sidecar reads as empty", async () => {82 const dir = await Deno.makeTempDir();83 try {84 const viaCli = await readTicketActivity(dir);85 assertEquals(viaCli.size, 0);86 const memoryOnly = await TicketActivity.load();87 memoryOnly.note("t1");88 await memoryOnly.flush();89 assert(memoryOnly.lastSeen("t1") !== undefined, "memory-only activity still tracks");90 } finally {91 await Deno.remove(dir, { recursive: true });92 }93});