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 }