draft
This commit is contained in:
326
backend/scripts/e2e_merchant/main.go
Normal file
326
backend/scripts/e2e_merchant/main.go
Normal file
@@ -0,0 +1,326 @@
|
||||
// e2e_merchant 商户进件端到端测试脚本
|
||||
//
|
||||
// 测试链路:本脚本 → pay-bridge 本地服务 → Heepay 沙盒
|
||||
//
|
||||
// 前置条件:
|
||||
// 1. 启动 pay-bridge 服务:go run ./cmd/server
|
||||
// 2. 数据库中已有测试 app(appID + appSecret),或通过 Admin API 创建
|
||||
// 3. 设置环境变量(见下方)
|
||||
//
|
||||
// 环境变量:
|
||||
//
|
||||
// PAY_BRIDGE_URL=http://localhost:8080 服务地址(默认 http://localhost:8080)
|
||||
// APP_ID=your_app_id 测试用 appID
|
||||
// APP_SECRET=your_app_secret 对应的明文 appSecret
|
||||
//
|
||||
// 运行:
|
||||
//
|
||||
// APP_ID=app_test APP_SECRET=secret123 go run ./scripts/e2e_merchant/
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ---- 配置 ----
|
||||
|
||||
func baseURL() string {
|
||||
if v := os.Getenv("PAY_BRIDGE_URL"); v != "" {
|
||||
return strings.TrimRight(v, "/")
|
||||
}
|
||||
return "http://localhost:8080"
|
||||
}
|
||||
|
||||
func mustEnv(key string) string {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
fatalf("环境变量 %s 未设置", key)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ---- HTTP 客户端(带 HMAC 签名) ----
|
||||
|
||||
type client struct {
|
||||
appID string
|
||||
appSecret string
|
||||
base string
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
func newClient() *client {
|
||||
return &client{
|
||||
appID: mustEnv("APP_ID"),
|
||||
appSecret: mustEnv("APP_SECRET"),
|
||||
base: baseURL(),
|
||||
http: &http.Client{Timeout: 30 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) do(method, path string, body any) (map[string]any, error) {
|
||||
var bodyBytes []byte
|
||||
if body != nil {
|
||||
var err error
|
||||
bodyBytes, err = json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
ts := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
sign := hmacSign(c.appID+ts+string(bodyBytes), c.appSecret)
|
||||
|
||||
req, err := http.NewRequest(method, c.base+path, bytes.NewReader(bodyBytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-App-Id", c.appID)
|
||||
req.Header.Set("X-Timestamp", ts)
|
||||
req.Header.Set("X-Sign", sign)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBytes, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(respBytes, &result); err != nil {
|
||||
return nil, fmt.Errorf("parse response [%d]: %s", resp.StatusCode, string(respBytes))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *client) post(path string, body any) (map[string]any, error) {
|
||||
return c.do("POST", path, body)
|
||||
}
|
||||
|
||||
func (c *client) get(path string) (map[string]any, error) {
|
||||
return c.do("GET", path, nil)
|
||||
}
|
||||
|
||||
func hmacSign(payload, secret string) string {
|
||||
h := hmac.New(sha256.New, []byte(secret))
|
||||
h.Write([]byte(payload))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// ---- 辅助函数 ----
|
||||
|
||||
func printStep(n int, name string) {
|
||||
fmt.Printf("\n[步骤 %d] %s\n%s\n", n, name, strings.Repeat("-", 40))
|
||||
}
|
||||
|
||||
func printResult(resp map[string]any) {
|
||||
b, _ := json.MarshalIndent(resp, " ", " ")
|
||||
fmt.Printf(" 响应: %s\n", string(b))
|
||||
}
|
||||
|
||||
func checkOK(resp map[string]any, step string) {
|
||||
code, _ := resp["code"].(string)
|
||||
if code != "0" {
|
||||
fatalf("[%s] 接口返回错误: code=%s message=%v", step, code, resp["message"])
|
||||
}
|
||||
}
|
||||
|
||||
func dataStr(resp map[string]any, key string) string {
|
||||
data, _ := resp["data"].(map[string]any)
|
||||
if data == nil {
|
||||
return ""
|
||||
}
|
||||
v, _ := data[key].(string)
|
||||
return v
|
||||
}
|
||||
|
||||
func fatalf(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, "❌ FAIL: "+format+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// ---- 测试流程 ----
|
||||
|
||||
func main() {
|
||||
c := newClient()
|
||||
merchantID := fmt.Sprintf("E2E_M_%d", time.Now().UnixNano()%1e9)
|
||||
|
||||
fmt.Printf("=== 商户进件 E2E 测试 ===\n")
|
||||
fmt.Printf("服务地址: %s\n", c.base)
|
||||
fmt.Printf("App ID: %s\n", c.appID)
|
||||
fmt.Printf("商户ID: %s\n", merchantID)
|
||||
|
||||
// ---- 步骤 1:创建商户 ----
|
||||
printStep(1, "创建商户")
|
||||
resp, err := c.post("/api/v1/merchant", map[string]any{
|
||||
"merchant_id": merchantID,
|
||||
"merchant_name": "E2E测试企业" + merchantID[len(merchantID)-6:],
|
||||
"license_no": "91110000E2ETEST01X",
|
||||
"legal_person": "测试法人",
|
||||
})
|
||||
if err != nil {
|
||||
fatalf("创建商户请求失败: %v", err)
|
||||
}
|
||||
printResult(resp)
|
||||
checkOK(resp, "创建商户")
|
||||
fmt.Printf(" ✓ 商户创建成功: merchant_id=%s\n", dataStr(resp, "merchant_id"))
|
||||
|
||||
// ---- 步骤 2:查询商户详情 ----
|
||||
printStep(2, "查询商户详情")
|
||||
resp, err = c.get("/api/v1/merchant/" + merchantID)
|
||||
if err != nil {
|
||||
fatalf("查询商户失败: %v", err)
|
||||
}
|
||||
printResult(resp)
|
||||
checkOK(resp, "查询商户")
|
||||
fmt.Printf(" ✓ 商户查询成功\n")
|
||||
|
||||
// ---- 步骤 3:上传营业执照(调用 Heepay 沙盒) ----
|
||||
printStep(3, "上传营业执照到 Heepay 沙盒")
|
||||
fmt.Printf(" → 调用 POST /api/v1/merchant/upload-file\n")
|
||||
fmt.Printf(" → 内部转调 Heepay customer.file.upload\n")
|
||||
|
||||
// 最小合法 JPEG(22字节)
|
||||
minJPEG := []byte{
|
||||
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01,
|
||||
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xD9,
|
||||
}
|
||||
|
||||
// 用 multipart 上传
|
||||
fileID, uploadErr := uploadFile(c, minJPEG, "license.jpg", "image/jpeg")
|
||||
if uploadErr != nil {
|
||||
fmt.Printf(" ⚠ 文件上传失败(Heepay 沙盒可能返回业务错误): %v\n", uploadErr)
|
||||
fmt.Printf(" → 继续后续步骤,submit_data 不含 file_id\n")
|
||||
fileID = ""
|
||||
} else {
|
||||
fmt.Printf(" ✓ 文件上传成功: file_id=%s\n", fileID)
|
||||
}
|
||||
|
||||
// ---- 步骤 4:提交进件申请(调用 Heepay 沙盒) ----
|
||||
printStep(4, "提交进件申请到 Heepay 沙盒")
|
||||
fmt.Printf(" → 调用 POST /api/v1/merchant/%s/apply\n", merchantID)
|
||||
fmt.Printf(" → 内部转调 Heepay customer.enter.enterprise.apply\n")
|
||||
|
||||
submitData := map[string]any{
|
||||
"merch_name": "E2E测试企业",
|
||||
"merch_short_name": "E2E测试",
|
||||
"merch_type": "ENTERPRISE",
|
||||
"contact_name": "测试联系人",
|
||||
"contact_phone": "13800138000",
|
||||
"contact_email": "e2e@example.com",
|
||||
"province": "北京市",
|
||||
"city": "北京市",
|
||||
"district": "朝阳区",
|
||||
"address": "朝阳区测试路1号",
|
||||
}
|
||||
if fileID != "" {
|
||||
submitData["license_img"] = fileID
|
||||
}
|
||||
|
||||
resp, err = c.post("/api/v1/merchant/"+merchantID+"/apply", map[string]any{
|
||||
"channel_code": "HEEPAY",
|
||||
"submit_data": submitData,
|
||||
})
|
||||
if err != nil {
|
||||
fatalf("提交进件失败: %v", err)
|
||||
}
|
||||
printResult(resp)
|
||||
|
||||
applicationID := dataStr(resp, "application_id")
|
||||
if resp["code"] != "0" {
|
||||
fmt.Printf(" ⚠ 进件接口返回错误(Heepay 沙盒字段验证): code=%v message=%v\n",
|
||||
resp["code"], resp["message"])
|
||||
fmt.Printf(" → 进件字段名需对照 Heepay 官方文档确认\n")
|
||||
} else {
|
||||
fmt.Printf(" ✓ 进件申请提交成功: application_id=%s\n", applicationID)
|
||||
}
|
||||
|
||||
// ---- 步骤 5:查询进件审核状态 ----
|
||||
printStep(5, "查询进件审核状态")
|
||||
resp, err = c.get("/api/v1/merchant/" + merchantID + "/audit")
|
||||
if err != nil {
|
||||
fatalf("查询审核状态失败: %v", err)
|
||||
}
|
||||
printResult(resp)
|
||||
if resp["code"] == "0" {
|
||||
fmt.Printf(" ✓ 审核状态查询成功\n")
|
||||
} else {
|
||||
fmt.Printf(" ⚠ 审核状态查询异常(可能进件未提交成功)\n")
|
||||
}
|
||||
|
||||
// ---- 步骤 6:验证商户列表隔离 ----
|
||||
printStep(6, "验证 appID 隔离:列表只返回本 app 的商户")
|
||||
resp, err = c.get("/api/v1/merchant?limit=10&offset=0")
|
||||
if err != nil {
|
||||
fatalf("查询列表失败: %v", err)
|
||||
}
|
||||
checkOK(resp, "商户列表")
|
||||
data, _ := resp["data"].(map[string]any)
|
||||
list, _ := data["list"].([]any)
|
||||
fmt.Printf(" ✓ 返回 %d 个商户(均属于 app_id=%s)\n", len(list), c.appID)
|
||||
|
||||
fmt.Printf("\n=== 测试完成 ===\n")
|
||||
fmt.Printf("步骤 1-2(本地 DB): ✓ 正常\n")
|
||||
fmt.Printf("步骤 3-4(Heepay 沙盒): 需对照官方文档确认字段名\n")
|
||||
fmt.Printf("步骤 6(数据隔离): ✓ 正常\n")
|
||||
}
|
||||
|
||||
// uploadFile 发送 multipart 文件上传请求
|
||||
func uploadFile(c *client, content []byte, filename, mediaType string) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
boundary := "----PayBridgeE2EBoundary"
|
||||
buf.WriteString("--" + boundary + "\r\n")
|
||||
buf.WriteString(fmt.Sprintf(`Content-Disposition: form-data; name="file"; filename="%s"`, filename) + "\r\n")
|
||||
buf.WriteString("Content-Type: " + mediaType + "\r\n\r\n")
|
||||
buf.Write(content)
|
||||
buf.WriteString("\r\n--" + boundary + "\r\n")
|
||||
buf.WriteString(`Content-Disposition: form-data; name="channel_code"` + "\r\n\r\n")
|
||||
buf.WriteString("HEEPAY")
|
||||
buf.WriteString("\r\n--" + boundary + "\r\n")
|
||||
buf.WriteString(`Content-Disposition: form-data; name="file_media_type"` + "\r\n\r\n")
|
||||
buf.WriteString(mediaType)
|
||||
buf.WriteString("\r\n--" + boundary + "--\r\n")
|
||||
|
||||
ts := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
// multipart 请求 body 不参与签名,签名 body 部分为空
|
||||
sign := hmacSign(c.appID+ts+"", c.appSecret)
|
||||
|
||||
req, err := http.NewRequest("POST", c.base+"/api/v1/merchant/upload-file", &buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
|
||||
req.Header.Set("X-App-Id", c.appID)
|
||||
req.Header.Set("X-Timestamp", ts)
|
||||
req.Header.Set("X-Sign", sign)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBytes, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(respBytes, &result); err != nil {
|
||||
return "", fmt.Errorf("parse upload response: %s", string(respBytes))
|
||||
}
|
||||
if result["code"] != "0" {
|
||||
return "", fmt.Errorf("code=%v message=%v", result["code"], result["message"])
|
||||
}
|
||||
data, _ := result["data"].(map[string]any)
|
||||
fileID, _ := data["file_id"].(string)
|
||||
return fileID, nil
|
||||
}
|
||||
Reference in New Issue
Block a user