//go:build test package main import ( "encoding/json" "net/http" "net/http/httptest" "testing" "crussell/db" ) func TestHealthCheck_OK(t *testing.T) { // Create request and recorder req := httptest.NewRequest(http.MethodGet, "/api/health", nil) w := httptest.NewRecorder() // Call handler directly healthCheckHandler(w, req) // Assert 200 OK if w.Code != http.StatusOK { t.Errorf("expected status %d, got %d. body: %s", http.StatusOK, w.Code, w.Body.String()) } // Parse JSON response var response map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("failed to parse JSON response: %v", err) } // Assert status == "ok" status, ok := response["status"].(string) if !ok || status != "ok" { t.Errorf("expected status 'ok', got '%v'", response["status"]) } // Assert services services, ok := response["services"].(map[string]interface{}) if !ok { t.Fatalf("services not found in response") } // Assert services.backend == "ok" backend, ok := services["backend"].(string) if !ok || backend != "ok" { t.Errorf("expected services.backend 'ok', got '%v'", services["backend"]) } // Assert services.database == "ok" database, ok := services["database"].(string) if !ok || database != "ok" { t.Errorf("expected services.database 'ok', got '%v'", services["database"]) } } func TestHealthCheck_Degraded(t *testing.T) { // Set db.Conn to nil to simulate degraded state originalDB := db.Conn db.Conn = nil // Create request and recorder req := httptest.NewRequest(http.MethodGet, "/api/health", nil) w := httptest.NewRecorder() // Call handler directly healthCheckHandler(w, req) // Assert 503 Service Unavailable if w.Code != http.StatusServiceUnavailable { t.Errorf("expected status %d, got %d. body: %s", http.StatusServiceUnavailable, w.Code, w.Body.String()) } // Parse JSON response var response map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("failed to parse JSON response: %v", err) } // Assert status == "degraded" status, ok := response["status"].(string) if !ok || status != "degraded" { t.Errorf("expected status 'degraded', got '%v'", response["status"]) } // Assert services services, ok := response["services"].(map[string]interface{}) if !ok { t.Fatalf("services not found in response") } // Assert services.database == "error" database, ok := services["database"].(string) if !ok || database != "error" { t.Errorf("expected services.database 'error', got '%v'", services["database"]) } // Restore original db.Conn db.Conn = originalDB } func TestCORS_OnlyAllowedOrigins(t *testing.T) { t.Setenv("FRONTEND_ORIGIN", "https://app.example.com, http://localhost:5173") handler := corsMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) tests := []struct { name string origin string expectAllowed bool }{ {name: "first configured origin is allowed", origin: "https://app.example.com", expectAllowed: true}, {name: "second configured origin is allowed", origin: "http://localhost:5173", expectAllowed: true}, {name: "unlisted origin is rejected", origin: "https://evil.example.com", expectAllowed: false}, {name: "prefix-confusion origin is rejected", origin: "https://app.example.com.evil.test", expectAllowed: false}, {name: "suffix-attack origin is rejected", origin: "https://app.example.com/evil", expectAllowed: false}, {name: "no origin header is not echoed", origin: "", expectAllowed: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/health", nil) if tt.origin != "" { req.Header.Set("Origin", tt.origin) } rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) got := rr.Header().Get("Access-Control-Allow-Origin") if tt.expectAllowed && got != tt.origin { t.Errorf("expected Access-Control-Allow-Origin %q, got %q", tt.origin, got) } if !tt.expectAllowed && got != "" { t.Errorf("expected no Access-Control-Allow-Origin header, got %q", got) } }) } } func TestCORS_DefaultOriginWhenEnvUnset(t *testing.T) { t.Setenv("FRONTEND_ORIGIN", "") handler := corsMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) allowed := httptest.NewRequest(http.MethodGet, "/", nil) allowed.Header.Set("Origin", "http://localhost:5173") aw := httptest.NewRecorder() handler.ServeHTTP(aw, allowed) if got := aw.Header().Get("Access-Control-Allow-Origin"); got != "http://localhost:5173" { t.Errorf("expected default dev origin to be allowed, got %q", got) } evil := httptest.NewRequest(http.MethodGet, "/", nil) evil.Header.Set("Origin", "https://evil.example.com") ew := httptest.NewRecorder() handler.ServeHTTP(ew, evil) if got := ew.Header().Get("Access-Control-Allow-Origin"); got != "" { t.Errorf("expected unlisted origin to be rejected with default config, got %q", got) } } func TestCORSPreflight_RejectsUnlistedOrigin(t *testing.T) { t.Setenv("FRONTEND_ORIGIN", "https://app.example.com") handler := corsMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) req := httptest.NewRequest(http.MethodOptions, "/api/bookings", nil) req.Header.Set("Origin", "https://evil.example.com") req.Header.Set("Access-Control-Request-Method", "POST") rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusNoContent { t.Errorf("expected preflight 204, got %d", rr.Code) } if got := rr.Header().Get("Access-Control-Allow-Origin"); got != "" { t.Errorf("expected no Access-Control-Allow-Origin on preflight for unlisted origin, got %q", got) } }