// Tests for the WebAuthn ceremony-error copy mapping, run in Node. // Same harness style as home/test/serverlist.test.mjs. // node home/test/ceremony-errors.test.mjs import assert from "node:assert/strict"; import { friendlyCeremonyError, formatCeremonyError } from "../web/static/ceremony-errors.js"; let failures = 0; async function test(name, fn) { try { await fn(); console.log("PASS " + name); } catch (e) { console.log("FAIL " + name + " — " + (e && e.message)); failures++; } } // A ceremony error as the browser throws it: name is a DOMException name, // phase is the tag backup-core.js attaches on the register path. const errWith = (name, phase) => Object.assign(new Error("boom"), { name, phase }); await test("cancelled sign-in reads as a cancel, not an exception", () => { const { message, detail } = friendlyCeremonyError(errWith("NotAllowedError"), "signin"); assert.equal(message, "Sign in was cancelled. If you didn't cancel, try again."); assert.equal(detail, "NotAllowedError"); }); await test("cancelled create names the create intent", () => { const { message } = friendlyCeremonyError(errWith("NotAllowedError"), "create"); assert.equal(message, "Creating your sign-in was cancelled. If you didn't cancel, try again."); }); await test("duplicate registration bridges back to sign-in", () => { const { message } = friendlyCeremonyError(errWith("InvalidStateError"), "create"); assert.equal(message, "That's already set up. Try signing in instead."); }); await test("InvalidStateError during sign-in falls through to the raw message", () => { const { message } = friendlyCeremonyError(errWith("InvalidStateError"), "signin"); assert.equal(message, "boom"); // no create-specific reading applies }); await test("no screen lock: ConstraintError and NotSupportedError both explain it", () => { for (const name of ["ConstraintError", "NotSupportedError"]) { const { message } = friendlyCeremonyError(errWith(name), "signin"); assert.equal(message, "This device needs a screen lock, PIN, fingerprint, or face unlock before it can sign in this way."); } }); await test("misconfigured RP reads as a site problem", () => { const { message } = friendlyCeremonyError(errWith("SecurityError"), "signin"); assert.equal(message, "Sign-in isn't configured for this website address."); }); await test("aborted ceremony asks for a retry", () => { const { message } = friendlyCeremonyError(errWith("AbortError"), "signin"); assert.equal(message, "Sign-in was interrupted. Try again."); }); await test("phase rides along in the detail for debugging", () => { const { detail } = friendlyCeremonyError(errWith("NotAllowedError", "create"), "create"); assert.equal(detail, "NotAllowedError [create]"); }); await test("unknown errors pass their own message through", () => { const { message, detail } = friendlyCeremonyError(new Error("HTTP 500"), "signin"); assert.equal(message, "HTTP 500"); assert.equal(detail, "Error"); }); await test("a nameless, messageless value still renders something", () => { const { message, detail } = friendlyCeremonyError({}, "signin"); assert.equal(message, "[object Object]"); assert.equal(detail, "Error"); }); await test("formatCeremonyError composes sentence + parenthesised detail", () => { const line = formatCeremonyError(errWith("NotAllowedError", "create"), "create"); assert.equal(line, "Creating your sign-in was cancelled. If you didn't cancel, try again. (NotAllowedError [create])"); }); if (failures) { console.error(failures + " test(s) failed"); process.exit(1); } console.log("ceremony-errors tests passed");