package testutil import ( "encoding/json" "fmt" "net/http" "testing" "time" "wails-app/internal/dao" "wails-app/internal/global" "wails-app/internal/model/common/response" dt_svc "wails-app/internal/modules/digital-ticket/service" ) // TestVehicleReEntry 同一车辆重复入场应被拦截 func TestVehicleReEntry(t *testing.T) { env := InitTestEnv(t) createVehicle(t, env, "JingR00001", "RFID_REENTRY", 1) entryResp, entryBody := env.DoPost("/vehicle/entry", map[string]interface{}{ "plate_number": "JingR00001", "rfid_tag": "RFID_REENTRY", "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg", }) AssertResponse(t, entryResp, entryBody, http.StatusOK, response.SUCCESS) _, entryBody2 := env.DoPost("/vehicle/entry", map[string]interface{}{ "plate_number": "JingR00001", "rfid_tag": "RFID_REENTRY", "parking_lot_id": 1, "parking_space_id": 2, "entry_image": "test2.jpg", }) var result struct { Code int `json:"code"`; Msg string `json:"msg"` } json.Unmarshal(entryBody2, &result) if result.Code == response.SUCCESS { t.Fatalf("BUG: Vehicle re-entry was NOT blocked! code=%d msg=%s", result.Code, result.Msg) } fmt.Println("PASS: Re-entry attempt correctly rejected") } // TestExitWithoutEntry 未入场车辆尝试出场应返回错误 func TestExitWithoutEntry(t *testing.T) { env := InitTestEnv(t) _, exitPreviewBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{ "plate_number": "JingZ99999", "rfid_tag": "RFID_NOENTRY", }) var result struct { Code int `json:"code"`; Msg string `json:"msg"` } json.Unmarshal(exitPreviewBody, &result) if result.Code == response.SUCCESS { t.Fatalf("BUG: Exit preview succeeded for non-entered vehicle!") } fmt.Println("PASS: Exit without entry correctly rejected") } // TestPaymentInsufficientAmount 支付金额不足时的行为 func TestPaymentInsufficientAmount(t *testing.T) { env := InitTestEnv(t) createVehicle(t, env, "JingP00001", "RFID_PAY", 1) _, _ = env.DoPost("/vehicle/entry", map[string]interface{}{ "plate_number": "JingP00001", "rfid_tag": "RFID_PAY", "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg", }) entryTime := time.Now().Add(-3 * time.Hour) global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingP00001").Update("entry_time", entryTime) _, exitConfirmBody := env.DoPost("/vehicle/exit/confirm", map[string]interface{}{ "plate_number": "JingP00001", "rfid_tag": "RFID_PAY", "payment_method": "cash", "paid_amount": 0.0, }) var result struct { Code int `json:"code"`; Msg string `json:"msg"` } json.Unmarshal(exitConfirmBody, &result) fmt.Printf("INFO: Insufficient payment response: code=%d msg=%s\n", result.Code, result.Msg) } // TestVIPExpired 过期 VIP 应恢复普通计费 func TestVIPExpired(t *testing.T) { env := InitTestEnv(t) ownerResp, ownerBody := env.DoPost("/owner/create", map[string]interface{}{ "owner_name": "VIP Expired Test", "owner_surname": "VIP", "owner_phone": "13900000001", "is_vip": true, "vip_expire_time": time.Now().Add(-24*time.Hour).Format(time.RFC3339), }) AssertResponse(t, ownerResp, ownerBody, http.StatusOK, response.SUCCESS) var owner dao.Owner env.DB.Where("owner_phone = ?", "13900000001").First(&owner) createVehicleWithOwner(t, env, "JingV00001", "RFID_VIP_EXP", owner.ID) _, _ = env.DoPost("/vehicle/entry", map[string]interface{}{ "plate_number": "JingV00001", "rfid_tag": "RFID_VIP_EXP", "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg", }) entryTime := time.Now().Add(-3 * time.Hour) global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingV00001").Update("entry_time", entryTime) exitResp, exitBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{ "plate_number": "JingV00001", "rfid_tag": "RFID_VIP_EXP", }) AssertResponse(t, exitResp, exitBody, http.StatusOK, response.SUCCESS) var result struct { Data struct { Fee float64 `json:"fee"` } `json:"data"` } json.Unmarshal(exitBody, &result) if result.Data.Fee == 0 { t.Fatalf("BUG: Expired VIP still getting free parking! Fee should be > 0") } fmt.Printf("PASS: Expired VIP charged correctly, fee=%.2f\n", result.Data.Fee) } // TestFreeDuration 免费时长内不应收费 func TestFreeDuration(t *testing.T) { env := InitTestEnv(t) createVehicle(t, env, "JingF00001", "RFID_FREE", 1) _, _ = env.DoPost("/vehicle/entry", map[string]interface{}{ "plate_number": "JingF00001", "rfid_tag": "RFID_FREE", "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg", }) entryTime := time.Now().Add(-10 * time.Minute) global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingF00001").Update("entry_time", entryTime) exitResp, exitBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{ "plate_number": "JingF00001", "rfid_tag": "RFID_FREE", }) AssertResponse(t, exitResp, exitBody, http.StatusOK, response.SUCCESS) var preview struct { Fee float64 `json:"fee"` } json.Unmarshal(exitBody, &preview) if preview.Fee != 0 { t.Fatalf("BUG: Vehicle within free duration should have zero fee, got %.2f", preview.Fee) } fmt.Println("PASS: Free duration correctly applied") } // TestDailyCap 超过每日封顶费用应封顶 func TestDailyCap(t *testing.T) { env := InitTestEnv(t) createVehicle(t, env, "JingD00001", "RFID_CAP", 1) _, _ = env.DoPost("/vehicle/entry", map[string]interface{}{ "plate_number": "JingD00001", "rfid_tag": "RFID_CAP", "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg", }) entryTime := time.Now().Add(-24 * time.Hour) global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingD00001").Update("entry_time", entryTime) exitResp, exitBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{ "plate_number": "JingD00001", "rfid_tag": "RFID_CAP", }) AssertResponse(t, exitResp, exitBody, http.StatusOK, response.SUCCESS) var preview struct { Fee float64 `json:"fee"` } json.Unmarshal(exitBody, &preview) if preview.Fee > 50.0 { t.Fatalf("BUG: Fee exceeds daily cap! Expected <= 50, got %.2f", preview.Fee) } fmt.Printf("PASS: Daily cap applied correctly, fee=%.2f\n", preview.Fee) } // TestNoFeeConfig 无收费配置时默认计费 func TestNoFeeConfig(t *testing.T) { env := InitTestEnv(t) var normalType dao.VehicleType env.DB.Where("is_system = ? AND name != ?", true, "临时车").First(&normalType) if normalType.ID == 0 { normalType = dao.VehicleType{Name: "特殊车", Remarks: "无收费配置"} env.DB.Create(&normalType) } env.DB.Where("vehicle_type_id = ?", normalType.ID).Delete(&dao.FeeConfig{}) createVehicleWithOwner(t, env, "JingN00001", "RFID_NOFEE", normalType.ID) _, _ = env.DoPost("/vehicle/entry", map[string]interface{}{ "plate_number": "JingN00001", "rfid_tag": "RFID_NOFEE", "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg", }) entryTime := time.Now().Add(-1 * time.Hour) global.GVA_DB.Model(&dao.VehicleRecord{}).Where("plate_number = ?", "JingN00001").Update("entry_time", entryTime) exitResp, exitBody := env.DoPost("/vehicle/exit/preview", map[string]interface{}{ "plate_number": "JingN00001", "rfid_tag": "RFID_NOFEE", }) AssertResponse(t, exitResp, exitBody, http.StatusOK, response.SUCCESS) var result struct { Data struct { Fee float64 `json:"fee"` } `json:"data"` } json.Unmarshal(exitBody, &result) if result.Data.Fee <= 0 { t.Fatalf("BUG: No fee config should use default rate, got fee=%.2f", result.Data.Fee) } fmt.Printf("PASS: Default fee applied correctly, fee=%.2f\n", result.Data.Fee) } // TestShortlistByPlateOrRFID 黑白名单按车牌/RFID查询 func TestShortlistByPlateOrRFID(t *testing.T) { env := InitTestEnv(t) createVehicle(t, env, "JingS00001", "RFID_FILTER", 1) var vehicle dao.Vehicle env.DB.Where("plate_number = ?", "JingS00001").First(&vehicle) _, _ = env.DoPost("/shortlist/createShortlist", dao.Shortlist{ ListType: "黑名单", VehicleId: int(vehicle.ID), ExpirationTime: nil, }) resp1, body1 := env.DoPost("/shortlist/queryShortlistList", map[string]interface{}{ "page": 1, "pageSize": 10, "list_type": "", "plate_number": "JingS00001", }) AssertResponse(t, resp1, body1, http.StatusOK, response.SUCCESS) list1 := ParsePageList(body1) if len(list1) < 1 { t.Fatalf("Expected at least 1 shortlist record by plate number") } fmt.Printf("PASS: Shortlist query by plate number returned %d records\n", len(list1)) resp2, body2 := env.DoPost("/shortlist/queryShortlistList", map[string]interface{}{ "page": 1, "pageSize": 10, "list_type": "", "rfid_tag": "RFID_FILTER", }) AssertResponse(t, resp2, body2, http.StatusOK, response.SUCCESS) list2 := ParsePageList(body2) if len(list2) < 1 { t.Fatalf("Expected at least 1 shortlist record by RFID") } fmt.Printf("PASS: Shortlist query by RFID returned %d records\n", len(list2)) } // TestShortlistUpdate 黑白名单更新 func TestShortlistUpdate(t *testing.T) { env := InitTestEnv(t) createVehicle(t, env, "JingU00001", "RFID_UPDATE", 1) var vehicle dao.Vehicle env.DB.Where("plate_number = ?", "JingU00001").First(&vehicle) _, _ = env.DoPost("/shortlist/createShortlist", dao.Shortlist{ ListType: "白名单", VehicleId: int(vehicle.ID), ExpirationTime: nil, }) queryResp, queryBody := env.DoGet("/shortlist/queryAllShortlists", nil) AssertResponse(t, queryResp, queryBody, http.StatusOK, response.SUCCESS) var result struct { Data []map[string]interface{} `json:"data"` } json.Unmarshal(queryBody, &result) if len(result.Data) == 0 { t.Fatalf("No shortlist found") } idVal, ok := result.Data[0]["ID"]; if !ok { idVal = result.Data[0]["id"] } id := int(idVal.(float64)) updateResp, updateBody := env.DoPut("/shortlist/updateShortlist", map[string]interface{}{ "ID": id, "list_type": "黑名单", "vehicle_id": vehicle.ID, "expiration_time": time.Now().Add(30*24*time.Hour).Format(time.RFC3339), }) AssertResponse(t, updateResp, updateBody, http.StatusOK, response.SUCCESS) updatedResp, updatedBody := env.DoGet("/shortlist/queryAllShortlists", nil) AssertResponse(t, updatedResp, updatedBody, http.StatusOK, response.SUCCESS) fmt.Println("PASS: Shortlist update successful") } // TestShortlistDelete 黑白名单删除 func TestShortlistDelete(t *testing.T) { env := InitTestEnv(t) createVehicle(t, env, "JingDel001", "RFID_DELETE", 1) var vehicle dao.Vehicle env.DB.Where("plate_number = ?", "JingDel001").First(&vehicle) _, _ = env.DoPost("/shortlist/createShortlist", dao.Shortlist{ ListType: "黑名单", VehicleId: int(vehicle.ID), ExpirationTime: nil, }) queryResp, queryBody := env.DoGet("/shortlist/queryAllShortlists", nil) AssertResponse(t, queryResp, queryBody, http.StatusOK, response.SUCCESS) var result struct { Data []map[string]interface{} `json:"data"` } json.Unmarshal(queryBody, &result) if len(result.Data) == 0 { t.Fatalf("No shortlist found") } idVal, ok := result.Data[0]["ID"]; if !ok { idVal = result.Data[0]["id"] } id := int(idVal.(float64)) deleteResp, deleteBody := env.DoDelete(fmt.Sprintf("/shortlist/deleteShortlist?id=%d", id)) AssertResponse(t, deleteResp, deleteBody, http.StatusOK, response.SUCCESS) fmt.Println("PASS: Shortlist delete successful") } // TestOwnerVehicleRelationship 车主关联车辆查询 func TestOwnerVehicleRelationship(t *testing.T) { env := InitTestEnv(t) ownerResp, ownerBody := env.DoPost("/owner/create", map[string]interface{}{ "owner_name": "Rel Test Owner", "owner_surname": "REL", "owner_phone": "13911111111", "is_vip": false, "vip_expire_time": nil, }) AssertResponse(t, ownerResp, ownerBody, http.StatusOK, response.SUCCESS) var owner dao.Owner env.DB.Where("owner_phone = ?", "13911111111").First(&owner) createVehicleWithOwner(t, env, "JingO00001", "RFID_OWNER", owner.ID) ownerListResp, ownerListBody := env.DoGet("/owner/list", map[string]string{"page": "1", "page_size": "10"}) AssertResponse(t, ownerListResp, ownerListBody, http.StatusOK, response.SUCCESS) list := ParsePageList(ownerListBody) found := false for _, item := range list { m, ok := item.(map[string]interface{}) if !ok { continue } if m["owner_phone"] == "13911111111" { found = true; break } } if !found { t.Fatalf("Owner not found in list") } fmt.Println("PASS: Owner-vehicle relationship verified") } // TestParkingLotCapacity 停车场容量检查 func TestParkingLotCapacity(t *testing.T) { env := InitTestEnv(t) lotResp, lotBody := env.DoPost("/parking/lot/create", map[string]interface{}{ "lot_code": "TEST_EMPTY", "lot_name": "Empty Lot", "capacity": 0, "description": "Zero capacity test", }) AssertResponse(t, lotResp, lotBody, http.StatusOK, response.SUCCESS) createVehicle(t, env, "JingC00001", "RFID_FULL", 1) _, entryBody := env.DoPost("/vehicle/entry", map[string]interface{}{ "plate_number": "JingC00001", "rfid_tag": "RFID_FULL", "parking_lot_id": 1, "parking_space_id": 1, "entry_image": "test.jpg", }) var result struct { Code int `json:"code"`; Msg string `json:"msg"` } json.Unmarshal(entryBody, &result) fmt.Printf("INFO: Entry into zero-capacity lot: code=%d msg=%s\n", result.Code, result.Msg) } // TestBoothCRUD 岗亭CRUD func TestBoothCRUD(t *testing.T) { env := InitTestEnv(t) boothResp, boothBody := env.DoPost("/parking/booth/create", map[string]interface{}{ "booth_code": "BT001", "booth_name": "Test Booth", "parking_lot_id": 1, "description": "Automated test booth", }) AssertResponse(t, boothResp, boothBody, http.StatusOK, response.SUCCESS) listResp, listBody := env.DoGet("/parking/booth/list", map[string]string{"page": "1", "page_size": "10"}) AssertResponse(t, listResp, listBody, http.StatusOK, response.SUCCESS) list := ParsePageList(listBody) if len(list) < 1 { t.Fatalf("Booth list is empty") } fmt.Printf("PASS: Booth CRUD successful, total %d booths\n", len(list)) } // TestChannelCRUD 通道CRUD func TestChannelCRUD(t *testing.T) { env := InitTestEnv(t) channelResp, channelBody := env.DoPost("/parking/channel/create", map[string]interface{}{ "channel_code": "CH-IN-TEST", "channel_name": "Test Entrance", "direction": "in", "parking_lot_id": 1, "booth_id": 1, "allow_temporary": true, "description": "Automated test channel", }) AssertResponse(t, channelResp, channelBody, http.StatusOK, response.SUCCESS) listResp, listBody := env.DoGet("/parking/channel/list", map[string]string{"page": "1", "page_size": "10"}) AssertResponse(t, listResp, listBody, http.StatusOK, response.SUCCESS) list := ParsePageList(listBody) if len(list) < 1 { t.Fatalf("Channel list is empty") } fmt.Printf("PASS: Channel CRUD successful, total %d channels\n", len(list)) } // TestDeviceCRUD 设备CRUD func TestDeviceCRUD(t *testing.T) { env := InitTestEnv(t) deviceResp, deviceBody := env.DoPost("/parking/device/create", map[string]interface{}{ "device_code": "DEV001", "device_name": "Test Device", "device_type": "TCP", "connect_type": "tcp", "ip_address": "127.0.0.1", "port": 9000, "is_active": true, "parking_lot_id": 1, "channel_id": 1, "description": "Automated test device", }) AssertResponse(t, deviceResp, deviceBody, http.StatusOK, response.SUCCESS) listResp, listBody := env.DoGet("/parking/device/list", map[string]string{"page": "1", "page_size": "10"}) AssertResponse(t, listResp, listBody, http.StatusOK, response.SUCCESS) list := ParsePageList(listBody) if len(list) < 1 { t.Fatalf("Device list is empty") } fmt.Printf("PASS: Device CRUD successful, total %d devices\n", len(list)) } // TestVehicleTypeCRUD 自定义车辆类型CRUD func TestVehicleTypeCRUD(t *testing.T) { env := InitTestEnv(t) typeResp, typeBody := env.DoPost("/vehicle/type/create", map[string]interface{}{ "name": "新能源车", "remarks": "测试新能源车辆类型", "is_system": false, }) AssertResponse(t, typeResp, typeBody, http.StatusOK, response.SUCCESS) listResp, listBody := env.DoGet("/vehicle/type/all", nil) AssertResponse(t, listResp, listBody, http.StatusOK, response.SUCCESS) list := ParseDataList(listBody) if len(list) < 1 { t.Fatalf("Vehicle type list is empty") } fmt.Printf("PASS: Vehicle type CRUD successful, total %d types\n", len(list)) } // ===================== 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) { t.Helper() _, body := env.DoPost("/vehicle/create", map[string]interface{}{ "plate_number": plateNumber, "vehicle_type_id": 1, "vehicle_brand": "TestBrand", "vehicle_color": "TestColor", "rfid_tag": rfidTag, "owner_id": ownerID, }) var result struct { Code int `json:"code"`; Msg string `json:"msg"` } json.Unmarshal(body, &result) if result.Code != response.SUCCESS { t.Fatalf("Failed to create vehicle with owner: %s", result.Msg) } }