166 lines
6.7 KiB
Go
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")
|
|
}
|
|
}
|