package httpapi import ( "bytes" "context" "net/http" "net/http/httptest" "strings" "testing" "github.com/descrybe/descrybe-v2/apps/api/internal/auth" "github.com/descrybe/descrybe-v2/apps/api/internal/support" "github.com/google/uuid" ) func TestRequireSupportDeskForbiddenAndAllow(t *testing.T) { t.Parallel() uid := uuid.MustParse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb") t.Run("unauthorized", func(t *testing.T) { t.Parallel() s := &Server{} called := false h := s.RequireSupportDesk(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { called = true w.WriteHeader(http.StatusNoContent) })) req := httptest.NewRequest(http.MethodGet, "/api/admin/support/tickets", nil) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Fatalf("status = %d, want 401", rec.Code) } if called { t.Fatal("handler must not run without session user") } }) t.Run("member_forbidden", func(t *testing.T) { t.Parallel() s := &Server{ testStaffAccess: func(context.Context, uuid.UUID) (auth.StaffAccess, error) { return auth.StaffAccess{}, nil }, } called := false h := s.RequireSupportDesk(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { called = true w.WriteHeader(http.StatusNoContent) })) ctx := context.WithValue(context.Background(), ctxUserID, uid) req := httptest.NewRequest(http.MethodGet, "/api/admin/support/tickets", nil).WithContext(ctx) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("status = %d, want 403", rec.Code) } if called { t.Fatal("handler must not run for non-staff") } }) t.Run("support_staff_allowed", func(t *testing.T) { t.Parallel() s := &Server{ testStaffAccess: func(context.Context, uuid.UUID) (auth.StaffAccess, error) { return auth.ResolveStaffAccess(false, auth.StaffRoleSupportStaff), nil }, } called := false h := s.RequireSupportDesk(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { called = true access, ok := StaffAccessFromContext(req.Context()) if !ok || !access.SupportDesk || !access.IsSupportOnly { t.Fatalf("expected support-only access in context, got ok=%v %+v", ok, access) } w.WriteHeader(http.StatusNoContent) })) ctx := context.WithValue(context.Background(), ctxUserID, uid) req := httptest.NewRequest(http.MethodGet, "/api/admin/support/tickets", nil).WithContext(ctx) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusNoContent { t.Fatalf("status = %d, want 204", rec.Code) } if !called { t.Fatal("handler must run for support_staff") } }) } func TestSupportStaffForbiddenOnPlanFeatures(t *testing.T) { t.Parallel() uid := uuid.MustParse("cccccccc-cccc-cccc-cccc-cccccccccccc") s := &Server{ testStaffAccess: func(context.Context, uuid.UUID) (auth.StaffAccess, error) { return auth.ResolveStaffAccess(true, auth.StaffRoleSupportStaff), nil }, } cases := []struct { name string body string }{ {name: "get_plan_features", body: ""}, {name: "put_plan_features", body: `{"features":{}}`}, {name: "enable_all", body: ""}, {name: "disable_all", body: ""}, {name: "get_gates", body: ""}, {name: "put_gates", body: `{"features":{}}`}, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() called := false h := s.RequirePlatformAdmin(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { called = true w.WriteHeader(http.StatusNoContent) })) ctx := context.WithValue(context.Background(), ctxUserID, uid) req := httptest.NewRequest(http.MethodPut, "/api/admin/plans/1/features", bytes.NewBufferString(tc.body)).WithContext(ctx) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("status = %d body=%s, want 403", rec.Code, rec.Body.String()) } if called { t.Fatal("plan feature handler must not run for support_staff") } }) } } func TestMemberForbiddenOnAdminSupportAndPlanRoutes(t *testing.T) { t.Parallel() uid := uuid.MustParse("dddddddd-dddd-dddd-dddd-dddddddddddd") s := &Server{ testStaffAccess: func(context.Context, uuid.UUID) (auth.StaffAccess, error) { return auth.StaffAccess{}, nil }, } t.Run("support_desk", func(t *testing.T) { t.Parallel() h := s.RequireSupportDesk(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) })) ctx := context.WithValue(context.Background(), ctxUserID, uid) req := httptest.NewRequest(http.MethodGet, "/api/admin/support/tickets", nil).WithContext(ctx) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("status = %d, want 403", rec.Code) } }) t.Run("platform_admin", func(t *testing.T) { t.Parallel() h := s.RequirePlatformAdmin(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) })) ctx := context.WithValue(context.Background(), ctxUserID, uid) req := httptest.NewRequest(http.MethodGet, "/api/admin/plans/1/features", nil).WithContext(ctx) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("status = %d, want 403", rec.Code) } }) } func TestUserReplyMassAssignmentRejected(t *testing.T) { t.Parallel() // UserReplyInput only accepts "body"; DisallowUnknownFields rejects status / is_internal_note. var dst support.UserReplyInput req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"body":"hi","is_internal_note":true,"status":"closed"}`)) err := DecodeJSON(req, &dst) if err == nil { t.Fatal("expected DecodeJSON to reject mass-assignment fields on UserReplyInput") } } func TestRedactForLog(t *testing.T) { t.Parallel() in := "smtp dial failed password=SuperSecret123 api_key=sk_live_abc token:xyz" out := redactForLog(in) if strings.Contains(out, "SuperSecret123") || strings.Contains(out, "sk_live_abc") || strings.Contains(out, ":xyz") { t.Fatalf("secrets leaked in log: %s", out) } if !strings.Contains(out, "[REDACTED]") { t.Fatalf("expected redaction markers, got %s", out) } } func TestStaffMayAccessTicket(t *testing.T) { t.Parallel() s := &Server{} actor := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") other := uuid.MustParse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb") t.Run("full_admin_sees_all", func(t *testing.T) { t.Parallel() ctx := withStaffAccess(context.Background(), auth.ResolveStaffAccess(true, "")) req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx) ticket := support.Ticket{AssigneeAdminUserID: &other} if !s.staffMayAccessTicket(req, actor, ticket) { t.Fatal("full admin must see assigned tickets") } }) t.Run("support_staff_own_or_unassigned", func(t *testing.T) { t.Parallel() ctx := withStaffAccess(context.Background(), auth.ResolveStaffAccess(false, auth.StaffRoleSupportStaff)) req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx) if !s.staffMayAccessTicket(req, actor, support.Ticket{Status: "open"}) { t.Fatal("unassigned open must be visible") } if s.staffMayAccessTicket(req, actor, support.Ticket{Status: "resolved"}) { t.Fatal("unassigned resolved must be hidden from claim queue") } own := actor if !s.staffMayAccessTicket(req, actor, support.Ticket{AssigneeAdminUserID: &own}) { t.Fatal("own assignment must be visible") } if s.staffMayAccessTicket(req, actor, support.Ticket{AssigneeAdminUserID: &other}) { t.Fatal("other assignee must be hidden") } }) } func TestRequirePlatformAdminExcludesSupportStaff(t *testing.T) { t.Parallel() uid := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") s := &Server{ testStaffAccess: func(context.Context, uuid.UUID) (auth.StaffAccess, error) { return auth.ResolveStaffAccess(true, auth.StaffRoleSupportStaff), nil }, } called := false h := s.RequirePlatformAdmin(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { called = true w.WriteHeader(http.StatusNoContent) })) ctx := context.WithValue(context.Background(), ctxUserID, uid) req := httptest.NewRequest(http.MethodGet, "/api/admin/users", nil).WithContext(ctx) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("status = %d, want 403", rec.Code) } if called { t.Fatal("full admin routes must reject support_staff even with is_platform_admin") } }