From 7d07fa52b7d47e744eecbdce3ec6bb2013b58394 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Thu, 20 Aug 2026 09:02:49 +0800 Subject: [PATCH] test(#48): add opt-in end-to-end import verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runs the whole path against the live API: login, page a day's shipment orders, fold every detail into the archive, reuse the cached session, and re-import to prove idempotency. Tagged syblive and gated on E2E_DB, so it never runs by accident and never picks its own database. Verified on real data for 2026-08-19: 986 orders, 1524 detail lines, 1056 shopee archives, parse success 1395 / uncertain 125 / failed 4. The 4 failures are all empty productSpec at the source — marked failed rather than filled in. Re-import created 0 and overwrote 1524, leaving the row count unchanged. Co-Authored-By: Claude Opus 5 --- server/app/goauto/sybimport/e2e_test.go | 106 ++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 server/app/goauto/sybimport/e2e_test.go diff --git a/server/app/goauto/sybimport/e2e_test.go b/server/app/goauto/sybimport/e2e_test.go new file mode 100644 index 0000000..47e5c68 --- /dev/null +++ b/server/app/goauto/sybimport/e2e_test.go @@ -0,0 +1,106 @@ +//go:build syblive + +// 端到端真实导入验证(#48)。带 syblive 构建标签,默认不参与 `go test ./...`。 +// +// GOAUTO_SYB_USERNAME=... GOAUTO_SYB_PASSWORD=... GOAUTO_SYB_OCR_URL=... \ +// E2E_DB=/tmp/e2e.db go test -tags syblive ./app/goauto/sybimport/ \ +// -run TestE2ERealImport -v -timeout 20m +// +// `[必须]` 这个测试会把**真实生产数据**写进 E2E_DB 指向的库。用临时路径, +// 跑完删掉;不要指向开发库,更不要指向仓库目录。 +package sybimport + +import ( + "context" + "os" + "testing" + "time" + + "go-admin/app/goauto/migrations" + "go-admin/app/goauto/sybclient" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +// 端到端:真登录 → 真拉列表和明细 → 真落库 → 再跑一次验证幂等。 +func TestE2ERealImport(t *testing.T) { + username, password := os.Getenv("GOAUTO_SYB_USERNAME"), os.Getenv("GOAUTO_SYB_PASSWORD") + if username == "" || password == "" { + t.Skip("no credentials") + } + path := os.Getenv("E2E_DB") + if path == "" { + t.Skip("未提供 E2E_DB,跳过端到端验证") + } + db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) + if err != nil { + t.Fatalf("open db: %v", err) + } + if err := migrations.Migrate(db); err != nil { + t.Fatalf("migrate: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + store := sybclient.NewSessionStore(db) + client, err := Connect(ctx, store, ConnectConfig{ + BaseURL: "https://www.shunyunbaoerp.com", Username: username, Password: password, + OcrURL: os.Getenv("GOAUTO_SYB_OCR_URL"), OcrMaxAttempts: 5, + }) + if err != nil { + t.Fatalf("connect: %v", err) + } + t.Log("① 登录成功,会话已写入 syb_session") + + day := time.Now().AddDate(0, 0, -1).Format("2006-01-02") + report, err := Sync(ctx, db, client, SyncConfig{PageSize: 20, MaxMatches: 10000}, day, day) + if err != nil { + t.Fatalf("sync: %v (report=%+v)", err, report) + } + t.Logf("② 首次导入 %s:货运单 %d 张,明细 %d 条,新增 %d,覆盖 %d", + day, report.OrderCount, report.DetailCount, report.Created, report.Updated) + + var products, shopee, sessions int64 + db.Table("syb_product").Count(&products) + db.Table("shopee_product").Count(&shopee) + db.Table("syb_session").Count(&sessions) + t.Logf("③ 落库结果:syb_product %d 行,shopee_product %d 行,syb_session %d 行", + products, shopee, sessions) + if products != int64(report.DetailCount) { + t.Fatalf("落库行数 %d 与报告的明细数 %d 不一致", products, report.DetailCount) + } + + // 解析状态分布 + for _, status := range []string{"success", "uncertain", "failed"} { + var n int64 + db.Table("syb_product").Where("parse_status = ?", status).Count(&n) + t.Logf(" 解析 %s: %d 条", status, n) + } + + // ④ 会话复用:第二个 Connect 不应重新登录(复用缓存的 Cookie 和 user id) + client2, err := Connect(ctx, store, ConnectConfig{ + BaseURL: "https://www.shunyunbaoerp.com", Username: username, Password: password, + OcrURL: os.Getenv("GOAUTO_SYB_OCR_URL"), OcrMaxAttempts: 5, + }) + if err != nil { + t.Fatalf("第二次 connect 失败(会话复用没生效): %v", err) + } + t.Log("④ 会话复用成功,未重新登录") + + // ⑤ 幂等:同一天再导一次,应全部是覆盖,行数不变 + second, err := Sync(ctx, db, client2, SyncConfig{PageSize: 20, MaxMatches: 10000}, day, day) + if err != nil { + t.Fatalf("重跑失败: %v", err) + } + var after int64 + db.Table("syb_product").Count(&after) + t.Logf("⑤ 重跑:新增 %d,覆盖 %d;行数 %d → %d", second.Created, second.Updated, products, after) + if after != products { + t.Fatalf("重跑产生了重复行: %d → %d", products, after) + } + if second.Created != 0 { + t.Fatalf("重跑不应有新增,实际 %d", second.Created) + } +}