Files
cmsp/internal/store/store_test.go
T

277 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package store
import (
"path/filepath"
"testing"
)
// newTestStore 开一个只存在于内存里的数据库。
// 测试之间互不影响,也不会在磁盘上留垃圾文件。
func newTestStore(t *testing.T) *Store {
t.Helper()
s, err := Open(":memory:")
if err != nil {
t.Fatalf("打开测试数据库失败:%v", err)
}
t.Cleanup(func() { s.Close() })
return s
}
func TestOpenCreatesAllTables(t *testing.T) {
s := newTestStore(t)
version, err := s.Version()
if err != nil {
t.Fatalf("读取版本失败:%v", err)
}
if version != len(migrations) {
t.Fatalf("版本应当等于建表语句条数 %d,实际 %d", len(migrations), version)
}
// 每张表都要真的存在。
for _, table := range []string{"products", "videos", "kv", "schema_version", "video_upload_operations"} {
var name string
err := s.DB().QueryRow(
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
table).Scan(&name)
if err != nil {
t.Fatalf("表 %s 不存在:%v", table, err)
}
}
}
// 程序每次启动都会跑一遍建表语句,重复打开不能出错,也不能重复升版本。
func TestMigrateIsIdempotent(t *testing.T) {
path := filepath.Join(t.TempDir(), "cmsp.db")
first, err := Open(path)
if err != nil {
t.Fatalf("第一次打开失败:%v", err)
}
v1, _ := first.Version()
first.Close()
second, err := Open(path)
if err != nil {
t.Fatalf("第二次打开失败:%v", err)
}
defer second.Close()
v2, _ := second.Version()
if v1 != v2 {
t.Fatalf("重复打开不应改变版本,第一次 %d,第二次 %d", v1, v2)
}
}
func sampleProducts() []Product {
return []Product{
{
ID: "1118154275420983296", ItemID: "40583431295",
ItemName: "短袖連衣裙", MainImage: "https://example.invalid/a.jpg",
ShopName: "集物生活life", PlatformShopID: "406655548",
Currency: "TWD", MinSkuPrice: 859, ItemStatus: "NORMAL",
CreatedAt: "2026-08-10 10:38:51",
},
{
ID: "1118154275420983297", ItemID: "40583429107",
ItemName: "純棉短袖T恤", MainImage: "https://example.invalid/b.jpg",
ShopName: "集物生活life", PlatformShopID: "406655548",
Currency: "TWD", MinSkuPrice: 299, ItemStatus: "NORMAL",
CreatedAt: "2026-08-11 09:12:00",
},
{
ID: "1118154275420983298", ItemID: "40571188442",
ItemName: "高腰闊腿牛仔褲", MainImage: "https://example.invalid/c.jpg",
ShopName: "YUNQISHI", PlatformShopID: "411902773",
Currency: "TWD", MinSkuPrice: 645, ItemStatus: "UNLIST",
CreatedAt: "2026-08-12 14:05:00",
},
}
}
func TestUpsertAndList(t *testing.T) {
s := newTestStore(t)
if err := s.UpsertProducts(sampleProducts(), "2026-09-02 10:00:00"); err != nil {
t.Fatalf("写入商品失败:%v", err)
}
page, err := s.ListProducts(ProductQuery{})
if err != nil {
t.Fatalf("查询失败:%v", err)
}
if page.Total != 3 {
t.Fatalf("应当有 3 条,实际 %d 条", page.Total)
}
// 默认按创建时间倒序,最新的应当排第一。
if page.Items[0].ItemID != "40571188442" {
t.Fatalf("应当按创建时间倒序,第一条却是 %s", page.Items[0].ItemID)
}
// 新写入的商品,三个处理状态都应当是 pending。
if page.Items[0].DownloadStatus != DownloadPending {
t.Fatalf("新商品下载状态应当是 pending,实际 %s", page.Items[0].DownloadStatus)
}
}
// 这是本包最关键的一条规则:重复同步不能把已有的处理进度清零。
func TestUpsertKeepsLocalStatus(t *testing.T) {
s := newTestStore(t)
items := sampleProducts()
if err := s.UpsertProducts(items, "2026-09-02 10:00:00"); err != nil {
t.Fatalf("首次写入失败:%v", err)
}
// 假装已经下载并上传完成了。
id := items[0].ID
if err := s.UpdateProductStatus(id, VideoFound, DownloadDone, UploadDone, ""); err != nil {
t.Fatalf("更新状态失败:%v", err)
}
// 再同步一次,标题在货憨憨那边被改了。
items[0].ItemName = "改过的标题"
if err := s.UpsertProducts(items, "2026-09-02 11:00:00"); err != nil {
t.Fatalf("再次写入失败:%v", err)
}
got, found, err := s.GetProduct(id)
if err != nil || !found {
t.Fatalf("读取商品失败:err=%v found=%v", err, found)
}
if got.ItemName != "改过的标题" {
t.Fatalf("来自货憨憨的字段应当被更新,实际 %q", got.ItemName)
}
if got.DownloadStatus != DownloadDone {
t.Fatalf("下载状态不能被同步覆盖,期望 %s,实际 %s", DownloadDone, got.DownloadStatus)
}
if got.UploadStatus != UploadDone {
t.Fatalf("上传状态不能被同步覆盖,期望 %s,实际 %s", UploadDone, got.UploadStatus)
}
}
func TestListProductsFilters(t *testing.T) {
s := newTestStore(t)
if err := s.UpsertProducts(sampleProducts(), "2026-09-02 10:00:00"); err != nil {
t.Fatalf("写入失败:%v", err)
}
cases := []struct {
name string
query ProductQuery
want int
}{
{"按店铺", ProductQuery{PlatformShopID: "406655548"}, 2},
{"按状态", ProductQuery{ItemStatus: "UNLIST"}, 1},
{"按单个蝦皮ID", ProductQuery{ItemIDs: "40583431295"}, 1},
{"按多个蝦皮ID", ProductQuery{ItemIDs: "40583431295,40583429107"}, 2},
{"中文逗号分隔", ProductQuery{ItemIDs: "40583431295,40583429107"}, 2},
{"空格分隔", ProductQuery{ItemIDs: "40583431295 40583429107"}, 2},
{"创建时间起", ProductQuery{CreatedFrom: "2026-08-11"}, 2},
{"创建时间止", ProductQuery{CreatedTo: "2026-08-11"}, 2},
{"时间范围", ProductQuery{CreatedFrom: "2026-08-11", CreatedTo: "2026-08-11"}, 1},
{"条件叠加", ProductQuery{PlatformShopID: "406655548", ItemStatus: "NORMAL"}, 2},
{"查不到", ProductQuery{PlatformShopID: "不存在的店铺"}, 0},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
page, err := s.ListProducts(tc.query)
if err != nil {
t.Fatalf("查询失败:%v", err)
}
if page.Total != tc.want {
t.Fatalf("期望 %d 条,实际 %d 条", tc.want, page.Total)
}
})
}
}
func TestListProductsPaging(t *testing.T) {
s := newTestStore(t)
if err := s.UpsertProducts(sampleProducts(), "2026-09-02 10:00:00"); err != nil {
t.Fatalf("写入失败:%v", err)
}
page, err := s.ListProducts(ProductQuery{Page: 2, PageSize: 2})
if err != nil {
t.Fatalf("查询失败:%v", err)
}
if page.Total != 3 {
t.Fatalf("总数应当是 3,实际 %d", page.Total)
}
if len(page.Items) != 1 {
t.Fatalf("第 2 页应当只有 1 条,实际 %d 条", len(page.Items))
}
}
// 没有货憨憨记录 ID 的数据要跳过,不能拿 item_id 顶替。
func TestUpsertSkipsRowsWithoutID(t *testing.T) {
s := newTestStore(t)
err := s.UpsertProducts([]Product{{ItemID: "40583431295"}}, "2026-09-02 10:00:00")
if err != nil {
t.Fatalf("不应报错:%v", err)
}
n, err := s.CountProducts()
if err != nil {
t.Fatalf("统计失败:%v", err)
}
if n != 0 {
t.Fatalf("缺少 ID 的数据不应写入,实际写入 %d 条", n)
}
}
func TestGetProductNotFound(t *testing.T) {
s := newTestStore(t)
_, found, err := s.GetProduct("不存在")
if err != nil {
t.Fatalf("查不到不应报错:%v", err)
}
if found {
t.Fatalf("不应当找到")
}
}
func TestKV(t *testing.T) {
s := newTestStore(t)
if _, found, err := s.GetKV("token"); err != nil || found {
t.Fatalf("键不存在时应当返回 false,err=%v found=%v", err, found)
}
if err := s.SetKV("token", "第一次的值", "2026-09-02 10:00:00"); err != nil {
t.Fatalf("写入失败:%v", err)
}
if err := s.SetKV("token", "第二次的值", "2026-09-02 11:00:00"); err != nil {
t.Fatalf("覆盖失败:%v", err)
}
value, found, err := s.GetKV("token")
if err != nil || !found {
t.Fatalf("读取失败:err=%v found=%v", err, found)
}
if value != "第二次的值" {
t.Fatalf("应当读到覆盖后的值,实际 %q", value)
}
}
func TestSplitIDs(t *testing.T) {
cases := []struct {
in string
want int
}{
{"", 0},
{" ", 0},
{"111", 1},
{"111,222", 2},
{"111,222", 2},
{"111 222 333", 3},
{"111,,222", 2},
{"111、222", 2},
{" 111 , 222 ", 2},
}
for _, tc := range cases {
if got := splitIDs(tc.in); len(got) != tc.want {
t.Fatalf("输入 %q 期望 %d 个 ID,实际 %d 个:%v", tc.in, tc.want, len(got), got)
}
}
}