| 1 | // Tests for the WebAuthn ceremony-error copy mapping, run in Node. |
| 2 | // Same harness style as home/test/serverlist.test.mjs. |
| 3 | // node home/test/ceremony-errors.test.mjs |
| 4 | import assert from "node:assert/strict"; |
| 5 | import { friendlyCeremonyError, formatCeremonyError } from "../web/static/ceremony-errors.js"; |
| 6 | |
| 7 | let failures = 0; |
| 8 | async function test(name, fn) { |
| 9 | try { |
| 10 | await fn(); |
| 11 | console.log("PASS " + name); |
| 12 | } catch (e) { |
| 13 | console.log("FAIL " + name + " — " + (e && e.message)); |
| 14 | failures++; |
| 15 | } |
| 16 | } |
| 17 | |
| 18 | // A ceremony error as the browser throws it: name is a DOMException name, |
| 19 | // phase is the tag backup-core.js attaches on the register path. |
| 20 | const errWith = (name, phase) => Object.assign(new Error("boom"), { name, phase }); |
| 21 | |
| 22 | await test("cancelled sign-in reads as a cancel, not an exception", () => { |
| 23 | const { message, detail } = friendlyCeremonyError(errWith("NotAllowedError"), "signin"); |
| 24 | assert.equal(message, "Sign in was cancelled. If you didn't cancel, try again."); |
| 25 | assert.equal(detail, "NotAllowedError"); |
| 26 | }); |
| 27 | |
| 28 | await test("cancelled create names the create intent", () => { |
| 29 | const { message } = friendlyCeremonyError(errWith("NotAllowedError"), "create"); |
| 30 | assert.equal(message, "Creating your sign-in was cancelled. If you didn't cancel, try again."); |
| 31 | }); |
| 32 | |
| 33 | await test("duplicate registration bridges back to sign-in", () => { |
| 34 | const { message } = friendlyCeremonyError(errWith("InvalidStateError"), "create"); |
| 35 | assert.equal(message, "That's already set up. Try signing in instead."); |
| 36 | }); |
| 37 | |
| 38 | await test("InvalidStateError during sign-in falls through to the raw message", () => { |
| 39 | const { message } = friendlyCeremonyError(errWith("InvalidStateError"), "signin"); |
| 40 | assert.equal(message, "boom"); // no create-specific reading applies |
| 41 | }); |
| 42 | |
| 43 | await test("no screen lock: ConstraintError and NotSupportedError both explain it", () => { |
| 44 | for (const name of ["ConstraintError", "NotSupportedError"]) { |
| 45 | const { message } = friendlyCeremonyError(errWith(name), "signin"); |
| 46 | assert.equal(message, |
| 47 | "This device needs a screen lock, PIN, fingerprint, or face unlock before it can sign in this way."); |
| 48 | } |
| 49 | }); |
| 50 | |
| 51 | await test("misconfigured RP reads as a site problem", () => { |
| 52 | const { message } = friendlyCeremonyError(errWith("SecurityError"), "signin"); |
| 53 | assert.equal(message, "Sign-in isn't configured for this website address."); |
| 54 | }); |
| 55 | |
| 56 | await test("aborted ceremony asks for a retry", () => { |
| 57 | const { message } = friendlyCeremonyError(errWith("AbortError"), "signin"); |
| 58 | assert.equal(message, "Sign-in was interrupted. Try again."); |
| 59 | }); |
| 60 | |
| 61 | await test("phase rides along in the detail for debugging", () => { |
| 62 | const { detail } = friendlyCeremonyError(errWith("NotAllowedError", "create"), "create"); |
| 63 | assert.equal(detail, "NotAllowedError [create]"); |
| 64 | }); |
| 65 | |
| 66 | await test("unknown errors pass their own message through", () => { |
| 67 | const { message, detail } = friendlyCeremonyError(new Error("HTTP 500"), "signin"); |
| 68 | assert.equal(message, "HTTP 500"); |
| 69 | assert.equal(detail, "Error"); |
| 70 | }); |
| 71 | |
| 72 | await test("a nameless, messageless value still renders something", () => { |
| 73 | const { message, detail } = friendlyCeremonyError({}, "signin"); |
| 74 | assert.equal(message, "[object Object]"); |
| 75 | assert.equal(detail, "Error"); |
| 76 | }); |
| 77 | |
| 78 | await test("formatCeremonyError composes sentence + parenthesised detail", () => { |
| 79 | const line = formatCeremonyError(errWith("NotAllowedError", "create"), "create"); |
| 80 | assert.equal(line, |
| 81 | "Creating your sign-in was cancelled. If you didn't cancel, try again. (NotAllowedError [create])"); |
| 82 | }); |
| 83 | |
| 84 | if (failures) { |
| 85 | console.error(failures + " test(s) failed"); |
| 86 | process.exit(1); |
| 87 | } |
| 88 | console.log("ceremony-errors tests passed"); |
| 89 | |