package service import ( "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "time" ) func TestIsQueueDeadError(t *testing.T) { tests := []struct { err error expected bool }{ {nil, false}, {errors.New("订单不可操作"), true}, {errors.New("补充发货账号失败: 订单不可操作"), true}, {errors.New("订单不存在"), true}, {errors.New("队列已失效"), true}, {errors.New("该订单正在处理中,请勿重复提交"), true}, {errors.New("创建发货队列失败: 该订单正在处理中,请勿重复提交"), true}, {errors.New("创建发货队列失败: 该订单已有发货记录,请续发未完成项或重试失败项"), true}, {errors.New("创建上游正式订单失败: 该子订单已处理,请勿重复提交"), true}, {errors.New("账号尚未绑定"), false}, {errors.New("请求过于频繁"), false}, } for _, tc := range tests { got := isQueueDeadError(tc.err) if got != tc.expected { t.Errorf("isQueueDeadError(%v) = %v, expected %v", tc.err, got, tc.expected) } } } func TestIsRateLimitError(t *testing.T) { tests := []struct { err error expected bool }{ {nil, false}, {errors.New("请求过于频繁,请稍后再试"), true}, {errors.New("系统繁忙,请稍后再试"), true}, {errors.New("Rate limit exceeded"), true}, {errors.New("Too Many Requests"), true}, {errors.New("账号尚未绑定"), false}, {errors.New("订单不存在"), false}, } for _, tc := range tests { got := isRateLimitError(tc.err) if got != tc.expected { t.Errorf("isRateLimitError(%v) = %v, expected %v", tc.err, got, tc.expected) } } } func TestIsTransientDeliveryError(t *testing.T) { tests := []struct { err error statusCode int message string expected bool }{ {errors.New("dial tcp timeout"), 0, "", true}, {nil, 429, "", false}, {nil, 502, "", true}, {nil, 503, "", true}, {nil, 504, "", true}, {nil, 400, "绑定校验失败: 请求过于频繁,请稍后再试", false}, {nil, 400, "请求过于频繁", false}, {nil, 400, "系统繁忙,请稍后再试", true}, {nil, 400, "网络繁忙", true}, {nil, 400, "账号尚未绑定,请扫码完成绑定后再提交", false}, {nil, 400, "订单不可操作", false}, } for _, tc := range tests { got := isTransientDeliveryError(tc.err, tc.statusCode, tc.message) if got != tc.expected { t.Errorf("isTransientDeliveryError(%v, %d, %q) = %v, expected %v", tc.err, tc.statusCode, tc.message, got, tc.expected) } } } func TestSignProxyRetriesOnTransientErrorAndSucceeds(t *testing.T) { var callCount int32 var capturedForwardedFor string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { capturedForwardedFor = r.Header.Get("X-Forwarded-For") count := atomic.AddInt32(&callCount, 1) w.Header().Set("Content-Type", "application/json") if count == 1 { // 第一次调用返回系统繁忙瞬时错误 w.WriteHeader(http.StatusBadRequest) _ = json.NewEncoder(w).Encode(map[string]interface{}{ "code": 1, "message": "系统繁忙,请稍后再试", }) return } // 第二次重试成功 w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(map[string]interface{}{ "code": 0, "message": "success", "data": map[string]interface{}{ "good": map[string]interface{}{ "good_id": "test-good", }, }, }) })) defer server.Close() client := &http.Client{Timeout: 5 * time.Second} p := NewDLCProvider(server.URL, "dlc", client) var out struct { Good map[string]interface{} `json:"good"` } err := p.signProxy("/public/goods/detail", "POST", map[string]interface{}{ "good_id": "test-good", }, &out) if err != nil { t.Fatalf("signProxy 预期在退避重试后成功,但返回错误: %v", err) } finalCount := atomic.LoadInt32(&callCount) if finalCount != 2 { t.Fatalf("预期总共调用 2 次(1次失败 + 1次重试成功),实际调用: %d", finalCount) } if out.Good == nil || out.Good["good_id"] != "test-good" { t.Fatalf("预期返回商品信息,实际: %v", out.Good) } if capturedForwardedFor == "" { t.Fatalf("预期包含 X-Forwarded-For 报头,实际为空") } } func TestSignProxyDoesNotRetryOnRateLimit(t *testing.T) { var callCount int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&callCount, 1) w.WriteHeader(http.StatusTooManyRequests) _ = json.NewEncoder(w).Encode(map[string]interface{}{ "code": 1, "message": "请求过于频繁,请稍后再试", }) })) defer server.Close() client := &http.Client{Timeout: 5 * time.Second} p := NewDLCProvider(server.URL, "dlc", client) var out struct { Good map[string]interface{} `json:"good"` } err := p.signProxy("/public/goods/detail", "POST", map[string]interface{}{ "good_id": "test-good", }, &out) if err == nil { t.Fatalf("预期返回错误,但返回成功") } finalCount := atomic.LoadInt32(&callCount) if finalCount != 1 { t.Fatalf("频控限流接口预期只调用 1 次且不触发盲目重试,实际调用: %d", finalCount) } } func TestDLCCreateOrderQueueKeepsOriginalOrderSN(t *testing.T) { const orderNo = "FO20261006180010b19fbe66ec3" var requestedSN string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var body struct { Path string `json:"path"` Data map[string]interface{} `json:"data"` } _ = json.NewDecoder(r.Body).Decode(&body) requestedSN, _ = body.Data["order_sn"].(string) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) _ = json.NewEncoder(w).Encode(map[string]interface{}{ "code": 1, "message": "该订单已有发货记录,请续发未完成项或重试失败项", }) })) defer server.Close() p := NewDLCProvider(server.URL, "dlc", &http.Client{Timeout: 5 * time.Second}) _, sn, err := p.createOrderQueue("good-1", orderNo, "suit_romantic_destiny") if err == nil { t.Fatal("已有发货记录时应返回代练仓原文,不能换成新单号重试") } if !strings.Contains(err.Error(), "请续发未完成项或重试失败项") { t.Fatalf("应保留代练仓原文,实际: %v", err) } if requestedSN != orderNo || sn != orderNo || strings.Contains(requestedSN, "_") { t.Fatalf("请求单号被改写: requested=%s returned=%s", requestedSN, sn) } } func TestSignProxyDoesNotRetryOnFormalOrderCreation(t *testing.T) { var callCount int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&callCount, 1) w.WriteHeader(http.StatusBadRequest) _ = json.NewEncoder(w).Encode(map[string]interface{}{ "code": 1, "message": "系统繁忙,请稍后再试", }) })) defer server.Close() client := &http.Client{Timeout: 5 * time.Second} p := NewDLCProvider(server.URL, "dlc", client) var out struct { Order map[string]interface{} `json:"order"` } err := p.signProxy("/public/users/orders", "POST", map[string]interface{}{ "order_sn": "FO123456", }, &out) if err == nil { t.Fatalf("预期返回错误,但返回成功") } finalCount := atomic.LoadInt32(&callCount) if finalCount != 1 { t.Fatalf("正式下单接口预期只调用 1 次且不触发重试,实际调用: %d", finalCount) } }