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!()}
endThe 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.StorageAdapterThis 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"
endUnit 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)
endSee the Testing LTI Launches cookbook for more examples, including role customization and raw claim overrides.
Summary
Functions
Build a JWKS map from a list of public JWKs.
Build a %LaunchContext{} directly for unit testing.
Options for Ltix.handle_callback/3 that work with setup_platform!/1.
Extract the nonce from a login redirect URI.
Generate an RSA key pair for testing.
Build POST params for Ltix.handle_callback/3.
Options for Ltix.handle_login/3 that work with setup_platform!/1.
Build POST params for Ltix.handle_login/3.
Sign claims as a JWT.
Set up a simulated LTI platform in one call.
Return a complete, valid LTI claim set.
Verify a Deep Linking response JWT signed by the tool.
Functions
@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.
@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"]}
)
@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 the nonce from a login redirect URI.
{:ok, result} = Ltix.handle_login(params, redirect_uri)
nonce = Ltix.Test.extract_nonce(result.redirect_uri)
@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.
@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 (useextract_nonce/1):state— the state from the login result
Optional
:message_type—:deep_linkingfor 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,:titlekeys:resource_link— map with:id,:titlekeys:deep_linking_settings— map of DL settings (used whenmessage_type: :deep_linking):claims— raw claim map merged last (for advanced overrides)
@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))
@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")
@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")
@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")
Return a complete, valid LTI claim set.
Caller can override individual claims via the overrides map.
@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"