package service import ( "fmt" "testing" "time" "github.com/glebarez/sqlite" "github.com/stretchr/testify/require" "gorm.io/gorm" "wails-app/internal/dao" "wails-app/internal/global" "wails-app/internal/modules/parking-session/model/request" ) func setupParkingSessionTest(t *testing.T) *ParkingSessionService { t.Helper() dsn := fmt.Sprintf("file:parking-session-%d?mode=memory&cache=shared", time.Now().UnixNano()) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) require.NoError(t, err) sqlDB, err := db.DB() require.NoError(t, err) t.Cleanup(func() { _ = sqlDB.Close() if global.GVA_DB == db { global.GVA_DB = nil } }) global.GVA_DB = db require.NoError(t, db.AutoMigrate(&dao.ParkingLot{}, &dao.VehicleRecord{})) require.NoError(t, db.Create(&dao.ParkingLot{ LotCode: "SESSION-LOT", LotName: "会话测试停车场", Capacity: 10, Available: 10, }).Error) return NewParkingSessionService() } func TestParkingSessionOpenResolveAndClose(t *testing.T) { svc := setupParkingSessionTest(t) var session *dao.VehicleRecord require.NoError(t, global.GVA_DB.Transaction(func(tx *gorm.DB) error { var err error session, err = svc.OpenSessionTx(tx, request.OpenSessionRequest{ PlateNumber: "SESSION-001", RFIDTag: "RFID-001", ParkingLotID: 1, EntryChannelID: 11, EntryChannelCode: "ENTRY-A", EntryChannelName: "东入口通道", EntryDeviceCode: "GATE-ENTRY-A", EntryDeviceName: "东入口读卡器", }) return err })) resolved, err := svc.ResolveActiveSession(request.ResolveActiveSessionRequest{ PlateNumber: "SESSION-001", RFIDTag: "RFID-001", }) require.NoError(t, err) require.Equal(t, session.ID, resolved.ID) require.NoError(t, global.GVA_DB.Transaction(func(tx *gorm.DB) error { if _, err := svc.MarkPaidTx(tx, session.ID, "counter", "cash", 12); err != nil { return err } _, err := svc.CloseSessionTx(tx, request.CloseSessionRequest{ SessionID: session.ID, StayTime: 60, Fee: 12, PaymentEntry: "counter", PaymentMethod: "cash", ExitChannelID: 22, ExitChannelCode: "EXIT-B", ExitChannelName: "西出口通道", ExitDeviceCode: "GATE-EXIT-B", ExitDeviceName: "西出口读卡器", }) return err })) var persisted dao.VehicleRecord require.NoError(t, global.GVA_DB.First(&persisted, session.ID).Error) require.EqualValues(t, 11, persisted.EntryChannelID) require.Equal(t, "ENTRY-A", persisted.EntryChannelCode) require.Equal(t, "东入口通道", persisted.EntryChannelName) require.Equal(t, "GATE-ENTRY-A", persisted.EntryDeviceCode) require.Equal(t, "东入口读卡器", persisted.EntryDeviceName) require.EqualValues(t, 22, persisted.ExitChannelID) require.Equal(t, "EXIT-B", persisted.ExitChannelCode) require.Equal(t, "西出口通道", persisted.ExitChannelName) require.Equal(t, "GATE-EXIT-B", persisted.ExitDeviceCode) require.Equal(t, "西出口读卡器", persisted.ExitDeviceName) _, err = svc.ResolveActiveSession(request.ResolveActiveSessionRequest{SessionID: session.ID}) require.ErrorIs(t, err, ErrSessionAlreadyClosed) } func TestParkingSessionRejectsDuplicateAndConflictingIdentity(t *testing.T) { svc := setupParkingSessionTest(t) first, err := svc.OpenSessionTx(global.GVA_DB, request.OpenSessionRequest{PlateNumber: "SESSION-A", RFIDTag: "RFID-A"}) require.NoError(t, err) _, err = svc.OpenSessionTx(global.GVA_DB, request.OpenSessionRequest{PlateNumber: "SESSION-A"}) require.ErrorIs(t, err, ErrSessionAlreadyOpen) second, err := svc.OpenSessionTx(global.GVA_DB, request.OpenSessionRequest{PlateNumber: "SESSION-B", RFIDTag: "RFID-B"}) require.NoError(t, err) require.NotEqual(t, first.ID, second.ID) _, err = svc.ResolveActiveSession(request.ResolveActiveSessionRequest{PlateNumber: "SESSION-A", RFIDTag: "RFID-B"}) require.ErrorIs(t, err, ErrSessionIdentityConflict) } func TestParkingSessionFullAdmissionPolicies(t *testing.T) { tests := []struct { name string profile request.EntryAccessProfile policy string allow bool wantErr bool }{ {name: "普通车辆满位拒绝", wantErr: true}, {name: "临时车未配置时拒绝", profile: request.EntryAccessProfile{IsTemporary: true}, policy: "allow_temporary_when_full", wantErr: true}, {name: "临时车配置后允许", profile: request.EntryAccessProfile{IsTemporary: true}, policy: "allow_temporary_when_full", allow: true}, {name: "月租车配置后允许", profile: request.EntryAccessProfile{HasValidMonthlyCard: true}, policy: "allow_monthly_when_full", allow: true}, {name: "VIP车配置后允许", profile: request.EntryAccessProfile{IsVIP: true}, policy: "allow_vip_when_full", allow: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { svc := setupParkingSessionTest(t) updates := map[string]interface{}{ "capacity": 0, "occupied": 0, "available": 0, "allow_temporary_when_full": false, "allow_monthly_when_full": false, "allow_vip_when_full": false, } if tt.policy != "" { updates[tt.policy] = tt.allow } require.NoError(t, global.GVA_DB.Model(&dao.ParkingLot{}).Where("id = ?", 1).Updates(updates).Error) session, err := svc.OpenSessionTx(global.GVA_DB, request.OpenSessionRequest{ PlateNumber: "FULL-" + tt.name, ParkingLotID: 1, AccessProfile: tt.profile, }) if tt.wantErr { require.ErrorIs(t, err, ErrParkingLotFull) require.Nil(t, session) } else { require.NoError(t, err) require.NotNil(t, session) } var lot dao.ParkingLot require.NoError(t, global.GVA_DB.First(&lot, 1).Error) if tt.wantErr { require.Zero(t, lot.Occupied) } else { require.Equal(t, 1, lot.Occupied) } require.Zero(t, lot.Available) }) } } func TestParkingSessionOverflowExitKeepsAvailabilityAccurate(t *testing.T) { svc := setupParkingSessionTest(t) require.NoError(t, global.GVA_DB.Model(&dao.ParkingLot{}).Where("id = ?", 1).Updates(map[string]interface{}{ "capacity": 1, "occupied": 0, "available": 1, "allow_vip_when_full": true, }).Error) regular, err := svc.OpenSessionTx(global.GVA_DB, request.OpenSessionRequest{PlateNumber: "REGULAR", ParkingLotID: 1}) require.NoError(t, err) overflow, err := svc.OpenSessionTx(global.GVA_DB, request.OpenSessionRequest{ PlateNumber: "VIP-OVERFLOW", ParkingLotID: 1, AccessProfile: request.EntryAccessProfile{IsVIP: true}, }) require.NoError(t, err) closeSession := func(session *dao.VehicleRecord) { require.NoError(t, global.GVA_DB.Transaction(func(tx *gorm.DB) error { if _, err := svc.MarkPaidTx(tx, session.ID, "counter", "free", 0); err != nil { return err } _, err := svc.CloseSessionTx(tx, request.CloseSessionRequest{SessionID: session.ID}) return err })) } closeSession(overflow) var lot dao.ParkingLot require.NoError(t, global.GVA_DB.First(&lot, 1).Error) require.Equal(t, 1, lot.Occupied) require.Zero(t, lot.Available) closeSession(regular) require.NoError(t, global.GVA_DB.First(&lot, 1).Error) require.Zero(t, lot.Occupied) require.Equal(t, 1, lot.Available) }