mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-31 13:17:09 +00:00
136 lines
4.5 KiB
Go
136 lines
4.5 KiB
Go
//go:build billing && stripe_integration
|
|
|
|
package stripeintegration
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
// Required blank imports for the in-process distribution registry that
|
|
// the testharness boots: without these the appview panics with
|
|
// "StorageDriver not registered: inmemory".
|
|
_ "github.com/distribution/distribution/v3/registry/auth/token"
|
|
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
|
|
|
|
"github.com/stripe/stripe-go/v84/webhook"
|
|
|
|
"atcr.io/internal/testharness"
|
|
"atcr.io/pkg/billing"
|
|
)
|
|
|
|
// TestWebhookEndpointAcceptsSignedPayload boots the full in-process stack
|
|
// (fake PDS + gofakes3 + hold + appview) with billing wired in, then POSTs
|
|
// a Stripe-signed event to /api/stripe/webhook over real HTTP. This is the
|
|
// piece the Manager-level tests in manager_test.go can't cover: that the
|
|
// route is actually mounted, the chi router forwards the body, and the
|
|
// signature secret in cfg.Billing reaches the handler unchanged.
|
|
//
|
|
// We assert on the HTTP status code rather than any tier-push side effect
|
|
// because the harness uses an in-memory hold whose crew table is seeded
|
|
// directly — there is no real "managed hold" to push tier updates to, and
|
|
// configuring one would couple this test to the holdclient transport layer
|
|
// which has its own coverage.
|
|
func TestWebhookEndpointAcceptsSignedPayload(t *testing.T) {
|
|
env := requireStripeEnv(t)
|
|
|
|
billingCfg := billing.Config{
|
|
StripeSecretKey: env.SecretKey,
|
|
WebhookSecret: env.WebhookSecret,
|
|
Currency: "usd",
|
|
SuccessURL: "{base_url}/billing/success",
|
|
CancelURL: "{base_url}/billing/cancel",
|
|
Tiers: []billing.BillingTierConfig{
|
|
{Name: "free", MaxWebhooks: 1},
|
|
{
|
|
Name: env.TierName,
|
|
StripePriceMonthly: env.PriceMonthly,
|
|
StripePriceYearly: env.PriceYearly,
|
|
MaxWebhooks: 10,
|
|
WebhookAllTriggers: true,
|
|
SupporterBadge: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
h := testharness.New(t, testharness.WithBilling(billingCfg))
|
|
|
|
// Build and sign a minimal event Stripe might send.
|
|
payload := buildEventPayload("evt_test_endpoint", "checkout.session.completed", map[string]any{
|
|
"id": "cs_test_endpoint",
|
|
"object": "checkout.session",
|
|
"customer": "cus_fake",
|
|
"subscription": "sub_fake",
|
|
})
|
|
signed := webhook.GenerateTestSignedPayload(&webhook.UnsignedPayload{
|
|
Payload: payload,
|
|
Secret: env.WebhookSecret,
|
|
Timestamp: time.Now(),
|
|
})
|
|
|
|
url := h.UIBaseURL + "/api/stripe/webhook"
|
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, url, strings.NewReader(string(signed.Payload)))
|
|
if err != nil {
|
|
t.Fatalf("build request: %v", err)
|
|
}
|
|
req.Header.Set("Stripe-Signature", signed.Header)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("POST webhook: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("webhook endpoint returned %d, want 200", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
// TestWebhookEndpointRejectsBadSignature verifies the same route returns
|
|
// a 4xx (not 200) when the signature was generated with a different secret.
|
|
// Without this, a misconfigured webhook secret would silently no-op rather
|
|
// than failing loudly at the boundary.
|
|
func TestWebhookEndpointRejectsBadSignature(t *testing.T) {
|
|
env := requireStripeEnv(t)
|
|
|
|
billingCfg := billing.Config{
|
|
StripeSecretKey: env.SecretKey,
|
|
WebhookSecret: env.WebhookSecret,
|
|
Tiers: []billing.BillingTierConfig{
|
|
{Name: "free", MaxWebhooks: 1},
|
|
{
|
|
Name: env.TierName,
|
|
StripePriceMonthly: env.PriceMonthly,
|
|
MaxWebhooks: 10,
|
|
},
|
|
},
|
|
}
|
|
|
|
h := testharness.New(t, testharness.WithBilling(billingCfg))
|
|
|
|
payload := buildEventPayload("evt_test_bad_sig", "checkout.session.completed", map[string]any{})
|
|
signed := webhook.GenerateTestSignedPayload(&webhook.UnsignedPayload{
|
|
Payload: payload,
|
|
Secret: "whsec_wrong_secret_for_test",
|
|
Timestamp: time.Now(),
|
|
})
|
|
|
|
url := h.UIBaseURL + "/api/stripe/webhook"
|
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, url, strings.NewReader(string(signed.Payload)))
|
|
if err != nil {
|
|
t.Fatalf("build request: %v", err)
|
|
}
|
|
req.Header.Set("Stripe-Signature", signed.Header)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("POST webhook: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode == http.StatusOK {
|
|
t.Fatalf("webhook endpoint accepted bad signature (200); want 4xx")
|
|
}
|
|
}
|