package httpapi import ( "bytes" "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/descrybe/descrybe-v2/apps/api/internal/billing" "github.com/descrybe/descrybe-v2/apps/api/internal/config" "github.com/google/uuid" ) func TestHandleStripePortalMockLocal(t *testing.T) { t.Parallel() s := &Server{ Config: config.Config{WebOrigin: "http://localhost:5174", StripeMock: true}, Stripe: &billing.StripeService{ Cfg: billing.StripeConfig{ForceMock: true, WebOrigin: "http://localhost:5174"}, }, } cid := uuid.New() uid := uuid.New() 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/billing/portal", nil) req = req.WithContext(ctx) rec := httptest.NewRecorder() s.handleStripePortal(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } var body billing.PortalResult if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatal(err) } if !body.Mock || body.URL != "http://localhost:5174/billing?portal=mock" { t.Fatalf("got %#v", body) } } func TestHandleStripeWebhookRejectsBadSignature(t *testing.T) { t.Parallel() secret := "whsec_handler_test" s := &Server{ Stripe: &billing.StripeService{ Cfg: billing.StripeConfig{ForceMock: true, WebhookSecret: secret}, }, } payload := []byte(`{"id":"evt_bad","type":"ping"}`) req := httptest.NewRequest(http.MethodPost, "/api/billing/webhook", bytes.NewReader(payload)) req.Header.Set("Stripe-Signature", "t=1,v1=deadbeef") rec := httptest.NewRecorder() s.handleStripeWebhook(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status=%d body=%s want 400", rec.Code, rec.Body.String()) } } func TestHandleStripeWebhookUnsignedWithoutSecretNeedsForceMock(t *testing.T) { t.Parallel() // Misconfigured live (no webhook secret, no ForceMock) must 503 — never process unsigned. s := &Server{ Stripe: &billing.StripeService{Cfg: billing.StripeConfig{}}, } req := httptest.NewRequest(http.MethodPost, "/api/billing/webhook", bytes.NewReader([]byte(`{"id":"evt_x","type":"ping"}`))) rec := httptest.NewRecorder() s.handleStripeWebhook(rec, req) if rec.Code != http.StatusServiceUnavailable { t.Fatalf("status=%d body=%s want 503", rec.Code, rec.Body.String()) } } func TestHandleStripeCheckoutMockRequiresAdmin(t *testing.T) { t.Parallel() s := &Server{ Config: config.Config{StripeMock: true}, Stripe: &billing.StripeService{Cfg: billing.StripeConfig{ForceMock: true}}, } cid := uuid.New() uid := uuid.New() ctx := context.WithValue(context.Background(), ctxUserID, uid) ctx = context.WithValue(ctx, ctxCompanyID, cid) ctx = context.WithValue(ctx, ctxRole, "member") req := httptest.NewRequest(http.MethodPost, "/api/billing/checkout", bytes.NewBufferString(`{"plan":"starter","term":"monthly"}`)) req = req.WithContext(ctx) rec := httptest.NewRecorder() s.handleStripeCheckout(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("status=%d want 403", rec.Code) } } func signHandlerStripePayload(t *testing.T, secret string, payload []byte) string { t.Helper() ts := time.Now().Unix() mac := hmac.New(sha256.New, []byte(secret)) _, _ = fmt.Fprintf(mac, "%d.", ts) _, _ = mac.Write(payload) return fmt.Sprintf("t=%d,v1=%s", ts, hex.EncodeToString(mac.Sum(nil))) } func TestHandleStripeWebhookValidSignatureStillVerifiedUnderMock(t *testing.T) { t.Parallel() secret := "whsec_handler_ok" // No Pool: claim fails closed after signature passes — proves verify runs before apply. s := &Server{ Stripe: &billing.StripeService{ Cfg: billing.StripeConfig{ForceMock: true, WebhookSecret: secret}, }, } payload := []byte(`{"id":"evt_ok","type":"ping"}`) req := httptest.NewRequest(http.MethodPost, "/api/billing/webhook", bytes.NewReader(payload)) req.Header.Set("Stripe-Signature", signHandlerStripePayload(t, secret, payload)) rec := httptest.NewRecorder() s.handleStripeWebhook(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status=%d body=%s — expect apply/store failure after verify", rec.Code, rec.Body.String()) } }