feat(t222): add freight ingestion admin flow

This commit is contained in:
QiuSW
2026-07-28 23:26:34 +08:00
parent c524e734fb
commit 6d052b0462
37 changed files with 3397 additions and 101 deletions
@@ -294,6 +294,107 @@ func TestAdminAPIAssetAndTaskLifecycle(t *testing.T) {
}
}
func TestAdminFreightAPIImportsAllItemsWithoutPII(t *testing.T) {
router := newAdminIntegrationRouter(t)
create := performAdminRequest(
t,
router,
http.MethodPost,
"/api/v1/freight-syncs",
"application/json",
strings.NewReader(
`{"mode":"ORDER_NUMBER","order_number":"SOURCE-12"}`,
),
"freight-sync-1",
)
if create.Code != http.StatusAccepted {
t.Fatalf("create status/body = %d / %s", create.Code, create.Body)
}
var createBody struct {
Sync struct {
ID string `json:"id"`
} `json:"sync"`
}
decodeResponse(t, create, &createBody)
if createBody.Sync.ID == "" ||
strings.Contains(create.Body.String(), "SOURCE-12") {
t.Fatalf("create response exposes query or lacks ID: %s", create.Body)
}
var sync *httptest.ResponseRecorder
for attempt := 0; attempt < 50; attempt++ {
sync = performAdminRequest(
t,
router,
http.MethodGet,
"/api/v1/freight-syncs/"+createBody.Sync.ID,
"",
nil,
"",
)
if strings.Contains(sync.Body.String(), `"status":"SUCCEEDED"`) {
break
}
time.Sleep(10 * time.Millisecond)
}
if sync == nil || sync.Code != http.StatusOK ||
!strings.Contains(sync.Body.String(), `"item_count":2`) {
t.Fatalf("sync status/body = %d / %s", sync.Code, sync.Body)
}
list := performAdminRequest(
t,
router,
http.MethodGet,
"/api/v1/freight-orders",
"",
nil,
"",
)
if list.Code != http.StatusOK ||
!strings.Contains(list.Body.String(), `"item_count":2`) ||
responseContainsKey(mustDecodeAny(t, list), "receiver") ||
responseContainsKey(mustDecodeAny(t, list), "receiverTel") ||
responseContainsKey(mustDecodeAny(t, list), "receiverAddr") {
t.Fatalf("freight list status/body = %d / %s", list.Code, list.Body)
}
var listBody struct {
Items []struct {
ID string `json:"id"`
} `json:"items"`
}
decodeResponse(t, list, &listBody)
detail := performAdminRequest(
t,
router,
http.MethodGet,
"/api/v1/freight-orders/"+listBody.Items[0].ID,
"",
nil,
"",
)
if detail.Code != http.StatusOK ||
!strings.Contains(detail.Body.String(), `"sku":"BLACK-L"`) ||
!strings.Contains(detail.Body.String(), `"sku":"WHITE-M"`) ||
detail.Header().Get("Cache-Control") != "no-store" {
t.Fatalf("freight detail status/body = %d / %s", detail.Code, detail.Body)
}
replay := performAdminRequest(
t,
router,
http.MethodPost,
"/api/v1/freight-syncs",
"application/json",
strings.NewReader(
`{"mode":"ORDER_NUMBER","order_number":"SOURCE-12"}`,
),
"freight-sync-1",
)
if replay.Code != http.StatusAccepted ||
!strings.Contains(replay.Body.String(), `"replayed":true`) ||
!strings.Contains(replay.Body.String(), createBody.Sync.ID) {
t.Fatalf("replay status/body = %d / %s", replay.Code, replay.Body)
}
}
func TestAdminOrderAuthorizationIsIdempotentAndRevisioned(t *testing.T) {
fixture := newAdminIntegrationFixture(t)
taskID, executionID, taskHash, firstKey, secondKey :=
@@ -414,6 +515,9 @@ func TestAdminOrderAuthorizationIsIdempotentAndRevisioned(t *testing.T) {
if err != nil {
t.Fatalf("migration.New() error = %v", err)
}
if err := runner.Down(context.Background()); err != nil {
t.Fatalf("freight migration down: %v", err)
}
if err := runner.Down(context.Background()); err != nil {
t.Fatalf("order submission migration down: %v", err)
}
@@ -738,12 +842,23 @@ func newAdminIntegrationFixture(t *testing.T) *adminIntegrationFixture {
if err != nil {
t.Fatalf("usecase.NewOrderAuthorizationService() error = %v", err)
}
freight, err := usecase.NewFreightService(
repositories,
staticFreightSource{},
clock,
ids,
time.Second,
)
if err != nil {
t.Fatalf("usecase.NewFreightService() error = %v", err)
}
registrar, err := NewAdminRouteRegistrar(
AdminServices{
Assets: assets,
Tasks: tasks,
Results: results,
Authorizations: authorizations,
Freight: freight,
},
emptyAdminWeb{},
)
@@ -765,6 +880,56 @@ func newAdminIntegrationFixture(t *testing.T) *adminIntegrationFixture {
return &adminIntegrationFixture{router: router, db: db}
}
type staticFreightSource struct{}
func (staticFreightSource) QueryOrder(
context.Context,
string,
) (domain.FreightSourceBatch, error) {
shop := "测试店铺"
created := "2026-07-28 08:00:00"
quantityOne := 1
quantityTwo := 2
return domain.FreightSourceBatch{
SchemaVersion: 1,
Query: domain.FreightSourceQuery{
Mode: domain.FreightSyncOrderNumber,
},
Orders: []domain.FreightSourceOrder{{
ExternalStockID: "12",
SourceCode: "SOURCE-12",
ShopName: &shop,
SourceCreatedAt: &created,
Items: []domain.FreightSourceItem{
{
ExternalItemID: "88",
Title: "商品一",
ProductSpec: "黑色,L",
SKU: "BLACK-L",
Quantity: &quantityOne,
},
{
ExternalItemID: "89",
Title: "商品二",
ProductSpec: "白色,M",
SKU: "WHITE-M",
Quantity: &quantityTwo,
},
},
}},
}, nil
}
func mustDecodeAny(
t *testing.T,
response *httptest.ResponseRecorder,
) any {
t.Helper()
var decoded any
decodeResponse(t, response, &decoded)
return decoded
}
func referenceUpload(t *testing.T, key string) (io.Reader, string) {
t.Helper()
var imageBytes bytes.Buffer