clone

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

feat/fix-sync-enrollment-startup
lofi / package / runtime / passkey-recovery_test.ts
passkey-recovery_test.ts 2.7K
1import {2 backupSecretWithPasskey,3 mapPasskeyBackupError,4 restoreSecretWithPasskey,5} from "./passkey-recovery.ts";6import { assert } from "./test-assert.ts";78function domError(name: string): DOMException {9 return new DOMException("vendor detail must not reach the UI", name);10}1112Deno.test("passkey backup errors become actionable non-secret recovery errors", () => {13 const cases = [14 [new PasskeyBackupError("not-supported"), "unsupported"],15 [new PasskeyBackupError("no-credential"), "credential-missing"],16 [new PasskeyBackupError("verification-failed"), "verification-failed"],17 [new PasskeyBackupError("invalid-credential"), "invalid-credential"],18 [new PasskeyBackupError("get-failed", domError("SecurityError")), "rp-id-mismatch"],19 [new PasskeyBackupError("get-failed", domError("NotAllowedError")), "cancelled"],20 ] as const;21 for (const [input, code] of cases) {22 const mapped = mapPasskeyBackupError(input, "restore");23 assert(mapped.code === code, `${input.code} mapped to ${mapped.code}, expected ${code}`);24 assert(25 !mapped.message.includes("vendor detail"),26 "vendor detail leaked into public error text",27 );28 if (code === "cancelled") {29 assert(30 mapped.message.includes("not created or opened") && !mapped.message.includes("cancelled"),31 "NotAllowedError guidance must not assume the person dismissed the prompt",32 );33 }34 }35});3637Deno.test("passkey adapter backs up and restores the exact account secret", async () => {38 const expected = "account-secret";39 let backedUp: { secret: string; displayName: string } | undefined;40 const adapter = {41 backup(secret: string, displayName: string) {42 backedUp = { secret, displayName };43 return Promise.resolve();44 },45 restore() {46 return Promise.resolve(expected);47 },48 };49 await backupSecretWithPasskey(adapter, expected, "Account recovery");50 assert(backedUp?.secret === expected, "backup changed the account secret");51 assert(backedUp?.displayName === "Account recovery", "backup changed the display name");52 assert(53 await restoreSecretWithPasskey(adapter) === expected,54 "restore changed the account secret",55 );56});5758Deno.test("unknown passkey failures distinguish backup from restore without leaking causes", () => {59 assert(60 mapPasskeyBackupError(new Error("secret payload"), "backup").code === "backup-failed",61 "unknown backup failure was misclassified",62 );63 assert(64 mapPasskeyBackupError(new Error("secret payload"), "restore").code === "restore-failed",65 "unknown restore failure was misclassified",66 );67});68class PasskeyBackupError extends Error {69 override readonly name = "PasskeyBackupError";70 constructor(readonly code: string, override readonly cause?: unknown) {71 super(code);72 }73}