Test assertion helpers for Sigra authentication.
This module provides convenience assertions for testing authentication flows in your application. Import it in your test cases:
use ExUnit.Case
import Sigra.TestingAvailable Assertions
assert_password_hashed/1- verifies a user has a properly hashed passwordassert_session_created/1- verifies a session was created on a conn (stub)assert_token_sent/2- verifies a token email was sent (stub)
Stub functions will be filled in by later phases as the corresponding features are implemented.
Summary
Functions
Asserts that the user account was permanently deleted or anonymized.
Asserts that an audit event matches the given map.
Asserts that the latest audit event matches the given field expectations.
Asserts that the latest audit event was logged for the expected organization.
Asserts that the user account deletion was cancelled.
Asserts that the user account is scheduled for deletion.
Asserts that an email was sent (via Swoosh test adapter).
Asserts that a membership belongs to the expected user, organization, and role.
Asserts that the user does not have MFA enabled.
Asserts that the user has MFA enabled (has credential with non-nil enabled_at).
Asserts that the user's password was recently changed.
Asserts that the given user struct has a properly hashed password.
Assert rate limited response (429 status with Retry-After header).
Asserts that a conn received a 403 insufficient scope response.
Asserts that a scope has the expected active organization id.
Asserts that a session was created on the given conn.
Asserts that all sessions except the optionally specified one were invalidated.
Asserts that a token has been revoked.
Asserts that a token email was sent to the given address for the given context.
Inserts an audit event directly via the configured repo, bypassing
Ecto.Multi wrapping. Test-only — production audit writes go through
Sigra.Audit.log/2 or Sigra.Audit.log_multi/3.
Marks user's session as MFA-completed without requiring code entry.
Creates an API token and returns {raw_key, token_record}.
Generates backup codes for a user and stores hashes in the DB.
Creates an identity struct for testing.
Creates a user fixture in fully deleted/anonymized state.
Creates an expired API token fixture.
Generates an expired JWT for testing.
Extracts the confirmation token from a confirmation URL string.
Extracts the reset password token from a reset URL string.
Creates a user fixture with the force password change flag set.
Generates a JWT access token for testing.
Generates a valid TOTP code for the given raw secret.
Generates a JWT with specific scopes for testing.
Creates a mock OAuth callback result for testing.
Creates a user registered via OAuth for testing.
Alias for put_bearer_token/2.
Adds a Bearer token header to a conn for API testing.
Creates a revoked API token fixture.
Creates a user fixture with scheduled deletion.
Creates a scoped API token fixture.
Creates a fully enrolled MFA credential for the user.
Simulates grace period expiry by setting scheduled_deletion_at to a past timestamp.
Simulate a locked out user by setting failed_login_attempts to threshold.
Simulates an MFA lockout by setting failed_attempts to threshold.
Sets the _sigra_mfa_trust cookie on the conn for the given user.
Temporarily overrides a hook for a test block.
Executes the given function with a test mailer configured.
Functions
Asserts that the user account was permanently deleted or anonymized.
Checks that the user either no longer exists or has an anonymized email.
Asserts that an audit event matches the given map.
By default checks the most recent event (ordered by inserted_at desc);
pass :position to check the Nth-most-recent (0 is newest).
Top-level keys (:action, :outcome, :actor_id, etc.) are compared
with strict equality. The :metadata key, if present, deep-matches a
subset — keys present in the expected map must equal the corresponding
values on the event, but extra keys on the event are ignored. Both atom
and string metadata keys are tolerated.
Raises ExUnit.AssertionError with a diff-style message on mismatch.
Options
:repo(required) — the Ecto repo module:audit_schema(required) — the audit_events schema module:position(default0) — 0-based offset from newest
Examples
assert_audit_event(
%{action: "billing.charge.created", outcome: "success"},
repo: MyApp.Repo,
audit_schema: MyApp.AuditEvent
)
assert_audit_event(
%{metadata: %{plan: "pro"}},
repo: MyApp.Repo,
audit_schema: MyApp.AuditEvent
)
Asserts that the latest audit event matches the given field expectations.
Thin alias for assert_audit_event/2 with a name aligned to REQ DX-02
(assert_audit_logged_for_org/2 family naming). Takes a map of expected
fields and a keyword options list. See assert_audit_event/2 for the
full option list (:repo, :audit_schema, :position).
Examples
assert_audit_logged(
%{
action: "auth.login.success",
actor_id: user.id,
effective_user_id: user.id,
organization_id: org.id
},
repo: MyApp.Repo,
audit_schema: MyApp.AuditEvent
)Signature note
This helper intentionally takes (map, keyword) — NOT (repo, fields). See
the deviations field in
.planning/phases/15-audit-integration/15-02-semantic-workers-credo-PLAN.md
for the D-31 refinement rationale (the (repo, fields) shape from
CONTEXT.md would require synthesizing :audit_schema via process-dict
magic and would no longer be a "thin alias" — it would either duplicate
the implementation or hide required options).
Asserts that the latest audit event was logged for the expected organization.
@spec assert_deletion_cancelled(struct()) :: true
Asserts that the user account deletion was cancelled.
Raises ExUnit.AssertionError if deleted_at or scheduled_deletion_at is not nil.
@spec assert_deletion_scheduled(struct()) :: true
Asserts that the user account is scheduled for deletion.
Raises ExUnit.AssertionError if deleted_at or scheduled_deletion_at is nil.
@spec assert_email_sent(keyword()) :: true
Asserts that an email was sent (via Swoosh test adapter).
Checks the Swoosh test mailbox for an email matching the given criteria.
Uses Swoosh.TestAssertions under the hood when available.
Options
:to- Expected recipient email:subject- Expected subject (substring match)
Asserts that a membership belongs to the expected user, organization, and role.
Asserts that the user does not have MFA enabled.
Raises ExUnit.AssertionError on failure.
Options
:config-%Sigra.Config{}(required):mfa_credential_schema- MFA credential schema module (required)
Asserts that the user has MFA enabled (has credential with non-nil enabled_at).
Raises ExUnit.AssertionError on failure.
Options
:config-%Sigra.Config{}(required):mfa_credential_schema- MFA credential schema module (required)
@spec assert_password_changed(struct()) :: true
Asserts that the user's password was recently changed.
Checks that password_changed_at is set and within the last 60 seconds.
@spec assert_password_hashed(map()) :: true
Asserts that the given user struct has a properly hashed password.
Checks that the hashed_password field starts with "$argon2id$",
indicating it was hashed with the Argon2id algorithm.
Examples
user = %{hashed_password: Sigra.Crypto.hash_password("password")}
assert_password_hashed(user)
@spec assert_rate_limited(Plug.Conn.t()) :: true
Assert rate limited response (429 status with Retry-After header).
Checks that the connection has a 429 status code and a non-empty
retry-after response header.
Examples
conn = post(conn, "/login", %{email: "test@example.com", password: "wrong"})
Sigra.Testing.assert_rate_limited(conn)
@spec assert_scope_denied(Plug.Conn.t()) :: true
Asserts that a conn received a 403 insufficient scope response.
Checks that the status is 403 and the connection is halted.
Asserts that a scope has the expected active organization id.
@spec assert_session_created(term()) :: true
Asserts that a session was created on the given conn.
Stub
This function is a stub that will be filled in by later phases when session management is fully implemented.
Asserts that all sessions except the optionally specified one were invalidated.
Options
:except_token- A session token to exclude from the count (the current session):session_schema- The session schema module (required)
@spec assert_token_revoked(Sigra.Config.t(), term()) :: true
Asserts that a token has been revoked.
Raises ExUnit.AssertionError if the token's revoked_at is nil.
Asserts that a token email was sent to the given address for the given context.
Delegates to assert_email_sent/1 with :to set to the given address.
Inserts an audit event directly via the configured repo, bypassing
Ecto.Multi wrapping. Test-only — production audit writes go through
Sigra.Audit.log/2 or Sigra.Audit.log_multi/3.
Options
:repo(required) — the Ecto repo module:audit_schema(required) — the generatedaudit_eventsschema module:action(default"test.event"):outcome(default"success"):actor_id,:actor_type(default"user"),:target_id,:target_type:metadata(default%{}):occurred_at(defaultDateTime.utc_now/0)
Examples
audit_event_fixture(repo: MyApp.Repo, audit_schema: MyApp.AuditEvent)
audit_event_fixture(
repo: MyApp.Repo,
audit_schema: MyApp.AuditEvent,
action: "billing.charge.created",
outcome: "failure",
metadata: %{amount: 99}
)
@spec bypass_mfa(Plug.Conn.t()) :: Plug.Conn.t()
Marks user's session as MFA-completed without requiring code entry.
For tests that don't care about the MFA verification flow. Returns
a conn with the session type set to :standard.
Examples
conn = Sigra.Testing.bypass_mfa(conn)
@spec create_api_token(Sigra.Config.t(), struct(), keyword()) :: {String.t(), struct()}
Creates an API token and returns {raw_key, token_record}.
Options
:name- Token name (default:"test-token"):scopes- List of scope strings (default:["*"]):expires_at- Expiration datetime (default:nil)
Generates backup codes for a user and stores hashes in the DB.
Returns a list of raw formatted codes (shown once to user).
Options
:config-%Sigra.Config{}(required):backup_code_schema- Backup code schema module (required):count- Number of codes to generate (default: 8)
@spec create_identity(keyword()) :: Sigra.Identity.t()
Creates an identity struct for testing.
Options
:provider- Provider string (default:"google"):provider_uid- UID string (default: auto-generated):user_id- Associated user ID (required):email- Provider email (default:"oauth@example.com"):name- Provider name (default:"Test User"):id- Identity ID (default: auto-generated)
Creates a user fixture in fully deleted/anonymized state.
Sets deleted_at to a past timestamp and anonymizes the email.
@spec expired_api_token_fixture(Sigra.Config.t(), struct(), keyword()) :: {String.t(), struct()}
Creates an expired API token fixture.
@spec expired_jwt(Sigra.Config.t(), struct(), keyword()) :: String.t()
Generates an expired JWT for testing.
Uses Joken directly to create a token with a past expiry timestamp.
Options
:scopes- List of scope strings (default:["*"])
Extracts the confirmation token from a confirmation URL string.
Parses /users/confirm/<token> and returns the token portion. Works
with absolute URLs, URLs with query strings, and bare paths.
Examples
iex> Sigra.Testing.extract_confirmation_token("https://example.com/users/confirm/abc123")
"abc123"
iex> Sigra.Testing.extract_confirmation_token("/users/confirm/abc123")
"abc123"
iex> Sigra.Testing.extract_confirmation_token("https://example.com/users/confirm/SFMyNTY.encoded-token")
"SFMyNTY.encoded-token"
Extracts the reset password token from a reset URL string.
Parses /users/reset-password/<token> and returns the token portion.
Same path-tail semantics as extract_confirmation_token/1.
Examples
iex> Sigra.Testing.extract_reset_token("https://example.com/users/reset-password/xyz789")
"xyz789"
iex> Sigra.Testing.extract_reset_token("/users/reset-password/xyz789")
"xyz789"
iex> Sigra.Testing.extract_reset_token("https://app.example.com/users/reset-password/SFMyNTY.tok")
"SFMyNTY.tok"
Creates a user fixture with the force password change flag set.
Sets must_change_password to true on the user.
@spec generate_jwt(Sigra.Config.t(), struct(), keyword()) :: String.t()
Generates a JWT access token for testing.
Options
:scopes- List of scope strings (default:["*"])
Generates a valid TOTP code for the given raw secret.
Uses NimbleTOTP.verification_code/1 to produce a real 6-digit code
valid for the current time window.
Examples
code = Sigra.Testing.generate_totp_code(raw_secret)
assert String.length(code) == 6
@spec jwt_with_scopes(Sigra.Config.t(), struct(), [String.t()]) :: String.t()
Generates a JWT with specific scopes for testing.
Creates a mock OAuth callback result for testing.
Returns a map matching the shape used by Sigra.OAuth.Callback.
Options
:provider- Provider atom (default::google):email- User email (default:"oauth@example.com"):uid- Provider UID (default:"provider_123"):name- User name (default:"OAuth User"):email_verified- Whether the email is verified (default:true)
Creates a user registered via OAuth for testing.
Returns %{user: user_attrs, identity: identity} map with default
OAuth-specific values (confirmed_at set since OAuth auto-confirms).
Options
:email- User email (default:"oauth@example.com"):provider- Provider string (default:"google"):provider_uid- Provider UID (default: auto-generated):user_id- User ID (default: auto-generated)
@spec put_api_token(Plug.Conn.t(), String.t()) :: Plug.Conn.t()
Alias for put_bearer_token/2.
Examples
iex> conn = Plug.Test.conn(:get, "/api/me")
iex> conn = Sigra.Testing.put_api_token(conn, "sigra_sk_xyz")
iex> Plug.Conn.get_req_header(conn, "authorization")
["Bearer sigra_sk_xyz"]
@spec put_bearer_token(Plug.Conn.t(), String.t()) :: Plug.Conn.t()
Adds a Bearer token header to a conn for API testing.
Overwrites any existing authorization header on the conn. The token is
wrapped in "Bearer " exactly — pass the raw sigra_sk_... value, not
a pre-formatted header.
Examples
iex> conn = Plug.Test.conn(:get, "/api/me")
iex> conn = Sigra.Testing.put_bearer_token(conn, "sigra_sk_abc123")
iex> Plug.Conn.get_req_header(conn, "authorization")
["Bearer sigra_sk_abc123"]
iex> conn = Plug.Test.conn(:get, "/api/me")
iex> conn = Sigra.Testing.put_bearer_token(conn, "raw-token-value")
iex> Plug.Conn.get_req_header(conn, "authorization")
["Bearer raw-token-value"]
@spec revoked_api_token_fixture(Sigra.Config.t(), struct(), keyword()) :: {String.t(), struct()}
Creates a revoked API token fixture.
Creates a user fixture with scheduled deletion.
Sets deleted_at and scheduled_deletion_at on the user, plus
stores the original email in original_email for restore on cancel.
Options
:grace_period_days- Days until permanent deletion (default: 14)
@spec scoped_api_token_fixture(Sigra.Config.t(), struct(), [String.t()], keyword()) :: {String.t(), struct()}
Creates a scoped API token fixture.
Creates a fully enrolled MFA credential for the user.
Generates a real TOTP secret, creates the credential in the DB, and generates backup codes. Returns the secret, credential, and raw formatted backup codes.
Options
:config-%Sigra.Config{}(required):mfa_credential_schema- MFA credential schema module (required):backup_code_schema- Backup code schema module (required):backup_code_count- Number of backup codes (default: 8)
Returns
%{secret: raw_secret, credential: credential, backup_codes: [formatted_codes]}
Simulates grace period expiry by setting scheduled_deletion_at to a past timestamp.
Useful for testing the Oban deletion worker.
Simulate a locked out user by setting failed_login_attempts to threshold.
Returns the updated user struct. Requires a repo module that supports
update!/1.
Options
:threshold- The lockout threshold to simulate. Default:5.
Examples
locked_user = Sigra.Testing.simulate_lockout(MyApp.Repo, user)
assert Sigra.Lockout.locked?(locked_user)
Simulates an MFA lockout by setting failed_attempts to threshold.
Sets failed_attempts to the lockout threshold and locked_until
to 15 minutes from now on the user's MFA credential.
Options
:config-%Sigra.Config{}(required):mfa_credential_schema- MFA credential schema module (required):threshold- Lockout threshold (default: 5):duration- Lockout duration in seconds (default: 900)
@spec trust_browser(Plug.Conn.t(), struct(), keyword()) :: Plug.Conn.t()
Sets the _sigra_mfa_trust cookie on the conn for the given user.
Simulates a trusted browser for MFA bypass in tests.
Options
:secret_key_base- The app's secret key base (default: generates a test key):trust_epoch- The user's trust epoch (default: 0):trust_ttl- Trust cookie TTL in seconds (default: 2_592_000 = 30 days)
Temporarily overrides a hook for a test block.
Swaps the hook configuration for the given operation, runs the test function, and restores the original configuration afterward.
Examples
Sigra.Testing.with_hook(:on_delete, {MyApp.TestHooks, :on_delete}, fn ->
# test code that exercises the hook
end)
Executes the given function with a test mailer configured.
Stub
This function is a stub that will be filled in by later phases when the Mox-based mailer testing infrastructure is implemented.