457 lines
12 KiB
Go
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
|
|
}
|
|
}
|