package stripe_test import ( "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" gostripe "github.com/stripe/stripe-go/v79" "github.com/stripe/stripe-go/v79/client" "github.com/wangjia/pay/internal/provider" st "github.com/wangjia/pay/internal/provider/stripe" ) const whSecret = "whsec_test_secret" func fakeStripeAPI(t *testing.T) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == http.MethodPost && strings.HasPrefix(r.URL.Path, "/v1/checkout/sessions"): // 创建 session fmt.Fprint(w, `{"id":"cs_test_123","object":"checkout.session","url":"https://checkout.stripe.com/c/pay/cs_test_123","amount_total":2999,"currency":"usd","payment_status":"unpaid"}`) case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/v1/checkout/sessions/cs_test_123"): // 查询 session — 已付 fmt.Fprint(w, `{"id":"cs_test_123","object":"checkout.session","amount_total":2999,"currency":"usd","payment_status":"paid"}`) default: http.Error(w, `{"error":{"message":"not found"}}`, http.StatusNotFound) } })) } func newStripe(t *testing.T, ts *httptest.Server) *st.Provider { backends := &gostripe.Backends{ API: gostripe.GetBackendWithConfig(gostripe.APIBackend, &gostripe.BackendConfig{ URL: gostripe.String(ts.URL), }), } sc := client.New("sk_test_x", backends) return st.New(sc, whSecret) } func TestCreateCheckoutRedirect(t *testing.T) { ts := fakeStripeAPI(t) defer ts.Close() p := newStripe(t, ts) sess, err := p.Create(context.Background(), provider.CreateRequest{ OutTradeNo: "PAY-1", Subject: "Pro Year", AmountMinor: 2999, Currency: "USD", ReturnURL: "https://x/return", }) if err != nil { t.Fatalf("create: %v", err) } if sess.RenderType != provider.RenderRedirect || sess.ProviderRef != "cs_test_123" { t.Fatalf("session = %+v", sess) } if !strings.Contains(sess.Payload["url"].(string), "cs_test_123") { t.Fatalf("url = %v", sess.Payload["url"]) } } func TestQueryPaid(t *testing.T) { ts := fakeStripeAPI(t) defer ts.Close() p := newStripe(t, ts) ev, err := p.Query(context.Background(), provider.QueryRequest{ProviderRef: "cs_test_123", Currency: "USD"}) if err != nil { t.Fatalf("query: %v", err) } if ev.Status != provider.PaidSucceeded || ev.PaidAmountMinor != 2999 || ev.PaidCurrency != "USD" { t.Fatalf("event = %+v", ev) } } func TestVerifyWebhook(t *testing.T) { ts := fakeStripeAPI(t) defer ts.Close() p := newStripe(t, ts) payload := `{"id":"evt_1","object":"event","type":"checkout.session.completed","data":{"object":{"id":"cs_test_123","object":"checkout.session","amount_total":2999,"currency":"usd","payment_status":"paid"}}}` sig := signStripe(payload, whSecret, time.Now().Unix()) ev, err := p.VerifyCallback(context.Background(), provider.CallbackInput{ Raw: []byte(payload), Headers: map[string]string{"Stripe-Signature": sig}, }) if err != nil { t.Fatalf("verify: %v", err) } if ev.ProviderRef != "cs_test_123" || ev.Status != provider.PaidSucceeded || ev.PaidAmountMinor != 2999 { t.Fatalf("event = %+v", ev) } } // 用错误的签名密钥(冒充攻击者伪造 webhook)→ ConstructEventWithOptions 内部 HMAC 校验 // 必失败,VerifyCallback 必须返回 error,绝不能返回 PaidEvent(哪怕 payload 里状态是 paid)。 func TestVerifyWebhookWrongSecretFails(t *testing.T) { ts := fakeStripeAPI(t) defer ts.Close() p := newStripe(t, ts) payload := `{"id":"evt_evil","object":"event","type":"checkout.session.completed","data":{"object":{"id":"cs_test_123","object":"checkout.session","amount_total":2999,"currency":"usd","payment_status":"paid"}}}` sig := signStripe(payload, "whsec_completely_different_secret", time.Now().Unix()) ev, err := p.VerifyCallback(context.Background(), provider.CallbackInput{ Raw: []byte(payload), Headers: map[string]string{"Stripe-Signature": sig}, }) if err == nil { t.Fatalf("want 验签失败(签名密钥不匹配), got event = %+v", ev) } if ev != nil { t.Fatalf("验签失败时不应返回 PaidEvent, got %+v", ev) } } // signStripe 复刻 Stripe webhook 签名头: t=,v1=hex(HMAC-SHA256(secret, ".")) func signStripe(payload, secret string, ts int64) string { mac := hmac.New(sha256.New, []byte(secret)) fmt.Fprintf(mac, "%d.%s", ts, payload) return fmt.Sprintf("t=%d,v1=%s", ts, hex.EncodeToString(mac.Sum(nil))) }