Sign-in in development and tests
Magic links in jadpo dev
In jadpo dev, no email leaves your machine. It lands in the local inbox
(The dev inbox), and its link and code work
there. The link opens your frontend's page. An agent reads the token or
code with jadpo email links <id> or jadpo email show <id>, and posts
it to POST /auth/magic-link/confirm itself.
Magic links in tests
Most route tests don't sign in through a magic link. A request's as:
signs the test in as a user directly
(Acting as a user).
To test the sign-in itself, a route test asks for a link with
POST /auth/magic-link, reads the email from email.sent, and follows
its link or posts its code. It calls the generated routes with
request { }, as it calls your own. Each takes a built-in input, named
as the API record names it, which only a test, a fixture or a seed
builds:
| Route | input: |
Its fields | output |
|---|---|---|---|
POST /auth/magic-link |
MagicLinkAsk |
address: Email, return_to: Text?, locale: Locale? |
none |
POST /auth/magic-link/confirm |
MagicLinkConfirmation |
token: SignInToken?, address: Email?, code: SignInCode? |
MagicLinkConfirmed, its return_to: Text |
POST /auth/magic-link/redeem |
MagicLinkCode |
address: Email, code: SignInCode |
MagicLinkToken, its token: ApiToken |
- The redeem route is there only when you declare
api_token:. - Leave out any field that may be
none, as a request's body may. - Every request a test sends carries your app's own
Origin, as a browser's does, so the confirm route takes it. - The sign-in email lands in
email.sentwith its link's token insign_in_tokenand its code insign_in_code. Both arenoneon every other email, so a test never reads them out of the text. - Built anywhere but a test, a fixture or a seed, an input is
type.not_record.
test "an emailed link signs a new person in" { var asked: Response = request { method: HttpMethod.post path: "/auth/magic-link" input: MagicLinkAsk { address: "ada@notes.example" } } assert asked.status == 202 match list.first(email.sent) { none -> { assert list.count(email.sent) == 1 } some(sent) -> { assert sent.to == "ada@notes.example" var came_back: Response = request { method: HttpMethod.post path: "/auth/magic-link/confirm" input: MagicLinkConfirmation { token: sent.sign_in_token } } assert came_back.status == 200 } }}test "the emailed code signs a new person in" { var asked: Response = request { method: HttpMethod.post path: "/auth/magic-link" input: MagicLinkAsk { address: "ada@notes.example" } } assert asked.status == 202 match list.first(email.sent) { none -> { assert list.count(email.sent) == 1 } some(sent) -> { var came_back: Response = request { method: HttpMethod.post path: "/auth/magic-link/confirm" input: MagicLinkConfirmation { address: "ada@notes.example" code: sent.sign_in_code } } assert came_back.status == 200 } }}
To test on_first_sign_in:'s action, call it directly with a
FirstSignIn you build, giving every field. A subscriber's action takes a
sign-in event built the same way:
test "an unproved address is refused" { assert create_member(FirstSignIn { method: SignInMethod.magic_link address: "ada@example.com" address_verified: false subject: none name: none }) fails SignUpClosed}
Passkeys in jadpo dev
Browsers allow passkeys on http://localhost without TLS. So in
jadpo dev, a browser's own passkeys work against a browser_origin of
http://localhost:<port>. An agent uses the browser's virtual
authenticator, Chrome DevTools' WebAuthn panel or WebDriver's virtual
authenticator commands, rather than a real device.
Passkeys in tests
A route test has test_passkeys, a software authenticator that drives
the generated routes for real. Only tests can call it:
| Operation | Does |
|---|---|
test_passkeys.register(user: Principal.user, name: Text) -> TestPasskey |
Adds a passkey for user through POST /auth/passkey/register/options and POST /auth/passkey/register, signed in as as: signs in, and gives the device holding it |
test_passkeys.sign_in(passkey: TestPasskey) -> Response |
Signs in with it through the two sign-in routes and gives the second's Response |
test_passkeys.confirm(passkey: TestPasskey) |
Confirms the next request the test sends to a route with confirm: Confirmation.passkey with it (Confirm one request with a passkey) |
Each TestPasskey is a device of its own holding one passkey, made by
the program running the tests, so only a test ever holds one. Called
anywhere else, test_passkeys is semantic.request_invalid.
If a route refuses register, the first step of sign_in or a
confirmation's options, the test ends as a fault naming the route and its
code, since the test meant to have a passkey. A refusal of the sign-in or
of the confirmed request itself comes back in its Response, for the test
to check.
test_passkeys.confirm sends the next request to the route's
…/confirmation first, with the same caller and input, and has the device
answer. The
device answers with its own passkey whatever the options list, as an
attacker's device would, so a test can show another person's passkey being
refused. A request to a route with confirm: the test didn't confirm goes
without an answer.
test "a passkey signs Ada in" { var ada: Principal.user = member_called("ada") var phone: TestPasskey = test_passkeys.register(ada, "Phone") var response: Response = test_passkeys.sign_in(phone) assert response.status == 204}
Markdown for agents: /docs/in-development-and-tests.md.Built from Jadpo main at aee2e8f.