Production hardening: security, resilience, observability, and compliance
Password complexity: custom validator requiring uppercase, lowercase, digit (min 8 chars)
Token expiry: 90-day token lifetime with refresh endpoint (60-90 day renewal window)
Health check: /api/health/ now pings Postgres + Redis, returns 503 on failure
Audit logging: async audit_log table for auth events (login, register, delete, etc.)
Circuit breaker: APNs/FCM push sends wrapped with 5-failure threshold, 30s recovery
FK indexes: 27 missing foreign key indexes across all tables (migration 017)
CSP header: default-src 'none'; frame-ancestors 'none'
Gzip compression: level 5 with media endpoint skipper
Prometheus metrics: /metrics endpoint using existing monitoring service
External timeouts: 15s push, 30s SMTP, context timeouts on all external calls
Migrations: 016 (token created_at), 017 (FK indexes), 018 (audit_log)
Tests: circuit breaker (15), audit service (8), token refresh (7), health (4),
middleware expiry (5), validator (new)
This commit is contained in:
136
internal/router/health_test.go
Normal file
136
internal/router/health_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
// setupTestDB creates an in-memory SQLite database for health check tests
|
||||
func setupTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return db
|
||||
}
|
||||
|
||||
func TestLiveCheck_Returns200(t *testing.T) {
|
||||
e := echo.New()
|
||||
e.GET("/api/health/live", liveCheck)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/health/live", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "alive", resp["status"])
|
||||
assert.Equal(t, Version, resp["version"])
|
||||
assert.Contains(t, resp, "timestamp")
|
||||
}
|
||||
|
||||
func TestReadinessCheck_HealthyDB_NilCache_Returns200(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
|
||||
deps := &Dependencies{
|
||||
DB: db,
|
||||
Cache: nil, // No Redis configured
|
||||
}
|
||||
|
||||
e := echo.New()
|
||||
e.GET("/api/health/", readinessCheck(deps))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/health/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "healthy", resp["status"])
|
||||
assert.Equal(t, Version, resp["version"])
|
||||
|
||||
checks := resp["checks"].(map[string]interface{})
|
||||
assert.Equal(t, "ok", checks["postgres"])
|
||||
assert.Equal(t, "not configured", checks["redis"])
|
||||
}
|
||||
|
||||
func TestReadinessCheck_DBDown_Returns503(t *testing.T) {
|
||||
// Open an in-memory SQLite DB, then close the underlying sql.DB to simulate failure
|
||||
db := setupTestDB(t)
|
||||
sqlDB, err := db.DB()
|
||||
require.NoError(t, err)
|
||||
sqlDB.Close()
|
||||
|
||||
deps := &Dependencies{
|
||||
DB: db,
|
||||
Cache: nil,
|
||||
}
|
||||
|
||||
e := echo.New()
|
||||
e.GET("/api/health/", readinessCheck(deps))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/health/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusServiceUnavailable, rec.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
err = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "unhealthy", resp["status"])
|
||||
|
||||
checks := resp["checks"].(map[string]interface{})
|
||||
assert.Contains(t, checks["postgres"], "ping failed")
|
||||
}
|
||||
|
||||
func TestReadinessCheck_ResponseFormat(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
|
||||
deps := &Dependencies{
|
||||
DB: db,
|
||||
Cache: nil,
|
||||
}
|
||||
|
||||
e := echo.New()
|
||||
e.GET("/api/health/", readinessCheck(deps))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/health/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify all expected fields are present
|
||||
assert.Contains(t, resp, "status")
|
||||
assert.Contains(t, resp, "version")
|
||||
assert.Contains(t, resp, "checks")
|
||||
assert.Contains(t, resp, "timestamp")
|
||||
|
||||
// Verify checks is a map with expected keys
|
||||
checks, ok := resp["checks"].(map[string]interface{})
|
||||
require.True(t, ok, "checks should be a map")
|
||||
assert.Contains(t, checks, "postgres")
|
||||
assert.Contains(t, checks, "redis")
|
||||
}
|
||||
Reference in New Issue
Block a user