//go:build test // +build test package testutils import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "crussell/db" "crussell/mw" "crussell/testutils/jwt" "crussell/testutils/testdb" "github.com/jackc/pgx/v5/pgxpool" ) // SetupTestDB resets test data by truncating tables // Assumes db.DB is already set by TestMain func SetupTestDB(t *testing.T) func() { t.Helper() testdb.TruncateTables(t, db.DB) return func() {} } // MakeRequest makes an HTTP request to a handler with optional JWT token // token can be user token or admin token. Pass empty string for no auth. func MakeRequest(handler http.Handler, method, path string, body interface{}, token string) *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) } 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 // Generates a valid user token and includes it in the Authorization header func MakeUserRequest(handler http.Handler, method, path string, body interface{}, userID string) *httptest.ResponseRecorder { token := jwt.GenerateUserToken(userID) return MakeRequest(handler, method, path, body, token) } // MakeAdminRequest makes a request as an authenticated admin // Generates a valid admin token and includes it in the Authorization header func MakeAdminRequest(handler http.Handler, method, path string, body interface{}, adminID string) *httptest.ResponseRecorder { token := jwt.GenerateTestToken(adminID, "admin") return MakeRequest(handler, method, path, body, token) } // MakeContextRequest makes a request with user context set // Useful for testing handlers that check context before validating token func MakeContextRequest(handler http.Handler, method, path string, body interface{}, userID string) *httptest.ResponseRecorder { ctx := context.WithValue(context.Background(), mw.UserIDKey, userID) return MakeRequestWithContext(handler, method, path, body, ctx) } // 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 (for testing unauthenticated endpoints) func MakeRequestNoAuth(handler http.Handler, method, path string, body interface{}) *httptest.ResponseRecorder { return MakeRequest(handler, method, path, body, "") } // 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 }