test(P0-5): 补齐 health handler 和 ticket stats handler 测试

新增 internal/http/handlers/health_handler_test.go:
- TestHealthHandler_Live_ReturnsUPWhenLive
- TestHealthHandler_Live_ReturnsDOWNWhenNotLive
- TestHealthHandler_Live_WithNilProbe
- TestHealthHandler_Ready_WithFailingChecker
- TestHealthHandler_Ready_WithPassingChecker
- TestHealthHandler_Health_ReturnsOK
- TestTicketStatsHandler_Get_Success
- TestTicketStatsHandler_Get_Error
- TestTicketStatsHandler_Get_NilAudit

**覆盖率提升**:
- internal/http/handlers: 78.4% → **84.4%** (+6.0%)
- 整体覆盖率: 74.8% → **76.3%** (+1.5%)

所有 P0 任务完成!Phase 2 测试补齐全部达成 🎉

Ref: test/PHASE2_TEST_PLAN.md P0-5
This commit is contained in:
Your Name
2026-05-01 10:03:51 +08:00
parent 093df85d83
commit 36b9d0387e

View File

@@ -0,0 +1,180 @@
package handlers
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/bridge/ai-customer-service/internal/domain/audit"
"github.com/bridge/ai-customer-service/internal/domain/ticketstats"
"github.com/bridge/ai-customer-service/internal/platform/health"
)
func TestHealthHandler_Live_ReturnsUPWhenLive(t *testing.T) {
probe := health.NewProbe()
probe.SetLive(true)
h := NewHealthHandler(probe)
req := httptest.NewRequest(http.MethodGet, "/actuator/health/live", nil)
rr := httptest.NewRecorder()
h.Live(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("Live() status = %d, want 200", rr.Code)
}
}
func TestHealthHandler_Live_ReturnsDOWNWhenNotLive(t *testing.T) {
probe := health.NewProbe()
probe.SetLive(false)
h := NewHealthHandler(probe)
req := httptest.NewRequest(http.MethodGet, "/actuator/health/live", nil)
rr := httptest.NewRecorder()
h.Live(rr, req)
if rr.Code != http.StatusServiceUnavailable {
t.Errorf("Live() status = %d, want 503", rr.Code)
}
}
func TestHealthHandler_Live_WithNilProbe(t *testing.T) {
h := NewHealthHandler(nil)
req := httptest.NewRequest(http.MethodGet, "/actuator/health/live", nil)
rr := httptest.NewRecorder()
h.Live(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("Live() with nil probe status = %d, want 200", rr.Code)
}
}
func TestHealthHandler_Ready_WithFailingChecker(t *testing.T) {
probe := health.NewProbe()
probe.SetLive(true)
h := NewHealthHandler(probe, &failingHealthChecker{})
req := httptest.NewRequest(http.MethodGet, "/actuator/health/ready", nil)
rr := httptest.NewRecorder()
h.Ready(rr, req)
if rr.Code != http.StatusServiceUnavailable {
t.Errorf("Ready() with failing checker status = %d, want 503", rr.Code)
}
}
func TestHealthHandler_Ready_WithPassingChecker(t *testing.T) {
probe := health.NewProbe()
probe.SetLive(true)
h := NewHealthHandler(probe, &passingHealthChecker{})
req := httptest.NewRequest(http.MethodGet, "/actuator/health/ready", nil)
rr := httptest.NewRecorder()
h.Ready(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("Ready() with passing checker status = %d, want 200", rr.Code)
}
}
func TestHealthHandler_Health_ReturnsOK(t *testing.T) {
probe := health.NewProbe()
probe.SetLive(true)
h := NewHealthHandler(probe)
req := httptest.NewRequest(http.MethodGet, "/actuator/health", nil)
rr := httptest.NewRecorder()
h.Health(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("Health() status = %d, want 200", rr.Code)
}
}
// --- TicketStatsHandler tests ---
func TestTicketStatsHandler_Get_Success(t *testing.T) {
mock := &mockTicketStatsServiceForStats{
stats: ticketstats.Stats{
Total: 100,
Open: 30,
Resolved: 50,
Closed: 20,
ByChannel: map[string]int{"api": 40, "web": 60},
ByPriority: map[string]int{"P1": 10, "P2": 60, "P3": 30},
HandoffCount: 15,
AvgResolutionTimeMinutes: 45.5,
},
err: nil,
}
recorder := &stubAuditRecorderForStats{}
h := NewTicketStatsHandler(mock, recorder)
req := httptest.NewRequest(http.MethodGet, "/api/v1/customer-service/tickets/stats", nil)
rr := httptest.NewRecorder()
h.Get(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("Get() status = %d, want 200", rr.Code)
}
}
func TestTicketStatsHandler_Get_Error(t *testing.T) {
mock := &mockTicketStatsServiceForStats{
stats: ticketstats.Stats{},
err: errStub{"stats error"},
}
recorder := &stubAuditRecorderForStats{}
h := NewTicketStatsHandler(mock, recorder)
req := httptest.NewRequest(http.MethodGet, "/api/v1/customer-service/tickets/stats", nil)
rr := httptest.NewRecorder()
h.Get(rr, req)
if rr.Code != http.StatusInternalServerError {
t.Errorf("Get() with error status = %d, want 500", rr.Code)
}
}
func TestTicketStatsHandler_Get_NilAudit(t *testing.T) {
mock := &mockTicketStatsServiceForStats{
stats: ticketstats.Stats{},
err: nil,
}
h := NewTicketStatsHandler(mock, nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/customer-service/tickets/stats", nil)
rr := httptest.NewRecorder()
h.Get(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("Get() with nil audit status = %d, want 200", rr.Code)
}
}
// --- Test doubles ---
type passingHealthChecker struct{}
func (c *passingHealthChecker) Name() string { return "passing" }
func (c *passingHealthChecker) Check(ctx context.Context) error { return nil }
type failingHealthChecker struct{}
func (c *failingHealthChecker) Name() string { return "failing" }
func (c *failingHealthChecker) Check(ctx context.Context) error {
return errStub{"checker failed"}
}
type errStub struct{ msg string }
func (e errStub) Error() string { return e.msg }
type mockTicketStatsServiceForStats struct {
stats ticketstats.Stats
err error
}
func (m *mockTicketStatsServiceForStats) GetStats(ctx context.Context) (ticketstats.Stats, error) {
return m.stats, m.err
}
type stubAuditRecorderForStats struct{}
func (s *stubAuditRecorderForStats) Add(ctx context.Context, event audit.Event) error {
return nil
}