test(#48): add opt-in end-to-end import verification

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 <noreply@anthropic.com>
This commit is contained in:
QiuSW
2026-08-20 09:02:49 +08:00
co-authored by Claude Opus 5
parent 4f7f3a612a
commit 7d07fa52b7
+106
View File
@@ -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)
}
}