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:
# config/test.exs
config :ltix, storage_adapter: Ltix.Test.StorageAdapterThen 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.
defmodule MyAppWeb.LtiControllerTest do
use MyAppWeb.ConnCase, async: true
setup do
%{platform: Ltix.Test.setup_platform!()}
end
endEach 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.
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"
endTesting 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.
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
endCustomizing launches
Roles
Pass atoms for common LIS context roles:
roles: [:instructor, :learner]For sub-roles, pass a %Role{} struct:
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:
roles: ["http://purl.imsglobal.org/vocab/lis/v2/institution/person#Faculty"]Context and resource link
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:
Ltix.Test.launch_params(platform,
nonce: nonce,
state: state,
claims: %{
"https://purl.imsglobal.org/spec/lti/claim/custom" => %{
"canvas_course_id" => "12345"
}
}
)