A handler that uses the current user and an endpoint that enforces authentication need different tests. Inject an identity when the question is what the handler does for that user. Send credentials through authentication middleware when the question is who can reach the handler. Neither shortcut replaces a test of your actual login mechanism.
The fragments below run inside tests with t *testing.T. Import context, net/http, net/http/httptest, arkoren.dev/core/http/auth, and arkoren.dev/core/http/router as needed. The examples use this test identity:
type testUser struct {
ID int
}
Inject Identity
For a handler test, attach the exact user type the handler reads with request.User[testUser]. With your handler named showProfile:
incoming := httptest.NewRequest(http.MethodGet, "/me", nil)
ctx := auth.NewContext(incoming.Context(), testUser{ID: 42})
res := router.Handler(showProfile).Record(incoming.WithContext(ctx))
Register response-body cleanup and assert the returned profile, following HTTP Tests. This bypasses credential verification intentionally. It proves neither that anonymous requests are rejected nor that one user may access another user’s data. Keep authorization checks in the route tests that exercise those decisions.
User types must match exactly: a testUser value does not satisfy a lookup for *testUser. auth.Require treats a mismatched type as absent and rejects it.
Exercise Enforcement
When testing the router’s authentication and authorization path, supply a test-only authenticator instead of production credentials. This verifier accepts one fixed token; never enable it outside test code:
authenticate := auth.Bearer(func(ctx context.Context, token string) (testUser, error) {
if token != "profile-test-token" {
return testUser{}, auth.ErrUnauthenticated
}
return testUser{ID: 42}, nil
})
Pass it into your route factory so tests reuse production middleware ordering and route registration. The essential wiring, with your showProfile handler, is:
routes := router.New()
routes.Use(auth.Middleware(authenticate))
routes.With(auth.Require[testUser]()).Get("/me", showProfile)
Send a valid token, register body cleanup, and assert the expected profile as well as HTTP 200:
incoming := httptest.NewRequest(http.MethodGet, "/me", nil)
incoming.Header.Set("Authorization", "Bearer profile-test-token")
res := routes.Record(incoming)
In separate cases, omit the header and send a rejected token. With the default configuration both return HTTP 401, but through different paths: missing credentials pass auth.Middleware anonymously and auth.Require rejects the request; rejected credentials stop at authentication middleware. Assert the challenge header when it is part of your endpoint contract, and assert that protected side effects did not happen.
For an authenticated user without permission, assert your application’s authorization denial separately. A successful identity lookup is not permission to access a resource.
Preserve Login Coverage
Keep a test that submits valid credentials to the real login route, carries its session cookie to a protected route, logs out, and verifies subsequent rejection. Multi-request HTTP tests explains cookie transfer and when to use a test server. Include invalid credentials and session persistence failure cases relevant to your application.
Share the production auth.NewSessionIdentity(load) definition between authentication, login, and logout. Check errors from identity.Login, identity.Logout, and session field mutations before asserting state; see Session Login and Save Changes.
For a focused session-state test, session.New(expiresAt, values) creates a low-level session. Initial values must already have the exact Go types expected by your typed fields; this initializer does not round-trip values through JSON. It does not test cookies, storage, or middleware persistence. A nil session pointer is absent, not usable state.
If production uses a custom session slot, allocate one session.NewHandle() and share it across session middleware, the identity, and request lookup. Attach test state with session.NewContextFor(ctx, handle, current). Separate handles select separate context slots; they do not isolate cookies or storage. Zero handles select the default slot.
Check Authenticators
For your own authenticator, authtest.Run checks the public authentication contract. Import arkoren.dev/core/http/auth/authtest. Here apiKeyAuthenticator is your implementation, and each application helper returns a fresh *http.Request carrying the credentials and context for its case:
func TestAPIKeyContract(t *testing.T) {
authtest.Run(t, apiKeyAuthenticator, anonymousRequest, validRequest,
authtest.WithInvalid[testUser](rejectedRequest),
authtest.WithUser(func(t *testing.T, user testUser) {
if user.ID != 42 {
t.Fatalf("user ID = %d, want 42", user.ID)
}
}),
)
}
The suite checks that absent credentials yield auth.ErrNoCredentials, valid credentials resolve and store a user, and rejected credentials yield auth.ErrUnauthenticated and stop middleware. WithInvalid enables rejection checks; without it those checks are skipped. Add application tests for credential expiry, revocation, and backend failures beyond that shared contract.
See the auth API reference and authtest suite.