clone

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

main
lofi / package / runtime / recovery_test.ts
recovery_test.ts 3.3K
1import {2 // Package contract tests.3 fromRecoveryPhrase,4 RECOVERY_PHRASE_WORDS,5 RecoveryError,6 toRecoveryPhrase,7} from "./recovery.ts";8import { assert } from "./test-assert.ts";910const test = (globalThis as unknown as {11 Deno: { test(name: string, body: () => void | Promise<void>): void };12}).Deno.test;1314// A 32-byte base64url value, the shape of a Jazz account secret.15function sampleSecret(seed = 3): string {16 const bytes = new Uint8Array(32).map((_, i) => (i * 7 + seed) & 0xff);17 let binary = "";18 for (const byte of bytes) binary += String.fromCharCode(byte);19 return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");20}2122test("toRecoveryPhrase encodes an account secret as the expected number of words", () => {23 const phrase = toRecoveryPhrase(sampleSecret());24 assert(25 phrase.split(" ").length === RECOVERY_PHRASE_WORDS,26 `a recovery phrase must be ${RECOVERY_PHRASE_WORDS} words`,27 );28});2930test("a phrase round-trips back to the exact same secret", () => {31 const secret = sampleSecret(11);32 const restored = fromRecoveryPhrase(toRecoveryPhrase(secret));33 assert(restored === secret, "the phrase must reconstruct the identical account secret");34});3536test("distinct secrets produce distinct phrases", () => {37 assert(38 toRecoveryPhrase(sampleSecret(1)) !== toRecoveryPhrase(sampleSecret(2)),39 "different accounts must not share a recovery phrase",40 );41});4243test("fromRecoveryPhrase tolerates messy whitespace and casing", () => {44 const secret = sampleSecret(5);45 const phrase = toRecoveryPhrase(secret);46 const messy = ` ${phrase.toUpperCase().replaceAll(" ", " ")}\n`;47 assert(fromRecoveryPhrase(messy) === secret, "an honest phrase must survive reformatting");48});4950test("fromRecoveryPhrase rejects a wrong word count with invalid-length", () => {51 let code: string | undefined;52 let message = "";53 try {54 fromRecoveryPhrase("one two three");55 } catch (error) {56 if (error instanceof RecoveryError) {57 code = error.code;58 message = error.message;59 }60 }61 assert(code === "invalid-length", "too few words must raise invalid-length");62 assert(63 message.includes(`${RECOVERY_PHRASE_WORDS} words`) && !message.includes("invalid-length"),64 "the invalid-length error must give person-readable word-count guidance",65 );66});6768test("fromRecoveryPhrase rejects an empty phrase with invalid-length", () => {69 let threw = false;70 try {71 fromRecoveryPhrase(" ");72 } catch (error) {73 threw = error instanceof RecoveryError && error.code === "invalid-length";74 }75 assert(threw, "an empty phrase must raise a RecoveryError");76});7778test("fromRecoveryPhrase rejects a full-length but invalid phrase", () => {79 const words = Array.from({ length: RECOVERY_PHRASE_WORDS }, () => "zzzzzz").join(" ");80 let threw = false;81 try {82 fromRecoveryPhrase(words);83 } catch (error) {84 threw = error instanceof RecoveryError;85 }86 assert(threw, "unknown words or a bad checksum must raise a RecoveryError");87});8889test("toRecoveryPhrase rejects a secret that is not a 32-byte base64url value", () => {90 let threw = false;91 try {92 toRecoveryPhrase("too-short");93 } catch (error) {94 threw = error instanceof RecoveryError;95 }96 assert(threw, "an ill-formed secret must raise a RecoveryError, never a fake phrase");97});