114 lines
5.4 KiB
Go
114 lines
5.4 KiB
Go
package service
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
|
|
_ "modernc.org/sqlite"
|
|
|
|
"cmautobuy/admin/repository"
|
|
)
|
|
|
|
func newCatalogTestDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
db, err := sql.Open("sqlite", ":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
db.SetMaxOpenConns(1)
|
|
statements := []string{
|
|
`CREATE TABLE shopee_products(goods_id TEXT PRIMARY KEY,title TEXT NOT NULL,shopee_status TEXT,main_sku_code TEXT,source TEXT,source_observed_at TEXT,pdd_goods_url TEXT,pdd_goods_id TEXT,created_at TEXT,updated_at TEXT)`,
|
|
`CREATE TABLE shopee_skus(sku_id TEXT PRIMARY KEY,goods_id TEXT NOT NULL,spec_raw TEXT,color TEXT,size TEXT,advice TEXT,parse_ok INTEGER,sku_code TEXT,is_manual INTEGER,source_observed_at TEXT,created_at TEXT,updated_at TEXT)`,
|
|
`CREATE TABLE pdd_products(id INTEGER PRIMARY KEY AUTOINCREMENT,goods_id TEXT UNIQUE,url TEXT,title TEXT,shop_name TEXT,skus_json TEXT,collect_status TEXT,collect_msg TEXT,artifact_ref TEXT,collected_at TEXT,deleted_at TEXT,source TEXT,source_observed_at TEXT,created_at TEXT,updated_at TEXT)`,
|
|
`CREATE TABLE catalog_import_runs(source TEXT,batch_id TEXT,request_hash TEXT,status TEXT,request_count INTEGER,conflict_count INTEGER,observed_at TEXT,last_request_at TEXT,last_conflict_at TEXT,shopee_created INTEGER DEFAULT 0,shopee_updated INTEGER DEFAULT 0,sku_created INTEGER DEFAULT 0,sku_updated INTEGER DEFAULT 0,pdd_created INTEGER DEFAULT 0,pdd_updated INTEGER DEFAULT 0,association_created INTEGER DEFAULT 0,association_unchanged INTEGER DEFAULT 0,failure_count INTEGER DEFAULT 0,error_summary TEXT,response_body TEXT,created_at TEXT,finished_at TEXT,PRIMARY KEY(source,batch_id))`,
|
|
}
|
|
for _, statement := range statements {
|
|
if _, err := db.Exec(statement); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
t.Cleanup(func() { db.Close() })
|
|
return db
|
|
}
|
|
|
|
func validCatalogBatch() CatalogBatchRequest {
|
|
price := int64(1299)
|
|
return CatalogBatchRequest{SchemaVersion: 1, BatchID: "batch-001", ObservedAt: "2026-08-11T08:00:00+08:00",
|
|
ShopeeProducts: []CatalogShopeeProduct{{GoodsID: "S-1", Title: "蝦皮上衣"}},
|
|
ShopeeSKUs: []CatalogShopeeSKU{{SKUID: "SKU-1", GoodsID: "S-1", SpecRaw: "黑色,M", Color: "黑色", Size: "M", ParseOK: true}},
|
|
PddProducts: []CatalogPddProduct{{GoodsID: "P-1", URL: "https://mobile.yangkeduo.com/goods.html?goods_id=P-1", Title: "PDD上衣", Dimensions: []CatalogPddDimension{{Key: "color", Name: "颜色"}}, SKUs: []CatalogPddSKU{{Options: map[string]string{"color": "黑色"}, PriceCent: &price, Available: true}}}},
|
|
Associations: []CatalogAssociation{{ShopeeGoodsID: "S-1", PddGoodsID: "P-1"}}}
|
|
}
|
|
|
|
func TestImportCatalogBatch_原子写入重放与冲突(t *testing.T) {
|
|
db := newCatalogTestDB(t)
|
|
req := validCatalogBatch()
|
|
raw, _ := json.Marshal(req)
|
|
got, err := ImportCatalogBatch(db, "script-a", req, raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Counts.ShopeeCreated != 1 || got.Counts.SKUCreated != 1 || got.Counts.PddCreated != 1 || got.Counts.AssociationCreated != 1 {
|
|
t.Fatalf("统计不正确:%+v", got)
|
|
}
|
|
replay, err := ImportCatalogBatch(db, "script-a", req, raw)
|
|
if err != nil || !replay.Replayed {
|
|
t.Fatalf("相同批次应重放:resp=%+v err=%v", replay, err)
|
|
}
|
|
changed := append([]byte{}, raw...)
|
|
changed = append(changed, ' ')
|
|
_, err = ImportCatalogBatch(db, "script-a", req, changed)
|
|
var catalogErr *CatalogError
|
|
if !errors.As(err, &catalogErr) || catalogErr.Code != "IDEMPOTENCY_CONFLICT" {
|
|
t.Fatalf("不同内容应冲突:%v", err)
|
|
}
|
|
run, err := repository.GetCatalogImportRun(db, "script-a", req.BatchID)
|
|
if err != nil || run.RequestCount != 3 || run.ConflictCount != 1 {
|
|
t.Fatalf("批次计数不正确:%+v err=%v", run, err)
|
|
}
|
|
var pddID string
|
|
if err := db.QueryRow(`SELECT pdd_goods_id FROM shopee_products WHERE goods_id='S-1'`).Scan(&pddID); err != nil || pddID != "P-1" {
|
|
t.Fatalf("关联未写入:%q %v", pddID, err)
|
|
}
|
|
}
|
|
|
|
func TestImportCatalogBatch_关联冲突整批回滚且同请求重放错误(t *testing.T) {
|
|
db := newCatalogTestDB(t)
|
|
now := "2026-08-11T00:00:00.000000000Z"
|
|
_, _ = db.Exec(`INSERT INTO shopee_products(goods_id,title,source,pdd_goods_id,created_at,updated_at) VALUES('S-1','旧标题','report','P-OLD',?,?)`, now, now)
|
|
_, _ = db.Exec(`INSERT INTO pdd_products(goods_id,url,collect_status,created_at,updated_at) VALUES('P-OLD','old','pending',?,?)`, now, now)
|
|
req := validCatalogBatch()
|
|
raw, _ := json.Marshal(req)
|
|
_, err := ImportCatalogBatch(db, "script-a", req, raw)
|
|
var first *CatalogError
|
|
if !errors.As(err, &first) || first.Code != "ASSOCIATION_CONFLICT" {
|
|
t.Fatalf("应返回关联冲突:%v", err)
|
|
}
|
|
var title string
|
|
_ = db.QueryRow(`SELECT title FROM shopee_products WHERE goods_id='S-1'`).Scan(&title)
|
|
if title != "旧标题" {
|
|
t.Fatalf("失败批次必须整体回滚,title=%q", title)
|
|
}
|
|
_, err = ImportCatalogBatch(db, "script-a", req, raw)
|
|
var replay *CatalogError
|
|
if !errors.As(err, &replay) || replay.Code != first.Code || replay.Status != first.Status {
|
|
t.Fatalf("失败重放应保持原错误:%+v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateCatalogBatch_拒绝旧版超限和负金额(t *testing.T) {
|
|
req := validCatalogBatch()
|
|
req.SchemaVersion = 2
|
|
if err := ValidateCatalogBatch(req); err == nil {
|
|
t.Fatal("应拒绝未知 schema")
|
|
}
|
|
req = validCatalogBatch()
|
|
negative := int64(-1)
|
|
req.PddProducts[0].SKUs[0].PriceCent = &negative
|
|
if err := ValidateCatalogBatch(req); err == nil {
|
|
t.Fatal("应拒绝负金额")
|
|
}
|
|
}
|