Files
Authorization/handlers/health_test.go
T
2026-01-27 13:14:22 +08:00

457 lines
12 KiB
Go

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
}
}