package httpapi import ( "bytes" "context" "net/http" "net/http/httptest" "testing" "github.com/descrybe/descrybe-v2/apps/api/internal/auth" "github.com/google/uuid" ) func TestMemberForbiddenOnSensitiveMutations(t *testing.T) { t.Parallel() s := &Server{} cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") uid := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") ctx := context.WithValue(context.Background(), ctxUserID, uid) ctx = context.WithValue(ctx, ctxCompanyID, cid) ctx = context.WithValue(ctx, ctxRole, "member") cases := []struct { name string fn http.HandlerFunc body string }{ {name: "create_api_key", fn: s.handleCreateAPIKey, body: `{"name":"x"}`}, {name: "update_api_key", fn: s.handleUpdateAPIKey, body: `{"name":"renamed"}`}, {name: "revoke_api_key", fn: s.handleRevokeAPIKey, body: ""}, {name: "put_email", fn: s.handlePutEmailIntegration, body: `{}`}, {name: "verify_email", fn: s.handleVerifyEmailIntegration, body: ""}, {name: "test_email", fn: s.handleTestEmailIntegration, body: `{}`}, {name: "send_email", fn: s.handleSendEmail, body: `{}`}, {name: "put_ai", fn: s.handlePutAIIntegration, body: `{}`}, {name: "test_ai", fn: s.handleTestAIIntegration, body: ""}, {name: "update_woo", fn: s.handleUpdateWooConfig, body: `{}`}, {name: "update_woo_maps", fn: s.handleUpdateWooMaps, body: `{}`}, {name: "update_woo_schedule", fn: s.handleUpdateWooSchedule, body: `{}`}, {name: "update_shopify", fn: s.handleUpdateShopifyConfig, body: `{}`}, {name: "update_shopify_schedule", fn: s.handleUpdateShopifySchedule, body: `{}`}, {name: "stripe_checkout", fn: s.handleStripeCheckout, body: `{}`}, {name: "stripe_portal", fn: s.handleStripePortal, body: `{}`}, {name: "reset_products", fn: s.handleResetProducts, body: `{"product_ids":[],"kind":"raw"}`}, {name: "import_csv", fn: s.handleImportCSV, body: ""}, {name: "put_category_attributes", fn: s.handlePutCategoryAttributes, body: `{"attribute_ids":[]}`}, {name: "delete_category", fn: s.handleDeleteCategory, body: ""}, {name: "delete_attribute", fn: s.handleDeleteAttribute, body: ""}, {name: "delete_feed", fn: s.handleDeleteFeed, body: ""}, {name: "delete_export_feed", fn: s.handleDeleteExportFeed, body: ""}, {name: "rotate_export_feed_token", fn: s.handleRotateExportFeedPublicToken, body: ""}, {name: "delete_file", fn: s.handleDeleteFile, body: ""}, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(tc.body)) req = req.WithContext(ctx) rec := httptest.NewRecorder() tc.fn(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("status = %d body=%s, want 403", rec.Code, rec.Body.String()) } }) } } func TestCompanyAdminAllowedRoles(t *testing.T) { t.Parallel() if !CompanyAdminAllowed(context.WithValue(context.Background(), ctxRole, "admin")) { t.Fatal("admin role should be allowed") } if !CompanyAdminAllowed(context.WithValue(context.Background(), ctxRole, "api")) { t.Fatal("api role should be allowed for catalog destructive ops") } if CompanyAdminAllowed(context.WithValue(context.Background(), ctxRole, "member")) { t.Fatal("member role must not be allowed") } if CompanyAdminAllowed(context.Background()) { t.Fatal("missing role must not be allowed") } } func TestAPIKeyContextRole(t *testing.T) { t.Parallel() if got := apiKeyContextRole("admin"); got != "api" { t.Fatalf("admin -> api, got %q", got) } if got := apiKeyContextRole("Admin"); got != "api" { t.Fatalf("Admin -> api, got %q", got) } if got := apiKeyContextRole("member"); got != "member" { t.Fatalf("member stays member, got %q", got) } if got := apiKeyContextRole(""); got != "member" { t.Fatalf("empty normalizes to member, got %q", got) } if CompanyAdminAllowed(context.WithValue(context.Background(), ctxRole, apiKeyContextRole("member"))) { t.Fatal("member-owned API key must not pass CompanyAdminAllowed") } if !CompanyAdminAllowed(context.WithValue(context.Background(), ctxRole, apiKeyContextRole("admin"))) { t.Fatal("admin-owned API key must pass CompanyAdminAllowed") } } func TestRequirePlatformAdminUnauthorized(t *testing.T) { t.Parallel() s := &Server{} called := false h := s.RequirePlatformAdmin(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { called = true w.WriteHeader(http.StatusNoContent) })) req := httptest.NewRequest(http.MethodGet, "/api/admin/users", 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") } } func TestRequirePlatformAdminForbiddenAndAllow(t *testing.T) { t.Parallel() uid := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") t.Run("forbidden", func(t *testing.T) { t.Parallel() s := &Server{ testPlatformAdmin: func(_ context.Context, got uuid.UUID) (bool, error) { if got != uid { t.Fatalf("userID = %s, want %s", got, uid) } return false, 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("handler must not run for non-admin") } }) t.Run("db_error_fail_closed", func(t *testing.T) { t.Parallel() s := &Server{ testPlatformAdmin: func(context.Context, uuid.UUID) (bool, error) { return false, context.DeadlineExceeded }, } 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/users", nil).WithContext(ctx) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("status = %d, want 403 on lookup error", rec.Code) } }) t.Run("allow", func(t *testing.T) { t.Parallel() s := &Server{ testPlatformAdmin: func(context.Context, uuid.UUID) (bool, error) { return true, 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.StatusNoContent { t.Fatalf("status = %d, want 204", rec.Code) } if !called { t.Fatal("handler must run for platform admin") } }) t.Run("support_staff_forbidden", func(t *testing.T) { t.Parallel() 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/plans", 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("support_staff must not reach full admin routes") } }) } func TestRequireSupportDesk(t *testing.T) { t.Parallel() uid := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") 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, _ *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.StatusNoContent || !called { t.Fatalf("status=%d called=%v", rec.Code, called) } }) t.Run("plain_user_forbidden", func(t *testing.T) { t.Parallel() s := &Server{ testStaffAccess: func(context.Context, uuid.UUID) (auth.StaffAccess, error) { return auth.StaffAccess{}, nil }, } 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) } }) }