Files
ai-customer-service/internal/http/router_test.go
Your Name 087de4e102 fix(audit): use uuid.New() for ticket workflow audit IDs
Fixes 'invalid input syntax for type uuid' error when writing ticket
workflow audit logs. The audit Event.ID field was using fmt.Sprintf
with nanoseconds ('wf-%d') which doesn't match PostgreSQL's uuid type.

Also adds uuid import to ticket_workflow.go.

Verified: full chain webhook→assign→resolve→close produces 3 audit
logs correctly, no more 'invalid uuid' errors in logs.
2026-05-04 13:44:39 +08:00

261 lines
8.4 KiB
Go

package httpserver
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/bridge/ai-customer-service/internal/http/handlers"
"github.com/bridge/ai-customer-service/internal/http/middleware"
"github.com/bridge/ai-customer-service/internal/platform/health"
)
func TestRouter_HealthEndpoint(t *testing.T) {
probe := health.NewProbe()
probe.SetReady(true)
h := handlers.NewHealthHandler(probe)
router := NewRouter(RouterDeps{Health: h})
tests := []struct {
name string
path string
wantStatus int
}{
{"health root returns 200", "/actuator/health", http.StatusOK},
{"live returns 200", "/actuator/health/live", http.StatusOK},
{"ready returns 200 when ready", "/actuator/health/ready", http.StatusOK},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, tc.path, nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != tc.wantStatus {
t.Errorf("GET %s = %d, want %d", tc.path, rr.Code, tc.wantStatus)
}
})
}
}
func TestRouter_UnknownPath_Returns404(t *testing.T) {
probe := health.NewProbe()
probe.SetReady(true)
h := handlers.NewHealthHandler(probe)
router := NewRouter(RouterDeps{Health: h})
tests := []struct {
name string
path string
}{
{"unknown root path", "/unknown"},
{"unknown nested path", "/api/v1/unknown"},
{"unknown deep path", "/api/v1/customer-service/unknown"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, tc.path, nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusNotFound {
t.Errorf("GET %s = %d, want 404", tc.path, rr.Code)
}
})
}
}
func TestRouter_WebhookChannel_MissingChannel_Returns400(t *testing.T) {
probe := health.NewProbe()
probe.SetReady(true)
h := handlers.NewHealthHandler(probe)
router := NewRouter(RouterDeps{Health: h})
req := httptest.NewRequest(http.MethodGet, "/api/v1/customer-service/webhook/", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Errorf("GET /webhook/ = %d, want 400; body: %s", rr.Code, rr.Body.String())
}
}
func TestRouter_WebhookPath_CanBeCalledWithGET(t *testing.T) {
probe := health.NewProbe()
probe.SetReady(true)
h := handlers.NewHealthHandler(probe)
router := NewRouter(RouterDeps{Health: h})
req := httptest.NewRequest(http.MethodGet, "/api/v1/customer-service/webhook", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusMethodNotAllowed {
t.Errorf("GET /webhook = %d, want 405", rr.Code)
}
}
func TestRouter_TicketsList_POST_Returns405(t *testing.T) {
probe := health.NewProbe()
probe.SetReady(true)
h := handlers.NewHealthHandler(probe)
ticketHandler := &handlers.TicketHandler{}
router := NewRouter(RouterDeps{Health: h, Tickets: ticketHandler})
req := httptest.NewRequest(http.MethodPost, "/api/v1/customer-service/tickets", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusMethodNotAllowed {
t.Errorf("POST /tickets = %d, want 405", rr.Code)
}
}
func TestRouter_SessionsRoute_OnlyPOST(t *testing.T) {
probe := health.NewProbe()
probe.SetReady(true)
h := handlers.NewHealthHandler(probe)
router := NewRouter(RouterDeps{Health: h, Sessions: nil})
req := httptest.NewRequest(http.MethodGet, "/api/v1/customer-service/sessions/s1/feedback", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusNotFound {
t.Errorf("GET /sessions/s1/feedback with nil Sessions = %d, want 404", rr.Code)
}
}
func TestRouter_TicketsSubpaths(t *testing.T) {
// Test that ticket subpaths are registered with Tickets != nil
// We use OPTIONS method to avoid triggering handler logic (which would panic with nil service)
probe := health.NewProbe()
probe.SetReady(true)
h := handlers.NewHealthHandler(probe)
ticketHandler := &handlers.TicketHandler{}
router := NewRouter(RouterDeps{Health: h, Tickets: ticketHandler})
// Just verify routes exist by checking non-404 response
// (we can't fully test without mocking service, which is integration test territory)
paths := []string{
"/api/v1/customer-service/tickets/t1/assign",
"/api/v1/customer-service/tickets/t1/resolve",
"/api/v1/customer-service/tickets/t1/close",
}
for _, path := range paths {
t.Run(path, func(t *testing.T) {
// Use HEAD method — less likely to panic
req := httptest.NewRequest(http.MethodHead, path, nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
// Should not be 404 (route is registered)
if rr.Code == http.StatusNotFound {
t.Errorf("%s returned 404 — route not registered", path)
}
})
}
}
func TestRouter_SessionsFeedbackHandoff(t *testing.T) {
// Test sessions routes are registered when Sessions != nil
probe := health.NewProbe()
probe.SetReady(true)
h := handlers.NewHealthHandler(probe)
sessionHandler := &handlers.SessionHandler{}
router := NewRouter(RouterDeps{Health: h, Sessions: sessionHandler})
paths := []string{
"/api/v1/customer-service/sessions/s1/feedback",
"/api/v1/customer-service/sessions/s1/handoff",
}
for _, path := range paths {
t.Run(path, func(t *testing.T) {
// Use HEAD method — less likely to panic
req := httptest.NewRequest(http.MethodHead, path, nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
// Should not be 404 (route is registered)
if rr.Code == http.StatusNotFound {
t.Errorf("%s returned 404 — route not registered", path)
}
})
}
}
func TestRouter_UnknownSessionsPath_Returns405(t *testing.T) {
probe := health.NewProbe()
probe.SetReady(true)
h := handlers.NewHealthHandler(probe)
sessionHandler := &handlers.SessionHandler{}
router := NewRouter(RouterDeps{Health: h, Sessions: sessionHandler})
// Path that doesn't match /feedback or /handoff should get 405
req := httptest.NewRequest(http.MethodPost, "/api/v1/customer-service/sessions/s1/unknown", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusMethodNotAllowed {
t.Errorf("POST /sessions/s1/unknown = %d, want 405", rr.Code)
}
}
func TestRouter_UnknownTicketsPath_Returns405(t *testing.T) {
probe := health.NewProbe()
probe.SetReady(true)
h := handlers.NewHealthHandler(probe)
ticketHandler := &handlers.TicketHandler{}
router := NewRouter(RouterDeps{Health: h, Tickets: ticketHandler})
// Path that doesn't match known subpaths should get 405
req := httptest.NewRequest(http.MethodPost, "/api/v1/customer-service/tickets/t1/unknown", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusMethodNotAllowed {
t.Errorf("POST /tickets/t1/unknown = %d, want 405", rr.Code)
}
}
func TestRouter_TicketAssign_RejectsWhenAuthHeadersMissing(t *testing.T) {
probe := health.NewProbe()
probe.SetReady(true)
h := handlers.NewHealthHandler(probe)
ticketHandler := &handlers.TicketHandler{}
router := NewRouter(RouterDeps{Health: h, Tickets: ticketHandler})
req := httptest.NewRequest(http.MethodPost, "/api/v1/customer-service/tickets/t1/assign?agent_id=a1", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusForbidden {
t.Fatalf("POST /tickets/t1/assign without auth = %d, want 403", rr.Code)
}
}
func TestRouter_TicketAssign_RejectsWhenRoleNotAllowed(t *testing.T) {
probe := health.NewProbe()
probe.SetReady(true)
h := handlers.NewHealthHandler(probe)
ticketHandler := &handlers.TicketHandler{}
router := NewRouter(RouterDeps{Health: h, Tickets: ticketHandler})
req := httptest.NewRequest(http.MethodPost, "/api/v1/customer-service/tickets/t1/assign?agent_id=a1", nil)
req.Header.Set(middleware.HeaderActorID, "agent-1")
req.Header.Set(middleware.HeaderActorRole, "agent")
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusForbidden {
t.Fatalf("POST /tickets/t1/assign with agent role = %d, want 403", rr.Code)
}
}
func TestRouter_SessionHandoff_RejectsWhenAuthHeadersMissing(t *testing.T) {
probe := health.NewProbe()
probe.SetReady(true)
h := handlers.NewHealthHandler(probe)
sessionHandler := &handlers.SessionHandler{}
router := NewRouter(RouterDeps{Health: h, Sessions: sessionHandler})
req := httptest.NewRequest(http.MethodPost, "/api/v1/customer-service/sessions/s1/handoff", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusForbidden {
t.Fatalf("POST /sessions/s1/handoff without auth = %d, want 403", rr.Code)
}
}