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

217 lines
7.3 KiB
Go

package shunyunbao
import (
"bytes"
"encoding/json"
"errors"
"os"
"strings"
"testing"
"cmroubao/backend-api/internal/domain"
)
func TestProtocolBuildsVerifiedHeadersAndPayloads(t *testing.T) {
headers, err := RequestHeaders("https://www.shunyunbaoerp.com")
if err != nil {
t.Fatalf("RequestHeaders() error = %v", err)
}
for name, want := range map[string]string{
"Accept": "application/json, text/plain, */*",
"Origin": "https://www.shunyunbaoerp.com",
"Referer": "https://www.shunyunbaoerp.com/sys/login",
"User-Agent": "shunyunbaoerp-client/0.1",
"X-Requested-With": "XMLHttpRequest",
} {
if got := headers.Get(name); got != want {
t.Fatalf("header %s = %q, want %q", name, got, want)
}
}
query, err := OrderNumberQuery(" SANITIZED-CODE-12 ")
if err != nil {
t.Fatalf("OrderNumberQuery() error = %v", err)
}
if query.Value != "SANITIZED-CODE-12" || query.ColumnName != "allcode" ||
query.Operator != 6 || query.OptionType != 1 {
t.Fatalf("order query = %#v", query)
}
payload, err := StockListPayload(query, 20, 2, 20)
if err != nil {
t.Fatalf("StockListPayload() error = %v", err)
}
if payload["history"] != 0 || payload["length"] != 20 ||
payload["start"] != 20 || payload["pageIndex"] != 2 ||
payload["store"] != false {
t.Fatalf("list payload = %#v", payload)
}
columns, ok := payload["columns"].([]Column)
if !ok || len(columns) != 72 || columns[44].FieldName != "receiverTel" {
t.Fatalf("columns = %#v", payload["columns"])
}
detail, err := StockDetailPayload([]string{"12", "12", "99"})
if err != nil {
t.Fatalf("StockDetailPayload() error = %v", err)
}
ids, ok := detail["ids"].([]uint64)
if !ok || len(ids) != 2 || ids[0] != 12 || ids[1] != 99 ||
StockDetailPath != "/am/stock/detail/listByStock" {
t.Fatalf("detail payload/path = %#v / %q", detail, StockDetailPath)
}
rangeQuery, err := CreatedRangeQuery("2026-07-22", "2026-07-28")
if err != nil || rangeQuery.Value != "2026-07-22,2026-07-28" ||
rangeQuery.ColumnName != "created" || rangeQuery.Type != 3 {
t.Fatalf("CreatedRangeQuery() = %#v, %v", rangeQuery, err)
}
}
func TestNormalizeFreightFixtureUsesOnlyAllowlist(t *testing.T) {
content, err := os.ReadFile("testdata/freight-result.json")
if err != nil {
t.Fatalf("read fixture: %v", err)
}
var fixture RawFreightResult
decoder := json.NewDecoder(bytes.NewReader(content))
decoder.UseNumber()
if err := decoder.Decode(&fixture); err != nil {
t.Fatalf("decode fixture: %v", err)
}
result, err := NormalizeFreightResult(fixture.Query, fixture.Records)
if err != nil {
t.Fatalf("NormalizeFreightResult() error = %v", err)
}
if result.SchemaVersion != 1 || result.Query.Mode != domain.FreightSyncOrderNumber ||
len(result.Orders) != 1 || len(result.Orders[0].Items) != 1 {
t.Fatalf("result = %#v", result)
}
order := result.Orders[0]
if order.ExternalStockID != "12" || order.SourceCode != "SANITIZED-CODE-12" ||
order.ShopName == nil || *order.ShopName != "Sanitized shop" ||
order.Items[0].ExternalItemID != "88" || order.Items[0].SKU != "Black,L" ||
order.Items[0].Quantity == nil || *order.Items[0].Quantity != 2 ||
order.Items[0].OriginalUnitPriceMinor == nil ||
*order.Items[0].OriginalUnitPriceMinor != 12950 ||
order.Items[0].OriginalCurrency != domain.FreightCurrencyTWD {
t.Fatalf("allowlist result = %#v", order)
}
encoded, err := json.Marshal(result)
if err != nil {
t.Fatalf("marshal result: %v", err)
}
for _, forbidden := range []string{
"receiver", "phone-not-allowed", "address-not-allowed",
"cookie-not-allowed", "jwt-not-allowed", "account-not-allowed",
"password-not-allowed", "captcha-not-allowed", "item-phone-not-allowed",
} {
if strings.Contains(string(encoded), forbidden) {
t.Fatalf("normalized result contains forbidden value %q: %s", forbidden, encoded)
}
}
}
func TestNormalizeFreightPriceAndThumbContract(t *testing.T) {
tests := []struct {
name string
price any
thumb any
wantMinor *int64
wantThumb *string
wantErr bool
}{
{name: "integer", price: json.Number("12"), thumb: json.Number("190"), wantMinor: int64Pointer(1200), wantThumb: stringPointer("190")},
{name: "one decimal", price: "12.3", thumb: "0190", wantMinor: int64Pointer(1230), wantThumb: stringPointer("190")},
{name: "two decimals", price: json.Number("12.34"), wantMinor: int64Pointer(1234)},
{name: "missing", price: "", thumb: "", wantMinor: nil, wantThumb: nil},
{name: "negative", price: "-1", wantErr: true},
{name: "exponent", price: json.Number("1e2"), wantErr: true},
{name: "excess precision", price: "1.001", wantErr: true},
{name: "overflow", price: "92233720368547758.08", wantErr: true},
{name: "invalid thumb text", price: "1", thumb: "https://invalid.example/image", wantErr: true},
{name: "invalid thumb zero", price: "1", thumb: 0, wantErr: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := NormalizeFreightResult(
RawQuery{Mode: domain.FreightSyncOrderNumber},
[]RawRecord{{
Stock: map[string]any{"id": 12, "code": "SANITIZED"},
Detail: map[string]any{
"id": 12,
"details": []any{map[string]any{
"id": 88,
"productSpec": "Black,L",
"productPrice": test.price,
"productThumb": test.thumb,
}},
},
}},
)
if test.wantErr {
if !errors.Is(err, domain.ErrFreightSourceProtocol) {
t.Fatalf("NormalizeFreightResult() error = %v", err)
}
return
}
if err != nil {
t.Fatalf("NormalizeFreightResult() error = %v", err)
}
item := result.Orders[0].Items[0]
if !equalInt64Pointer(item.OriginalUnitPriceMinor, test.wantMinor) ||
!equalStringPointer(item.ProductThumbRef, test.wantThumb) ||
item.ProductSpec != "Black,L" || item.SKU != "Black,L" ||
item.OriginalCurrency != domain.FreightCurrencyTWD {
t.Fatalf("normalized item = %#v", item)
}
})
}
}
func int64Pointer(value int64) *int64 {
return &value
}
func equalInt64Pointer(left, right *int64) bool {
return left == nil && right == nil ||
left != nil && right != nil && *left == *right
}
func equalStringPointer(left, right *string) bool {
return left == nil && right == nil ||
left != nil && right != nil && *left == *right
}
func TestNormalizeFreightNeverLeaksRawValuesInErrors(t *testing.T) {
_, err := NormalizeFreightResult(
RawQuery{Mode: domain.FreightSyncOrderNumber},
[]RawRecord{{
Stock: map[string]any{"id": "not-an-id-private"},
Detail: map[string]any{"id": "12", "details": []any{}},
}},
)
if !errors.Is(err, domain.ErrFreightSourceProtocol) {
t.Fatalf("NormalizeFreightResult() error = %v", err)
}
if strings.Contains(err.Error(), "not-an-id-private") {
t.Fatalf("error leaked raw ERP value: %v", err)
}
}
func TestNormalizeFreightRejectsConflictingDuplicateIdentity(t *testing.T) {
record := RawRecord{
Stock: map[string]any{"id": "12", "code": "SANITIZED"},
Detail: map[string]any{
"id": "12",
"details": []any{
map[string]any{"id": "88", "productTitle": "first"},
map[string]any{"id": "88", "productTitle": "second"},
},
},
}
_, err := NormalizeFreightResult(
RawQuery{Mode: domain.FreightSyncOrderNumber},
[]RawRecord{record},
)
if !errors.Is(err, domain.ErrFreightSourceProtocol) {
t.Fatalf("NormalizeFreightResult() error = %v", err)
}
}