From 3d18b1a34dacea52ba9a5b2e54a6f4eafbac9692 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 1 May 2026 10:03:51 +0800 Subject: [PATCH] =?UTF-8?q?test(P0-5):=20=E8=A1=A5=E9=BD=90=20health=20han?= =?UTF-8?q?dler=20=E5=92=8C=20ticket=20stats=20handler=20=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 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 --- internal/http/handlers/health_handler_test.go | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 internal/http/handlers/health_handler_test.go diff --git a/internal/http/handlers/health_handler_test.go b/internal/http/handlers/health_handler_test.go new file mode 100644 index 0000000..6c2486b --- /dev/null +++ b/internal/http/handlers/health_handler_test.go @@ -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 +}