From 20ec7b4942a0add919ce65c3865f773cbe34cf68 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Fri, 10 Jul 2026 10:57:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(v2):=20fake=20provider(=E7=A1=AE=E5=AE=9A?= =?UTF-8?q?=E6=80=A7,=E9=AA=8C=E8=AF=81=E6=94=B6=E6=AC=BE=E7=AE=A1?= =?UTF-8?q?=E7=BA=BF;=E7=9C=9F=E6=B8=A0=E9=81=93=20P3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/provider/fake/fake.go | 86 +++++++++++++++++++++++++++++ internal/provider/fake/fake_test.go | 52 +++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 internal/provider/fake/fake.go create mode 100644 internal/provider/fake/fake_test.go diff --git a/internal/provider/fake/fake.go b/internal/provider/fake/fake.go new file mode 100644 index 0000000..5597468 --- /dev/null +++ b/internal/provider/fake/fake.go @@ -0,0 +1,86 @@ +// Package fake is an in-process Provider used to exercise the pay v2 pipeline +// end-to-end without any real channel. Real adapters (crypto/alipay/stripe) land +// in P3. Deterministic: provider_ref = "FAKE-"+OutTradeNo; render_type = crypto_address. +package fake + +import ( + "context" + "encoding/json" + "errors" + "sync" + "time" + + "github.com/wangjia/pay/internal/provider" +) + +type Provider struct { + mu sync.Mutex + queryResults map[string]provider.PaidEvent +} + +func New() *Provider { return &Provider{queryResults: map[string]provider.PaidEvent{}} } + +func (p *Provider) Method() string { return "fake" } + +func (p *Provider) Capabilities() provider.Capabilities { + return provider.Capabilities{ + RenderTypes: []provider.RenderType{provider.RenderCryptoAddress}, + SupportsRefund: false, + SettleCurrencies: []string{"USDT"}, + Regions: []string{"global"}, + } +} + +func (p *Provider) Create(_ context.Context, req provider.CreateRequest) (*provider.Session, error) { + exp := time.Now().Add(15 * time.Minute) + return &provider.Session{ + ProviderRef: "FAKE-" + req.OutTradeNo, + RenderType: provider.RenderCryptoAddress, + Payload: map[string]any{ + "address": "TFake" + req.Account.AccountID + req.OutTradeNo, + "amount_minor": req.AmountMinor, + "currency": req.Currency, + }, + ExpiresAt: &exp, + }, nil +} + +// VerifyCallback 解析测试注入的 JSON 回调体成归一化 PaidEvent。 +func (p *Provider) VerifyCallback(_ context.Context, in provider.CallbackInput) (*provider.PaidEvent, error) { + var b struct { + ProviderRef string `json:"provider_ref"` + Status string `json:"status"` + AmountMinor int64 `json:"amount_minor"` + Currency string `json:"currency"` + } + if err := json.Unmarshal(in.Raw, &b); err != nil { + return nil, err + } + if b.ProviderRef == "" { + return nil, errors.New("fake: missing provider_ref") + } + return &provider.PaidEvent{ + ProviderRef: b.ProviderRef, + Status: provider.PaidStatus(b.Status), + PaidAmountMinor: b.AmountMinor, + PaidCurrency: b.Currency, + Raw: string(in.Raw), + }, nil +} + +func (p *Provider) Query(_ context.Context, req provider.QueryRequest) (*provider.PaidEvent, error) { + p.mu.Lock() + defer p.mu.Unlock() + if ev, ok := p.queryResults[req.ProviderRef]; ok { + e := ev + return &e, nil + } + return &provider.PaidEvent{ProviderRef: req.ProviderRef, Status: provider.PaidPending}, nil +} + +// SetQueryResult primes Query to report a specific event (test seam). +func (p *Provider) SetQueryResult(providerRef string, ev provider.PaidEvent) { + p.mu.Lock() + defer p.mu.Unlock() + p.queryResults[providerRef] = ev +} diff --git a/internal/provider/fake/fake_test.go b/internal/provider/fake/fake_test.go new file mode 100644 index 0000000..7dbc4af --- /dev/null +++ b/internal/provider/fake/fake_test.go @@ -0,0 +1,52 @@ +package fake_test + +import ( + "context" + "testing" + + "github.com/wangjia/pay/internal/provider" + "github.com/wangjia/pay/internal/provider/fake" +) + +func TestFakeCreateAndVerify(t *testing.T) { + var _ provider.Provider = fake.New() // 编译期断言满足接口 + + p := fake.New() + ctx := context.Background() + + sess, err := p.Create(ctx, provider.CreateRequest{OutTradeNo: "PAY-1", AmountMinor: 29990000, Currency: "USDT"}) + if err != nil { + t.Fatalf("create: %v", err) + } + if sess.ProviderRef != "FAKE-PAY-1" || sess.RenderType != provider.RenderCryptoAddress { + t.Fatalf("session = %+v", sess) + } + + ev, err := p.VerifyCallback(ctx, provider.CallbackInput{ + Raw: []byte(`{"provider_ref":"FAKE-PAY-1","status":"succeeded","amount_minor":29990000,"currency":"USDT"}`), + }) + if err != nil { + t.Fatalf("verify: %v", err) + } + if ev.ProviderRef != "FAKE-PAY-1" || ev.Status != provider.PaidSucceeded || ev.PaidAmountMinor != 29990000 { + t.Fatalf("event = %+v", ev) + } +} + +func TestFakeQueryPreset(t *testing.T) { + p := fake.New() + ctx := context.Background() + + ev, _ := p.Query(ctx, provider.QueryRequest{ProviderRef: "FAKE-PAY-2"}) + if ev.Status != provider.PaidPending { + t.Fatalf("默认应 pending, got %v", ev.Status) + } + + p.SetQueryResult("FAKE-PAY-2", provider.PaidEvent{ + ProviderRef: "FAKE-PAY-2", Status: provider.PaidSucceeded, PaidAmountMinor: 100, PaidCurrency: "USDT", + }) + ev2, _ := p.Query(ctx, provider.QueryRequest{ProviderRef: "FAKE-PAY-2"}) + if ev2.Status != provider.PaidSucceeded || ev2.PaidAmountMinor != 100 { + t.Fatalf("预置后应 succeeded, got %+v", ev2) + } +}