package erpconnector import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "time" ) func TestQueryOrderAcceptsAllowlistResponse(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func( writer http.ResponseWriter, request *http.Request, ) { if request.Header.Get("X-API-Key") != "12345678901234567890123456789012" { t.Fatal("service key was not forwarded") } writer.Header().Set("Content-Type", "application/json") _, _ = writer.Write([]byte(`{ "schema_version":1, "query":{"mode":"ORDER_NUMBER"}, "orders":[{ "external_stock_id":"12", "source_code":"SOURCE-12", "platform_order_no":null, "shop_name":"测试店铺", "source_created_at":"2026-07-28 08:00:00", "order_status":"0", "purchase_status":"1", "is_canceled":false, "items":[{ "external_item_id":"88", "title":"商品", "product_spec":"黑色,L", "sku":"BLACK-L", "quantity":2, "product_thumb_ref":"190", "purchase_status":"0" }] }] }`)) })) defer server.Close() client, err := New( server.URL, "12345678901234567890123456789012", time.Second, ) if err != nil { t.Fatalf("New() error = %v", err) } result, err := client.QueryOrder(context.Background(), "SOURCE-12") if err != nil { t.Fatalf("QueryOrder() error = %v", err) } if len(result.Orders) != 1 || len(result.Orders[0].Items) != 1 || result.Orders[0].Items[0].SKU != "BLACK-L" { t.Fatalf("result = %+v", result) } } func TestQueryCreatedRangeUsesStrictContract(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func( writer http.ResponseWriter, request *http.Request, ) { var body map[string]string if err := json.NewDecoder(request.Body).Decode(&body); err != nil { t.Fatalf("decode request: %v", err) } if body["mode"] != "CREATED_RANGE" || body["created_from"] != "2026-07-22" || body["created_to"] != "2026-07-28" || len(body) != 3 { t.Fatalf("request body = %#v", body) } writer.Header().Set("Content-Type", "application/json") _, _ = writer.Write([]byte(`{ "schema_version":1, "query":{ "mode":"CREATED_RANGE", "created_from":"2026-07-22", "created_to":"2026-07-28" }, "orders":[] }`)) })) defer server.Close() client, _ := New( server.URL, "12345678901234567890123456789012", time.Second, ) result, err := client.QueryCreatedRange( context.Background(), "2026-07-22", "2026-07-28", ) if err != nil || result.Query.CreatedFrom == nil || *result.Query.CreatedFrom != "2026-07-22" { t.Fatalf("QueryCreatedRange() = %+v, %v", result, err) } } func TestQueryCreatedRangeRejectsMismatchedResponseWindow(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func( writer http.ResponseWriter, _ *http.Request, ) { writer.Header().Set("Content-Type", "application/json") _, _ = writer.Write([]byte(`{ "schema_version":1, "query":{ "mode":"CREATED_RANGE", "created_from":"2026-07-21", "created_to":"2026-07-28" }, "orders":[] }`)) })) defer server.Close() client, _ := New( server.URL, "12345678901234567890123456789012", time.Second, ) _, err := client.QueryCreatedRange( context.Background(), "2026-07-22", "2026-07-28", ) if !errors.Is(err, ErrProtocol) { t.Fatalf("QueryCreatedRange() error = %v", err) } } func TestQueryOrderRejectsUnexpectedPIIField(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func( writer http.ResponseWriter, _ *http.Request, ) { writer.Header().Set("Content-Type", "application/json") _, _ = writer.Write([]byte(`{ "schema_version":1, "query":{"mode":"ORDER_NUMBER"}, "orders":[], "receiverTel":"private" }`)) })) defer server.Close() client, _ := New(server.URL, "12345678901234567890123456789012", time.Second) _, err := client.QueryOrder(context.Background(), "SOURCE-12") if !errors.Is(err, ErrProtocol) { t.Fatalf("QueryOrder() error = %v, want protocol error", err) } } func TestQueryOrderMapsStableErrorsWithoutReadingDetails(t *testing.T) { tests := []struct { status int want error }{ {http.StatusUnauthorized, ErrSessionRequired}, {http.StatusNotFound, ErrNotFound}, {http.StatusBadGateway, ErrUnavailable}, } for _, test := range tests { t.Run(http.StatusText(test.status), func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func( writer http.ResponseWriter, _ *http.Request, ) { writer.WriteHeader(test.status) _, _ = writer.Write([]byte(`{"detail":"private upstream text"}`)) })) defer server.Close() client, _ := New( server.URL, "12345678901234567890123456789012", time.Second, ) _, err := client.QueryOrder(context.Background(), "SOURCE-12") if !errors.Is(err, test.want) { t.Fatalf("QueryOrder() error = %v, want %v", err, test.want) } }) } } func TestQueryOrderDoesNotFollowRedirects(t *testing.T) { followed := false target := httptest.NewServer(http.HandlerFunc(func( http.ResponseWriter, *http.Request, ) { followed = true })) defer target.Close() redirect := httptest.NewServer(http.HandlerFunc(func( writer http.ResponseWriter, _ *http.Request, ) { writer.Header().Set("Location", target.URL) writer.WriteHeader(http.StatusTemporaryRedirect) })) defer redirect.Close() client, _ := New( redirect.URL, "12345678901234567890123456789012", time.Second, ) _, err := client.QueryOrder(context.Background(), "SOURCE-12") if !errors.Is(err, ErrUnavailable) || followed { t.Fatalf("redirect error/followed = %v / %t", err, followed) } }