|
@@ -10,6 +10,7 @@ import (
|
|
|
"wails-app/internal/dao"
|
|
"wails-app/internal/dao"
|
|
|
"wails-app/internal/global"
|
|
"wails-app/internal/global"
|
|
|
"wails-app/internal/model/common/response"
|
|
"wails-app/internal/model/common/response"
|
|
|
|
|
+ dt_svc "wails-app/internal/modules/digital-ticket/service"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
// TestVehicleReEntry 同一车辆重复入场应被拦截
|
|
// TestVehicleReEntry 同一车辆重复入场应被拦截
|
|
@@ -361,6 +362,419 @@ func TestVehicleTypeCRUD(t *testing.T) {
|
|
|
|
|
|
|
|
// ===================== Helper functions =====================
|
|
// ===================== Helper functions =====================
|
|
|
|
|
|
|
|
|
|
+
|
|
|
|
|
+// TestVehicleUpdate 车辆信息更新
|
|
|
|
|
+func TestVehicleUpdate(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ createVehicle(t, env, "JingUPD001", "RFID_UPDATE_TEST", 1)
|
|
|
|
|
+ var v dao.Vehicle
|
|
|
|
|
+ env.DB.Where("plate_number = ?", "JingUPD001").First(&v)
|
|
|
|
|
+ resp, body := env.DoPut("/vehicle/update", map[string]interface{}{
|
|
|
|
|
+ "ID": v.ID, "plate_number": "JingUPD001", "vehicle_type_id": 1,
|
|
|
|
|
+ "vehicle_brand": "UpdatedBrand",
|
|
|
|
|
+ "vehicle_color": "Blue", "rfid_tag": "RFID_UPDATE_TEST",
|
|
|
|
|
+ })
|
|
|
|
|
+ AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ fmt.Println("PASS: Vehicle update successful")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestVehicleDelete 车辆删除
|
|
|
|
|
+func TestVehicleDelete(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ createVehicle(t, env, "JingDEL001", "RFID_DELETE_TEST", 1)
|
|
|
|
|
+ var v dao.Vehicle
|
|
|
|
|
+ env.DB.Where("plate_number = ?", "JingDEL001").First(&v)
|
|
|
|
|
+ resp, body := env.DoDelete(fmt.Sprintf("/vehicle/delete?id=%d", v.ID))
|
|
|
|
|
+ AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ fmt.Println("PASS: Vehicle delete successful")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestVehicleList 车辆分页列表
|
|
|
|
|
+func TestVehicleList(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ createVehicle(t, env, "JingLST001", "RFID_LIST_TEST", 1)
|
|
|
|
|
+ resp, body := env.DoGet("/vehicle/list", map[string]string{"page": "1", "page_size": "10"})
|
|
|
|
|
+ AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ list := ParsePageList(body)
|
|
|
|
|
+ if len(list) < 1 { t.Fatalf("Vehicle list is empty") }
|
|
|
|
|
+ fmt.Printf("PASS: Vehicle list returned %d records\n", len(list))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestOwnerUpdate 业主信息更新
|
|
|
|
|
+func TestOwnerUpdate(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ ownerResp, ownerBody := env.DoPost("/owner/create", map[string]interface{}{
|
|
|
|
|
+ "owner_name": "Update Test", "owner_surname": "Upd",
|
|
|
|
|
+ "owner_phone": "13900000999", "is_vip": false,
|
|
|
|
|
+ })
|
|
|
|
|
+ AssertResponse(t, ownerResp, ownerBody, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ var o dao.Owner
|
|
|
|
|
+ env.DB.Where("owner_phone = ?", "13900000999").First(&o)
|
|
|
|
|
+ updResp, updBody := env.DoPut("/owner/update", map[string]interface{}{
|
|
|
|
|
+ "ID": o.ID, "owner_name": "Updated Name", "owner_surname": "Upd2",
|
|
|
|
|
+ "owner_phone": "13900000999", "is_vip": true,
|
|
|
|
|
+ })
|
|
|
|
|
+ AssertResponse(t, updResp, updBody, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ fmt.Println("PASS: Owner update successful")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestOwnerGetByPhone 按手机号查询业主
|
|
|
|
|
+func TestOwnerGetByPhone(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ _, _ = env.DoPost("/owner/create", map[string]interface{}{
|
|
|
|
|
+ "owner_name": "Phone Query", "owner_surname": "Ph",
|
|
|
|
|
+ "owner_phone": "13900000888", "is_vip": false,
|
|
|
|
|
+ })
|
|
|
|
|
+ resp, body := env.DoGet("/owner/get-by-phone", map[string]string{"phone": "13900000888"})
|
|
|
|
|
+ AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ fmt.Println("PASS: Owner query by phone successful")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestFeeConfigCreate 收费配置创建
|
|
|
|
|
+func TestFeeConfigCreate(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ // 创建一个新的车辆类型并为其创建收费配置
|
|
|
|
|
+ typeResp, typeBody := env.DoPost("/vehicle/type/create", map[string]interface{}{
|
|
|
|
|
+ "name": "FeeConfigTest", "remarks": "Fee config test type", "is_system": false,
|
|
|
|
|
+ })
|
|
|
|
|
+ AssertResponse(t, typeResp, typeBody, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ var vt2 dao.VehicleType
|
|
|
|
|
+ env.DB.Where("name = ?", "FeeConfigTest").First(&vt2)
|
|
|
|
|
+ resp, body := env.DoPost("/vehicle/fee-config/create", map[string]interface{}{
|
|
|
|
|
+ "vehicle_type_id": vt2.ID, "start_time": 0, "start_fee": 0,
|
|
|
|
|
+ "unit_time": 60, "unit_fee": 10, "daily_max_fee": 80,
|
|
|
|
|
+ })
|
|
|
|
|
+ AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ fmt.Println("PASS: Fee config create successful")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestParkingLotUpdate 停车场信息更新
|
|
|
|
|
+func TestParkingLotUpdate(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ var lot dao.ParkingLot
|
|
|
|
|
+ env.DB.Where("lot_code = ?", "TEST001").First(&lot)
|
|
|
|
|
+ resp, body := env.DoPut("/parking/lot/update", map[string]interface{}{
|
|
|
|
|
+ "ID": lot.ID, "lot_code": "TEST001", "lot_name": "Updated Lot",
|
|
|
|
|
+ "capacity": 200, "description": "Updated desc",
|
|
|
|
|
+ })
|
|
|
|
|
+ AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ fmt.Println("PASS: Parking lot update successful")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// ===================== 月租车管理模块测试 =====================
|
|
|
|
|
+
|
|
|
|
|
+// TestMonthlyCardCreate 办理月卡
|
|
|
|
|
+func TestMonthlyCardCreate(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ createVehicle(t, env, "JingMC001", "RFID_MC001", 1)
|
|
|
|
|
+ var v dao.Vehicle
|
|
|
|
|
+ env.DB.Where("plate_number = ?", "JingMC001").First(&v)
|
|
|
|
|
+
|
|
|
|
|
+ resp, body := env.DoPost("/monthly-card/create", map[string]interface{}{
|
|
|
|
|
+ "vehicle_id": v.ID, "card_type": "month", "fee": 300.0, "remark": "test month card",
|
|
|
|
|
+ })
|
|
|
|
|
+ AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+
|
|
|
|
|
+ var card dao.MonthlyCard
|
|
|
|
|
+ env.DB.Where("vehicle_id = ?", v.ID).First(&card)
|
|
|
|
|
+ if card.ID == 0 { t.Fatalf("Monthly card not created in DB") }
|
|
|
|
|
+ if card.PaymentStatus != "paid" { t.Fatalf("Expected paid, got %s", card.PaymentStatus) }
|
|
|
|
|
+
|
|
|
|
|
+ // Verify shortlist (whitelist) was created
|
|
|
|
|
+ var sl dao.Shortlist
|
|
|
|
|
+ env.DB.Where("vehicle_id = ? AND list_type = ?", v.ID, "白名单").First(&sl)
|
|
|
|
|
+ if sl.ID == 0 { t.Fatalf("Shortlist not created") }
|
|
|
|
|
+
|
|
|
|
|
+ fmt.Println("PASS: Monthly card created successfully")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestMonthlyCardRenew 月卡续费
|
|
|
|
|
+func TestMonthlyCardRenew(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ createVehicle(t, env, "JingMC002", "RFID_MC002", 1)
|
|
|
|
|
+ var v dao.Vehicle
|
|
|
|
|
+ env.DB.Where("plate_number = ?", "JingMC002").First(&v)
|
|
|
|
|
+ _, _ = env.DoPost("/monthly-card/create", map[string]interface{}{
|
|
|
|
|
+ "vehicle_id": v.ID, "card_type": "month", "fee": 300.0,
|
|
|
|
|
+ })
|
|
|
|
|
+ var card dao.MonthlyCard
|
|
|
|
|
+ env.DB.Where("vehicle_id = ?", v.ID).First(&card)
|
|
|
|
|
+ origEnd := card.EndDate
|
|
|
|
|
+
|
|
|
|
|
+ resp, body := env.DoPost("/monthly-card/renew", map[string]interface{}{
|
|
|
|
|
+ "card_id": card.ID, "card_type": "month", "fee": 300.0,
|
|
|
|
|
+ })
|
|
|
|
|
+ AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+
|
|
|
|
|
+ env.DB.Where("vehicle_id = ?", v.ID).First(&card)
|
|
|
|
|
+ expectedEnd := origEnd.AddDate(0, 0, 30)
|
|
|
|
|
+ if !card.EndDate.Equal(expectedEnd) && !card.EndDate.After(expectedEnd.Add(-time.Hour*23)) {
|
|
|
|
|
+ t.Fatalf("EndDate not extended correctly: orig=%v, new=%v", origEnd, card.EndDate)
|
|
|
|
|
+ }
|
|
|
|
|
+ fmt.Println("PASS: Monthly card renewed successfully")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestMonthlyCardRefund 月卡退卡
|
|
|
|
|
+func TestMonthlyCardRefund(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ createVehicle(t, env, "JingMC003", "RFID_MC003", 1)
|
|
|
|
|
+ var v dao.Vehicle
|
|
|
|
|
+ env.DB.Where("plate_number = ?", "JingMC003").First(&v)
|
|
|
|
|
+ _, _ = env.DoPost("/monthly-card/create", map[string]interface{}{
|
|
|
|
|
+ "vehicle_id": v.ID, "card_type": "month", "fee": 300.0,
|
|
|
|
|
+ })
|
|
|
|
|
+ var card dao.MonthlyCard
|
|
|
|
|
+ env.DB.Where("vehicle_id = ?", v.ID).First(&card)
|
|
|
|
|
+
|
|
|
|
|
+ resp, body := env.DoDelete(fmt.Sprintf("/monthly-card/refund?id=%d", card.ID))
|
|
|
|
|
+ AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+
|
|
|
|
|
+ env.DB.First(&card, card.ID)
|
|
|
|
|
+ if card.PaymentStatus != "refunded" { t.Fatalf("Expected refunded, got %s", card.PaymentStatus) }
|
|
|
|
|
+
|
|
|
|
|
+ var count int64
|
|
|
|
|
+ env.DB.Model(&dao.Shortlist{}).Where("vehicle_id = ? AND list_type = ?", v.ID, "白名单").Count(&count)
|
|
|
|
|
+ if count != 0 { t.Fatalf("Shortlist should be removed after refund, found %d", count) }
|
|
|
|
|
+
|
|
|
|
|
+ fmt.Println("PASS: Monthly card refunded successfully")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestMonthlyCardList 月卡列表查询
|
|
|
|
|
+func TestMonthlyCardList(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ createVehicle(t, env, "JingMC004", "RFID_MC004", 1)
|
|
|
|
|
+ var v dao.Vehicle
|
|
|
|
|
+ env.DB.Where("plate_number = ?", "JingMC004").First(&v)
|
|
|
|
|
+ _, _ = env.DoPost("/monthly-card/create", map[string]interface{}{
|
|
|
|
|
+ "vehicle_id": v.ID, "card_type": "quarter", "fee": 800.0,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ resp, body := env.DoGet("/monthly-card/list", map[string]string{"page": "1", "page_size": "10"})
|
|
|
|
|
+ AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ list := ParsePageList(body)
|
|
|
|
|
+ if len(list) < 1 { t.Fatalf("Monthly card list is empty") }
|
|
|
|
|
+
|
|
|
|
|
+ resp, body2 := env.DoGet("/monthly-card/list", map[string]string{
|
|
|
|
|
+ "page": "1", "page_size": "10", "card_type": "quarter",
|
|
|
|
|
+ })
|
|
|
|
|
+ AssertResponse(t, resp, body2, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ list2 := ParsePageList(body2)
|
|
|
|
|
+ if len(list2) < 1 { t.Fatalf("Filtered list by card_type is empty") }
|
|
|
|
|
+
|
|
|
|
|
+ fmt.Printf("PASS: Monthly card list returned %d records\\n", len(list))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestMonthlyCardDuplicate 重复办卡拦截
|
|
|
|
|
+func TestMonthlyCardDuplicate(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ createVehicle(t, env, "JingMC005", "RFID_MC005", 1)
|
|
|
|
|
+ var v dao.Vehicle
|
|
|
|
|
+ env.DB.Where("plate_number = ?", "JingMC005").First(&v)
|
|
|
|
|
+
|
|
|
|
|
+ _, _ = env.DoPost("/monthly-card/create", map[string]interface{}{
|
|
|
|
|
+ "vehicle_id": v.ID, "card_type": "month", "fee": 300.0,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ _, body2 := env.DoPost("/monthly-card/create", map[string]interface{}{
|
|
|
|
|
+ "vehicle_id": v.ID, "card_type": "year", "fee": 3000.0,
|
|
|
|
|
+ })
|
|
|
|
|
+ var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
|
|
|
|
|
+ json.Unmarshal(body2, &result)
|
|
|
|
|
+ if result.Code == response.SUCCESS {
|
|
|
|
|
+ t.Fatalf("BUG: Duplicate monthly card creation was NOT blocked!")
|
|
|
|
|
+ }
|
|
|
|
|
+ fmt.Printf("PASS: Duplicate card creation correctly rejected: %s\\n", result.Msg)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestMonthlyCardNonexistentRefund 不存在的月卡退卡
|
|
|
|
|
+func TestMonthlyCardNonexistentRefund(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ _, body := env.DoDelete("/monthly-card/refund?id=99999")
|
|
|
|
|
+ // Should fail with error code
|
|
|
|
|
+ var result struct { Code int `json:"code"`; Msg string `json:"msg"` }
|
|
|
|
|
+ json.Unmarshal(body, &result)
|
|
|
|
|
+ if result.Code == response.SUCCESS {
|
|
|
|
|
+ t.Fatalf("BUG: Refunding non-existent card was NOT blocked!")
|
|
|
|
|
+ }
|
|
|
|
|
+ // HTTP status may vary, just check code != 0
|
|
|
|
|
+ fmt.Printf("PASS: Non-existent card refund correctly rejected: code=%d msg=%s\\n", result.Code, result.Msg)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestMonthlyCardDifferentTypes 不同套餐类型
|
|
|
|
|
+func TestMonthlyCardDifferentTypes(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ createVehicle(t, env, "JingMC006", "RFID_MC006", 1)
|
|
|
|
|
+ var v dao.Vehicle
|
|
|
|
|
+ env.DB.Where("plate_number = ?", "JingMC006").First(&v)
|
|
|
|
|
+
|
|
|
|
|
+ resp, body := env.DoPost("/monthly-card/create", map[string]interface{}{
|
|
|
|
|
+ "vehicle_id": v.ID, "card_type": "year", "fee": 3000.0,
|
|
|
|
|
+ })
|
|
|
|
|
+ AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ var card dao.MonthlyCard
|
|
|
|
|
+ env.DB.Where("vehicle_id = ?", v.ID).First(&card)
|
|
|
|
|
+
|
|
|
|
|
+ duration := card.EndDate.Sub(card.StartDate).Hours() / 24
|
|
|
|
|
+ if duration < 360 || duration > 370 {
|
|
|
|
|
+ t.Fatalf("Year card should be ~365 days, got %.0f days", duration)
|
|
|
|
|
+ }
|
|
|
|
|
+ fmt.Printf("PASS: Year card duration = %.0f days\\n", duration)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// ===================== 数字票模块测试 =====================
|
|
|
|
|
+
|
|
|
|
|
+func createTicket(t *testing.T, env *TestEnv, plate, trigger string) (string, uint) {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ createVehicle(t, env, plate, "RFID_"+plate, 1)
|
|
|
|
|
+ var v dao.Vehicle
|
|
|
|
|
+ env.DB.Where("plate_number = ?", plate).First(&v)
|
|
|
|
|
+ rec := dao.VehicleRecord{PlateNumber: plate, RFIDTag: "RFID_" + plate, EntryTime: time.Now(), ParkingLotID: 1}
|
|
|
|
|
+ env.DB.Create(&rec)
|
|
|
|
|
+ var svc = new(dt_svc.TicketService)
|
|
|
|
|
+ ticket, err := svc.Create(plate, trigger, rec.ID)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("Failed to create ticket: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ return ticket.TicketNo, ticket.ID
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestDigitalTicketDetail DT01: 查询数字票详情
|
|
|
|
|
+func TestDigitalTicketDetail(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ ticketNo, _ := createTicket(t, env, "DT001", "manual")
|
|
|
|
|
+ resp, body := env.DoGet("/digital-ticket/"+ticketNo, nil)
|
|
|
|
|
+ AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ data := ParseDataMap(body)
|
|
|
|
|
+ if data["state"] != "pending_payment" {
|
|
|
|
|
+ t.Fatalf("Expected state=pending_payment, got %v", data["state"])
|
|
|
|
|
+ }
|
|
|
|
|
+ if data["ticket_no"] != ticketNo {
|
|
|
|
|
+ t.Fatalf("TicketNo mismatch")
|
|
|
|
|
+ }
|
|
|
|
|
+ if data["trigger_mode"] != "manual" {
|
|
|
|
|
+ t.Fatalf("TriggerMode mismatch")
|
|
|
|
|
+ }
|
|
|
|
|
+ fmt.Println("PASS: Digital ticket detail query successful")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestDigitalTicketPay DT02: 支付数字票
|
|
|
|
|
+// BUG: paid_amount 字段在 digital_ticket 表中不存在,PayTicket API 会报 SQL 错误
|
|
|
|
|
+// 预期: 该测试应失败,暴露后端缺陷
|
|
|
|
|
+func TestDigitalTicketPay(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ ticketNo, _ := createTicket(t, env, "DT002", "plate_recognition")
|
|
|
|
|
+ resp, body := env.DoPost("/digital-ticket/"+ticketNo+"/pay", map[string]interface{}{
|
|
|
|
|
+ "payment_method": "cash", "payment_order_no": "PO002", "paid_amount": 20.0,
|
|
|
|
|
+ })
|
|
|
|
|
+ AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ fmt.Println("PASS: Pay API works correctly")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestDigitalTicketExit DT03: 离场验证
|
|
|
|
|
+func TestDigitalTicketExit(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ ticketNo, _ := createTicket(t, env, "DT003", "rfid")
|
|
|
|
|
+ // 支付
|
|
|
|
|
+ _, _ = env.DoPost("/digital-ticket/"+ticketNo+"/pay", map[string]interface{}{
|
|
|
|
|
+ "payment_method": "cash", "payment_order_no": "PO003", "paid_amount": 15.0,
|
|
|
|
|
+ })
|
|
|
|
|
+ // 支付成功后离场
|
|
|
|
|
+ resp, body := env.DoPost("/digital-ticket/"+ticketNo+"/exit", nil)
|
|
|
|
|
+ AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ fmt.Println("PASS: Pay → Exit flow works correctly")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestDigitalTicketList DT04: 数字票列表
|
|
|
|
|
+func TestDigitalTicketList(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ createTicket(t, env, "DT004", "manual")
|
|
|
|
|
+ resp, body := env.DoGet("/digital-ticket/list", map[string]string{"page": "1", "page_size": "10"})
|
|
|
|
|
+ AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ list := ParsePageList(body)
|
|
|
|
|
+ if len(list) < 1 {
|
|
|
|
|
+ t.Fatalf("Ticket list is empty")
|
|
|
|
|
+ }
|
|
|
|
|
+ fmt.Printf("PASS: Digital ticket list returned %d records\n", len(list))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestDigitalTicketFullFlow DT05: 完整流程
|
|
|
|
|
+// BUG: Pay API 因 paid_amount 缺失而失败,完整流程无法通过 pay API 完成
|
|
|
|
|
+// 测试验证: exit API 本身在状态正确时可正常工作
|
|
|
|
|
+func TestDigitalTicketFullFlow(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ ticketNo, _ := createTicket(t, env, "DT005", "manual")
|
|
|
|
|
+ // Pay
|
|
|
|
|
+ resp1, body1 := env.DoPost("/digital-ticket/"+ticketNo+"/pay", map[string]interface{}{
|
|
|
|
|
+ "payment_method": "cash", "payment_order_no": "PO005", "paid_amount": 25.0,
|
|
|
|
|
+ })
|
|
|
|
|
+ AssertResponse(t, resp1, body1, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ // Exit
|
|
|
|
|
+ resp2, body2 := env.DoPost("/digital-ticket/"+ticketNo+"/exit", nil)
|
|
|
|
|
+ AssertResponse(t, resp2, body2, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ // Verify state
|
|
|
|
|
+ _, getBody := env.DoGet("/digital-ticket/"+ticketNo, nil)
|
|
|
|
|
+ data := ParseDataMap(getBody)
|
|
|
|
|
+ if data["state"] != "exited" {
|
|
|
|
|
+ t.Fatalf("Expected exited, got state=%v", data["state"])
|
|
|
|
|
+ }
|
|
|
|
|
+ fmt.Println("PASS: Full flow (pay → exit) works correctly")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestDigitalTicketInvalidExit DT06: 未支付直接离场被拒
|
|
|
|
|
+func TestDigitalTicketInvalidExit(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ ticketNo, _ := createTicket(t, env, "DT006", "manual")
|
|
|
|
|
+ _, body := env.DoPost("/digital-ticket/"+ticketNo+"/exit", nil)
|
|
|
|
|
+ var result struct{ Code int `json:"code"`; Msg string `json:"msg"` }
|
|
|
|
|
+ json.Unmarshal(body, &result)
|
|
|
|
|
+ if result.Code == response.SUCCESS {
|
|
|
|
|
+ t.Fatalf("BUG: Exit without payment was NOT blocked!")
|
|
|
|
|
+ }
|
|
|
|
|
+ fmt.Printf("PASS: Exit without payment correctly rejected: %s\n", result.Msg)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestDigitalTicketNonexistent DT08: 不存在的票号
|
|
|
|
|
+func TestDigitalTicketNonexistent(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ _, body := env.DoGet("/digital-ticket/FAKE", nil)
|
|
|
|
|
+ var result struct{ Code int `json:"code"`; Msg string `json:"msg"` }
|
|
|
|
|
+ json.Unmarshal(body, &result)
|
|
|
|
|
+ if result.Code == response.SUCCESS {
|
|
|
|
|
+ t.Fatalf("BUG: Non-existent ticket returned success!")
|
|
|
|
|
+ }
|
|
|
|
|
+ fmt.Printf("PASS: Non-existent ticket correctly rejected: %s\n", result.Msg)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestDigitalTicketListFilter DT09+10: 列表过滤
|
|
|
|
|
+func TestDigitalTicketListFilter(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ createTicket(t, env, "DTFILTER", "manual")
|
|
|
|
|
+ resp, body := env.DoGet("/digital-ticket/list", map[string]string{
|
|
|
|
|
+ "page": "1", "page_size": "10", "plate_number": "DTFILTER",
|
|
|
|
|
+ })
|
|
|
|
|
+ AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ list := ParsePageList(body)
|
|
|
|
|
+ if len(list) < 1 {
|
|
|
|
|
+ t.Fatalf("Filter by plate returned empty")
|
|
|
|
|
+ }
|
|
|
|
|
+ fmt.Printf("PASS: Ticket list filter returned %d records\n", len(list))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestDigitalTicketEmptyList DT13: 空列表
|
|
|
|
|
+func TestDigitalTicketListPagination(t *testing.T) {
|
|
|
|
|
+ env := InitTestEnv(t)
|
|
|
|
|
+ createTicket(t, env, "DTPAGE", "manual")
|
|
|
|
|
+ resp, body := env.DoGet("/digital-ticket/list", map[string]string{"page": "1", "page_size": "5"})
|
|
|
|
|
+ AssertResponse(t, resp, body, http.StatusOK, response.SUCCESS)
|
|
|
|
|
+ list := ParsePageList(body)
|
|
|
|
|
+ if len(list) < 1 {
|
|
|
|
|
+ t.Fatalf("Paginated list is empty")
|
|
|
|
|
+ }
|
|
|
|
|
+ fmt.Printf("PASS: Ticket list pagination returned %d records\n", len(list))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
func createVehicleWithOwner(t *testing.T, env *TestEnv, plateNumber, rfidTag string, ownerID uint) {
|
|
func createVehicleWithOwner(t *testing.T, env *TestEnv, plateNumber, rfidTag string, ownerID uint) {
|
|
|
t.Helper()
|
|
t.Helper()
|
|
|
_, body := env.DoPost("/vehicle/create", map[string]interface{}{
|
|
_, body := env.DoPost("/vehicle/create", map[string]interface{}{
|