zee-solution/internal/transport/http/handler/health_handler_test.go

304 lines
7.8 KiB
Go

package handler
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"zee/internal/helper/config"
)
// MockConfig is a mock of config.Config
type MockConfig struct {
mock.Mock
App config.AppConfig
}
func (m *MockConfig) GetAppConfig() *config.AppConfig {
args := m.Called()
if args.Get(0) == nil {
return nil
}
return args.Get(0).(*config.AppConfig)
}
func TestNewHealthHandler(t *testing.T) {
t.Run("creates new health handler with config", func(t *testing.T) {
cfg := &config.Config{
App: config.AppConfig{
Name: "test-app",
Version: "1.0.0",
Environment: "test",
},
}
handler := NewHealthHandler(cfg)
assert.NotNil(t, handler)
assert.Equal(t, cfg.App.Version, handler.appVersion)
assert.False(t, handler.startTime.IsZero())
})
}
func TestHealthCheck(t *testing.T) {
// Setup test cases
tests := []struct {
name string
setupMock func(*MockConfig)
expectedCode int
expectedKeys []string
checkUptime bool
checkAppInfo bool
expectedValues map[string]interface{}
}{
{
name: "successful health check",
setupMock: func(mc *MockConfig) {
mc.App = config.AppConfig{
Name: "test-app",
Version: "1.0.0",
Environment: "test",
}
},
expectedCode: http.StatusOK,
expectedKeys: []string{"status", "app", "uptime", "components", "timestamp"},
expectedValues: map[string]interface{}{
"status": "ok",
"app": map[string]interface{}{
"name": "test-app",
"version": "1.0.0",
"env": "test",
},
"components": map[string]interface{}{
"database": "ok",
"cache": "ok",
},
},
checkUptime: true,
checkAppInfo: true,
},
{
name: "health check with empty config",
setupMock: func(mc *MockConfig) {
mc.App = config.AppConfig{}
},
expectedCode: http.StatusOK,
expectedValues: map[string]interface{}{
"status": "ok",
"app": map[string]interface{}{
"name": "",
"version": "",
"env": "",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup mock config
mockCfg := new(MockConfig)
tt.setupMock(mockCfg)
// Setup mock expectations
mockCfg.On("GetAppConfig").Return(&mockCfg.App)
// Create handler with mock config
handler := NewHealthHandler(&config.Config{
App: *mockCfg.GetAppConfig(),
})
// Create a new request
req := httptest.NewRequest("GET", "/health", nil)
// Create a response recorder
w := httptest.NewRecorder()
// Create a new router and register the handler
r := gin.New()
r.GET("/health", handler.HealthCheck)
// Serve the request
r.ServeHTTP(w, req)
// Assert the status code
assert.Equal(t, tt.expectedCode, w.Code)
// Parse the response body
var response map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &response), "Failed to parse response body")
// Check expected keys exist
for _, key := range tt.expectedKeys {
_, exists := response[key]
assert.True(t, exists, "Response should contain key: %s", key)
}
// Check expected values
for key, expectedValue := range tt.expectedValues {
switch v := expectedValue.(type) {
case map[string]interface{}:
actual, exists := response[key].(map[string]interface{})
require.True(t, exists, "Expected %s to be a map", key)
for subKey, subValue := range v {
assert.Equal(t, subValue, actual[subKey], "Mismatch for %s.%s", key, subKey)
}
default:
assert.Equal(t, expectedValue, response[key], "Mismatch for %s", key)
}
}
// Check uptime if needed
if tt.checkUptime {
_, exists := response["uptime"]
assert.True(t, exists, "Response should contain uptime")
}
// Check app info if needed
if tt.checkAppInfo {
appInfo, ok := response["app"].(map[string]interface{})
assert.True(t, ok, "app should be a map")
assert.Equal(t, "test-app", appInfo["name"])
assert.Equal(t, "1.0.0", appInfo["version"])
assert.Equal(t, "test", appInfo["env"])
}
// Check uptime is a valid duration string
if tt.checkUptime {
uptime, ok := response["uptime"].(string)
assert.True(t, ok, "uptime should be a string")
_, err := time.ParseDuration(uptime)
assert.NoError(t, err, "uptime should be a valid duration string")
}
// Check components
components, ok := response["components"].(map[string]interface{})
assert.True(t, ok, "components should be a map")
assert.Equal(t, "ok", components["database"])
assert.Equal(t, "ok", components["cache"])
// Check timestamp format
timestamp, ok := response["timestamp"].(string)
assert.True(t, ok, "timestamp should be a string")
_, err := time.Parse(time.RFC3339, timestamp)
assert.NoError(t, err, "timestamp should be in RFC3339 format")
// Assert that all expectations were met
mockCfg.AssertExpectations(t)
})
}
}
func TestPing(t *testing.T) {
// Setup test cases
tests := []struct {
name string
setupMock func(*MockConfig)
expectedCode int
expectedValues map[string]string
}{
{
name: "successful ping",
setupMock: func(mc *MockConfig) {
mc.App = config.AppConfig{
Name: "test-app",
Version: "1.0.0",
Environment: "test",
}
},
expectedCode: http.StatusOK,
expectedValues: map[string]string{
"status": "ok",
"message": "pong",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup mock config
mockCfg := new(MockConfig)
tt.setupMock(mockCfg)
// Setup mock expectations
mockCfg.On("GetAppConfig").Return(&mockCfg.App)
// Create handler with mock config
handler := NewHealthHandler(&config.Config{
App: *mockCfg.GetAppConfig(),
})
// Create a new request
req := httptest.NewRequest("GET", "/ping", nil)
// Create a response recorder
w := httptest.NewRecorder()
// Create a new router and register the handler
r := gin.New()
r.GET("/ping", handler.Ping)
// Serve the request
r.ServeHTTP(w, req)
// Assert the status code
assert.Equal(t, tt.expectedCode, w.Code)
// Parse the response body
var response map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &response), "Failed to parse response body")
// Check expected values
for key, expectedValue := range tt.expectedValues {
actual, exists := response[key]
require.True(t, exists, "Expected key %s not found in response", key)
assert.Equal(t, expectedValue, actual, "Mismatch for key %s", key)
}
// Check timestamp is in the correct format
timestamp, ok := response["timestamp"].(string)
require.True(t, ok, "timestamp should be a string")
_, err := time.Parse(time.RFC3339, timestamp)
assert.NoError(t, err, "timestamp should be in RFC3339 format")
// Assert that all expectations were met
mockCfg.AssertExpectations(t)
})
}
// Test with nil config
t.Run("ping with nil config", func(t *testing.T) {
handler := &HealthHandler{}
// Create a new request
req := httptest.NewRequest("GET", "/ping", nil)
// Create a response recorder
w := httptest.NewRecorder()
// Create a new router and register the handler
r := gin.New()
r.GET("/ping", handler.Ping)
// Serve the request
r.ServeHTTP(w, req)
// Should still work with default values
assert.Equal(t, http.StatusOK, w.Code)
// Parse the response body
var response map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &response), "Failed to parse response body")
assert.Equal(t, "pong", response["message"], "Response should contain message 'pong'")
assert.Equal(t, "ok", response["status"], "Response should contain status 'ok'")
})
}