feat(v2): fake provider(确定性,验证收款管线;真渠道 P3)
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user