Files
2026-08-16 11:37:42 +02:00

153 lines
5.1 KiB
Go

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 TestHandleStripeCheckoutMockBlocksNonPlatformAdmin(t *testing.T) {
t.Parallel()
s := &Server{
Config: config.Config{StripeMock: true},
Stripe: &billing.StripeService{Cfg: billing.StripeConfig{ForceMock: true}},
testPlatformAdmin: func(context.Context, uuid.UUID) (bool, error) {
return false, nil
},
}
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/checkout", bytes.NewBufferString(`{"plan":"starter","term":"monthly"}`))
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
s.handleStripeCheckout(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("status=%d body=%s want 503", rec.Code, rec.Body.String())
}
}
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())
}
}