Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 5 Nov 2026. What's built · Install 0.3.0
jadpo
ReferenceTesting

Testing

Acting as a user

On this page

An action receives its caller, the person or service signed in, as a typed parameter (The principal). So a test passes one in:

example
test "bob cannot complete alice's todo" {    var alice: Principal.user = alice_with_todos()    var bob: Principal.user = bob_with_no_todos()    var todo: Todo = first_todo(alice)    assert complete_todo(todo.id, bob) fails TodoNotFound}

Here Bob tries to complete one of Alice's todos. Policy reports a record the caller may not see as the same not-found failure as a record that doesn't exist (Concealing records), so the test checks exactly what Bob would see.

Making a caller

Tests, fixtures and seeds make a user caller with Principal.user(user), and a service caller with Principal.service(service). Application code can't, because there a caller only comes from signing in.

Signing in for a route test

A route test signs in with as:, which goes through the application's real authentication. A service signs in with a key from Service.issue_key. So a disabled user is refused exactly as in production, and the sign-in checks are tested too. To test signing in itself, test_passkeys signs in with a passkey and test_providers through a sign-in provider, with the test run's local issuer as the provider (Providers). A route that asks the signed-in person for a passkey is confirmed with test_passkeys.confirm(passkey) (Passkeys in tests).

A role can count only for someone who signed in a particular way (Roles that need a way in), so a user's request says how they did, beside as::

example
var stock: Response = request {    method: HttpMethod.get    path: "/staff/stock"    as: Principal.user(ann)    sign_in: SignInMethod.google    strength: AuthenticationStrength.multi_factor}
  • sign_in: is a SignInMethod, and strength: an AuthenticationStrength. The session is kept as one started that way is, a provider's lasting a day where a role's requirement names it, without the round trip to the provider.
  • Without them, as: is someone who signed in by magic link with one factor. So a test of an admin's route fails until it says how the admin signed in, as the admin's own request would.
  • Either without as: naming a user is semantic.request_invalid.

A call with a caller, outside a request, runs as someone signed in by magic link with one factor too, so a role that needs a way in is tested through a route.

A caller signs in once in a test, as a browser keeps its cookie. The first request as: a person signs them in, and each later request by the same caller with the same sign_in: and strength: sends that session again. So the session behaves as a real one does across the test:

  • After clock.advance(24h), a session through a provider that a role's requirement names has ended, and the next request is 401 invalid_credentials. A magic-link session ends after its 30 days.
  • A role taken away, or a person made inactive, is refused on the next request, as the session's next read finds it.
  • A different caller, or the same caller with a different sign_in: or strength:, starts a session of its own, as a second browser would.

A service keeps its key the same way. A user on an API that takes only external tokens gets a fresh token from the local issuer for each request, as a client refreshes its provider's token, since no session keeps it. A request keeps the credential it signed in with, not a signed part a response renews, so each request reads the caller's record.

example
test "a Google session ends after a day" {    var ann: Patron = staff_at("ann@lending.example", StaffRole.front_desk)    var signed_in: Response = request {        method: HttpMethod.get        path: "/staff/stock"        as: Principal.user(ann)        sign_in: SignInMethod.google    }    assert signed_in.status == 200    clock.advance(24h)    var a_day_later: Response = request {        method: HttpMethod.get        path: "/staff/stock"        as: Principal.user(ann)        sign_in: SignInMethod.google    }    assert a_day_later.status == 401}

Calls with no caller

A call a test, fixture or seed makes runs as the caller it passes, as complete_todo runs as Bob here. A call that passes none, such as one setting up data, runs without policy. So a call with no caller proves behaviour and data, and a route test or a call with a caller proves policy.

Markdown for agents: /docs/acting-as-a-user.md.Built from Jadpo main at aee2e8f.