Skip to main content

Testing the full stack

HTTP handler tests exercise API endpoints from request to response. A single request might authenticate a user, check permissions, validate input, query a database, update a cache, and write an audit log. Testing handlers end to end catches bugs that unit tests miss. Every handler must have tests for success, validation errors, authentication errors, and authorization errors. Each handler test must make the API guarantee visible. The reader must know which contract the endpoint promises, which client-visible response is expected, and which side effects are required or forbidden.

Anatomy of a handler test

Create a test harness, configure the handler with harness dependencies, register the handler, create credentials, make a request, and verify the response.
func TestCreateApi_Success(t *testing.T) {
    h := testutil.NewHarness(t)

    route := &handler.Handler{
        Logger:    h.Logger,
        DB:        h.DB,
        Keys:      h.Keys,
        Auditlogs: h.Auditlogs,
    }

    h.Register(route)

    rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "api.*.create_api")
    headers := http.Header{
        "Content-Type":  {"application/json"},
        "Authorization": {fmt.Sprintf("Bearer %s", rootKey)},
    }

    req := handler.Request{Name: "my-new-api"}
    res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req)

    require.Equal(t, http.StatusOK, res.Status)
    require.NotEmpty(t, res.Body.ApiID)
}

Organizing test files

Organize tests by behavior so the intent is clear. Example directory: svc/api/routes/v2_apis_create_api/. Typical files:

Testing success cases

Test minimal requests, full requests, and any important variations. Verify side effects such as audit log writes when relevant.

Testing validation errors

Test boundary conditions, missing required fields, and invalid formats. These tests document the API contract. Use test names and docstrings to identify the contract. For example, document whether invalid input must return 400, whether the error code is stable for SDKs, and whether the handler must avoid writing partial state.

Testing authentication

Reject missing headers, malformed tokens, and revoked credentials. Authentication tests protect security guarantees. Document the guarantee when a case covers token confusion, revoked credentials, replay behavior, or information disclosure.

Testing authorization

Verify that permissions are enforced and cross-workspace access is rejected. Authorization tests must state the resource boundary they protect. If a response intentionally hides existence with 404, document that behavior in the test so future changes do not weaken it by accident.

Helper functions

Extract repeated setup into helpers. If a helper asserts, it must call t.Helper().

Debugging failed requests

Use the raw response body to inspect failures:
if res.Status != http.StatusOK {
    t.Logf("Response body: %s", res.RawBody)
}