package httpapi import ( "bytes" "context" "fmt" "net/http" "net/http/httptest" "strings" "testing" "github.com/descrybe/descrybe-v2/apps/api/internal/billing" "github.com/descrybe/descrybe-v2/apps/api/internal/campaigns" "github.com/descrybe/descrybe-v2/apps/api/internal/config" "github.com/google/uuid" ) func TestRequireFeatureBillingNilFailsClosedInProduction(t *testing.T) { t.Parallel() s := &Server{Config: config.Config{AppEnv: "production"}} h := s.RequireFeature("capability.api_access")(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) })) rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/gated", nil)) if rec.Code != http.StatusServiceUnavailable { t.Fatalf("status=%d want 503 body=%s", rec.Code, rec.Body.String()) } } func TestRequireFeatureBillingNilPassThroughOutsideProduction(t *testing.T) { t.Parallel() s := &Server{Config: config.Config{AppEnv: "development"}} called := false h := s.RequireFeature("capability.api_access")(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { called = true w.WriteHeader(http.StatusNoContent) })) rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/gated", nil)) if rec.Code != http.StatusNoContent || !called { t.Fatalf("status=%d called=%v want 204 pass-through", rec.Code, called) } } func TestRequireFeaturesPlanGateViaHook(t *testing.T) { t.Parallel() cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") s := &Server{ testAssertFeatures: func(_ context.Context, keys ...string) error { return fmt.Errorf("%w: %s", billing.ErrFeatureDisabled, keys[0]) }, } ctx := context.WithValue(context.Background(), ctxCompanyID, cid) req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx) rec := httptest.NewRecorder() if s.requireFeatures(rec, req, "marketing.campaigns") { t.Fatal("requireFeatures should reject disabled feature") } if rec.Code != http.StatusPaymentRequired { t.Fatalf("status=%d want 402 body=%s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), "feature_disabled") { t.Fatalf("body=%s want feature_disabled", rec.Body.String()) } } func TestCreateCampaignPlanGate(t *testing.T) { t.Parallel() cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") uid := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") s := &Server{ Campaigns: &campaigns.Service{}, testAssertFeatures: func(_ context.Context, keys ...string) error { return fmt.Errorf("%w: %s", billing.ErrFeatureDisabled, keys[0]) }, } ctx := context.WithValue(context.Background(), ctxUserID, uid) ctx = context.WithValue(ctx, ctxCompanyID, cid) ctx = context.WithValue(ctx, ctxRole, "admin") t.Run("list", func(t *testing.T) { t.Parallel() req := httptest.NewRequest(http.MethodGet, "/api/campaigns", nil).WithContext(ctx) rec := httptest.NewRecorder() s.handleListCampaigns(rec, req) if rec.Code != http.StatusPaymentRequired { t.Fatalf("status=%d want 402 body=%s", rec.Code, rec.Body.String()) } }) t.Run("create", func(t *testing.T) { t.Parallel() req := httptest.NewRequest(http.MethodPost, "/api/campaigns", bytes.NewBufferString(`{"name":"x"}`)).WithContext(ctx) rec := httptest.NewRecorder() s.handleCreateCampaign(rec, req) if rec.Code != http.StatusPaymentRequired { t.Fatalf("status=%d want 402 body=%s", rec.Code, rec.Body.String()) } }) } func TestCreateAPIKeyPlanGate(t *testing.T) { t.Parallel() cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") uid := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") s := &Server{ testAssertFeatures: func(_ context.Context, keys ...string) error { return fmt.Errorf("%w: settings.api_keys", billing.ErrFeatureDisabled) }, } ctx := context.WithValue(context.Background(), ctxUserID, uid) ctx = context.WithValue(ctx, ctxCompanyID, cid) ctx = context.WithValue(ctx, ctxRole, "admin") req := httptest.NewRequest(http.MethodPost, "/api/api-keys", bytes.NewBufferString(`{"name":"x"}`)).WithContext(ctx) rec := httptest.NewRecorder() s.handleCreateAPIKey(rec, req) if rec.Code != http.StatusPaymentRequired { t.Fatalf("status=%d want 402 body=%s", rec.Code, rec.Body.String()) } } func TestCreateAPIKeyBillingNilFailsClosedInProduction(t *testing.T) { t.Parallel() cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") uid := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") s := &Server{Config: config.Config{AppEnv: "production"}} ctx := context.WithValue(context.Background(), ctxUserID, uid) ctx = context.WithValue(ctx, ctxCompanyID, cid) ctx = context.WithValue(ctx, ctxRole, "admin") req := httptest.NewRequest(http.MethodPost, "/api/api-keys", bytes.NewBufferString(`{"name":"x"}`)).WithContext(ctx) rec := httptest.NewRecorder() s.handleCreateAPIKey(rec, req) if rec.Code != http.StatusServiceUnavailable { t.Fatalf("status=%d want 503 body=%s", rec.Code, rec.Body.String()) } } func TestUpdateShopifyConfigPlanGate(t *testing.T) { t.Parallel() cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") uid := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") s := &Server{ testAssertFeatures: func(_ context.Context, keys ...string) error { return fmt.Errorf("%w: stores.shopify", billing.ErrFeatureDisabled) }, } ctx := context.WithValue(context.Background(), ctxUserID, uid) ctx = context.WithValue(ctx, ctxCompanyID, cid) ctx = context.WithValue(ctx, ctxRole, "admin") req := httptest.NewRequest(http.MethodPut, "/api/shopify", bytes.NewBufferString(`{}`)).WithContext(ctx) rec := httptest.NewRecorder() s.handleUpdateShopifyConfig(rec, req) if rec.Code != http.StatusPaymentRequired { t.Fatalf("status=%d want 402 body=%s", rec.Code, rec.Body.String()) } } func TestHandleListPublicPlansBillingNilReturnsEmpty(t *testing.T) { t.Parallel() s := &Server{Config: config.Config{WebOrigin: "http://localhost:5173"}} rec := httptest.NewRecorder() s.handleListPublicPlans(rec, httptest.NewRequest(http.MethodGet, "/api/public/plans", nil)) if rec.Code != http.StatusOK { t.Fatalf("nil billing status=%d want 200 body=%s", rec.Code, rec.Body.String()) } s.Billing = &billing.Service{} // non-nil service without pool must not 500 rec2 := httptest.NewRecorder() s.handleListPublicPlans(rec2, httptest.NewRequest(http.MethodGet, "/api/public/plans", nil)) if rec2.Code != http.StatusOK { t.Fatalf("empty billing status=%d want 200 body=%s", rec2.Code, rec2.Body.String()) } } func TestHandleListPlansBillingNilServiceUnavailable(t *testing.T) { t.Parallel() s := &Server{} rec := httptest.NewRecorder() s.handleListPlans(rec, httptest.NewRequest(http.MethodGet, "/api/admin/plans", nil)) if rec.Code != http.StatusServiceUnavailable { t.Fatalf("status=%d want 503 body=%s", rec.Code, rec.Body.String()) } }