//go:build test // +build test package testutils import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "crussell/testutils/jwt" "crussell/testutils/testdb" "github.com/jackc/pgx/v5/pgxpool" ) // contextKey matches mw.UserIDKey to avoid import cycle with auth package. type contextKey string const userIDKey contextKey = "user_id" // MakeRequest makes an HTTP request to a handler with optional JWT token. // ctx should carry a per-test transaction (from SetupTestTx) for PoolProxy routing. func MakeRequest(handler http.Handler, method, path string, body interface{}, token string, ctx context.Context) *httptest.ResponseRecorder { var req *http.Request if body != nil { bodyBytes, _ := json.Marshal(body) req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") } else { req = httptest.NewRequest(method, path, nil) } req = req.WithContext(ctx) if token != "" { req.Header.Set("Authorization", "Bearer "+token) } w := httptest.NewRecorder() handler.ServeHTTP(w, req) return w } // MakeRequestWithContext makes an HTTP request with context values (for auth middleware testing) // Use this when you need to test handlers that rely on context values set by middleware func MakeRequestWithContext(handler http.Handler, method, path string, body interface{}, ctx context.Context) *httptest.ResponseRecorder { var req *http.Request if body != nil { bodyBytes, _ := json.Marshal(body) req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") } else { req = httptest.NewRequest(method, path, nil) } req = req.WithContext(ctx) w := httptest.NewRecorder() handler.ServeHTTP(w, req) return w } // MakeUserRequest makes a request as an authenticated user. // ctx should carry a per-test transaction (from SetupTestTx) for PoolProxy routing. func MakeUserRequest(handler http.Handler, method, path string, body interface{}, userID string, ctx context.Context) *httptest.ResponseRecorder { token := jwt.GenerateUserToken(userID) return MakeRequest(handler, method, path, body, token, ctx) } // MakeAdminRequest makes a request as an authenticated admin. // ctx should carry a per-test transaction (from SetupTestTx) for PoolProxy routing. func MakeAdminRequest(handler http.Handler, method, path string, body interface{}, adminID string, ctx context.Context) *httptest.ResponseRecorder { token := jwt.GenerateTestToken(adminID, "admin") return MakeRequest(handler, method, path, body, token, ctx) } // MakeContextRequest makes a request with user context set. // ctx should carry a per-test transaction (from SetupTestTx) for PoolProxy routing. // The user ID is layered on top of the transaction context. func MakeContextRequest(handler http.Handler, method, path string, body interface{}, userID string, ctx context.Context) *httptest.ResponseRecorder { reqCtx := context.WithValue(ctx, userIDKey, userID) return MakeRequestWithContext(handler, method, path, body, reqCtx) } // ParseResponseBody unmarshals the response body into dest func ParseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { return json.Unmarshal(w.Body.Bytes(), dest) } // AssertStatusCode checks that the response has the expected HTTP status code func AssertStatusCode(t *testing.T, w *httptest.ResponseRecorder, expectedCode int) { t.Helper() if w.Code != expectedCode { t.Errorf("expected status %d, got %d. body: %s", expectedCode, w.Code, w.Body.String()) } } // AssertStatusCodeWithMessage checks status code and logs full response on mismatch func AssertStatusCodeWithMessage(t *testing.T, w *httptest.ResponseRecorder, expectedCode int, message string) { t.Helper() if w.Code != expectedCode { t.Errorf("%s: expected status %d, got %d.\nResponse: %s", message, expectedCode, w.Code, w.Body.String()) } } // AssertJSONResponse checks that the response is valid JSON and unmarshals it func AssertJSONResponse(t *testing.T, w *httptest.ResponseRecorder, dest interface{}) { t.Helper() if err := ParseResponseBody(w, dest); err != nil { t.Errorf("failed to parse JSON response: %v\nBody: %s", err, w.Body.String()) } } // AssertResponseContains checks that the response body contains a substring func AssertResponseContains(t *testing.T, w *httptest.ResponseRecorder, substring string) { t.Helper() if !bytes.Contains(w.Body.Bytes(), []byte(substring)) { t.Errorf("expected response to contain '%s', but got:\n%s", substring, w.Body.String()) } } // AssertResponseNotContains checks that the response body does NOT contain a substring func AssertResponseNotContains(t *testing.T, w *httptest.ResponseRecorder, substring string) { t.Helper() if bytes.Contains(w.Body.Bytes(), []byte(substring)) { t.Errorf("expected response to NOT contain '%s', but got:\n%s", substring, w.Body.String()) } } // TestDBSnapshot creates a snapshot of the test database for transaction rollback // Returns the pool and a cleanup function func TestDBSnapshot(t *testing.T) (*pgxpool.Pool, func()) { t.Helper() pool := testdb.Pool(t) testdb.Migrate(t, pool) return pool, func() { pool.Close() } } // GetBodyAsString returns the response body as a string func GetBodyAsString(w *httptest.ResponseRecorder) string { return w.Body.String() } // GetBodyAsJSON unmarshals and returns the response body // Returns error if JSON is invalid func GetBodyAsJSON(w *httptest.ResponseRecorder) (map[string]interface{}, error) { var result map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &result) return result, err } // MakeRequestNoAuth makes an HTTP request without authentication. // ctx should carry a per-test transaction (from SetupTestTx) for PoolProxy routing. func MakeRequestNoAuth(handler http.Handler, method, path string, body interface{}, ctx context.Context) *httptest.ResponseRecorder { return MakeRequest(handler, method, path, body, "", ctx) } // AssertErrorStatusCode checks status code and validates error is in response body func AssertErrorStatusCode(t *testing.T, w *httptest.ResponseRecorder, expectedCode int, expectedErrorSubstring string) { t.Helper() AssertStatusCode(t, w, expectedCode) AssertResponseContains(t, w, expectedErrorSubstring) } // GetResponseStatus returns just the status code for convenient assertions func GetResponseStatus(w *httptest.ResponseRecorder) int { return w.Code }