testenv.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. package testutil
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "net/http/httptest"
  9. "os"
  10. "sync"
  11. "testing"
  12. "time"
  13. "github.com/gin-gonic/gin"
  14. "github.com/glebarez/sqlite"
  15. "gorm.io/gorm"
  16. "wails-app/internal/core"
  17. "wails-app/internal/dao"
  18. "wails-app/internal/global"
  19. "wails-app/internal/initialize"
  20. "wails-app/internal/model/common/response"
  21. "wails-app/internal/model/system/request"
  22. "wails-app/internal/service/system"
  23. "wails-app/internal/utils"
  24. )
  25. // TestEnv 测试环境:DB + Router + HTTP Client + Token
  26. type TestEnv struct {
  27. DB *gorm.DB
  28. Router *gin.Engine
  29. Client *http.Client
  30. Token string
  31. UserID uint
  32. BaseURL string
  33. }
  34. var (
  35. mu sync.Mutex
  36. testEnv *TestEnv
  37. )
  38. // InitTestEnv 初始化测试环境(所有测试共享一个环境)
  39. func InitTestEnv(t *testing.T) *TestEnv {
  40. t.Helper()
  41. mu.Lock()
  42. defer mu.Unlock()
  43. if testEnv != nil {
  44. return testEnv
  45. }
  46. // 1. 初始化 Viper 配置和 Zap Logger
  47. configPath := "D:\\lq\\Smart Parking\\lc_garage\\config.yaml"
  48. core.Viper(configPath)
  49. global.GVA_LOG = core.Zap()
  50. // 2. 初始化 BlackCache(JWT 中间件依赖)
  51. initialize.OtherInit()
  52. // 3. 创建独立 SQLite 数据库
  53. tmpDB := fmt.Sprintf("test_%d_%d.db", time.Now().UnixNano(), os.Getpid())
  54. db, err := gorm.Open(sqlite.Open(tmpDB), &gorm.Config{})
  55. if err != nil {
  56. t.Fatalf("打开测试数据库失败: %v", err)
  57. }
  58. global.GVA_DB = db
  59. // 4. 建表 + 种子数据
  60. initialize.RegisterTables()
  61. initialize.SeedSystemData()
  62. system.LoadAll()
  63. // 5. 添加 Casbin 权限规则(必须在 RegisterTables + SeedSystemData 之后)
  64. addTestCasbinRules(t, db)
  65. // 6. 创建测试基础设施
  66. createTestInfrastructure(t, db)
  67. // 7. 注册路由
  68. gin.SetMode(gin.TestMode)
  69. router := initialize.Routers()
  70. // 8. 生成 admin JWT Token
  71. token, userID := generateAdminToken(t, db)
  72. testEnv = &TestEnv{
  73. DB: db,
  74. Router: router,
  75. Client: &http.Client{Timeout: 15 * time.Second},
  76. BaseURL: "http://127.0.0.1:8888",
  77. Token: token,
  78. UserID: userID,
  79. }
  80. return testEnv
  81. }
  82. // createTestInfrastructure 创建测试基础设施
  83. func createTestInfrastructure(t *testing.T, db *gorm.DB) {
  84. t.Helper()
  85. var lotCount int64
  86. db.Model(&dao.ParkingLot{}).Count(&lotCount)
  87. if lotCount > 0 {
  88. return
  89. }
  90. lot := dao.ParkingLot{
  91. LotCode: "TEST001",
  92. LotName: "测试停车场-A区",
  93. Capacity: 100,
  94. Available: 100,
  95. Description: "自动化测试停车场",
  96. }
  97. db.Create(&lot)
  98. booth := dao.Booth{
  99. BoothCode: "B001",
  100. BoothName: "测试岗亭1号",
  101. ParkingLotID: lot.ID,
  102. }
  103. db.Create(&booth)
  104. channelIn := dao.Channel{
  105. ChannelCode: "CH-IN-001",
  106. ChannelName: "入口通道1",
  107. Direction: "in",
  108. ParkingLotID: lot.ID,
  109. BoothID: booth.ID,
  110. AllowTemporary: true,
  111. }
  112. db.Create(&channelIn)
  113. channelOut := dao.Channel{
  114. ChannelCode: "CH-OUT-001",
  115. ChannelName: "出口通道1",
  116. Direction: "out",
  117. ParkingLotID: lot.ID,
  118. BoothID: booth.ID,
  119. AllowTemporary: true,
  120. }
  121. db.Create(&channelOut)
  122. reader := dao.UHFReader{
  123. DeviceCode: "UHF001",
  124. DeviceName: "测试UHF读卡器",
  125. DeviceType: "TCP",
  126. ConnectType: "tcp",
  127. IPAddress: "127.0.0.1",
  128. Port: 8000,
  129. IsActive: true,
  130. ParkingLotID: lot.ID,
  131. ChannelID: channelIn.ID,
  132. }
  133. db.Create(&reader)
  134. var tempType dao.VehicleType
  135. db.Where("is_system = ?", true).First(&tempType)
  136. feeConfig := dao.FeeConfig{
  137. VehicleTypeID: tempType.ID,
  138. StartFee: 0,
  139. StartTime: 30,
  140. UnitTime: 60,
  141. UnitFee: 5,
  142. DailyMaxFee: 50,
  143. IsVIPFree: false,
  144. VIPDiscount: 0.8,
  145. }
  146. db.Create(&feeConfig)
  147. var normalType dao.VehicleType
  148. db.Where("is_system = ? AND name != ?", true, "临时车").First(&normalType)
  149. if normalType.ID == 0 {
  150. normalType = dao.VehicleType{Name: "月租车", Remarks: "测试月租车类型"}
  151. db.Create(&normalType)
  152. }
  153. feeConfig2 := dao.FeeConfig{
  154. VehicleTypeID: normalType.ID,
  155. StartFee: 0,
  156. StartTime: 0,
  157. UnitTime: 60,
  158. UnitFee: 3,
  159. DailyMaxFee: 30,
  160. IsVIPFree: true,
  161. VIPDiscount: 0.5,
  162. }
  163. db.Create(&feeConfig2)
  164. }
  165. // addTestCasbinRules 为 admin(888) 添加测试所需的 Casbin 路由权限
  166. // 注意:实际注册的路由不带 /v1 前缀(config.yaml 中 router-prefix 为空)
  167. func addTestCasbinRules(t *testing.T, db *gorm.DB) {
  168. t.Helper()
  169. rules := []struct {
  170. v0, v1, v2 string
  171. }{
  172. // 系统管理
  173. {"888", "/user/getUserList", "POST"},
  174. // 停车场管理
  175. {"888", "/parking/lot/create", "POST"},
  176. {"888", "/parking/lot/list", "GET"},
  177. {"888", "/parking/lot/get", "GET"},
  178. {"888", "/parking/lot/update", "PUT"},
  179. {"888", "/parking/lot/delete", "DELETE"},
  180. // 岗亭管理
  181. {"888", "/parking/booth/create", "POST"},
  182. {"888", "/parking/booth/list", "GET"},
  183. // 通道管理
  184. {"888", "/parking/channel/create", "POST"},
  185. {"888", "/parking/channel/list", "GET"},
  186. // 设备管理
  187. {"888", "/parking/device/create", "POST"},
  188. {"888", "/parking/device/list", "GET"},
  189. // 车辆管理
  190. {"888", "/vehicle/create", "POST"},
  191. {"888", "/vehicle/get-by-plate", "GET"},
  192. {"888", "/vehicle/entry", "POST"},
  193. {"888", "/vehicle/exit/preview", "POST"},
  194. {"888", "/vehicle/exit/confirm", "POST"},
  195. // 收费配置
  196. {"888", "/vehicle/fee-config/list", "GET"},
  197. // 收费记录
  198. {"888", "/payment/list", "GET"},
  199. // 黑白名单
  200. {"888", "/shortlist/createShortlist", "POST"},
  201. {"888", "/shortlist/queryAllShortlists", "GET"},
  202. // 黑白名单查询/更新/删除
  203. {"888", "/shortlist/queryShortlistList", "POST"},
  204. {"888", "/shortlist/updateShortlist", "PUT"},
  205. {"888", "/shortlist/deleteShortlist", "DELETE"},
  206. // 车辆类型管理
  207. {"888", "/vehicle/type/create", "POST"},
  208. {"888", "/vehicle/type/all", "GET"},
  209. // 车主管理
  210. {"888", "/owner/create", "POST"},
  211. {"888", "/owner/list", "GET"},
  212. {"888", "/vehicle/update", "PUT"},
  213. {"888", "/vehicle/delete", "DELETE"},
  214. {"888", "/vehicle/list", "GET"},
  215. {"888", "/vehicle/all", "GET"},
  216. {"888", "/vehicle/type/update", "PUT"},
  217. {"888", "/vehicle/type/delete", "DELETE"},
  218. {"888", "/vehicle/type/list", "POST"},
  219. {"888", "/vehicle/fee-config/create", "POST"},
  220. {"888", "/vehicle/fee-config/update", "PUT"},
  221. {"888", "/vehicle/fee-config/delete", "DELETE"},
  222. {"888", "/vehicle/fee-config/get", "GET"},
  223. {"888", "/vehicle/record/get", "GET"},
  224. {"888", "/vehicle/record/list-by-plate", "GET"},
  225. {"888", "/vehicle/record/list", "POST"},
  226. {"888", "/owner/update", "PUT"},
  227. {"888", "/owner/delete", "DELETE"},
  228. {"888", "/owner/get-by-phone", "GET"},
  229. {"888", "/owner/get", "GET"},
  230. {"888", "/parking/lot/all", "GET"},
  231. {"888", "/parking/booth/update", "PUT"},
  232. {"888", "/parking/channel/update", "PUT"},
  233. {"888", "/parking/device/update", "PUT"},
  234. {"888", "/monthly-card/create", "POST"},
  235. {"888", "/monthly-card/renew", "POST"},
  236. {"888", "/monthly-card/refund", "DELETE"},
  237. {"888", "/monthly-card/list", "GET"},
  238. // 数字票
  239. {"888", "/digital-ticket/list", "GET"},
  240. {"888", "/digital-ticket/*", "GET"},
  241. {"888", "/digital-ticket/*/pay", "POST"},
  242. {"888", "/digital-ticket/*/exit", "POST"},
  243. }
  244. for _, r := range rules {
  245. var count int64
  246. db.Raw("SELECT COUNT(*) FROM casbin_rule WHERE ptype=? AND v0=? AND v1=? AND v2=?",
  247. "p", r.v0, r.v1, r.v2).Scan(&count)
  248. if count == 0 {
  249. db.Exec("INSERT INTO casbin_rule (ptype, v0, v1, v2, v3, v4, v5) VALUES (?, ?, ?, ?, '', '', '')",
  250. "p", r.v0, r.v1, r.v2)
  251. }
  252. }
  253. // 重新加载 Casbin 策略缓存(因为刚插入了新规则)
  254. system.CasbinServiceApp.FreshCasbin()
  255. }
  256. // generateAdminToken 直接生成 JWT Token(绕过验证码)
  257. func generateAdminToken(t *testing.T, db *gorm.DB) (string, uint) {
  258. t.Helper()
  259. var user dao.SysUser
  260. if err := db.Where("username = ?", "admin").First(&user).Error; err != nil {
  261. t.Fatalf("查询 admin 用户失败: %v", err)
  262. }
  263. j := &utils.JWT{SigningKey: []byte(global.GVA_CONFIG.JWT.SigningKey)}
  264. claims := j.CreateClaims(request.BaseClaims{
  265. UUID: user.UUID,
  266. ID: user.ID,
  267. NickName: user.NickName,
  268. Username: user.Username,
  269. AuthorityId: user.AuthorityId,
  270. })
  271. token, err := j.CreateToken(claims)
  272. if err != nil {
  273. t.Fatalf("生成 Token 失败: %v", err)
  274. }
  275. return token, user.ID
  276. }
  277. // doRaw 发送原始 HTTP 请求
  278. func (e *TestEnv) doRaw(method, path string, body interface{}) (*http.Response, []byte) {
  279. var reqBody io.Reader = bytes.NewReader([]byte{})
  280. if body != nil {
  281. data, _ := json.Marshal(body)
  282. reqBody = bytes.NewReader(data)
  283. }
  284. req, err := http.NewRequest(method, e.BaseURL+path, reqBody)
  285. if err != nil {
  286. panic(fmt.Sprintf("创建请求失败: %v", err))
  287. }
  288. req.Header.Set("Content-Type", "application/json")
  289. req.Header.Set("x-token", e.Token)
  290. w := httptest.NewRecorder()
  291. e.Router.ServeHTTP(w, req)
  292. resp := &http.Response{
  293. StatusCode: w.Code,
  294. Header: w.Header(),
  295. Body: io.NopCloser(bytes.NewReader(w.Body.Bytes())),
  296. }
  297. return resp, w.Body.Bytes()
  298. }
  299. // DoGet GET 请求
  300. func (e *TestEnv) DoGet(path string, params map[string]string) (*http.Response, []byte) {
  301. url := path
  302. if len(params) > 0 {
  303. query := ""
  304. for k, v := range params {
  305. if query != "" {
  306. query += "&"
  307. }
  308. query += fmt.Sprintf("%s=%s", k, v)
  309. }
  310. url += "?" + query
  311. }
  312. return e.doRaw("GET", url, nil)
  313. }
  314. // DoPost POST 请求
  315. func (e *TestEnv) DoPost(path string, body interface{}) (*http.Response, []byte) {
  316. return e.doRaw("POST", path, body)
  317. }
  318. // DoPut PUT 请求
  319. func (e *TestEnv) DoPut(path string, body interface{}) (*http.Response, []byte) {
  320. return e.doRaw("PUT", path, body)
  321. }
  322. // DoDelete DELETE 请求
  323. func (e *TestEnv) DoDelete(path string) (*http.Response, []byte) {
  324. return e.doRaw("DELETE", path, nil)
  325. }
  326. // DoGetRaw GET 请求(不带 Token,用于公共接口)
  327. func (e *TestEnv) DoGetRaw(path string, params map[string]string) (*http.Response, []byte) {
  328. url := e.BaseURL + path
  329. if len(params) > 0 {
  330. query := ""
  331. for k, v := range params {
  332. if query != "" {
  333. query += "&"
  334. }
  335. query += fmt.Sprintf("%s=%s", k, v)
  336. }
  337. url += "?" + query
  338. }
  339. req, err := http.NewRequest("GET", url, nil)
  340. if err != nil {
  341. panic(fmt.Sprintf("创建请求失败: %v", err))
  342. }
  343. w := httptest.NewRecorder()
  344. e.Router.ServeHTTP(w, req)
  345. resp := &http.Response{
  346. StatusCode: w.Code,
  347. Header: w.Header(),
  348. Body: io.NopCloser(bytes.NewReader(w.Body.Bytes())),
  349. }
  350. return resp, w.Body.Bytes()
  351. }
  352. // AssertResponse 断言响应状态码和 code
  353. func AssertResponse(t *testing.T, resp *http.Response, body []byte, expectedStatus, expectCode int) {
  354. t.Helper()
  355. if resp.StatusCode != expectedStatus {
  356. t.Errorf("HTTP 状态码不匹配: 期望 %d, 实际 %d | Body: %s", expectedStatus, resp.StatusCode, truncate(string(body), 300))
  357. }
  358. var result response.Response
  359. if err := json.Unmarshal(body, &result); err != nil {
  360. t.Fatalf("JSON 解析失败: %v | Body: %s", err, truncate(string(body), 300))
  361. }
  362. if result.Code != expectCode {
  363. t.Errorf("code 不匹配: 期望 %d, 实际 %d | Msg: %s", expectCode, result.Code, result.Msg)
  364. }
  365. }
  366. // ParseDataMap 解析响应 data 为 map
  367. func ParseDataMap(body []byte) map[string]interface{} {
  368. var result response.Response
  369. json.Unmarshal(body, &result)
  370. m, _ := result.Data.(map[string]interface{})
  371. return m
  372. }
  373. // ParseDataList 解析响应 data 为列表
  374. func ParseDataList(body []byte) []interface{} {
  375. var result response.Response
  376. json.Unmarshal(body, &result)
  377. list, _ := result.Data.([]interface{})
  378. return list
  379. }
  380. // ParsePageList 解析分页响应 data.list(如 {"total":N,"list":[...]})
  381. func ParsePageList(body []byte) []interface{} {
  382. var result response.Response
  383. json.Unmarshal(body, &result)
  384. m, ok := result.Data.(map[string]interface{})
  385. if !ok {
  386. return nil
  387. }
  388. rawList, exists := m["list"]
  389. if !exists {
  390. return nil
  391. }
  392. list, ok := rawList.([]interface{})
  393. if !ok {
  394. return nil
  395. }
  396. return list
  397. }
  398. func truncate(s string, n int) string {
  399. if len(s) <= n {
  400. return s
  401. }
  402. return s[:n] + "..."
  403. }
  404. // mapKeys returns the keys of a map for diagnostic purposes
  405. func mapKeys(m map[string]interface{}) []string {
  406. keys := make([]string, 0, len(m))
  407. for k := range m {
  408. keys = append(keys, k)
  409. }
  410. return keys
  411. }
  412. // ParsePageList 解析分页响应 data.list