e2e_edge_cases_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. package testutil
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "testing"
  7. "time"
  8. "wails-app/internal/dao"
  9. "wails-app/internal/global"
  10. "wails-app/internal/model/common/response"
  11. )
  12. // TestVehicleReEntry 同一车辆重复入场应被拦截
  13. func TestVehicleReEntry(t *testing.T) {
  14. env := InitTestEnv(t)
  15. createVehicle(t, env, "JingR00001", "RFID_REENTRY", 1)
  16. entryResp, entryBody := env.DoPost("/vehicle/entry", map[string]interface{}{
  17. "plate_number": "JingR00001", "rfid_tag": "RFID_REENTRY",
  18. "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
  19. })
  20. AssertResponse(t, entryResp, entryBody, http.StatusOK, response.SUCCESS)
  21. _, entryBody2 := env.DoPost("/vehicle/entry", map[string]interface{}{
  22. "plate_number": "JingR00001", "rfid_tag": "RFID_REENTRY",
  23. "parking_lot_id": 1, "parking_space_id": 2, "entry_image": "test2.jpg",
  24. })
  25. var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
  26. json.Unmarshal(entryBody2, &result)
  27. if result.Code == response.SUCCESS {
  28. t.Fatalf("BUG: Vehicle re-entry was NOT blocked! code=%d msg=%s", result.Code, result.Msg)
  29. }
  30. fmt.Println("PASS: Re-entry attempt correctly rejected")
  31. }
  32. // TestExitWithoutEntry 未入场车辆尝试出场应返回错误
  33. func TestExitWithoutEntry(t *testing.T) {
  34. env := InitTestEnv(t)
  35. _, exitPreviewBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{
  36. "plate_number": "JingZ99999", "rfid_tag": "RFID_NOENTRY",
  37. })
  38. var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
  39. json.Unmarshal(exitPreviewBody, &result)
  40. if result.Code == response.SUCCESS {
  41. t.Fatalf("BUG: Exit preview succeeded for non-entered vehicle!")
  42. }
  43. fmt.Println("PASS: Exit without entry correctly rejected")
  44. }
  45. // TestPaymentInsufficientAmount 支付金额不足时的行为
  46. func TestPaymentInsufficientAmount(t *testing.T) {
  47. env := InitTestEnv(t)
  48. createVehicle(t, env, "JingP00001", "RFID_PAY", 1)
  49. _, _ = env.DoPost("/vehicle/entry", map[string]interface{}{
  50. "plate_number": "JingP00001", "rfid_tag": "RFID_PAY",
  51. "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
  52. })
  53. entryTime := time.Now().Add(-3 * time.Hour)
  54. global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingP00001").Update("entry_time", entryTime)
  55. _, exitConfirmBody := env.DoPost("/vehicle/exit/confirm", map[string]interface{}{
  56. "plate_number": "JingP00001", "rfid_tag": "RFID_PAY",
  57. "payment_method": "cash", "paid_amount": 0.0,
  58. })
  59. var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
  60. json.Unmarshal(exitConfirmBody, &result)
  61. fmt.Printf("INFO: Insufficient payment response: code=%d msg=%s\n", result.Code, result.Msg)
  62. }
  63. // TestVIPExpired 过期 VIP 应恢复普通计费
  64. func TestVIPExpired(t *testing.T) {
  65. env := InitTestEnv(t)
  66. ownerResp, ownerBody := env.DoPost("/owner/create", map[string]interface{}{
  67. "owner_name": "VIP Expired Test", "owner_surname": "VIP",
  68. "owner_phone": "13900000001", "is_vip": true,
  69. "vip_expire_time": time.Now().Add(-24*time.Hour).Format(time.RFC3339),
  70. })
  71. AssertResponse(t, ownerResp, ownerBody, http.StatusOK, response.SUCCESS)
  72. var owner dao.Owner
  73. env.DB.Where("owner_phone = ?", "13900000001").First(&owner)
  74. createVehicleWithOwner(t, env, "JingV00001", "RFID_VIP_EXP", owner.ID)
  75. _, _ = env.DoPost("/vehicle/entry", map[string]interface{}{
  76. "plate_number": "JingV00001", "rfid_tag": "RFID_VIP_EXP",
  77. "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
  78. })
  79. entryTime := time.Now().Add(-3 * time.Hour)
  80. global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingV00001").Update("entry_time", entryTime)
  81. exitResp, exitBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{
  82. "plate_number": "JingV00001", "rfid_tag": "RFID_VIP_EXP",
  83. })
  84. AssertResponse(t, exitResp, exitBody, http.StatusOK, response.SUCCESS)
  85. var result struct {
  86. Data struct {
  87. Fee float64 `json:"fee"`
  88. } `json:"data"`
  89. }
  90. json.Unmarshal(exitBody, &result)
  91. if result.Data.Fee == 0 {
  92. t.Fatalf("BUG: Expired VIP still getting free parking! Fee should be > 0")
  93. }
  94. fmt.Printf("PASS: Expired VIP charged correctly, fee=%.2f\n", result.Data.Fee)
  95. }
  96. // TestFreeDuration 免费时长内不应收费
  97. func TestFreeDuration(t *testing.T) {
  98. env := InitTestEnv(t)
  99. createVehicle(t, env, "JingF00001", "RFID_FREE", 1)
  100. _, _ = env.DoPost("/vehicle/entry", map[string]interface{}{
  101. "plate_number": "JingF00001", "rfid_tag": "RFID_FREE",
  102. "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
  103. })
  104. entryTime := time.Now().Add(-10 * time.Minute)
  105. global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingF00001").Update("entry_time", entryTime)
  106. exitResp, exitBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{
  107. "plate_number": "JingF00001", "rfid_tag": "RFID_FREE",
  108. })
  109. AssertResponse(t, exitResp, exitBody, http.StatusOK, response.SUCCESS)
  110. var preview struct { Fee float64 `json:"fee"` }
  111. json.Unmarshal(exitBody, &preview)
  112. if preview.Fee != 0 {
  113. t.Fatalf("BUG: Vehicle within free duration should have zero fee, got %.2f", preview.Fee)
  114. }
  115. fmt.Println("PASS: Free duration correctly applied")
  116. }
  117. // TestDailyCap 超过每日封顶费用应封顶
  118. func TestDailyCap(t *testing.T) {
  119. env := InitTestEnv(t)
  120. createVehicle(t, env, "JingD00001", "RFID_CAP", 1)
  121. _, _ = env.DoPost("/vehicle/entry", map[string]interface{}{
  122. "plate_number": "JingD00001", "rfid_tag": "RFID_CAP",
  123. "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
  124. })
  125. entryTime := time.Now().Add(-24 * time.Hour)
  126. global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingD00001").Update("entry_time", entryTime)
  127. exitResp, exitBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{
  128. "plate_number": "JingD00001", "rfid_tag": "RFID_CAP",
  129. })
  130. AssertResponse(t, exitResp, exitBody, http.StatusOK, response.SUCCESS)
  131. var preview struct { Fee float64 `json:"fee"` }
  132. json.Unmarshal(exitBody, &preview)
  133. if preview.Fee > 50.0 {
  134. t.Fatalf("BUG: Fee exceeds daily cap! Expected <= 50, got %.2f", preview.Fee)
  135. }
  136. fmt.Printf("PASS: Daily cap applied correctly, fee=%.2f\n", preview.Fee)
  137. }
  138. // TestNoFeeConfig 无收费配置时默认计费
  139. func TestNoFeeConfig(t *testing.T) {
  140. env := InitTestEnv(t)
  141. var normalType dao.VehicleType
  142. env.DB.Where("is_system = ? AND name != ?", true, "临时车").First(&normalType)
  143. if normalType.ID == 0 {
  144. normalType = dao.VehicleType{Name: "特殊车", Remarks: "无收费配置"}
  145. env.DB.Create(&normalType)
  146. }
  147. env.DB.Where("vehicle_type_id = ?", normalType.ID).Delete(&dao.FeeConfig{})
  148. createVehicleWithOwner(t, env, "JingN00001", "RFID_NOFEE", normalType.ID)
  149. _, _ = env.DoPost("/vehicle/entry", map[string]interface{}{
  150. "plate_number": "JingN00001", "rfid_tag": "RFID_NOFEE",
  151. "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
  152. })
  153. entryTime := time.Now().Add(-1 * time.Hour)
  154. global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingN00001").Update("entry_time", entryTime)
  155. exitResp, exitBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{
  156. "plate_number": "JingN00001", "rfid_tag": "RFID_NOFEE",
  157. })
  158. AssertResponse(t, exitResp, exitBody, http.StatusOK, response.SUCCESS)
  159. var result struct {
  160. Data struct {
  161. Fee float64 `json:"fee"`
  162. } `json:"data"`
  163. }
  164. json.Unmarshal(exitBody, &result)
  165. if result.Data.Fee <= 0 {
  166. t.Fatalf("BUG: No fee config should use default rate, got fee=%.2f", result.Data.Fee)
  167. }
  168. fmt.Printf("PASS: Default fee applied correctly, fee=%.2f\n", result.Data.Fee)
  169. }
  170. // TestShortlistByPlateOrRFID 黑白名单按车牌/RFID查询
  171. func TestShortlistByPlateOrRFID(t *testing.T) {
  172. env := InitTestEnv(t)
  173. createVehicle(t, env, "JingS00001", "RFID_FILTER", 1)
  174. var vehicle dao.Vehicle
  175. env.DB.Where("plate_number = ?", "JingS00001").First(&vehicle)
  176. _, _ = env.DoPost("/shortlist/createShortlist", dao.Shortlist{
  177. ListType: "黑名单", VehicleId: int(vehicle.ID), ExpirationTime: nil,
  178. })
  179. resp1, body1 := env.DoPost("/shortlist/queryShortlistList", map[string]interface{}{
  180. "page": 1, "pageSize": 10, "list_type": "", "plate_number": "JingS00001",
  181. })
  182. AssertResponse(t, resp1, body1, http.StatusOK, response.SUCCESS)
  183. list1 := ParsePageList(body1)
  184. if len(list1) < 1 { t.Fatalf("Expected at least 1 shortlist record by plate number") }
  185. fmt.Printf("PASS: Shortlist query by plate number returned %d records\n", len(list1))
  186. resp2, body2 := env.DoPost("/shortlist/queryShortlistList", map[string]interface{}{
  187. "page": 1, "pageSize": 10, "list_type": "", "rfid_tag": "RFID_FILTER",
  188. })
  189. AssertResponse(t, resp2, body2, http.StatusOK, response.SUCCESS)
  190. list2 := ParsePageList(body2)
  191. if len(list2) < 1 { t.Fatalf("Expected at least 1 shortlist record by RFID") }
  192. fmt.Printf("PASS: Shortlist query by RFID returned %d records\n", len(list2))
  193. }
  194. // TestShortlistUpdate 黑白名单更新
  195. func TestShortlistUpdate(t *testing.T) {
  196. env := InitTestEnv(t)
  197. createVehicle(t, env, "JingU00001", "RFID_UPDATE", 1)
  198. var vehicle dao.Vehicle
  199. env.DB.Where("plate_number = ?", "JingU00001").First(&vehicle)
  200. _, _ = env.DoPost("/shortlist/createShortlist", dao.Shortlist{
  201. ListType: "白名单", VehicleId: int(vehicle.ID), ExpirationTime: nil,
  202. })
  203. queryResp, queryBody := env.DoGet("/shortlist/queryAllShortlists", nil)
  204. AssertResponse(t, queryResp, queryBody, http.StatusOK, response.SUCCESS)
  205. var result struct { Data []map[string]interface{} `json:"data"` }
  206. json.Unmarshal(queryBody, &result)
  207. if len(result.Data) == 0 { t.Fatalf("No shortlist found") }
  208. idVal, ok := result.Data[0]["ID"]; if !ok { idVal = result.Data[0]["id"] }
  209. id := int(idVal.(float64))
  210. updateResp, updateBody := env.DoPut("/shortlist/updateShortlist", map[string]interface{}{
  211. "ID": id, "list_type": "黑名单", "vehicle_id": vehicle.ID,
  212. "expiration_time": time.Now().Add(30*24*time.Hour).Format(time.RFC3339),
  213. })
  214. AssertResponse(t, updateResp, updateBody, http.StatusOK, response.SUCCESS)
  215. updatedResp, updatedBody := env.DoGet("/shortlist/queryAllShortlists", nil)
  216. AssertResponse(t, updatedResp, updatedBody, http.StatusOK, response.SUCCESS)
  217. fmt.Println("PASS: Shortlist update successful")
  218. }
  219. // TestShortlistDelete 黑白名单删除
  220. func TestShortlistDelete(t *testing.T) {
  221. env := InitTestEnv(t)
  222. createVehicle(t, env, "JingDel001", "RFID_DELETE", 1)
  223. var vehicle dao.Vehicle
  224. env.DB.Where("plate_number = ?", "JingDel001").First(&vehicle)
  225. _, _ = env.DoPost("/shortlist/createShortlist", dao.Shortlist{
  226. ListType: "黑名单", VehicleId: int(vehicle.ID), ExpirationTime: nil,
  227. })
  228. queryResp, queryBody := env.DoGet("/shortlist/queryAllShortlists", nil)
  229. AssertResponse(t, queryResp, queryBody, http.StatusOK, response.SUCCESS)
  230. var result struct { Data []map[string]interface{} `json:"data"` }
  231. json.Unmarshal(queryBody, &result)
  232. if len(result.Data) == 0 { t.Fatalf("No shortlist found") }
  233. idVal, ok := result.Data[0]["ID"]; if !ok { idVal = result.Data[0]["id"] }
  234. id := int(idVal.(float64))
  235. deleteResp, deleteBody := env.DoDelete(fmt.Sprintf("/shortlist/deleteShortlist?id=%d", id))
  236. AssertResponse(t, deleteResp, deleteBody, http.StatusOK, response.SUCCESS)
  237. fmt.Println("PASS: Shortlist delete successful")
  238. }
  239. // TestOwnerVehicleRelationship 车主关联车辆查询
  240. func TestOwnerVehicleRelationship(t *testing.T) {
  241. env := InitTestEnv(t)
  242. ownerResp, ownerBody := env.DoPost("/owner/create", map[string]interface{}{
  243. "owner_name": "Rel Test Owner", "owner_surname": "REL",
  244. "owner_phone": "13911111111", "is_vip": false, "vip_expire_time": nil,
  245. })
  246. AssertResponse(t, ownerResp, ownerBody, http.StatusOK, response.SUCCESS)
  247. var owner dao.Owner
  248. env.DB.Where("owner_phone = ?", "13911111111").First(&owner)
  249. createVehicleWithOwner(t, env, "JingO00001", "RFID_OWNER", owner.ID)
  250. ownerListResp, ownerListBody := env.DoGet("/owner/list", map[string]string{"page": "1", "page_size": "10"})
  251. AssertResponse(t, ownerListResp, ownerListBody, http.StatusOK, response.SUCCESS)
  252. list := ParsePageList(ownerListBody)
  253. found := false
  254. for _, item := range list {
  255. m, ok := item.(map[string]interface{})
  256. if !ok { continue }
  257. if m["owner_phone"] == "13911111111" { found = true; break }
  258. }
  259. if !found { t.Fatalf("Owner not found in list") }
  260. fmt.Println("PASS: Owner-vehicle relationship verified")
  261. }
  262. // TestParkingLotCapacity 停车场容量检查
  263. func TestParkingLotCapacity(t *testing.T) {
  264. env := InitTestEnv(t)
  265. lotResp, lotBody := env.DoPost("/parking/lot/create", map[string]interface{}{
  266. "lot_code": "TEST_EMPTY", "lot_name": "Empty Lot",
  267. "capacity": 0, "description": "Zero capacity test",
  268. })
  269. AssertResponse(t, lotResp, lotBody, http.StatusOK, response.SUCCESS)
  270. createVehicle(t, env, "JingC00001", "RFID_FULL", 1)
  271. _, entryBody := env.DoPost("/vehicle/entry", map[string]interface{}{
  272. "plate_number": "JingC00001", "rfid_tag": "RFID_FULL",
  273. "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg",
  274. })
  275. var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
  276. json.Unmarshal(entryBody, &result)
  277. fmt.Printf("INFO: Entry into zero-capacity lot: code=%d msg=%s\n", result.Code, result.Msg)
  278. }
  279. // TestBoothCRUD 岗亭CRUD
  280. func TestBoothCRUD(t *testing.T) {
  281. env := InitTestEnv(t)
  282. boothResp, boothBody := env.DoPost("/parking/booth/create", map[string]interface{}{
  283. "booth_code": "BT001", "booth_name": "Test Booth",
  284. "parking_lot_id": 1, "description": "Automated test booth",
  285. })
  286. AssertResponse(t, boothResp, boothBody, http.StatusOK, response.SUCCESS)
  287. listResp, listBody := env.DoGet("/parking/booth/list", map[string]string{"page": "1", "page_size": "10"})
  288. AssertResponse(t, listResp, listBody, http.StatusOK, response.SUCCESS)
  289. list := ParsePageList(listBody)
  290. if len(list) < 1 { t.Fatalf("Booth list is empty") }
  291. fmt.Printf("PASS: Booth CRUD successful, total %d booths\n", len(list))
  292. }
  293. // TestChannelCRUD 通道CRUD
  294. func TestChannelCRUD(t *testing.T) {
  295. env := InitTestEnv(t)
  296. channelResp, channelBody := env.DoPost("/parking/channel/create", map[string]interface{}{
  297. "channel_code": "CH-IN-TEST", "channel_name": "Test Entrance",
  298. "direction": "in", "parking_lot_id": 1, "booth_id": 1,
  299. "allow_temporary": true, "description": "Automated test channel",
  300. })
  301. AssertResponse(t, channelResp, channelBody, http.StatusOK, response.SUCCESS)
  302. listResp, listBody := env.DoGet("/parking/channel/list", map[string]string{"page": "1", "page_size": "10"})
  303. AssertResponse(t, listResp, listBody, http.StatusOK, response.SUCCESS)
  304. list := ParsePageList(listBody)
  305. if len(list) < 1 { t.Fatalf("Channel list is empty") }
  306. fmt.Printf("PASS: Channel CRUD successful, total %d channels\n", len(list))
  307. }
  308. // TestDeviceCRUD 设备CRUD
  309. func TestDeviceCRUD(t *testing.T) {
  310. env := InitTestEnv(t)
  311. deviceResp, deviceBody := env.DoPost("/parking/device/create", map[string]interface{}{
  312. "device_code": "DEV001", "device_name": "Test Device",
  313. "device_type": "TCP", "connect_type": "tcp", "ip_address": "127.0.0.1",
  314. "port": 9000, "is_active": true, "parking_lot_id": 1, "channel_id": 1,
  315. "description": "Automated test device",
  316. })
  317. AssertResponse(t, deviceResp, deviceBody, http.StatusOK, response.SUCCESS)
  318. listResp, listBody := env.DoGet("/parking/device/list", map[string]string{"page": "1", "page_size": "10"})
  319. AssertResponse(t, listResp, listBody, http.StatusOK, response.SUCCESS)
  320. list := ParsePageList(listBody)
  321. if len(list) < 1 { t.Fatalf("Device list is empty") }
  322. fmt.Printf("PASS: Device CRUD successful, total %d devices\n", len(list))
  323. }
  324. // TestVehicleTypeCRUD 自定义车辆类型CRUD
  325. func TestVehicleTypeCRUD(t *testing.T) {
  326. env := InitTestEnv(t)
  327. typeResp, typeBody := env.DoPost("/vehicle/type/create", map[string]interface{}{
  328. "name": "新能源车", "remarks": "测试新能源车辆类型", "is_system": false,
  329. })
  330. AssertResponse(t, typeResp, typeBody, http.StatusOK, response.SUCCESS)
  331. listResp, listBody := env.DoGet("/vehicle/type/all", nil)
  332. AssertResponse(t, listResp, listBody, http.StatusOK, response.SUCCESS)
  333. list := ParseDataList(listBody)
  334. if len(list) < 1 { t.Fatalf("Vehicle type list is empty") }
  335. fmt.Printf("PASS: Vehicle type CRUD successful, total %d types\n", len(list))
  336. }
  337. // ===================== Helper functions =====================
  338. func createVehicleWithOwner(t *testing.T, env *TestEnv, plateNumber, rfidTag string, ownerID uint) {
  339. t.Helper()
  340. _, body := env.DoPost("/vehicle/create", map[string]interface{}{
  341. "plate_number": plateNumber, "vehicle_type_id": 1,
  342. "vehicle_brand": "TestBrand", "vehicle_color": "TestColor",
  343. "rfid_tag": rfidTag, "owner_id": ownerID,
  344. })
  345. var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
  346. json.Unmarshal(body, &result)
  347. if result.Code != response.SUCCESS {
  348. t.Fatalf("Failed to create vehicle with owner: %s", result.Msg)
  349. }
  350. }