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 / recovery_test.ts
recovery_test.ts 3.0K
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 try {53 fromRecoveryPhrase("one two three");54 } catch (error) {55 if (error instanceof RecoveryError) code = error.code;56 }57 assert(code === "invalid-length", "too few words must raise invalid-length");58});5960test("fromRecoveryPhrase rejects an empty phrase with invalid-length", () => {61 let threw = false;62 try {63 fromRecoveryPhrase(" ");64 } catch (error) {65 threw = error instanceof RecoveryError && error.code === "invalid-length";66 }67 assert(threw, "an empty phrase must raise a RecoveryError");68});6970test("fromRecoveryPhrase rejects a full-length but invalid phrase", () => {71 const words = Array.from({ length: RECOVERY_PHRASE_WORDS }, () => "zzzzzz").join(" ");72 let threw = false;73 try {74 fromRecoveryPhrase(words);75 } catch (error) {76 threw = error instanceof RecoveryError;77 }78 assert(threw, "unknown words or a bad checksum must raise a RecoveryError");79});8081test("toRecoveryPhrase rejects a secret that is not a 32-byte base64url value", () => {82 let threw = false;83 try {84 toRecoveryPhrase("too-short");85 } catch (error) {86 threw = error instanceof RecoveryError;87 }88 assert(threw, "an ill-formed secret must raise a RecoveryError, never a fake phrase");89});