完善本地质量门禁与资金链路测试
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -9,16 +11,55 @@ import (
|
||||
|
||||
type HealthHandler struct {
|
||||
startedAt time.Time
|
||||
checks map[string]ReadinessCheck
|
||||
}
|
||||
|
||||
func NewHealthHandler() *HealthHandler {
|
||||
return &HealthHandler{startedAt: time.Now()}
|
||||
type ReadinessCheck func(context.Context) error
|
||||
|
||||
func NewHealthHandler(checks map[string]ReadinessCheck) *HealthHandler {
|
||||
return &HealthHandler{startedAt: time.Now(), checks: checks}
|
||||
}
|
||||
|
||||
func (h *HealthHandler) Check(c *gin.Context) {
|
||||
func (h *HealthHandler) Live(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
"service": "hfb-api",
|
||||
"started_at": h.startedAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *HealthHandler) Ready(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
names := make([]string, 0, len(h.checks))
|
||||
for name := range h.checks {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
ready := true
|
||||
dependencies := make(map[string]string, len(names))
|
||||
for _, name := range names {
|
||||
check := h.checks[name]
|
||||
if check == nil || check(ctx) != nil {
|
||||
dependencies[name] = "unavailable"
|
||||
ready = false
|
||||
continue
|
||||
}
|
||||
dependencies[name] = "ok"
|
||||
}
|
||||
|
||||
statusCode := http.StatusOK
|
||||
status := "ok"
|
||||
if !ready {
|
||||
statusCode = http.StatusServiceUnavailable
|
||||
status = "unavailable"
|
||||
}
|
||||
c.JSON(statusCode, gin.H{
|
||||
"status": status,
|
||||
"service": "hfb-api",
|
||||
"started_at": h.startedAt.Format(time.RFC3339),
|
||||
"dependencies": dependencies,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestHealthLiveDoesNotDependOnReadiness(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
h := NewHealthHandler(map[string]ReadinessCheck{
|
||||
"mysql": func(context.Context) error { return errors.New("down") },
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
|
||||
h.Live(ctx)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d", recorder.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthReadyReportsDependencyState(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
tests := []struct {
|
||||
name string
|
||||
checks map[string]ReadinessCheck
|
||||
wantStatus int
|
||||
wantBody string
|
||||
}{
|
||||
{
|
||||
name: "all dependencies ready",
|
||||
checks: map[string]ReadinessCheck{
|
||||
"mysql": func(context.Context) error { return nil },
|
||||
"redis": func(context.Context) error { return nil },
|
||||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantBody: `"status":"ok"`,
|
||||
},
|
||||
{
|
||||
name: "dependency unavailable",
|
||||
checks: map[string]ReadinessCheck{
|
||||
"mysql": func(context.Context) error { return errors.New("down") },
|
||||
"redis": func(context.Context) error { return nil },
|
||||
},
|
||||
wantStatus: http.StatusServiceUnavailable,
|
||||
wantBody: `"mysql":"unavailable"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := NewHealthHandler(tt.checks)
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, "/health/ready", nil)
|
||||
|
||||
h.Ready(ctx)
|
||||
|
||||
if recorder.Code != tt.wantStatus {
|
||||
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, tt.wantStatus, recorder.Body.String())
|
||||
}
|
||||
if body := recorder.Body.String(); !strings.Contains(body, tt.wantBody) {
|
||||
t.Fatalf("body = %s, want substring %s", body, tt.wantBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user