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)
137 lines
3.3 KiB
Go
137 lines
3.3 KiB
Go
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")
|
|
}
|