176 lines
6.5 KiB
Go
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())
|
|
}
|
|
}
|