package handlers import ( "authorization/db" "authorization/models" "authorization/redisclient" "database/sql" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/DATA-DOG/go-sqlmock" "github.com/alicebob/miniredis/v2" "github.com/redis/go-redis/v9" ) func TestHealthHandler(t *testing.T) { tests := []struct { name string wantStatus int wantBodyStatus string }{ { name: "returns 200 OK with ok status", wantStatus: http.StatusOK, wantBodyStatus: "ok", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, HealthCheckEndpoint, nil) w := httptest.NewRecorder() HealthHandler(w, req) resp := w.Result() defer resp.Body.Close() if resp.StatusCode != tt.wantStatus { t.Errorf(StatusMismatchMessage, resp.StatusCode, tt.wantStatus) } var healthResp models.HealthResponse if err := json.NewDecoder(resp.Body).Decode(&healthResp); err != nil { t.Fatalf(FailedToDecodeResponseMessage, err) } if healthResp.Status != tt.wantBodyStatus { t.Errorf(StatusMismatchMessage, healthResp.Status, tt.wantBodyStatus) } contentType := resp.Header.Get("Content-Type") if contentType != "application/json" { t.Errorf("Content-Type = %v, want application/json", contentType) } }) } } func TestReadyHandlerAllHealthy(t *testing.T) { // Setup mock DB mockDB, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true)) if err != nil { t.Fatalf(FailedToCreateMockDBMessage, err) } defer mockDB.Close() // Expect successful ping mock.ExpectPing() // Save original and set mock originalDB := db.DB db.DB = mockDB defer func() { db.DB = originalDB }() // Save original Redis and set to nil (not checking Redis in this test) originalRedis := redisclient.RDB redisclient.RDB = nil defer func() { redisclient.RDB = originalRedis }() req := httptest.NewRequest(http.MethodGet, ReadyCheckEndpoint, nil) w := httptest.NewRecorder() ReadyHandler(w, req) resp := w.Result() defer resp.Body.Close() var healthResp models.HealthResponse if err := json.NewDecoder(resp.Body).Decode(&healthResp); err != nil { t.Fatalf(FailedToDecodeResponseMessage, err) } if healthResp.Services["database"] != "healthy" { t.Errorf("database status = %v, want healthy", healthResp.Services["database"]) } // Verify mock expectations if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("unfulfilled mock expectations: %v", err) } } func TestReadyHandlerDBUnhealthy(t *testing.T) { // Setup mock DB that fails ping mockDB, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true)) if err != nil { t.Fatalf(FailedToCreateMockDBMessage, err) } defer mockDB.Close() // Expect ping to fail mock.ExpectPing().WillReturnError(sql.ErrConnDone) // Save original and set mock originalDB := db.DB db.DB = mockDB defer func() { db.DB = originalDB }() // Save original Redis and set to nil originalRedis := redisclient.RDB redisclient.RDB = nil defer func() { redisclient.RDB = originalRedis }() req := httptest.NewRequest(http.MethodGet, ReadyCheckEndpoint, nil) w := httptest.NewRecorder() ReadyHandler(w, req) resp := w.Result() defer resp.Body.Close() if resp.StatusCode != http.StatusServiceUnavailable { t.Errorf(StatusMismatchMessage, resp.StatusCode, http.StatusServiceUnavailable) } var healthResp models.HealthResponse if err := json.NewDecoder(resp.Body).Decode(&healthResp); err != nil { t.Fatalf(FailedToDecodeResponseMessage, err) } if healthResp.Status != "AuthZ not Capy!" { t.Errorf("status = %v, want 'AuthZ not Capy!'", healthResp.Status) } if healthResp.Services["database"] != "unhealthy" { t.Errorf("database status = %v, want unhealthy", healthResp.Services["database"]) } if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("unfulfilled mock expectations: %v", err) } } func TestReadyHandlerDBNotInitialized(t *testing.T) { // Save original and set to nil originalDB := db.DB db.DB = nil defer func() { db.DB = originalDB }() originalRedis := redisclient.RDB redisclient.RDB = nil defer func() { redisclient.RDB = originalRedis }() req := httptest.NewRequest(http.MethodGet, ReadyCheckEndpoint, nil) w := httptest.NewRecorder() ReadyHandler(w, req) resp := w.Result() defer resp.Body.Close() if resp.StatusCode != http.StatusServiceUnavailable { t.Errorf(StatusMismatchMessage, resp.StatusCode, http.StatusServiceUnavailable) } var healthResp models.HealthResponse if err := json.NewDecoder(resp.Body).Decode(&healthResp); err != nil { t.Fatalf(FailedToDecodeResponseMessage, err) } if healthResp.Status != "AuthZ not Capy!" { t.Errorf("status = %v, want 'AuthZ not Capy!'", healthResp.Status) } if healthResp.Services["database"] != "not_initialized" { t.Errorf("database status = %v, want not_initialized", healthResp.Services["database"]) } if healthResp.Services["redis"] != "not_initialized" { t.Errorf("redis status = %v, want not_initialized", healthResp.Services["redis"]) } } func TestReadyHandlerContentType(t *testing.T) { originalDB := db.DB db.DB = nil defer func() { db.DB = originalDB }() originalRedis := redisclient.RDB redisclient.RDB = nil defer func() { redisclient.RDB = originalRedis }() req := httptest.NewRequest(http.MethodGet, ReadyCheckEndpoint, nil) w := httptest.NewRecorder() ReadyHandler(w, req) resp := w.Result() defer resp.Body.Close() contentType := resp.Header.Get("Content-Type") if contentType != "application/json" { t.Errorf("Content-Type = %v, want application/json", contentType) } } // Additional comprehensive test cases func TestHealthHandlerMultipleRequests(t *testing.T) { // Test that multiple concurrent requests work correctly concurrency := 10 done := make(chan bool, concurrency) for i := 0; i < concurrency; i++ { go func() { req := httptest.NewRequest(http.MethodGet, HealthCheckEndpoint, nil) w := httptest.NewRecorder() HealthHandler(w, req) if w.Code != http.StatusOK { t.Errorf(ExpectedStatus200Message, w.Code) } done <- true }() } for i := 0; i < concurrency; i++ { <-done } } func TestHealthHandlerDifferentMethods(t *testing.T) { methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH"} for _, method := range methods { t.Run(method, func(t *testing.T) { req := httptest.NewRequest(method, HealthCheckEndpoint, nil) w := httptest.NewRecorder() HealthHandler(w, req) // Handler should always return 200 OK regardless of method if w.Code != http.StatusOK { t.Errorf("Expected status 200 for method %s, got %d", method, w.Code) } }) } } func TestHealthHandlerResponseFormat(t *testing.T) { req := httptest.NewRequest(http.MethodGet, HealthCheckEndpoint, nil) w := httptest.NewRecorder() HealthHandler(w, req) var response models.HealthResponse if err := json.NewDecoder(w.Body).Decode(&response); err != nil { t.Fatalf(FailedToDecodeResponseMessage, err) } if response.Status == "" { t.Error("Status field should not be empty") } if response.Status != "ok" { t.Errorf("Expected status 'ok', got '%s'", response.Status) } } func TestReadyHandlerDatabaseTimeout(t *testing.T) { mockDB, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true)) if err != nil { t.Fatalf(FailedToCreateMockDBMessage, err) } defer mockDB.Close() // Simulate timeout by expecting ping but not responding properly mock.ExpectPing().WillDelayFor(5 * 1000000000) // 5 seconds originalDB := db.DB db.DB = mockDB defer func() { db.DB = originalDB }() originalRedis := redisclient.RDB redisclient.RDB = nil defer func() { redisclient.RDB = originalRedis }() req := httptest.NewRequest(http.MethodGet, ReadyCheckEndpoint, nil) w := httptest.NewRecorder() // This should timeout and return unhealthy ReadyHandler(w, req) if w.Code != http.StatusServiceUnavailable { t.Logf("Expected status 503, got %d (timeout may have been handled differently)", w.Code) } } func TestReadyHandlerBothServicesHealthy(t *testing.T) { // Use miniredis for Redis mock mr, err := miniredis.Run() if err != nil { t.Fatalf("Failed to create miniredis: %v", err) } defer mr.Close() // Setup mock Redis client originalRedis := redisclient.RDB redisclient.RDB = redis.NewClient(&redis.Options{ Addr: mr.Addr(), }) defer func() { redisclient.RDB = originalRedis }() // Setup mock database mockDB, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true)) if err != nil { t.Fatalf(FailedToCreateMockDBMessage, err) } defer mockDB.Close() originalDB := db.DB db.DB = mockDB defer func() { db.DB = originalDB }() // Expect ping mock.ExpectPing() req := httptest.NewRequest(http.MethodGet, ReadyCheckEndpoint, nil) w := httptest.NewRecorder() ReadyHandler(w, req) if w.Code != http.StatusOK { t.Errorf(ExpectedStatus200Message, w.Code) } var response models.HealthResponse json.NewDecoder(w.Body).Decode(&response) if response.Status != "AuthZ Capy!" { t.Errorf("Expected status 'AuthZ Capy!', got '%s'", response.Status) } if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("Unmet expectations: %v", err) } } func TestReadyHandlerNilDatabaseAndRedis(t *testing.T) { originalDB := db.DB db.DB = nil defer func() { db.DB = originalDB }() originalRedis := redisclient.RDB redisclient.RDB = nil defer func() { redisclient.RDB = originalRedis }() req := httptest.NewRequest(http.MethodGet, ReadyCheckEndpoint, nil) w := httptest.NewRecorder() ReadyHandler(w, req) if w.Code != http.StatusServiceUnavailable { t.Errorf("Expected status 503 when both services are nil, got %d", w.Code) } var response models.HealthResponse if err := json.NewDecoder(w.Body).Decode(&response); err != nil { t.Fatalf(FailedToDecodeResponseMessage, err) } // The handler returns "AuthZ not Capy!" when services are down if response.Status != "AuthZ not Capy!" { t.Errorf("Expected status 'AuthZ not Capy!', got '%s'", response.Status) } } func TestReadyHandlerResponseStructure(t *testing.T) { originalDB := db.DB db.DB = nil defer func() { db.DB = originalDB }() originalRedis := redisclient.RDB redisclient.RDB = nil defer func() { redisclient.RDB = originalRedis }() req := httptest.NewRequest(http.MethodGet, ReadyCheckEndpoint, nil) w := httptest.NewRecorder() ReadyHandler(w, req) // Verify response is valid JSON var response map[string]interface{} if err := json.NewDecoder(w.Body).Decode(&response); err != nil { t.Fatalf(FailedToDecodeResponseMessage, err) } // Check that response has expected fields if _, ok := response["status"]; !ok { t.Error("Response should have 'status' field") } } func TestHealthHandlerWithCustomHeaders(t *testing.T) { req := httptest.NewRequest(http.MethodGet, HealthCheckEndpoint, nil) req.Header.Set("X-Request-ID", "test-123") req.Header.Set("User-Agent", "Test-Agent/1.0") w := httptest.NewRecorder() HealthHandler(w, req) if w.Code != http.StatusOK { t.Errorf(ExpectedStatus200Message, w.Code) } } func TestReadyHandlerConcurrentRequests(t *testing.T) { originalDB := db.DB db.DB = nil defer func() { db.DB = originalDB }() originalRedis := redisclient.RDB redisclient.RDB = nil defer func() { redisclient.RDB = originalRedis }() concurrency := 20 done := make(chan bool, concurrency) for i := 0; i < concurrency; i++ { go func() { req := httptest.NewRequest(http.MethodGet, ReadyCheckEndpoint, nil) w := httptest.NewRecorder() ReadyHandler(w, req) if w.Code != http.StatusServiceUnavailable && w.Code != http.StatusOK { t.Errorf("Expected status 503 or 200, got %d", w.Code) } done <- true }() } for i := 0; i < concurrency; i++ { <-done } }