217 lines
7.3 KiB
Go
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)
|
|
}
|
|
}
|