service_test.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. package service
  2. import (
  3. "fmt"
  4. "testing"
  5. "time"
  6. "github.com/glebarez/sqlite"
  7. "github.com/stretchr/testify/require"
  8. "gorm.io/gorm"
  9. "wails-app/internal/dao"
  10. "wails-app/internal/global"
  11. "wails-app/internal/modules/parking-session/model/request"
  12. )
  13. func setupParkingSessionTest(t *testing.T) *ParkingSessionService {
  14. t.Helper()
  15. dsn := fmt.Sprintf("file:parking-session-%d?mode=memory&cache=shared", time.Now().UnixNano())
  16. db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
  17. require.NoError(t, err)
  18. sqlDB, err := db.DB()
  19. require.NoError(t, err)
  20. t.Cleanup(func() {
  21. _ = sqlDB.Close()
  22. if global.GVA_DB == db {
  23. global.GVA_DB = nil
  24. }
  25. })
  26. global.GVA_DB = db
  27. require.NoError(t, db.AutoMigrate(&dao.ParkingLot{}, &dao.VehicleRecord{}))
  28. require.NoError(t, db.Create(&dao.ParkingLot{
  29. LotCode: "SESSION-LOT", LotName: "会话测试停车场", Capacity: 10, Available: 10,
  30. }).Error)
  31. return NewParkingSessionService()
  32. }
  33. func TestParkingSessionOpenResolveAndClose(t *testing.T) {
  34. svc := setupParkingSessionTest(t)
  35. var session *dao.VehicleRecord
  36. require.NoError(t, global.GVA_DB.Transaction(func(tx *gorm.DB) error {
  37. var err error
  38. session, err = svc.OpenSessionTx(tx, request.OpenSessionRequest{
  39. PlateNumber: "SESSION-001", RFIDTag: "RFID-001", ParkingLotID: 1,
  40. EntryChannelID: 11, EntryChannelCode: "ENTRY-A", EntryChannelName: "东入口通道",
  41. EntryDeviceCode: "GATE-ENTRY-A", EntryDeviceName: "东入口读卡器",
  42. })
  43. return err
  44. }))
  45. resolved, err := svc.ResolveActiveSession(request.ResolveActiveSessionRequest{
  46. PlateNumber: "SESSION-001", RFIDTag: "RFID-001",
  47. })
  48. require.NoError(t, err)
  49. require.Equal(t, session.ID, resolved.ID)
  50. require.NoError(t, global.GVA_DB.Transaction(func(tx *gorm.DB) error {
  51. if _, err := svc.MarkPaidTx(tx, session.ID, "counter", "cash", 12); err != nil {
  52. return err
  53. }
  54. _, err := svc.CloseSessionTx(tx, request.CloseSessionRequest{
  55. SessionID: session.ID, StayTime: 60, Fee: 12, PaymentEntry: "counter", PaymentMethod: "cash",
  56. ExitChannelID: 22, ExitChannelCode: "EXIT-B", ExitChannelName: "西出口通道",
  57. ExitDeviceCode: "GATE-EXIT-B", ExitDeviceName: "西出口读卡器",
  58. })
  59. return err
  60. }))
  61. var persisted dao.VehicleRecord
  62. require.NoError(t, global.GVA_DB.First(&persisted, session.ID).Error)
  63. require.EqualValues(t, 11, persisted.EntryChannelID)
  64. require.Equal(t, "ENTRY-A", persisted.EntryChannelCode)
  65. require.Equal(t, "东入口通道", persisted.EntryChannelName)
  66. require.Equal(t, "GATE-ENTRY-A", persisted.EntryDeviceCode)
  67. require.Equal(t, "东入口读卡器", persisted.EntryDeviceName)
  68. require.EqualValues(t, 22, persisted.ExitChannelID)
  69. require.Equal(t, "EXIT-B", persisted.ExitChannelCode)
  70. require.Equal(t, "西出口通道", persisted.ExitChannelName)
  71. require.Equal(t, "GATE-EXIT-B", persisted.ExitDeviceCode)
  72. require.Equal(t, "西出口读卡器", persisted.ExitDeviceName)
  73. _, err = svc.ResolveActiveSession(request.ResolveActiveSessionRequest{SessionID: session.ID})
  74. require.ErrorIs(t, err, ErrSessionAlreadyClosed)
  75. }
  76. func TestParkingSessionRejectsDuplicateAndConflictingIdentity(t *testing.T) {
  77. svc := setupParkingSessionTest(t)
  78. first, err := svc.OpenSessionTx(global.GVA_DB, request.OpenSessionRequest{PlateNumber: "SESSION-A", RFIDTag: "RFID-A"})
  79. require.NoError(t, err)
  80. _, err = svc.OpenSessionTx(global.GVA_DB, request.OpenSessionRequest{PlateNumber: "SESSION-A"})
  81. require.ErrorIs(t, err, ErrSessionAlreadyOpen)
  82. second, err := svc.OpenSessionTx(global.GVA_DB, request.OpenSessionRequest{PlateNumber: "SESSION-B", RFIDTag: "RFID-B"})
  83. require.NoError(t, err)
  84. require.NotEqual(t, first.ID, second.ID)
  85. _, err = svc.ResolveActiveSession(request.ResolveActiveSessionRequest{PlateNumber: "SESSION-A", RFIDTag: "RFID-B"})
  86. require.ErrorIs(t, err, ErrSessionIdentityConflict)
  87. }
  88. func TestParkingSessionFullAdmissionPolicies(t *testing.T) {
  89. tests := []struct {
  90. name string
  91. profile request.EntryAccessProfile
  92. policy string
  93. allow bool
  94. wantErr bool
  95. }{
  96. {name: "普通车辆满位拒绝", wantErr: true},
  97. {name: "临时车未配置时拒绝", profile: request.EntryAccessProfile{IsTemporary: true}, policy: "allow_temporary_when_full", wantErr: true},
  98. {name: "临时车配置后允许", profile: request.EntryAccessProfile{IsTemporary: true}, policy: "allow_temporary_when_full", allow: true},
  99. {name: "月租车配置后允许", profile: request.EntryAccessProfile{HasValidMonthlyCard: true}, policy: "allow_monthly_when_full", allow: true},
  100. {name: "VIP车配置后允许", profile: request.EntryAccessProfile{IsVIP: true}, policy: "allow_vip_when_full", allow: true},
  101. }
  102. for _, tt := range tests {
  103. t.Run(tt.name, func(t *testing.T) {
  104. svc := setupParkingSessionTest(t)
  105. updates := map[string]interface{}{
  106. "capacity": 0, "occupied": 0, "available": 0,
  107. "allow_temporary_when_full": false,
  108. "allow_monthly_when_full": false,
  109. "allow_vip_when_full": false,
  110. }
  111. if tt.policy != "" {
  112. updates[tt.policy] = tt.allow
  113. }
  114. require.NoError(t, global.GVA_DB.Model(&dao.ParkingLot{}).Where("id = ?", 1).Updates(updates).Error)
  115. session, err := svc.OpenSessionTx(global.GVA_DB, request.OpenSessionRequest{
  116. PlateNumber: "FULL-" + tt.name, ParkingLotID: 1, AccessProfile: tt.profile,
  117. })
  118. if tt.wantErr {
  119. require.ErrorIs(t, err, ErrParkingLotFull)
  120. require.Nil(t, session)
  121. } else {
  122. require.NoError(t, err)
  123. require.NotNil(t, session)
  124. }
  125. var lot dao.ParkingLot
  126. require.NoError(t, global.GVA_DB.First(&lot, 1).Error)
  127. if tt.wantErr {
  128. require.Zero(t, lot.Occupied)
  129. } else {
  130. require.Equal(t, 1, lot.Occupied)
  131. }
  132. require.Zero(t, lot.Available)
  133. })
  134. }
  135. }
  136. func TestParkingSessionOverflowExitKeepsAvailabilityAccurate(t *testing.T) {
  137. svc := setupParkingSessionTest(t)
  138. require.NoError(t, global.GVA_DB.Model(&dao.ParkingLot{}).Where("id = ?", 1).Updates(map[string]interface{}{
  139. "capacity": 1, "occupied": 0, "available": 1, "allow_vip_when_full": true,
  140. }).Error)
  141. regular, err := svc.OpenSessionTx(global.GVA_DB, request.OpenSessionRequest{PlateNumber: "REGULAR", ParkingLotID: 1})
  142. require.NoError(t, err)
  143. overflow, err := svc.OpenSessionTx(global.GVA_DB, request.OpenSessionRequest{
  144. PlateNumber: "VIP-OVERFLOW", ParkingLotID: 1, AccessProfile: request.EntryAccessProfile{IsVIP: true},
  145. })
  146. require.NoError(t, err)
  147. closeSession := func(session *dao.VehicleRecord) {
  148. require.NoError(t, global.GVA_DB.Transaction(func(tx *gorm.DB) error {
  149. if _, err := svc.MarkPaidTx(tx, session.ID, "counter", "free", 0); err != nil {
  150. return err
  151. }
  152. _, err := svc.CloseSessionTx(tx, request.CloseSessionRequest{SessionID: session.ID})
  153. return err
  154. }))
  155. }
  156. closeSession(overflow)
  157. var lot dao.ParkingLot
  158. require.NoError(t, global.GVA_DB.First(&lot, 1).Error)
  159. require.Equal(t, 1, lot.Occupied)
  160. require.Zero(t, lot.Available)
  161. closeSession(regular)
  162. require.NoError(t, global.GVA_DB.First(&lot, 1).Error)
  163. require.Zero(t, lot.Occupied)
  164. require.Equal(t, 1, lot.Available)
  165. }