Files
cmsp/app_sync_test.go
T

166 lines
6.7 KiB
Go

package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"cmsp/internal/config"
"cmsp/internal/store"
)
func newSyncTestApp(t *testing.T, handler http.HandlerFunc) *App {
t.Helper()
server := httptest.NewServer(handler)
t.Cleanup(server.Close)
a := NewApp()
a.ctx = context.Background()
a.cfg.ERPGo = config.ERPGoConfig{BaseURL: server.URL, APIKey: "fictional-query-key"}
db, err := store.Open(filepath.Join(t.TempDir(), "demo.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = db.Close() })
a.db = db
if err := db.SyncProducts([]store.Product{{ID: "demo-product", ItemID: "demo-item", PlatformShopID: "demo-shop", ItemName: "old-title", VideoDiagnosis: store.VideoDiagnosisOK}}, map[string][]store.Diagnosis{"demo-product": {{Type: "old-diagnosis"}}}, "old-time"); err != nil {
t.Fatal(err)
}
if err := db.UpdateProductStatus("demo-product", store.VideoFound, store.DownloadDone, store.UploadDone, "old-error"); err != nil {
t.Fatal(err)
}
if err := db.ReplaceShops([]store.Shop{{ID: "old-shop", PlatformShopID: "demo-shop", ShopName: "old-name", Platform: "0"}}, "old-time"); err != nil {
t.Fatal(err)
}
return a
}
func queryResponse(w http.ResponseWriter, status int, data any, code string) {
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{"code": status, "data": data, "errorCode": code, "msg": "fictional-query-key", "requestId": "demo-reference"})
}
func appProductPage(current, pages int) map[string]any {
total := 1
if pages == 2 {
total = 201
}
return map[string]any{
"source": "huohanhan", "fetchedAt": "2026-09-28T10:00:00+08:00", "platformShopId": "demo-shop", "itemStatus": "NORMAL", "size": 200, "current": current, "pages": pages, "total": total,
"records": []any{map[string]any{"id": "demo-product", "itemId": "demo-item", "platformShopId": "demo-shop", "itemStatus": "NORMAL", "itemName": "new-title", "createTime": "2026-09-01 10:00:00", "videoDiagnosis": "missing", "diagnoses": []any{map[string]any{"field": "ALL", "type": "缺少视频", "solution": "上传视频"}}, "uploadStatus": "pending", "downloadStatus": "pending", "videoStatus": "none"}},
}
}
func assertOldSyncData(t *testing.T, a *App) {
t.Helper()
p, _, err := a.db.GetProduct("demo-product")
d, _ := a.db.ListDiagnoses("demo-product")
if err != nil || p.ItemName != "old-title" || p.SyncedAt != "old-time" || p.VideoStatus != store.VideoFound || p.DownloadStatus != store.DownloadDone || p.UploadStatus != store.UploadDone || p.LastError != "old-error" || len(d) != 1 || d[0].Type != "old-diagnosis" {
t.Fatal("failed sync modified original data or states")
}
}
func TestAppQueryAndAtomicProductSync(t *testing.T) {
for _, failSecondPage := range []bool{false, true} {
t.Run(map[bool]string{false: "success", true: "second-page-failure"}[failSecondPage], func(t *testing.T) {
a := newSyncTestApp(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" || r.Header.Get("X-API-Key") != "fictional-query-key" || r.URL.Path != "/api/v1/integrations/huohanhan/products" {
t.Error("unexpected request or side effect")
}
if failSecondPage && r.URL.Query().Get("current") == "2" {
queryResponse(w, 502, nil, "HHH_UPSTREAM_ERROR")
return
}
pages := 1
if failSecondPage {
pages = 2
}
queryResponse(w, 200, appProductPage(1, pages), "")
})
err := a.DownloadProductData("demo-shop")
if failSecondPage {
if err == nil || !strings.Contains(err.Error(), "HHH_UPSTREAM_ERROR") {
t.Fatalf("missing stable error: %v", err)
}
assertOldSyncData(t, a)
} else {
p, _, _ := a.db.GetProduct("demo-product")
d, _ := a.db.ListDiagnoses("demo-product")
if err != nil || p.ItemName != "new-title" || p.VideoDiagnosis != store.VideoDiagnosisMissing || p.CreatedAt != "2026-09-01 10:00:00" || p.VideoStatus != store.VideoFound || p.DownloadStatus != store.DownloadDone || p.UploadStatus != store.UploadDone || p.LastError != "old-error" || len(d) != 1 || d[0].Type != "缺少视频" {
t.Fatalf("sync lost local states or mapping: %v", err)
}
}
if strings.Contains(a.log.Text(), "fictional-query-key") {
t.Fatal("query credential exposed in logs")
}
})
}
}
func TestAppDiagnosisWriteFailureRollsBackSync(t *testing.T) {
a := newSyncTestApp(t, func(w http.ResponseWriter, _ *http.Request) { queryResponse(w, 200, appProductPage(1, 1), "") })
if _, err := a.db.DB().Exec(`CREATE TRIGGER fail_sync BEFORE INSERT ON product_diagnoses BEGIN SELECT RAISE(ABORT,'fictional write failure'); END`); err != nil {
t.Fatal(err)
}
err := a.DownloadProductData("demo-shop")
if err == nil || !strings.Contains(err.Error(), "LOCAL_SYNC_FAILED") {
t.Fatalf("missing write error: %v", err)
}
assertOldSyncData(t, a)
}
func TestAppShopsFailurePreservesCacheAndSuccessRefreshes(t *testing.T) {
for _, fail := range []bool{false, true} {
a := newSyncTestApp(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" || r.URL.Path != "/api/v1/integrations/huohanhan/shops" {
t.Error("unexpected shop query")
}
if fail {
queryResponse(w, 401, nil, "API_KEY_INVALID")
return
}
queryResponse(w, 200, map[string]any{"source": "huohanhan", "fetchedAt": "2026-09-28T10:00:00+08:00", "items": []any{map[string]any{"id": "demo-internal-shop", "platformShopId": "demo-shop", "shopName": "new-name", "platform": "0"}}}, "")
})
_, err := a.RefreshShops()
shops, _ := a.GetCachedShops()
if len(shops) != 1 {
t.Fatal("cache disappeared")
}
if fail && (err == nil || shops[0].ShopName != "old-name" || shops[0].UpdatedAt != "old-time") {
t.Fatal("failed shop query changed cache")
}
if !fail && (err != nil || shops[0].ShopName != "new-name") {
t.Fatal("shops not saved")
}
if strings.Contains(a.log.Text(), "fictional-query-key") {
t.Fatal("credential in log")
}
}
}
func TestAppMissingQuerySettingsRetainsDataAndUploadSettings(t *testing.T) {
a := newSyncTestApp(t, func(w http.ResponseWriter, _ *http.Request) { t.Error("unconfigured query made a request") })
a.cfg.ERPGo = config.ERPGoConfig{}
a.cfg.Huohanhan.Account = "fictional-upload-account"
a.cfg.Huohanhan.Password = "fictional-upload-password"
err := a.DownloadProductData("demo-shop")
if err == nil || !strings.Contains(err.Error(), "ERPGo_NOT_CONFIGURED") {
t.Fatal("missing configuration not reported")
}
assertOldSyncData(t, a)
a.cfgPath = filepath.Join(t.TempDir(), "config.yaml")
if err := a.SaveConfig(a.cfg); err != nil {
t.Fatal(err)
}
loaded, err := config.Load(a.cfgPath)
if err != nil || loaded.Huohanhan.Password != "fictional-upload-password" {
t.Fatal("old upload settings changed")
}
if strings.Contains(a.log.Text(), "fictional-upload-password") {
t.Fatal("save exposed upload password")
}
}