Selaa lähdekoodia

fix: 更新数字票测试——Bug修复后Pay/Exit/FullFlow应从失败改为成功

lq 1 kuukausi sitten
vanhempi
commit
2b29def42c
1 muutettua tiedostoa jossa 414 lisäystä ja 0 poistoa
  1. 414 0
      test/testutil/e2e_edge_cases_test.go

+ 414 - 0
test/testutil/e2e_edge_cases_test.go

@@ -10,6 +10,7 @@ import (
 	"wails-app/internal/dao"
 	"wails-app/internal/global"
 	"wails-app/internal/model/common/response"
+	dt_svc "wails-app/internal/modules/digital-ticket/service"
 )
 
 // TestVehicleReEntry 同一车辆重复入场应被拦截
@@ -361,6 +362,419 @@ func TestVehicleTypeCRUD(t *testing.T) {
 
 // ===================== 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{}{