298 lines
9.9 KiB
Go
298 lines
9.9 KiB
Go
package repository
|
|
|
|
import (
|
|
"database/sql"
|
|
"testing"
|
|
|
|
"cmautobuy/admin/model"
|
|
)
|
|
|
|
func newSybTestDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
db, err := Open(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("打开测试库失败: %v", err)
|
|
}
|
|
t.Cleanup(func() { db.Close() })
|
|
if err := Migrate(db); err != nil {
|
|
t.Fatalf("迁移失败: %v", err)
|
|
}
|
|
return db
|
|
}
|
|
|
|
func TestUpsertSybOrder_已有的ShopeeSKUID同步后仍在(t *testing.T) {
|
|
// 这是工单 #46 最要紧的一条:shopee_sku_id 是人工规格匹配的结果,
|
|
// 顺运宝同步绝不能把它覆盖成空值——覆盖了不会报错,等到建采购任务
|
|
// 才会发现匹配成果被洗掉,那时已经找不回来了。
|
|
db := newSybTestDB(t)
|
|
|
|
first := model.SybOrder{
|
|
SybID: "SYB-1", OrderNo: "ORDER-1", Title: "初始标题",
|
|
ProductSpec: "黑色,M", ShopeeGoodsID: "50209124255",
|
|
Quantity: 1, PriceTwdCent: 23900, ImageURL: "https://x/img.jpg",
|
|
SybData: `{"stock":{}}`,
|
|
}
|
|
created, err := UpsertSybOrder(db, first)
|
|
if err != nil {
|
|
t.Fatalf("首次写入失败: %v", err)
|
|
}
|
|
if !created {
|
|
t.Fatal("首次写入应该是新建")
|
|
}
|
|
|
|
// 模拟操作员在界面上完成了规格匹配,手工把 shopee_sku_id 填上。
|
|
if _, err := db.Exec(`UPDATE syb_orders SET shopee_sku_id = ? WHERE syb_id = ?`,
|
|
"TEST-SKU-123", "SYB-1"); err != nil {
|
|
t.Fatalf("模拟人工匹配失败: %v", err)
|
|
}
|
|
|
|
// 再次同步:标题、数量、价格都变了(模拟顺运宝那边数据更新),
|
|
// 但 upsert 调用方不会传 shopee_sku_id 的新值(顺运宝根本没有这个字段)。
|
|
second := model.SybOrder{
|
|
SybID: "SYB-1", OrderNo: "ORDER-1", Title: "更新后的标题",
|
|
ProductSpec: "黑色,M", ShopeeGoodsID: "50209124255",
|
|
Quantity: 3, PriceTwdCent: 25900, ImageURL: "https://x/img2.jpg",
|
|
SybData: `{"stock":{"updated":true}}`,
|
|
// 注意:这里刻意不设置 ShopeeSKUID(零值,即空字符串),
|
|
// 模拟"同步流程从来不知道匹配结果,只管顺运宝返回的字段"。
|
|
}
|
|
created2, err := UpsertSybOrder(db, second)
|
|
if err != nil {
|
|
t.Fatalf("二次写入失败: %v", err)
|
|
}
|
|
if created2 {
|
|
t.Fatal("二次写入应该是更新,不是新建")
|
|
}
|
|
|
|
var skuID, title string
|
|
var qty int
|
|
if err := db.QueryRow(`SELECT shopee_sku_id, title, quantity FROM syb_orders WHERE syb_id = ?`,
|
|
"SYB-1").Scan(&skuID, &title, &qty); err != nil {
|
|
t.Fatalf("查询失败: %v", err)
|
|
}
|
|
if skuID != "TEST-SKU-123" {
|
|
t.Fatalf("shopee_sku_id 应该还是人工匹配的 TEST-SKU-123,实际 %q"+
|
|
"——同步把匹配成果覆盖掉了,这是本工单最不能接受的问题", skuID)
|
|
}
|
|
// 其余允许覆盖的字段应该已经更新,证明这不是"upsert 整个没生效"的假通过。
|
|
if title != "更新后的标题" || qty != 3 {
|
|
t.Errorf("title/quantity 应该被同步更新,实际 title=%q quantity=%d", title, qty)
|
|
}
|
|
}
|
|
|
|
func TestUpsertSybOrder_ShopeeGoodsID会被同步更新(t *testing.T) {
|
|
db := newSybTestDB(t)
|
|
|
|
if _, err := UpsertSybOrder(db, model.SybOrder{
|
|
SybID: "SYB-2", OrderNo: "ORDER-2", ShopeeGoodsID: "11111111111", Quantity: 1,
|
|
SybData: "{}",
|
|
}); err != nil {
|
|
t.Fatalf("首次写入失败: %v", err)
|
|
}
|
|
if _, err := UpsertSybOrder(db, model.SybOrder{
|
|
SybID: "SYB-2", OrderNo: "ORDER-2", ShopeeGoodsID: "22222222222", Quantity: 1,
|
|
SybData: "{}",
|
|
}); err != nil {
|
|
t.Fatalf("二次写入失败: %v", err)
|
|
}
|
|
|
|
var goodsID string
|
|
if err := db.QueryRow(`SELECT shopee_goods_id FROM syb_orders WHERE syb_id = ?`,
|
|
"SYB-2").Scan(&goodsID); err != nil {
|
|
t.Fatalf("查询失败: %v", err)
|
|
}
|
|
if goodsID != "22222222222" {
|
|
t.Errorf("shopee_goods_id 应该被同步更新为顺运宝返回的新值,实际 %q", goodsID)
|
|
}
|
|
}
|
|
|
|
func TestUpsertSybOrder_重复同步同一订单行数不翻倍(t *testing.T) {
|
|
db := newSybTestDB(t)
|
|
|
|
for i := 0; i < 3; i++ {
|
|
if _, err := UpsertSybOrder(db, model.SybOrder{
|
|
SybID: "SYB-3", OrderNo: "ORDER-3", Quantity: 1, SybData: "{}",
|
|
}); err != nil {
|
|
t.Fatalf("第 %d 次写入失败: %v", i+1, err)
|
|
}
|
|
}
|
|
|
|
n, err := CountSybOrdersTotal(db)
|
|
if err != nil {
|
|
t.Fatalf("统计失败: %v", err)
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("重复同步同一个 syb_id 应该只有 1 行,实际 %d 行", n)
|
|
}
|
|
}
|
|
|
|
func TestSybSession_保存读取和清除(t *testing.T) {
|
|
db := newSybTestDB(t)
|
|
|
|
if got, err := GetSybSession(db, "tester"); err != nil || got != nil {
|
|
t.Fatalf("没有会话时应该返回 (nil, nil),实际 got=%v err=%v", got, err)
|
|
}
|
|
|
|
if err := SaveSybSession(db, "tester", `[{"name":"JSESSIONID","value":"abc"}]`, "2026-08-10T00:00:00Z"); err != nil {
|
|
t.Fatalf("保存会话失败: %v", err)
|
|
}
|
|
|
|
got, err := GetSybSession(db, "tester")
|
|
if err != nil {
|
|
t.Fatalf("读取会话失败: %v", err)
|
|
}
|
|
if got == nil || got.ExpiresAt != "2026-08-10T00:00:00Z" {
|
|
t.Fatalf("读取到的会话不对: %+v", got)
|
|
}
|
|
|
|
// upsert:同一用户名再保存一次应该覆盖,不是新增一行。
|
|
if err := SaveSybSession(db, "tester", `[{"name":"JSESSIONID","value":"xyz"}]`, "2026-08-11T00:00:00Z"); err != nil {
|
|
t.Fatalf("二次保存会话失败: %v", err)
|
|
}
|
|
got2, err := GetSybSession(db, "tester")
|
|
if err != nil || got2 == nil || got2.ExpiresAt != "2026-08-11T00:00:00Z" {
|
|
t.Fatalf("二次保存后应该读到新值: got=%+v err=%v", got2, err)
|
|
}
|
|
|
|
if err := DeleteSybSession(db, "tester"); err != nil {
|
|
t.Fatalf("删除会话失败: %v", err)
|
|
}
|
|
if got3, err := GetSybSession(db, "tester"); err != nil || got3 != nil {
|
|
t.Fatalf("删除后应该查不到,实际 got=%v err=%v", got3, err)
|
|
}
|
|
}
|
|
|
|
func TestSybSyncState_首次为空之后可更新(t *testing.T) {
|
|
db := newSybTestDB(t)
|
|
|
|
_, found, err := GetSybLastSyncedAt(db)
|
|
if err != nil {
|
|
t.Fatalf("查询失败: %v", err)
|
|
}
|
|
if found {
|
|
t.Fatal("从没同步过时 found 应该是 false")
|
|
}
|
|
|
|
if err := SetSybLastSyncedAt(db, "2026-08-09T14:30:00Z"); err != nil {
|
|
t.Fatalf("更新失败: %v", err)
|
|
}
|
|
at, found, err := GetSybLastSyncedAt(db)
|
|
if err != nil || !found || at != "2026-08-09T14:30:00Z" {
|
|
t.Fatalf("读取错误: at=%q found=%v err=%v", at, found, err)
|
|
}
|
|
|
|
// 再更新一次,确认是 upsert 而不是报主键冲突。
|
|
if err := SetSybLastSyncedAt(db, "2026-08-10T09:00:00Z"); err != nil {
|
|
t.Fatalf("二次更新失败: %v", err)
|
|
}
|
|
at2, _, err := GetSybLastSyncedAt(db)
|
|
if err != nil || at2 != "2026-08-10T09:00:00Z" {
|
|
t.Fatalf("二次更新后应该读到新值: at2=%q err=%v", at2, err)
|
|
}
|
|
}
|
|
|
|
func createSybSyncTestUser(t *testing.T, db *sql.DB) model.User {
|
|
t.Helper()
|
|
user := model.User{
|
|
UserID: "USR-SYNC", Username: "buyer", PasswordHash: "test-hash",
|
|
Role: model.RolePurchaser, Status: model.UserActive,
|
|
PasswordChangedAt: "2026-08-09T00:00:00Z", CreatedAt: "2026-08-09T00:00:00Z",
|
|
UpdatedAt: "2026-08-09T00:00:00Z",
|
|
}
|
|
if err := CreateInitialAdmin(db, user); err != nil {
|
|
t.Fatalf("创建同步记录测试用户失败: %v", err)
|
|
}
|
|
return user
|
|
}
|
|
|
|
func TestSybSyncRun_创建完成并分页读取(t *testing.T) {
|
|
db := newSybTestDB(t)
|
|
user := createSybSyncTestUser(t, db)
|
|
run := model.SybSyncRun{
|
|
RunID: "SYB-RUN-1", UserID: user.UserID, DateFrom: "2026-08-07",
|
|
DateTo: "2026-08-09", StartedAt: "2026-08-09T01:00:00Z",
|
|
}
|
|
if err := CreateSybSyncRun(db, run); err != nil {
|
|
t.Fatalf("创建同步记录失败: %v", err)
|
|
}
|
|
if err := FinishSybSyncRun(db, model.SybSyncRun{
|
|
RunID: "SYB-RUN-1", Status: model.SybSyncSucceeded,
|
|
StockCount: 3, DetailCount: 4, Created: 2, Updated: 2, Skipped: 1,
|
|
CursorAdvanced: true, FinishedAt: "2026-08-09T01:02:00Z",
|
|
}); err != nil {
|
|
t.Fatalf("完成同步记录失败: %v", err)
|
|
}
|
|
|
|
rows, err := ListSybSyncRuns(db, 10, 0)
|
|
if err != nil || len(rows) != 1 {
|
|
t.Fatalf("读取同步记录失败: rows=%+v err=%v", rows, err)
|
|
}
|
|
got := rows[0]
|
|
if got.Username != "buyer" || got.Status != model.SybSyncSucceeded ||
|
|
got.DetailCount != 4 || !got.CursorAdvanced || got.FinishedAt == "" {
|
|
t.Fatalf("同步记录字段不正确: %+v", got)
|
|
}
|
|
if count, err := CountSybSyncRuns(db); err != nil || count != 1 {
|
|
t.Fatalf("同步记录总数错误: count=%d err=%v", count, err)
|
|
}
|
|
}
|
|
|
|
func TestInterruptRunningSybSyncRuns_只中断未完成记录(t *testing.T) {
|
|
db := newSybTestDB(t)
|
|
user := createSybSyncTestUser(t, db)
|
|
for _, id := range []string{"RUNNING", "DONE"} {
|
|
if err := CreateSybSyncRun(db, model.SybSyncRun{
|
|
RunID: id, UserID: user.UserID, DateFrom: "2026-08-09", DateTo: "2026-08-09",
|
|
StartedAt: "2026-08-09T01:00:00Z",
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := FinishSybSyncRun(db, model.SybSyncRun{
|
|
RunID: "DONE", Status: model.SybSyncSucceeded, FinishedAt: "2026-08-09T01:01:00Z",
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
affected, err := InterruptRunningSybSyncRuns(db, "2026-08-09T02:00:00Z")
|
|
if err != nil || affected != 1 {
|
|
t.Fatalf("应该只中断一条 running 记录: affected=%d err=%v", affected, err)
|
|
}
|
|
rows, _ := ListSybSyncRuns(db, 10, 0)
|
|
statuses := map[string]model.SybSyncRunStatus{}
|
|
for _, row := range rows {
|
|
statuses[row.RunID] = row.Status
|
|
}
|
|
if statuses["RUNNING"] != model.SybSyncInterrupted || statuses["DONE"] != model.SybSyncSucceeded {
|
|
t.Fatalf("中断状态错误: %+v", statuses)
|
|
}
|
|
}
|
|
|
|
func TestListSybOrders_关键字筛选订单号和标题(t *testing.T) {
|
|
db := newSybTestDB(t)
|
|
mustUpsert := func(sybID, orderNo, title string) {
|
|
if _, err := UpsertSybOrder(db, model.SybOrder{
|
|
SybID: sybID, OrderNo: orderNo, Title: title, Quantity: 1, SybData: "{}",
|
|
}); err != nil {
|
|
t.Fatalf("写入 %s 失败: %v", sybID, err)
|
|
}
|
|
}
|
|
mustUpsert("SYB-A", "260728AAA", "纯棉上衣")
|
|
mustUpsert("SYB-B", "260728BBB", "牛仔裤")
|
|
|
|
rows, err := ListSybOrders(db, SybOrderFilter{Keyword: "AAA"}, 20, 0)
|
|
if err != nil {
|
|
t.Fatalf("查询失败: %v", err)
|
|
}
|
|
if len(rows) != 1 || rows[0].SybID != "SYB-A" {
|
|
t.Fatalf("按订单号筛选应该只查到 SYB-A,实际 %+v", rows)
|
|
}
|
|
|
|
rows2, err := ListSybOrders(db, SybOrderFilter{Keyword: "牛仔"}, 20, 0)
|
|
if err != nil {
|
|
t.Fatalf("查询失败: %v", err)
|
|
}
|
|
if len(rows2) != 1 || rows2[0].SybID != "SYB-B" {
|
|
t.Fatalf("按标题筛选应该只查到 SYB-B,实际 %+v", rows2)
|
|
}
|
|
}
|