Files
goauto/server/app/goauto/sybimport/import_handler_test.go
T

176 lines
6.5 KiB
Go

package sybimport
import (
"bytes"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"go-admin/app/goauto/models"
"go-admin/config"
"github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// 没有启用店铺时必须在读取凭据、登录、OCR 和任何 SYB 请求之前拒绝。
// 这里不配置任何 SYB 客户端;若前置顺序回退,响应会变成凭据错误或尝试联网。
func TestImportRejectsNoEnabledShopBeforeConnect(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:import-no-shop?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("open db: %v", err)
}
if err := db.AutoMigrate(&models.SYBShop{}); err != nil {
t.Fatalf("migrate: %v", err)
}
gin.SetMode(gin.TestMode)
engine := gin.New()
engine.Use(func(c *gin.Context) { c.Set(jwt.JwtPayloadKey, jwt.MapClaims{"rolekey": "admin"}); c.Next() })
engine.POST("/api/admin/v1/syb-products/import", Handler{DB: db}.Import)
body := bytes.NewBufferString(`{"dateFrom":"2026-08-19","dateTo":"2026-08-19"}`)
request := httptest.NewRequest(http.MethodPost, "/api/admin/v1/syb-products/import", body)
request.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, request)
if recorder.Code != http.StatusUnprocessableEntity {
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
}
var response struct {
Message string `json:"message"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatalf("decode response: %v", err)
}
if !strings.Contains(response.Message, "没有启用任何店铺") {
t.Fatalf("unexpected response: %s", recorder.Body.String())
}
}
func TestImportRoleBoundary(t *testing.T) {
for _, role := range []string{"admin", "purchaser", "viewer", "", "custom-role"} {
t.Run(role, func(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
engine.Use(func(c *gin.Context) { c.Set(jwt.JwtPayloadKey, jwt.MapClaims{"rolekey": role}); c.Next() })
engine.POST("/api/admin/v1/syb-products/import", Handler{}.Import)
// Authorized roles reach JSON validation; no DB or external SYB call is made.
request := httptest.NewRequest(http.MethodPost, "/api/admin/v1/syb-products/import", bytes.NewBufferString(`{`))
request.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, request)
want := http.StatusForbidden
if role == "admin" || role == "purchaser" {
want = http.StatusUnprocessableEntity
}
if recorder.Code != want {
t.Fatalf("role=%q status=%d want=%d body=%s", role, recorder.Code, want, recorder.Body.String())
}
})
}
}
func TestImportShopPreflightFailureHasStableCodeAndSafeLog(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:import-shop-preflight-failure?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("open db: %v", err)
}
var loggedStage, loggedCause string
recorder := performAdminImport(t, Handler{
DB: db,
internalErrorLogger: func(stage, safeCause string) {
loggedStage, loggedCause = stage, safeCause
},
})
assertImportInternalFailure(t, recorder, CodeSyncShopPreflightFailed)
if loggedStage != "shop_preflight" || !strings.Contains(loggedCause, "syb_shop") {
t.Fatalf("unexpected safe log: stage=%q cause=%q", loggedStage, loggedCause)
}
}
func TestImportSyncRunCreateFailureHasStableCodeAndSafeLog(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:import-run-create-failure?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("open db: %v", err)
}
if err := db.AutoMigrate(&models.SYBShop{}); err != nil {
t.Fatalf("migrate shop: %v", err)
}
if err := db.Create(&models.SYBShop{DisplayName: "测试店铺", NormalizedName: "测试店铺", Enabled: true}).Error; err != nil {
t.Fatalf("create shop: %v", err)
}
originalSYB := config.ExtConfig.SYB
config.ExtConfig.SYB.Username = "test-user"
config.ExtConfig.SYB.Password = "test-password"
t.Cleanup(func() { config.ExtConfig.SYB = originalSYB })
var loggedStage, loggedCause string
recorder := performAdminImport(t, Handler{
DB: db,
internalErrorLogger: func(stage, safeCause string) {
loggedStage, loggedCause = stage, safeCause
},
})
assertImportInternalFailure(t, recorder, CodeSyncRunCreateFailed)
if loggedStage != "sync_run_create" || !strings.Contains(loggedCause, "syb_sync_run") {
t.Fatalf("unexpected safe log: stage=%q cause=%q", loggedStage, loggedCause)
}
}
func TestSanitizeInternalErrorRedactsCredentialsAndBoundsLength(t *testing.T) {
raw := "password=hunter2 token=abc Cookie=session Authorization Bearer jwt.payload.signature root:dbpass@tcp(127.0.0.1) https://user:urlpass@example.test " + strings.Repeat("x", internalErrorLogLimit+200)
safe := sanitizeInternalError(errors.New(raw))
for _, secret := range []string{"hunter2", "abc", "session", "jwt.payload.signature", "dbpass", "urlpass"} {
if strings.Contains(safe, secret) {
t.Fatalf("safe log leaked %q: %s", secret, safe)
}
}
if !strings.Contains(safe, "[REDACTED]") {
t.Fatalf("safe log should mark redactions: %s", safe)
}
if len(safe) > internalErrorLogLimit+3 {
t.Fatalf("safe log length = %d", len(safe))
}
}
func performAdminImport(t *testing.T, handler Handler) *httptest.ResponseRecorder {
t.Helper()
gin.SetMode(gin.TestMode)
engine := gin.New()
engine.Use(func(c *gin.Context) {
c.Set(jwt.JwtPayloadKey, jwt.MapClaims{"rolekey": "admin", "identity": float64(1), "nice": "admin"})
c.Next()
})
engine.POST("/api/admin/v1/syb-products/import", handler.Import)
request := httptest.NewRequest(http.MethodPost, "/api/admin/v1/syb-products/import", bytes.NewBufferString(`{"dateFrom":"2026-08-19","dateTo":"2026-08-19"}`))
request.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, request)
return recorder
}
func assertImportInternalFailure(t *testing.T, recorder *httptest.ResponseRecorder, expectedCode string) {
t.Helper()
if recorder.Code != http.StatusInternalServerError {
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
}
var response struct {
Code string `json:"code"`
Message string `json:"message"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatalf("decode response: %v", err)
}
if response.Code != expectedCode || response.Message != "服务端处理失败" {
t.Fatalf("unexpected response: %s", recorder.Body.String())
}
}