Files
cmroubao/backend-api/internal/platform/shunyunbao/source_test.go
T

270 lines
9.4 KiB
Go

package shunyunbao
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"cmroubao/backend-api/internal/domain"
)
func TestSessionManagerQueryOrderUsesVerifiedSessionAndAllowlist(t *testing.T) {
calls := make([]string, 0, 8)
server := httptest.NewServer(http.HandlerFunc(func(
writer http.ResponseWriter,
request *http.Request,
) {
calls = append(calls, request.URL.Path)
switch request.URL.Path {
case CaptchaPath:
http.SetCookie(writer, &http.Cookie{Name: "captcha", Value: "ready", Path: "/"})
writer.Header().Set("Content-Type", "image/png")
_, _ = writer.Write([]byte("captcha"))
case LoginPath:
assertERPHeaders(t, request)
if cookie, err := request.Cookie("captcha"); err != nil || cookie.Value != "ready" {
t.Fatalf("login captcha cookie = %v / %v", cookie, err)
}
http.SetCookie(writer, &http.Cookie{Name: "authenticated", Value: "yes", Path: "/"})
_, _ = writer.Write([]byte(`{"status":true,"data":{"user":{"id":1,"username":"test-user"}}}`))
case UserPath:
if request.URL.Query().Get("id") != "1" {
t.Fatalf("user query = %q", request.URL.RawQuery)
}
if cookie, err := request.Cookie("authenticated"); err != nil || cookie.Value != "yes" {
t.Fatalf("user cookie = %v / %v", cookie, err)
}
_, _ = writer.Write([]byte(`{"status":true,"data":{"id":1,"username":"test-user"}}`))
case StockListTotalPath:
assertStockPayload(t, request, "SOURCE-12", 0, 1, 20)
_, _ = writer.Write([]byte(`{"status":true,"data":1}`))
case StockListPath:
assertStockPayload(t, request, "SOURCE-12", 0, 1, 20)
_, _ = writer.Write([]byte(`{"status":true,"data":{"list":[{"id":12,"code":"SOURCE-12","orderCode":"PLATFORM-12","receiver":"private-recipient","receiverTel":"private-phone"}]}}`))
case StockDetailPath:
if request.URL.Query().Get("hist") != "0" {
t.Fatalf("detail query = %q", request.URL.RawQuery)
}
assertDetailPayload(t, request, []uint64{12})
_, _ = writer.Write([]byte(`{"status":true,"data":{"list":[{"id":12,"shopName":"测试店铺","created":"2026-07-28 08:00:00","details":[{"id":88,"productTitle":"商品一","productSpec":"黑色,L","sku":"IGNORED-1","productQty":2,"productPrice":129.5,"productThumb":190,"receiverTel":"private-item-phone"},{"id":89,"productTitle":"商品二","productSpec":"黑色,XL","variationSku":"IGNORED-2","productQty":1,"productPrice":"88","productThumb":"191"}]}]}}`))
default:
writer.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
manager := testSessionManager(t, server.URL, "test-user", "test-password")
loginForSource(t, manager)
result, err := manager.QueryOrder(context.Background(), "SOURCE-12")
if err != nil {
t.Fatalf("QueryOrder() error = %v", err)
}
if result.SchemaVersion != 1 || result.Query.Mode != domain.FreightSyncOrderNumber ||
len(result.Orders) != 1 || result.Orders[0].ExternalStockID != "12" ||
result.Orders[0].ShopName == nil || *result.Orders[0].ShopName != "测试店铺" ||
len(result.Orders[0].Items) != 2 || result.Orders[0].Items[0].SKU != "黑色,L" ||
result.Orders[0].Items[1].SKU != "黑色,XL" ||
result.Orders[0].Items[0].OriginalUnitPriceMinor == nil ||
*result.Orders[0].Items[0].OriginalUnitPriceMinor != 12950 {
t.Fatalf("QueryOrder() = %#v", result)
}
encoded, err := json.Marshal(result)
if err != nil {
t.Fatalf("marshal result: %v", err)
}
for _, forbidden := range []string{"private-recipient", "private-phone", "private-item-phone"} {
if strings.Contains(string(encoded), forbidden) {
t.Fatalf("result leaked raw ERP value %q: %s", forbidden, encoded)
}
}
wantCalls := []string{
CaptchaPath, LoginPath, UserPath, UserPath,
StockListTotalPath, StockListPath, StockDetailPath,
}
if strings.Join(calls, ",") != strings.Join(wantCalls, ",") {
t.Fatalf("endpoint calls = %#v, want %#v", calls, wantCalls)
}
}
func TestSessionManagerQueryCreatedRangePaginatesAndDeduplicates(t *testing.T) {
listCalls := 0
detailIDs := make([]uint64, 0)
server := httptest.NewServer(http.HandlerFunc(func(
writer http.ResponseWriter,
request *http.Request,
) {
switch request.URL.Path {
case CaptchaPath:
http.SetCookie(writer, &http.Cookie{Name: "captcha", Value: "ready", Path: "/"})
writer.Header().Set("Content-Type", "image/png")
_, _ = writer.Write([]byte("captcha"))
case LoginPath:
http.SetCookie(writer, &http.Cookie{Name: "authenticated", Value: "yes", Path: "/"})
_, _ = writer.Write([]byte(`{"status":true,"data":{"user":{"id":1,"username":"test-user"}}}`))
case UserPath:
if request.URL.Query().Get("id") != "1" {
t.Fatalf("user query = %q", request.URL.RawQuery)
}
_, _ = writer.Write([]byte(`{"status":true,"data":{"id":1,"username":"test-user"}}`))
case StockListTotalPath:
assertStockPayload(t, request, "2026-07-22,2026-07-28", 0, 1, 20)
_, _ = writer.Write([]byte(`{"status":true,"data":21}`))
case StockListPath:
listCalls++
if listCalls == 1 {
assertStockPayload(t, request, "2026-07-22,2026-07-28", 0, 1, 20)
_, _ = writer.Write(stockListEnvelope(1, 20))
return
}
assertStockPayload(t, request, "2026-07-22,2026-07-28", 20, 2, 20)
_, _ = writer.Write(stockListIDsEnvelope([]int{20}))
case StockDetailPath:
ids := decodeDetailPayload(t, request)
detailIDs = append(detailIDs, ids...)
_, _ = writer.Write(stockDetailEnvelope(ids))
default:
writer.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
manager := testSessionManager(t, server.URL, "test-user", "test-password")
loginForSource(t, manager)
result, err := manager.QueryCreatedRange(
context.Background(),
"2026-07-22",
"2026-07-28",
)
if err != nil {
t.Fatalf("QueryCreatedRange() error = %v", err)
}
if listCalls != 2 || len(detailIDs) != 20 || len(result.Orders) != 20 ||
result.Query.CreatedFrom == nil || *result.Query.CreatedFrom != "2026-07-22" ||
result.Query.CreatedTo == nil || *result.Query.CreatedTo != "2026-07-28" {
t.Fatalf("range query result = %#v, list calls = %d, detail IDs = %#v", result, listCalls, detailIDs)
}
seen := make(map[string]struct{}, len(result.Orders))
for _, order := range result.Orders {
if _, exists := seen[order.ExternalStockID]; exists {
t.Fatalf("duplicate normalized order = %q", order.ExternalStockID)
}
seen[order.ExternalStockID] = struct{}{}
}
}
func TestSessionManagerQueryRequiresConfiguredAuthenticatedSession(t *testing.T) {
manager := testSessionManager(t, "http://127.0.0.1:1", "", "")
if _, err := manager.QueryOrder(context.Background(), "SOURCE-12"); !errors.Is(err, domain.ErrFreightSourceNotConfigured) {
t.Fatalf("unconfigured QueryOrder() error = %v", err)
}
manager = testSessionManager(t, "http://127.0.0.1:1", "test-user", "test-password")
if _, err := manager.QueryOrder(context.Background(), "SOURCE-12"); !errors.Is(err, domain.ErrFreightSourceSessionNeeded) {
t.Fatalf("unauthenticated QueryOrder() error = %v", err)
}
}
func loginForSource(t *testing.T, manager *SessionManager) {
t.Helper()
status, err := manager.FetchCaptcha(context.Background())
if err != nil {
t.Fatalf("FetchCaptcha() error = %v", err)
}
if _, err := manager.Login(context.Background(), status.CaptchaTicket, "1234"); err != nil {
t.Fatalf("Login() error = %v", err)
}
}
func assertStockPayload(
t *testing.T,
request *http.Request,
wantQuery string,
wantStart, wantPage, wantLength int,
) {
t.Helper()
assertERPHeaders(t, request)
if request.Method != http.MethodPost {
t.Fatalf("stock method = %s", request.Method)
}
var payload struct {
Length int `json:"length"`
Start int `json:"start"`
PageIndex int `json:"pageIndex"`
Queries []struct {
Value string `json:"dvalue"`
} `json:"queries"`
}
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
t.Fatalf("decode stock payload: %v", err)
}
if payload.Length != wantLength || payload.Start != wantStart ||
payload.PageIndex != wantPage || len(payload.Queries) != 1 ||
payload.Queries[0].Value != wantQuery {
t.Fatalf("stock payload = %#v", payload)
}
}
func assertDetailPayload(t *testing.T, request *http.Request, want []uint64) {
t.Helper()
got := decodeDetailPayload(t, request)
if len(got) != len(want) {
t.Fatalf("detail ids = %#v, want %#v", got, want)
}
for index := range want {
if got[index] != want[index] {
t.Fatalf("detail ids = %#v, want %#v", got, want)
}
}
}
func decodeDetailPayload(t *testing.T, request *http.Request) []uint64 {
t.Helper()
assertERPHeaders(t, request)
var payload struct {
IDs []uint64 `json:"ids"`
}
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
t.Fatalf("decode detail payload: %v", err)
}
return payload.IDs
}
func stockListEnvelope(first, last int) []byte {
ids := make([]int, 0, last-first+1)
for value := first; value <= last; value++ {
ids = append(ids, value)
}
return stockListIDsEnvelope(ids)
}
func stockListIDsEnvelope(ids []int) []byte {
rows := make([]map[string]any, 0, len(ids))
for _, id := range ids {
rows = append(rows, map[string]any{"id": id, "code": "SOURCE-" + strconv.Itoa(id)})
}
content, _ := json.Marshal(map[string]any{
"status": true,
"data": map[string]any{"list": rows},
})
return content
}
func stockDetailEnvelope(ids []uint64) []byte {
rows := make([]map[string]any, 0, len(ids))
for _, id := range ids {
rows = append(rows, map[string]any{
"id": id,
"details": []any{},
})
}
content, _ := json.Marshal(map[string]any{
"status": true,
"data": map[string]any{"list": rows},
})
return content
}