| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467 |
- package testutil
- import (
- "bytes"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "net/http/httptest"
- "os"
- "sync"
- "testing"
- "time"
- "github.com/gin-gonic/gin"
- "github.com/glebarez/sqlite"
- "gorm.io/gorm"
- "wails-app/internal/core"
- "wails-app/internal/dao"
- "wails-app/internal/global"
- "wails-app/internal/initialize"
- "wails-app/internal/model/common/response"
- "wails-app/internal/model/system/request"
- "wails-app/internal/service/system"
- "wails-app/internal/utils"
- )
- // TestEnv 测试环境:DB + Router + HTTP Client + Token
- type TestEnv struct {
- DB *gorm.DB
- Router *gin.Engine
- Client *http.Client
- Token string
- UserID uint
- BaseURL string
- }
- var (
- mu sync.Mutex
- testEnv *TestEnv
- )
- // InitTestEnv 初始化测试环境(所有测试共享一个环境)
- func InitTestEnv(t *testing.T) *TestEnv {
- t.Helper()
- mu.Lock()
- defer mu.Unlock()
- if testEnv != nil {
- return testEnv
- }
- // 1. 初始化 Viper 配置和 Zap Logger
- configPath := "D:\\lq\\Smart Parking\\lc_garage\\config.yaml"
- core.Viper(configPath)
- global.GVA_LOG = core.Zap()
- // 2. 初始化 BlackCache(JWT 中间件依赖)
- initialize.OtherInit()
- // 3. 创建独立 SQLite 数据库
- tmpDB := fmt.Sprintf("test_%d_%d.db", time.Now().UnixNano(), os.Getpid())
- db, err := gorm.Open(sqlite.Open(tmpDB), &gorm.Config{})
- if err != nil {
- t.Fatalf("打开测试数据库失败: %v", err)
- }
- global.GVA_DB = db
- // 4. 建表 + 种子数据
- initialize.RegisterTables()
- initialize.SeedSystemData()
- system.LoadAll()
- // 5. 添加 Casbin 权限规则(必须在 RegisterTables + SeedSystemData 之后)
- addTestCasbinRules(t, db)
- // 6. 创建测试基础设施
- createTestInfrastructure(t, db)
- // 7. 注册路由
- gin.SetMode(gin.TestMode)
- router := initialize.Routers()
- // 8. 生成 admin JWT Token
- token, userID := generateAdminToken(t, db)
- testEnv = &TestEnv{
- DB: db,
- Router: router,
- Client: &http.Client{Timeout: 15 * time.Second},
- BaseURL: "http://127.0.0.1:8888",
- Token: token,
- UserID: userID,
- }
- return testEnv
- }
- // createTestInfrastructure 创建测试基础设施
- func createTestInfrastructure(t *testing.T, db *gorm.DB) {
- t.Helper()
- var lotCount int64
- db.Model(&dao.ParkingLot{}).Count(&lotCount)
- if lotCount > 0 {
- return
- }
- lot := dao.ParkingLot{
- LotCode: "TEST001",
- LotName: "测试停车场-A区",
- Capacity: 100,
- Available: 100,
- Description: "自动化测试停车场",
- }
- db.Create(&lot)
- booth := dao.Booth{
- BoothCode: "B001",
- BoothName: "测试岗亭1号",
- ParkingLotID: lot.ID,
- }
- db.Create(&booth)
- channelIn := dao.Channel{
- ChannelCode: "CH-IN-001",
- ChannelName: "入口通道1",
- Direction: "in",
- ParkingLotID: lot.ID,
- BoothID: booth.ID,
- AllowTemporary: true,
- }
- db.Create(&channelIn)
- channelOut := dao.Channel{
- ChannelCode: "CH-OUT-001",
- ChannelName: "出口通道1",
- Direction: "out",
- ParkingLotID: lot.ID,
- BoothID: booth.ID,
- AllowTemporary: true,
- }
- db.Create(&channelOut)
- reader := dao.UHFReader{
- DeviceCode: "UHF001",
- DeviceName: "测试UHF读卡器",
- DeviceType: "TCP",
- ConnectType: "tcp",
- IPAddress: "127.0.0.1",
- Port: 8000,
- IsActive: true,
- ParkingLotID: lot.ID,
- ChannelID: channelIn.ID,
- }
- db.Create(&reader)
- var tempType dao.VehicleType
- db.Where("is_system = ?", true).First(&tempType)
- feeConfig := dao.FeeConfig{
- VehicleTypeID: tempType.ID,
- StartFee: 0,
- StartTime: 30,
- UnitTime: 60,
- UnitFee: 5,
- DailyMaxFee: 50,
- IsVIPFree: false,
- VIPDiscount: 0.8,
- }
- db.Create(&feeConfig)
- var normalType dao.VehicleType
- db.Where("is_system = ? AND name != ?", true, "临时车").First(&normalType)
- if normalType.ID == 0 {
- normalType = dao.VehicleType{Name: "月租车", Remarks: "测试月租车类型"}
- db.Create(&normalType)
- }
- feeConfig2 := dao.FeeConfig{
- VehicleTypeID: normalType.ID,
- StartFee: 0,
- StartTime: 0,
- UnitTime: 60,
- UnitFee: 3,
- DailyMaxFee: 30,
- IsVIPFree: true,
- VIPDiscount: 0.5,
- }
- db.Create(&feeConfig2)
- }
- // addTestCasbinRules 为 admin(888) 添加测试所需的 Casbin 路由权限
- // 注意:实际注册的路由不带 /v1 前缀(config.yaml 中 router-prefix 为空)
- func addTestCasbinRules(t *testing.T, db *gorm.DB) {
- t.Helper()
- rules := []struct {
- v0, v1, v2 string
- }{
- // 系统管理
- {"888", "/user/getUserList", "POST"},
- // 停车场管理
- {"888", "/parking/lot/create", "POST"},
- {"888", "/parking/lot/list", "GET"},
- {"888", "/parking/lot/get", "GET"},
- {"888", "/parking/lot/update", "PUT"},
- {"888", "/parking/lot/delete", "DELETE"},
- // 岗亭管理
- {"888", "/parking/booth/create", "POST"},
- {"888", "/parking/booth/list", "GET"},
- // 通道管理
- {"888", "/parking/channel/create", "POST"},
- {"888", "/parking/channel/list", "GET"},
- // 设备管理
- {"888", "/parking/device/create", "POST"},
- {"888", "/parking/device/list", "GET"},
- // 车辆管理
- {"888", "/vehicle/create", "POST"},
- {"888", "/vehicle/get-by-plate", "GET"},
- {"888", "/vehicle/entry", "POST"},
- {"888", "/vehicle/exit/preview", "POST"},
- {"888", "/vehicle/exit/confirm", "POST"},
- // 收费配置
- {"888", "/vehicle/fee-config/list", "GET"},
- // 收费记录
- {"888", "/payment/list", "GET"},
- // 黑白名单
- {"888", "/shortlist/createShortlist", "POST"},
- {"888", "/shortlist/queryAllShortlists", "GET"},
- // 黑白名单查询/更新/删除
- {"888", "/shortlist/queryShortlistList", "POST"},
- {"888", "/shortlist/updateShortlist", "PUT"},
- {"888", "/shortlist/deleteShortlist", "DELETE"},
- // 车辆类型管理
- {"888", "/vehicle/type/create", "POST"},
- {"888", "/vehicle/type/all", "GET"},
- // 车主管理
- {"888", "/owner/create", "POST"},
- {"888", "/owner/list", "GET"},
- {"888", "/vehicle/update", "PUT"},
- {"888", "/vehicle/delete", "DELETE"},
- {"888", "/vehicle/list", "GET"},
- {"888", "/vehicle/all", "GET"},
- {"888", "/vehicle/type/update", "PUT"},
- {"888", "/vehicle/type/delete", "DELETE"},
- {"888", "/vehicle/type/list", "POST"},
- {"888", "/vehicle/fee-config/create", "POST"},
- {"888", "/vehicle/fee-config/update", "PUT"},
- {"888", "/vehicle/fee-config/delete", "DELETE"},
- {"888", "/vehicle/fee-config/get", "GET"},
- {"888", "/vehicle/record/get", "GET"},
- {"888", "/vehicle/record/list-by-plate", "GET"},
- {"888", "/vehicle/record/list", "POST"},
- {"888", "/owner/update", "PUT"},
- {"888", "/owner/delete", "DELETE"},
- {"888", "/owner/get-by-phone", "GET"},
- {"888", "/owner/get", "GET"},
- {"888", "/parking/lot/all", "GET"},
- {"888", "/parking/booth/update", "PUT"},
- {"888", "/parking/channel/update", "PUT"},
- {"888", "/parking/device/update", "PUT"},
- {"888", "/monthly-card/create", "POST"},
- {"888", "/monthly-card/renew", "POST"},
- {"888", "/monthly-card/refund", "DELETE"},
- {"888", "/monthly-card/list", "GET"},
- // 数字票
- {"888", "/digital-ticket/list", "GET"},
- {"888", "/digital-ticket/*", "GET"},
- {"888", "/digital-ticket/*/pay", "POST"},
- {"888", "/digital-ticket/*/exit", "POST"},
- }
- for _, r := range rules {
- var count int64
- db.Raw("SELECT COUNT(*) FROM casbin_rule WHERE ptype=? AND v0=? AND v1=? AND v2=?",
- "p", r.v0, r.v1, r.v2).Scan(&count)
- if count == 0 {
- db.Exec("INSERT INTO casbin_rule (ptype, v0, v1, v2, v3, v4, v5) VALUES (?, ?, ?, ?, '', '', '')",
- "p", r.v0, r.v1, r.v2)
- }
- }
- // 重新加载 Casbin 策略缓存(因为刚插入了新规则)
- system.CasbinServiceApp.FreshCasbin()
- }
- // generateAdminToken 直接生成 JWT Token(绕过验证码)
- func generateAdminToken(t *testing.T, db *gorm.DB) (string, uint) {
- t.Helper()
- var user dao.SysUser
- if err := db.Where("username = ?", "admin").First(&user).Error; err != nil {
- t.Fatalf("查询 admin 用户失败: %v", err)
- }
- j := &utils.JWT{SigningKey: []byte(global.GVA_CONFIG.JWT.SigningKey)}
- claims := j.CreateClaims(request.BaseClaims{
- UUID: user.UUID,
- ID: user.ID,
- NickName: user.NickName,
- Username: user.Username,
- AuthorityId: user.AuthorityId,
- })
- token, err := j.CreateToken(claims)
- if err != nil {
- t.Fatalf("生成 Token 失败: %v", err)
- }
- return token, user.ID
- }
- // doRaw 发送原始 HTTP 请求
- func (e *TestEnv) doRaw(method, path string, body interface{}) (*http.Response, []byte) {
- var reqBody io.Reader = bytes.NewReader([]byte{})
- if body != nil {
- data, _ := json.Marshal(body)
- reqBody = bytes.NewReader(data)
- }
- req, err := http.NewRequest(method, e.BaseURL+path, reqBody)
- if err != nil {
- panic(fmt.Sprintf("创建请求失败: %v", err))
- }
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("x-token", e.Token)
- w := httptest.NewRecorder()
- e.Router.ServeHTTP(w, req)
- resp := &http.Response{
- StatusCode: w.Code,
- Header: w.Header(),
- Body: io.NopCloser(bytes.NewReader(w.Body.Bytes())),
- }
- return resp, w.Body.Bytes()
- }
- // DoGet GET 请求
- func (e *TestEnv) DoGet(path string, params map[string]string) (*http.Response, []byte) {
- url := path
- if len(params) > 0 {
- query := ""
- for k, v := range params {
- if query != "" {
- query += "&"
- }
- query += fmt.Sprintf("%s=%s", k, v)
- }
- url += "?" + query
- }
- return e.doRaw("GET", url, nil)
- }
- // DoPost POST 请求
- func (e *TestEnv) DoPost(path string, body interface{}) (*http.Response, []byte) {
- return e.doRaw("POST", path, body)
- }
- // DoPut PUT 请求
- func (e *TestEnv) DoPut(path string, body interface{}) (*http.Response, []byte) {
- return e.doRaw("PUT", path, body)
- }
- // DoDelete DELETE 请求
- func (e *TestEnv) DoDelete(path string) (*http.Response, []byte) {
- return e.doRaw("DELETE", path, nil)
- }
- // DoGetRaw GET 请求(不带 Token,用于公共接口)
- func (e *TestEnv) DoGetRaw(path string, params map[string]string) (*http.Response, []byte) {
- url := e.BaseURL + path
- if len(params) > 0 {
- query := ""
- for k, v := range params {
- if query != "" {
- query += "&"
- }
- query += fmt.Sprintf("%s=%s", k, v)
- }
- url += "?" + query
- }
- req, err := http.NewRequest("GET", url, nil)
- if err != nil {
- panic(fmt.Sprintf("创建请求失败: %v", err))
- }
- w := httptest.NewRecorder()
- e.Router.ServeHTTP(w, req)
- resp := &http.Response{
- StatusCode: w.Code,
- Header: w.Header(),
- Body: io.NopCloser(bytes.NewReader(w.Body.Bytes())),
- }
- return resp, w.Body.Bytes()
- }
- // AssertResponse 断言响应状态码和 code
- func AssertResponse(t *testing.T, resp *http.Response, body []byte, expectedStatus, expectCode int) {
- t.Helper()
- if resp.StatusCode != expectedStatus {
- t.Errorf("HTTP 状态码不匹配: 期望 %d, 实际 %d | Body: %s", expectedStatus, resp.StatusCode, truncate(string(body), 300))
- }
- var result response.Response
- if err := json.Unmarshal(body, &result); err != nil {
- t.Fatalf("JSON 解析失败: %v | Body: %s", err, truncate(string(body), 300))
- }
- if result.Code != expectCode {
- t.Errorf("code 不匹配: 期望 %d, 实际 %d | Msg: %s", expectCode, result.Code, result.Msg)
- }
- }
- // ParseDataMap 解析响应 data 为 map
- func ParseDataMap(body []byte) map[string]interface{} {
- var result response.Response
- json.Unmarshal(body, &result)
- m, _ := result.Data.(map[string]interface{})
- return m
- }
- // ParseDataList 解析响应 data 为列表
- func ParseDataList(body []byte) []interface{} {
- var result response.Response
- json.Unmarshal(body, &result)
- list, _ := result.Data.([]interface{})
- return list
- }
- // ParsePageList 解析分页响应 data.list(如 {"total":N,"list":[...]})
- func ParsePageList(body []byte) []interface{} {
- var result response.Response
- json.Unmarshal(body, &result)
- m, ok := result.Data.(map[string]interface{})
- if !ok {
- return nil
- }
- rawList, exists := m["list"]
- if !exists {
- return nil
- }
- list, ok := rawList.([]interface{})
- if !ok {
- return nil
- }
- return list
- }
- func truncate(s string, n int) string {
- if len(s) <= n {
- return s
- }
- return s[:n] + "..."
- }
- // mapKeys returns the keys of a map for diagnostic purposes
- func mapKeys(m map[string]interface{}) []string {
- keys := make([]string, 0, len(m))
- for k := range m {
- keys = append(keys, k)
- }
- return keys
- }
- // ParsePageList 解析分页响应 data.list
|