200 lines
5.8 KiB
Go
200 lines
5.8 KiB
Go
package lakala
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"hfb_sys/backend/internal/timeutil"
|
|
)
|
|
|
|
func TestVerifyNotifyDetail(t *testing.T) {
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("generate key: %v", err)
|
|
}
|
|
privatePEM, certPEM := testKeyPairPEM(t, key)
|
|
client := NewClient(Config{
|
|
GatewayURL: "https://example.com",
|
|
AppID: "app-1",
|
|
SerialNo: "serial-1",
|
|
MerchantID: "merchant-1",
|
|
PrivateKey: privatePEM,
|
|
NotifyCert: certPEM,
|
|
})
|
|
|
|
body := `{"req_data":{"out_order_no":"NO1","total_amount":100,"trade_state":"SUCCESS"}}`
|
|
message := "1710000000\nabc123\n" + body + "\n"
|
|
signature, err := signRSA(key, []byte(message))
|
|
if err != nil {
|
|
t.Fatalf("signRSA: %v", err)
|
|
}
|
|
authorization := `LKLAPI-SHA256withRSA timestamp="1710000000",nonce_str="abc123",signature="` + signature + `"`
|
|
|
|
result := client.VerifyNotifyDetail(body, authorization)
|
|
if !result.OK {
|
|
t.Fatalf("VerifyNotifyDetail().OK = false, got=%s expected=%v base=%v", result.Got, result.Expected, result.BaseString)
|
|
}
|
|
|
|
tampered := client.VerifyNotifyDetail(strings.Replace(body, "100", "101", 1), authorization)
|
|
if tampered.OK {
|
|
t.Fatal("VerifyNotifyDetail() = true after body changed, want false")
|
|
}
|
|
}
|
|
|
|
func TestParsePayloadNormalizesAliases(t *testing.T) {
|
|
params, err := ParsePayload([]byte(`{
|
|
"req_data": {
|
|
"out_order_no": "ORDER1",
|
|
"pay_order_no": "PAY1",
|
|
"total_amount": 88,
|
|
"trade_state": "SUCCESS",
|
|
"trade_time": "20240521101010"
|
|
}
|
|
}`))
|
|
if err != nil {
|
|
t.Fatalf("ParsePayload error = %v", err)
|
|
}
|
|
if params["third_order_id"] != "ORDER1" {
|
|
t.Fatalf("third_order_id = %q, want ORDER1", params["third_order_id"])
|
|
}
|
|
if params["provider_order_id"] != "PAY1" {
|
|
t.Fatalf("provider_order_id = %q, want PAY1", params["provider_order_id"])
|
|
}
|
|
if params["amount"] != "88" {
|
|
t.Fatalf("amount = %q, want 88", params["amount"])
|
|
}
|
|
if params["status"] != "paid" {
|
|
t.Fatalf("status = %q, want paid", params["status"])
|
|
}
|
|
}
|
|
|
|
func TestNormalizeRefundStatusTreatsSuccessCodeAsRefunded(t *testing.T) {
|
|
status := normalizeRefundStatus(map[string]string{
|
|
"code": "BBS00000",
|
|
"msg": "成功",
|
|
})
|
|
if status != "refunded" {
|
|
t.Fatalf("normalizeRefundStatus() = %q, want refunded", status)
|
|
}
|
|
}
|
|
|
|
func TestCreatePaymentOmitsCounterParamWhenPayModeBlank(t *testing.T) {
|
|
body := captureCreatePaymentBody(t, "")
|
|
reqData := body["req_data"].(map[string]any)
|
|
if _, ok := reqData["counter_param"]; ok {
|
|
t.Fatalf("counter_param exists when pay_mode is blank: %#v", reqData["counter_param"])
|
|
}
|
|
}
|
|
|
|
func TestCreatePaymentSetsCounterParamWhenPayModeConfigured(t *testing.T) {
|
|
body := captureCreatePaymentBody(t, "WECHAT")
|
|
reqData := body["req_data"].(map[string]any)
|
|
if reqData["counter_param"] != `{"pay_mode":"WECHAT"}` {
|
|
t.Fatalf("counter_param = %#v, want WECHAT", reqData["counter_param"])
|
|
}
|
|
}
|
|
|
|
func TestCreatePaymentUsesShanghaiTimeWhenLocalIsUTC(t *testing.T) {
|
|
oldLocal := time.Local
|
|
time.Local = time.UTC
|
|
defer func() {
|
|
time.Local = oldLocal
|
|
}()
|
|
|
|
loc := timeutil.ShanghaiLocation()
|
|
before := time.Now().In(loc)
|
|
body := captureCreatePaymentBody(t, "")
|
|
after := time.Now().In(loc)
|
|
reqData := body["req_data"].(map[string]any)
|
|
|
|
reqTime := parseLakalaTestTime(t, body["req_time"].(string))
|
|
if reqTime.Before(before.Add(-2*time.Second)) || reqTime.After(after.Add(2*time.Second)) {
|
|
t.Fatalf("req_time = %s, want between %s and %s", reqTime, before, after)
|
|
}
|
|
|
|
efficientTime := parseLakalaTestTime(t, reqData["order_efficient_time"].(string))
|
|
wantMin := before.Add(15*time.Minute - 2*time.Second)
|
|
wantMax := after.Add(15*time.Minute + 2*time.Second)
|
|
if efficientTime.Before(wantMin) || efficientTime.After(wantMax) {
|
|
t.Fatalf("order_efficient_time = %s, want between %s and %s", efficientTime, wantMin, wantMax)
|
|
}
|
|
}
|
|
|
|
func parseLakalaTestTime(t *testing.T, value string) time.Time {
|
|
t.Helper()
|
|
parsed, err := time.ParseInLocation("20060102150405", value, timeutil.ShanghaiLocation())
|
|
if err != nil {
|
|
t.Fatalf("parse lakala time %q: %v", value, err)
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func testKeyPairPEM(t *testing.T, key *rsa.PrivateKey) (string, string) {
|
|
t.Helper()
|
|
privateDER := x509.MarshalPKCS1PrivateKey(key)
|
|
privatePEM := string(pem.EncodeToMemory(&pem.Block{
|
|
Type: "RSA PRIVATE KEY",
|
|
Bytes: privateDER,
|
|
}))
|
|
template := &x509.Certificate{}
|
|
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
|
if err != nil {
|
|
t.Fatalf("create certificate: %v", err)
|
|
}
|
|
certPEM := string(pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: certDER,
|
|
}))
|
|
return privatePEM, certPEM
|
|
}
|
|
|
|
func captureCreatePaymentBody(t *testing.T, payMode string) map[string]any {
|
|
t.Helper()
|
|
var captured map[string]any
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != endpointCounterCreate {
|
|
t.Fatalf("path = %s, want %s", r.URL.Path, endpointCounterCreate)
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&captured); err != nil {
|
|
t.Fatalf("decode request: %v", err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"resp_code":"0","result_code":"0","pay_order_no":"LK1"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("generate key: %v", err)
|
|
}
|
|
privatePEM, certPEM := testKeyPairPEM(t, key)
|
|
client := NewClient(Config{
|
|
GatewayURL: server.URL,
|
|
AppID: "app-1",
|
|
SerialNo: "serial-1",
|
|
MerchantID: "merchant-1",
|
|
PrivateKey: privatePEM,
|
|
NotifyCert: certPEM,
|
|
PayMode: payMode,
|
|
})
|
|
_, err = client.CreatePayment(context.Background(), CreatePaymentRequest{
|
|
ThirdOrderID: "ORDER1",
|
|
AmountCent: 100,
|
|
NotifyURL: "https://example.com/notify",
|
|
Body: "测试订单",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreatePayment error = %v", err)
|
|
}
|
|
return captured
|
|
}
|