# Testing LTI Launches

`Ltix.Test` provides helpers for testing your LTI-powered application.
Set up a simulated platform in one call, then test your controllers,
authorization logic, and role-based behavior without a real LMS.

## Setup

Add the test storage adapter to your `config/test.exs` so your
controllers can resolve registrations and nonces during tests:

```elixir
# config/test.exs
config :ltix, storage_adapter: Ltix.Test.StorageAdapter
```

Then create a test platform in your setup block. This gives you
everything a real platform would provide: signed JWTs, a JWKS endpoint,
a registration, and a deployment.

```elixir
defmodule MyAppWeb.LtiControllerTest do
  use MyAppWeb.ConnCase, async: true

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

Each call to `setup_platform!/1` starts its own in-memory storage agent
scoped to the calling process, so `async: true` tests are safe without
any cleanup.

## Testing your controller

Simulate a full platform-initiated launch against your controller
endpoints. This exercises your routes, session handling, and response
logic end-to-end.

```elixir
test "instructor launch renders the dashboard", %{conn: conn, platform: platform} do
  # Platform initiates login
  conn =
    conn
    |> post(~p"/lti/login", Ltix.Test.login_params(platform))

  # Follow the redirect back to your launch endpoint
  assert redirected_to(conn, 302) =~ "https://platform.example.com/auth"
  state = get_session(conn, :lti_state)
  redirect_uri = redirected_to(conn, 302)
  nonce = Ltix.Test.extract_nonce(redirect_uri)

  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"
  assert html_response(conn, 200) =~ "Jane Doe"
end

test "learner launch renders the assignment view", %{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: [:learner],
        context: %{id: "course-1", title: "Elixir 101"}
      )
    )

  assert html_response(conn, 200) =~ "Elixir 101"
  refute html_response(conn, 200) =~ "Grade"
end
```

## Testing business logic

When testing code that receives a `%LaunchContext{}`, skip the OIDC
flow entirely with `build_launch_context/2`. This is faster and isolates
your logic from controller and HTTP concerns.

```elixir
defmodule MyApp.PermissionsTest do
  use ExUnit.Case, async: true

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

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

    assert MyApp.Permissions.can_manage_grades?(context)
  end

  test "TAs can view but not manage grades", %{platform: platform} do
    alias Ltix.LaunchClaims.Role

    context = Ltix.Test.build_launch_context(platform,
      roles: [%Role{type: :context, name: :instructor, sub_role: :teaching_assistant}]
    )

    assert MyApp.Permissions.can_view_grades?(context)
    refute MyApp.Permissions.can_manage_grades?(context)
  end

  test "learners see their own submissions only", %{platform: platform} do
    context = Ltix.Test.build_launch_context(platform,
      roles: [:learner],
      subject: "student-42",
      context: %{id: "course-1"}
    )

    submissions = MyApp.Submissions.list_for(context)
    assert Enum.all?(submissions, &(&1.user_id == "student-42"))
  end
end
```

## Customizing launches

### Roles

Pass atoms for common LIS context roles:

```elixir
roles: [:instructor, :learner]
```

For sub-roles, pass a `%Role{}` struct:

```elixir
alias Ltix.LaunchClaims.Role
roles: [%Role{type: :context, name: :instructor, sub_role: :teaching_assistant}]
```

For institution or system roles, or custom role URIs, pass the full URI
string:

```elixir
roles: ["http://purl.imsglobal.org/vocab/lis/v2/institution/person#Faculty"]
```

### Context and resource link

```elixir
Ltix.Test.build_launch_context(platform,
  roles: [:instructor],
  context: %{id: "course-1", label: "CS101", title: "Intro to CS"},
  resource_link: %{id: "assignment-1", title: "Quiz 1"}
)
```

### Raw claim overrides

For claims not covered by the convenience options, use `:claims` to
merge arbitrary key-value pairs into the JWT:

```elixir
Ltix.Test.launch_params(platform,
  nonce: nonce,
  state: state,
  claims: %{
    "https://purl.imsglobal.org/spec/lti/claim/custom" => %{
      "canvas_course_id" => "12345"
    }
  }
)
```
