incident_hook_test.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. package parking
  2. import (
  3. "errors"
  4. "fmt"
  5. "testing"
  6. "time"
  7. "github.com/glebarez/sqlite"
  8. "github.com/stretchr/testify/require"
  9. "go.uber.org/zap"
  10. "gorm.io/gorm"
  11. "wails-app/internal/dao"
  12. "wails-app/internal/global"
  13. incidentService "wails-app/internal/modules/incident/service"
  14. )
  15. // failingGateController 注入失败的闸机控制器,模拟开闸失败。
  16. type failingGateController struct{}
  17. func (c *failingGateController) OpenGate(deviceCode string, _ byte) error {
  18. return errors.New("relay timeout")
  19. }
  20. func (c *failingGateController) CloseGate(deviceCode string, _ byte) error {
  21. return errors.New("relay timeout")
  22. }
  23. func (c *failingGateController) IsGateConnected(string) bool { return true }
  24. func setupIncidentHookTest(t *testing.T) {
  25. t.Helper()
  26. dsn := fmt.Sprintf("file:incident-hook-%d?mode=memory&cache=shared", time.Now().UnixNano())
  27. db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
  28. require.NoError(t, err)
  29. sqlDB, err := db.DB()
  30. require.NoError(t, err)
  31. t.Cleanup(func() {
  32. _ = sqlDB.Close()
  33. if global.GVA_DB == db {
  34. global.GVA_DB = nil
  35. }
  36. })
  37. global.GVA_DB = db
  38. global.GVA_LOG = zap.NewNop() // 避免 nil 日志
  39. t.Cleanup(func() { global.GVA_LOG = nil })
  40. require.NoError(t, db.AutoMigrate(&dao.IncidentRecord{}, &dao.DeviceCommandLog{}, &dao.UHFReader{}))
  41. }
  42. // TestOpenGateFailureCreatesIncidentAndCommandLog 开闸失败时:
  43. // 指令流水(failed) + 异常事件(gate_failed) 同时产生,且流水回填异常关联。
  44. func TestOpenGateFailureCreatesIncidentAndCommandLog(t *testing.T) {
  45. setupIncidentHookTest(t)
  46. previous := currentGateController()
  47. SetGateController(&failingGateController{})
  48. t.Cleanup(func() { SetGateController(previous) })
  49. require.NoError(t, global.GVA_DB.Create(&dao.UHFReader{DeviceCode: "GATE-01", DeviceName: "东门道闸"}).Error)
  50. svc := &PassageService{}
  51. commandLogID, err := svc.OpenGateWithContext(GateCommandContext{
  52. Source: incidentService.SourcePassage, SessionID: 7,
  53. }, "GATE-01", 0)
  54. require.Error(t, err)
  55. require.Contains(t, err.Error(), "relay timeout")
  56. // 调用方(passage.go 同款逻辑):记录异常并回填指令流水关联
  57. incidentID := incidentSvc.RecordIncident(incidentService.RecordIncidentRequest{
  58. Category: incidentService.CategoryGateFailed,
  59. Source: incidentService.SourcePassage,
  60. VehicleRecordID: 7,
  61. DeviceCode: "GATE-01",
  62. Description: "入场记录已创建,但开闸失败",
  63. Detail: err.Error(),
  64. })
  65. require.NotZero(t, incidentID)
  66. incidentSvc.LinkIncidentToCommand(commandLogID, incidentID)
  67. // 指令流水已写入且为 failed
  68. require.NotZero(t, commandLogID)
  69. var cmd dao.DeviceCommandLog
  70. require.NoError(t, global.GVA_DB.First(&cmd, commandLogID).Error)
  71. require.Equal(t, "open", cmd.Action)
  72. require.Equal(t, incidentService.CommandFailed, cmd.Result)
  73. require.Equal(t, "东门道闸", cmd.DeviceName)
  74. require.Equal(t, uint(7), cmd.SessionID)
  75. // 异常事件已生成(gate_failed)
  76. var incident dao.IncidentRecord
  77. require.NoError(t, global.GVA_DB.Where("category = ?", incidentService.CategoryGateFailed).First(&incident).Error)
  78. require.Equal(t, incidentService.StatusPending, incident.Status)
  79. require.Equal(t, incidentService.SourcePassage, incident.Source)
  80. require.Equal(t, incidentService.LevelCritical, incident.Level)
  81. require.Equal(t, "GATE-01", incident.DeviceCode)
  82. // 指令流水回填了异常关联
  83. require.NoError(t, global.GVA_DB.First(&cmd, commandLogID).Error)
  84. require.Equal(t, incident.ID, cmd.IncidentID)
  85. }
  86. // TestOpenGateSuccessRecordsCommandLog 开闸成功只写指令流水,不产生异常。
  87. func TestOpenGateSuccessRecordsCommandLog(t *testing.T) {
  88. setupIncidentHookTest(t)
  89. previous := currentGateController()
  90. SetGateController(&gateControllerStub{})
  91. t.Cleanup(func() { SetGateController(previous) })
  92. svc := &PassageService{}
  93. commandLogID, err := svc.OpenGateWithContext(GateCommandContext{
  94. Source: incidentService.SourceManual, OperatorID: 5,
  95. }, "GATE-01", 0)
  96. require.NoError(t, err)
  97. require.NotZero(t, commandLogID)
  98. var cmd dao.DeviceCommandLog
  99. require.NoError(t, global.GVA_DB.First(&cmd, commandLogID).Error)
  100. require.Equal(t, incidentService.CommandSuccess, cmd.Result)
  101. require.Equal(t, incidentService.SourceManual, cmd.Source)
  102. require.Equal(t, uint(5), cmd.OperatorID)
  103. var count int64
  104. require.NoError(t, global.GVA_DB.Model(&dao.IncidentRecord{}).Count(&count).Error)
  105. require.Equal(t, int64(0), count)
  106. }
  107. // TestManualRaiseRecordedAsResolved 人工抬杆(带原因)应生成已解决的 manual_raise 记录。
  108. func TestManualRaiseRecordedAsResolved(t *testing.T) {
  109. setupIncidentHookTest(t)
  110. // 模拟 API 层人工开闸成功后的埋点调用
  111. incidentID := incidentService.NewIncidentService().RecordIncident(incidentService.RecordIncidentRequest{
  112. Category: incidentService.CategoryManualRaise,
  113. Source: incidentService.SourceManual,
  114. VehicleRecordID: 3,
  115. PlateNumber: "SU-001",
  116. DeviceCode: "GATE-01",
  117. OperatorID: 5,
  118. Description: "人工开闸",
  119. HandleType: incidentService.HandleManualGate,
  120. HandleRemark: "车主称设备未识别",
  121. })
  122. require.NotZero(t, incidentID)
  123. var incident dao.IncidentRecord
  124. require.NoError(t, global.GVA_DB.First(&incident, incidentID).Error)
  125. require.Equal(t, incidentService.StatusResolved, incident.Status)
  126. require.Equal(t, incidentService.HandleManualGate, incident.HandleType)
  127. require.Equal(t, "车主称设备未识别", incident.HandleRemark)
  128. require.NotNil(t, incident.HandledAt)
  129. require.Equal(t, uint(5), incident.HandlerID)
  130. }