clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

fix/175-template-quality-pass
lofi / package / runtime / sync-owner_test.ts
sync-owner_test.ts 3.9K
1// Package contract tests for the sync-owner pin: fingerprint derivation,2// record round-trip, and the adjudication verdicts the election guard and3// session snapshot read.4import { anchorAppId } from "./data-sink.ts";5import {6 adjudicateSyncOwner,7 clearSyncOwner,8 isSyncOwnerError,9 readSyncOwner,10 readSyncOwnerVerdict,11 recordSyncOwner,12 secretFingerprint,13 SyncOwnerError,14} from "./sync-owner.ts";15import { assert } from "./test-assert.ts";1617const test = (globalThis as unknown as {18 Deno: { test(name: string, body: () => void | Promise<void>): void };19}).Deno.test;2021const ownerKey = `lofi:sync-owner:${anchorAppId}`;2223function withCleanState(body: () => void | Promise<void>): () => Promise<void> {24 return async () => {25 localStorage.removeItem(ownerKey);26 clearSyncOwner();27 try {28 await body();29 } finally {30 localStorage.removeItem(ownerKey);31 clearSyncOwner();32 }33 };34}3536test("the fingerprint is deterministic, non-empty, and secret-sensitive", async () => {37 const first = await secretFingerprint("secret-a");38 assert(first === await secretFingerprint("secret-a"), "one secret must map to one fingerprint");39 assert(/^[0-9a-f]{16}$/.test(first), "the fingerprint is 8 bytes of lowercase hex");40 assert(41 first !== await secretFingerprint("secret-b"),42 "different secrets must map to different fingerprints",43 );44});4546test(47 "the owner record round-trips and clears",48 withCleanState(() => {49 assert(readSyncOwner() === null, "a clean device has no owner record");50 recordSyncOwner({ fingerprint: "aabbccddeeff0011", user_id: "user-1" });51 const record = readSyncOwner();52 assert(53 record !== null && record.fingerprint === "aabbccddeeff0011" && record.user_id === "user-1",54 "the persisted record must read back verbatim",55 );56 clearSyncOwner();57 assert(readSyncOwner() === null, "clearing must remove the record");58 }),59);6061test(62 "a malformed persisted record reads as unclaimed",63 withCleanState(() => {64 localStorage.setItem(ownerKey, "not json");65 assert(readSyncOwner() === null, "garbage must not adjudicate");66 localStorage.setItem(ownerKey, JSON.stringify({ v: 1, fingerprint: "" }));67 assert(readSyncOwner() === null, "an empty fingerprint must not adjudicate");68 localStorage.setItem(ownerKey, JSON.stringify({ v: 99, fingerprint: "aabbccddeeff0011" }));69 assert(readSyncOwner() === null, "an unknown record version must not adjudicate");70 }),71);7273test(74 "adjudication answers unclaimed, self, and foreign, and the verdict is cached",75 withCleanState(() => {76 assert(77 readSyncOwnerVerdict().state === "unadjudicated",78 "before any adjudication the verdict is unadjudicated",79 );80 assert(adjudicateSyncOwner("aabbccddeeff0011").state === "unclaimed", "no record → unclaimed");81 recordSyncOwner({ fingerprint: "aabbccddeeff0011", user_id: "user-1" });82 assert(83 adjudicateSyncOwner("aabbccddeeff0011").state === "self",84 "the recorded fingerprint adjudicates as self",85 );86 const foreign = adjudicateSyncOwner("1122334455667788");87 assert(88 foreign.state === "foreign" && foreign.owner_user_id === "user-1",89 "a different fingerprint adjudicates as foreign, naming the owner",90 );91 assert(92 readSyncOwnerVerdict().state === "foreign",93 "the cached verdict must reflect the latest adjudication",94 );95 clearSyncOwner();96 assert(97 readSyncOwnerVerdict().state === "unadjudicated",98 "clearing the pin resets the cached verdict",99 );100 }),101);102103test("the owner error carries its code and the owning account", () => {104 const error = new SyncOwnerError("user-1");105 assert(isSyncOwnerError(error), "the guard must recognize its own error");106 assert(error.code === "owner-mismatch", "the error carries its stable code");107 assert(error.owner_user_id === "user-1", "the error names the owning account");108 assert(!isSyncOwnerError(new Error("other")), "foreign errors are not owner errors");109});