# 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](built-in-services.md)), 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](testing.md)).

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.sent` with its link's token in
  `sign_in_token` and its code in `sign_in_code`. Both are `none` on
  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`.

```jadpo
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:

```jadpo
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` |

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`, or the first step of `sign_in`, 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 itself comes back in its
`Response`, for the test to check.

```jadpo
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
}
```
