Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user