# `Ltix.Test`
[🔗](https://github.com/DecoyLex/ltix/blob/main/lib/ltix/test.ex#L1)

Helpers for testing applications that use Ltix.

Reduces LTI test setup to a single call. Instead of manually generating
RSA keys, building JWKS payloads, creating registrations and deployments,
starting storage adapters, and stubbing HTTP endpoints, call
`setup_platform!/1`:

    setup do
      %{platform: Ltix.Test.setup_platform!()}
    end

The in-memory storage adapter state is scoped to the calling process
via the process dictionary, so async tests are safe without any cleanup.

## Configuration

If your app's controllers call `Ltix.handle_login/3` or
`Ltix.handle_callback/3` without passing `:storage_adapter` in opts
(relying on application config), add this to `config/test.exs`:

    config :ltix, storage_adapter: Ltix.Test.StorageAdapter

This is safe for `async: true` tests — each test process gets its own
in-memory storage via the process dictionary.

## Controller tests (full OIDC flow)

Simulate a platform-initiated launch against your controller endpoints:

    test "instructor launch renders dashboard", %{conn: conn, platform: platform} do
      conn = post(conn, ~p"/lti/login", Ltix.Test.login_params(platform))

      state = get_session(conn, :lti_state)
      nonce = Ltix.Test.extract_nonce(redirected_to(conn, 302))

      conn =
        conn
        |> recycle()
        |> Plug.Test.init_test_session(%{lti_state: state})
        |> post(~p"/lti/launch",
          Ltix.Test.launch_params(platform,
            nonce: nonce,
            state: state,
            roles: [:instructor],
            name: "Jane Doe"
          )
        )

      assert html_response(conn, 200) =~ "Dashboard"
    end

## Unit tests (direct context construction)

When testing business logic that receives a `%LaunchContext{}`, skip
the OIDC flow entirely with `build_launch_context/2`:

    test "instructors can manage grades", %{platform: platform} do
      context = Ltix.Test.build_launch_context(platform,
        roles: [:instructor],
        name: "Jane Smith"
      )

      assert MyApp.Permissions.can_manage_grades?(context)
    end

See the [Testing LTI Launches](testing-lti-launches.md) cookbook for
more examples, including role customization and raw claim overrides.

# `build_jwks`

```elixir
@spec build_jwks([JOSE.JWK.t()]) :: map()
```

Build a JWKS map from a list of public JWKs.

Returns `%{"keys" => [...]}`. Delegates to `Ltix.JWK.to_jwks/1`.

# `build_launch_context`

```elixir
@spec build_launch_context(
  Ltix.Test.Platform.t(),
  keyword()
) :: Ltix.LaunchContext.t()
```

Build a `%LaunchContext{}` directly for unit testing.

Constructs the context without going through the OIDC flow.
Accepts the same claim options as `launch_params/2` (except `:nonce`
and `:state`, which are not needed).

    context = Ltix.Test.build_launch_context(platform,
      roles: [:instructor, :teaching_assistant],
      name: "Jane Smith",
      context: %{id: "course-1", title: "Elixir 101"}
    )

For Deep Linking contexts, pass `message_type: :deep_linking`:

    context = Ltix.Test.build_launch_context(platform,
      message_type: :deep_linking,
      deep_linking_settings: %{accept_types: ["ltiResourceLink"]}
    )

# `callback_opts`

```elixir
@spec callback_opts(Ltix.Test.Platform.t()) :: keyword()
```

Options for `Ltix.handle_callback/3` that work with `setup_platform!/1`.

    Ltix.handle_callback(params, state, Ltix.Test.callback_opts(platform))

# `extract_nonce`

```elixir
@spec extract_nonce(String.t()) :: String.t()
```

Extract the nonce from a login redirect URI.

    {:ok, result} = Ltix.handle_login(params, redirect_uri)
    nonce = Ltix.Test.extract_nonce(result.redirect_uri)

# `generate_rsa_key_pair`

```elixir
@spec generate_rsa_key_pair() :: {JOSE.JWK.t(), JOSE.JWK.t(), String.t()}
```

Generate an RSA key pair for testing.

Returns `{private_jwk, public_jwk, kid}`. Delegates to `Ltix.JWK.generate_key_pair/0`.

# `launch_params`

```elixir
@spec launch_params(
  Ltix.Test.Platform.t(),
  keyword()
) :: map()
```

Build POST params for `Ltix.handle_callback/3`.

Signs a JWT with the platform's private key and returns
`%{"id_token" => jwt, "state" => state}`.

## Required options

  * `:nonce` — the nonce from the login redirect (use `extract_nonce/1`)
  * `:state` — the state from the login result

## Optional

  * `:message_type` — `:deep_linking` for Deep Linking requests
    (default: resource link)
  * `:roles` — list of role atoms (e.g., `[:instructor]`), `%Role{}` structs,
    or URI strings
  * `:subject` — user identifier (default: `"user-12345"`)
  * `:name`, `:email`, `:given_name`, `:family_name` — user PII
  * `:context` — map with `:id`, `:label`, `:title` keys
  * `:resource_link` — map with `:id`, `:title` keys
  * `:deep_linking_settings` — map of DL settings (used when
    `message_type: :deep_linking`)
  * `:claims` — raw claim map merged last (for advanced overrides)

# `login_opts`

```elixir
@spec login_opts(Ltix.Test.Platform.t()) :: keyword()
```

Options for `Ltix.handle_login/3` that work with `setup_platform!/1`.

    Ltix.handle_login(params, redirect_uri, Ltix.Test.login_opts(platform))

# `login_params`

```elixir
@spec login_params(
  Ltix.Test.Platform.t(),
  keyword()
) :: map()
```

Build POST params for `Ltix.handle_login/3`.

## Options

  * `:login_hint` — login hint value (default: `"user-hint"`)
  * `:target_link_uri` — launch URL (default: `"https://tool.example.com/launch"`)

# `mint_id_token`

```elixir
@spec mint_id_token(map(), JOSE.JWK.t(), keyword()) :: String.t()
```

Sign claims as a JWT.

## Options

  * `:kid` — key ID for the JWT header
  * `:alg` — algorithm (default: `"RS256"`)

# `setup_platform!`

```elixir
@spec setup_platform!(keyword()) :: Ltix.Test.Platform.t()
```

Set up a simulated LTI platform in one call.

Generates RSA keys, creates a registration and deployment, starts the
in-memory storage adapter, and stubs the JWKS HTTP endpoint.

## Options

  * `:issuer` — platform issuer URL (default: `"https://platform.example.com"`)
  * `:client_id` — OAuth client ID (default: `"tool-client-id"`)
  * `:deployment_id` — deployment identifier (default: `"deployment-001"`)

# `valid_lti_claims`

```elixir
@spec valid_lti_claims(map()) :: map()
```

Return a complete, valid LTI claim set.

Caller can override individual claims via the `overrides` map.

# `verify_deep_linking_response`

```elixir
@spec verify_deep_linking_response(Ltix.Test.Platform.t(), String.t()) ::
  {:ok, map()} | {:error, term()}
```

Verify a Deep Linking response JWT signed by the tool.

Decodes the JWT using the tool's public key (derived from
`registration.tool_jwk`) and returns the parsed claims on success.

    {:ok, response} = Ltix.DeepLinking.build_response(context, items)
    {:ok, claims} = Ltix.Test.verify_deep_linking_response(platform, response.jwt)
    assert claims["https://purl.imsglobal.org/spec/lti/claim/message_type"] ==
             "LtiDeepLinkingResponse"

---

*Consult [api-reference.md](api-reference.md) for complete listing*
