@@ -2,6 +2,9 @@ package incident
22
33import (
44 "context"
5+ "testing"
6+ "time"
7+
58 "github.com/icinga/icinga-go-library/database"
69 "github.com/icinga/icinga-go-library/logging"
710 baseEv "github.com/icinga/icinga-go-library/notifications/event"
@@ -13,14 +16,16 @@ import (
1316 "github.com/jmoiron/sqlx"
1417 "github.com/stretchr/testify/assert"
1518 "github.com/stretchr/testify/require"
19+ "go.uber.org/zap"
20+ "go.uber.org/zap/zapcore"
1621 "go.uber.org/zap/zaptest"
17- "testing"
18- "time"
1922)
2023
21- func TestLoadOpenIncidents (t * testing.T ) {
22- ctx := context .Background ()
23- db := testutils .GetTestDB (ctx , t )
24+ func TestIncidents (t * testing.T ) {
25+ db := testutils .GetTestDB (t .Context (), t )
26+ logs := logging .NewLoggingWithFactory ("testing" , zapcore .DebugLevel , time .Second , func (level zap.AtomicLevel ) zapcore.Core {
27+ return zaptest .NewLogger (t , zaptest .Level (level .Level ())).Core ()
28+ })
2429
2530 // Insert a dummy source for our test cases!
2631 source := & config.Source {
@@ -31,7 +36,7 @@ func TestLoadOpenIncidents(t *testing.T) {
3136 source .ChangedAt = types .UnixMilli (time .Date (2009 , time .November , 10 , 23 , 0 , 0 , 0 , time .UTC ))
3237 source .Deleted = types.Bool {Bool : false , Valid : true }
3338
34- err := db .ExecTx (ctx , nil , func (ctx context.Context , tx * sqlx.Tx ) error {
39+ err := db .ExecTx (t . Context () , nil , func (ctx context.Context , tx * sqlx.Tx ) error {
3540 id , err := database .InsertObtainID (ctx , tx , database .BuildInsertStmtWithout (db , source , "id" ), source )
3641 require .NoError (t , err , "populating source table should not fail" )
3742
@@ -40,80 +45,160 @@ func TestLoadOpenIncidents(t *testing.T) {
4045 })
4146 require .NoError (t , err , "db.ExecTx should not fail" )
4247
43- // Reduce the default placeholders per statement to a meaningful number, so that we can
44- // test some parallelism when loading the incidents.
45- db .Options .MaxPlaceholdersPerStatement = 100
48+ runtimeConfig := config .NewRuntimeConfig (logs , db )
49+ require .NoError (t , runtimeConfig .UpdateFromDatabase (t .Context ()))
4650
47- // Due to the 10*maxPlaceholders constraint, only 10 goroutines are going to process simultaneously.
48- // Therefore, reduce the default maximum number of connections per table to 4 in order to fully simulate
49- // semaphore lock wait cycles for a given table .
50- db .Options .MaxConnectionsPerTable = 4
51+ t . Run ( "LoadOpenIncidents" , func ( t * testing. T ) {
52+ // Reduce the default placeholders per statement to a meaningful number, so that we can
53+ // test some parallelism when loading the incidents .
54+ db .Options .MaxPlaceholdersPerStatement = 100
5155
52- testData := make (map [string ]* Incident , 10 * db .Options .MaxPlaceholdersPerStatement )
53- for j := 1 ; j <= 10 * db .Options .MaxPlaceholdersPerStatement ; j ++ {
54- i := makeIncident (ctx , db , t , source .ID , false )
55- testData [i .ObjectID .String ()] = i
56- }
56+ // Due to the 10*maxPlaceholders constraint, only 10 goroutines are going to process simultaneously.
57+ // Therefore, reduce the default maximum number of connections per table to 4 in order to fully simulate
58+ // semaphore lock wait cycles for a given table.
59+ db .Options .MaxConnectionsPerTable = 4
5760
58- t .Run ("WithNoRecoveredIncidents" , func (t * testing.T ) {
59- assertIncidents (ctx , db , t , testData )
60- })
61+ testData := make (map [string ]* Incident , 10 * db .Options .MaxPlaceholdersPerStatement )
62+ for j := 1 ; j <= 10 * db .Options .MaxPlaceholdersPerStatement ; j ++ {
63+ i := makeIncident (db , logs , runtimeConfig , t , makeEvent (t , source .ID , withIncident (), withSeverity (baseEv .SeverityCrit )))
64+ testData [i .ObjectID .String ()] = i
65+ }
66+
67+ t .Run ("WithNoRecoveredIncidents" , func (t * testing.T ) {
68+ assertIncidents (t .Context (), db , runtimeConfig , t , testData )
69+ })
6170
62- t .Run ("WithSomeRecoveredIncidents" , func (t * testing.T ) {
63- tx , err := db .BeginTxx (ctx , nil )
64- require .NoError (t , err , "starting a transaction should not fail" )
71+ t .Run ("WithSomeRecoveredIncidents" , func (t * testing.T ) {
72+ // Drop all cached incidents before re-loading them!
73+ for _ , i := range GetCurrentIncidents () {
74+ // Mark some of the existing incidents as recovered.
75+ if i .Id % 20 == 0 { // 1000 / 20 => 50 existing incidents will be marked as recovered!
76+ require .NoError (t , ProcessEvent (t .Context (), db , logs , runtimeConfig , makeEvent (t , source .ID , withClose (), withTags (i .Object .Tags ))))
77+ require .False (t , i .RecoveredAt .Time ().IsZero (), "incident should be marked as recovered after processing a close event" )
6578
66- // Drop all cached incidents before re-loading them!
67- for _ , i := range GetCurrentIncidents () {
68- RemoveCurrent (i .Object )
79+ // Drop it from our test data as it's recovered
80+ delete (testData , i .ObjectID .String ())
81+ } else {
82+ RemoveCurrent (i .Object ) // Drop it from the cache to simulate a daemon reload.
83+ }
84+ }
6985
70- // Mark some of the existing incidents as recovered.
71- if i .Id % 20 == 0 { // 1000 / 20 => 50 existing incidents will be marked as recovered!
72- i .RecoveredAt = types .UnixMilli (time .Now ())
86+ assert .Len (t , GetCurrentIncidents (), 0 , "there should be no cached incidents" )
7387
74- require .NoError (t , i .Sync (ctx , tx ), "failed to update/insert incident" )
88+ for j := 1 ; j <= db .Options .MaxPlaceholdersPerStatement / 2 ; j ++ {
89+ require .NoError (t , ProcessEvent (t .Context (), db , logs , runtimeConfig , makeEvent (t , source .ID , withIncident (), withClose (), withSeverity (baseEv .SeverityAlert ))))
7590
76- // Drop it from our test data as it's recovered
77- delete (testData , i .ObjectID .String ())
91+ if j % 2 == 0 {
92+ // Add some extra new not recovered incidents to fully simulate a daemon reload.
93+ i := makeIncident (db , logs , runtimeConfig , t , makeEvent (t , source .ID , withIncident (), withSeverity (baseEv .SeverityWarning )))
94+ testData [i .ObjectID .String ()] = i
95+ }
7896 }
79- }
80- require .NoError (t , tx .Commit (), "committing a transaction should not fail" )
81-
82- assert .Len (t , GetCurrentIncidents (), 0 , "there should be no cached incidents" )
8397
84- for j := 1 ; j <= db .Options .MaxPlaceholdersPerStatement / 2 ; j ++ {
85- // We don't need to cache recovered incidents in memory.
86- _ = makeIncident (ctx , db , t , source .ID , true )
98+ assertIncidents (t .Context (), db , runtimeConfig , t , testData )
8799
88- if j % 2 == 0 {
89- // Add some extra new not recovered incidents to fully simulate a daemon reload.
90- i := makeIncident (ctx , db , t , source .ID , false )
91- testData [i .ObjectID .String ()] = i
100+ // Close all remaining incidents to clean up the database for the next test run.
101+ for _ , i := range GetCurrentIncidents () {
102+ require .NoError (t , ProcessEvent (t .Context (), db , logs , runtimeConfig , makeEvent (t , source .ID , withClose (), withTags (i .Object .Tags ))))
92103 }
93- }
104+ assert .Len (t , GetCurrentIncidents (), 0 , "there should be no cached incidents" )
105+ testData = make (map [string ]* Incident ) // Reset test data for the next test run.
106+ })
107+ })
94108
95- assertIncidents (ctx , db , t , testData )
109+ t .Run ("Severity Change" , func (t * testing.T ) {
110+ i := makeIncident (db , logs , runtimeConfig , t , makeEvent (t , source .ID , withIncident (), withSeverity (baseEv .SeverityDebug )))
111+ assert .NotZero (t , i .ID ())
112+ assert .True (t , i .RecoveredAt .Time ().IsZero ())
113+ assert .Equal (t , baseEv .SeverityDebug , i .Severity )
114+
115+ require .NoError (t , ProcessEvent (t .Context (), db , logs , runtimeConfig , makeEvent (t , source .ID , withIncident (), withSeverity (baseEv .SeverityEmerg ), withTags (i .Object .Tags ))))
116+ assert .Equal (t , baseEv .SeverityEmerg , i .Severity )
117+
118+ err := ProcessEvent (t .Context (), db , logs , runtimeConfig , makeEvent (t , source .ID , withSeverity (baseEv .SeverityNotice ), withTags (i .Object .Tags )))
119+ require .ErrorIs (t , err , ErrSeverityChangeWithoutIncidentFlag )
120+ assert .Equal (t , baseEv .SeverityEmerg , i .Severity )
121+ })
122+
123+ t .Run ("Close Flag" , func (t * testing.T ) {
124+ // Incident opened and closed immediately, so it won't be in the cache anymore.
125+ require .Nil (t , makeIncident (db , logs , runtimeConfig , t , makeEvent (t , source .ID , withIncident (), withClose (), withSeverity (baseEv .SeverityDebug ))))
126+
127+ i := makeIncident (db , logs , runtimeConfig , t , makeEvent (t , source .ID , withIncident (), withSeverity (baseEv .SeverityInfo )))
128+ assert .True (t , i .RecoveredAt .Time ().IsZero ())
129+ assert .Equal (t , baseEv .SeverityInfo , i .Severity )
130+
131+ // Closing incident with the incident flag will mark it as recovered and update the severity at the same time.
132+ require .NoError (t , ProcessEvent (t .Context (), db , logs , runtimeConfig , makeEvent (t , source .ID , withIncident (), withClose (), withSeverity (baseEv .SeverityEmerg ), withTags (i .Object .Tags ))))
133+ assert .False (t , i .RecoveredAt .Time ().IsZero ())
134+ assert .Equal (t , baseEv .SeverityEmerg , i .Severity )
135+
136+ i = makeIncident (db , logs , runtimeConfig , t , makeEvent (t , source .ID , withIncident (), withSeverity (baseEv .SeverityWarning )))
137+ assert .True (t , i .RecoveredAt .Time ().IsZero ())
138+ assert .Equal (t , baseEv .SeverityWarning , i .Severity )
139+
140+ // Closing incident without the incident flag have no other side effects than marking it as recovered.
141+ require .NoError (t , ProcessEvent (t .Context (), db , logs , runtimeConfig , makeEvent (t , source .ID , withClose (), withTags (i .Object .Tags ))))
142+ assert .False (t , i .RecoveredAt .Time ().IsZero ())
143+ assert .Equal (t , baseEv .SeverityWarning , i .Severity )
144+ })
145+
146+ t .Run ("Notify Flag" , func (t * testing.T ) {
147+ t .Skipf ("Skipping Notify Flag test, as it requires to verify whether notifications were sent" )
148+ })
149+
150+ t .Run ("Muted Flag" , func (t * testing.T ) {
151+ i := makeIncident (db , logs , runtimeConfig , t , makeEvent (t , source .ID , withIncident (), withSeverity (baseEv .SeverityDebug ), withMuted (true )))
152+ assert .Equal (t , baseEv .SeverityDebug , i .Severity )
153+ assert .True (t , i .IsMuted ())
154+ assert .Equal (t , "You're gonna have a bad time!" , i .MuteReason .String )
155+
156+ // Unmute it with the incident flag still set...
157+ require .NoError (t , ProcessEvent (t .Context (), db , logs , runtimeConfig , makeEvent (t , source .ID , withIncident (), withMuted (false ), withTags (i .Object .Tags ))))
158+ assert .Equal (t , baseEv .SeverityDebug , i .Severity )
159+ assert .False (t , i .IsMuted ())
160+ assert .Equal (t , "" , i .MuteReason .String )
161+
162+ i = makeIncident (db , logs , runtimeConfig , t , makeEvent (t , source .ID , withIncident (), withSeverity (baseEv .SeverityDebug ), withMuted (true )))
163+ assert .Equal (t , baseEv .SeverityDebug , i .Severity )
164+ assert .True (t , i .IsMuted ())
165+ assert .Equal (t , "You're gonna have a bad time!" , i .MuteReason .String )
166+
167+ // Unmute it without the incident flag set...
168+ require .NoError (t , ProcessEvent (t .Context (), db , logs , runtimeConfig , makeEvent (t , source .ID , withMuted (false ), withTags (i .Object .Tags ))))
169+ assert .Equal (t , baseEv .SeverityDebug , i .Severity )
170+ assert .False (t , i .IsMuted ())
171+ assert .Equal (t , "" , i .MuteReason .String )
172+
173+ // Muted flag without the incident flag has no effect on non-existing incidents.
174+ i = makeIncident (db , logs , runtimeConfig , t , makeEvent (t , source .ID , withMuted (true )))
175+ require .Nil (t , i )
96176 })
97177}
98178
99179// assertIncidents restores all not recovered incidents from the database and asserts them based on the given testData.
100180//
101181// The incident loading process is limited to a maximum duration of 10 seconds and will be
102182// aborted and causes the entire test suite to fail immediately, if it takes longer.
103- func assertIncidents (ctx context.Context , db * database.DB , t * testing.T , testData map [string ]* Incident ) {
183+ func assertIncidents (ctx context.Context , db * database.DB , runtimeConfig * config. RuntimeConfig , t * testing.T , testData map [string ]* Incident ) {
104184 logger := logging .NewLogger (zaptest .NewLogger (t ).Sugar (), time .Hour )
105185
106186 // Since we have been using object.FromEvent() to persist the test objects to the database,
107187 // these will be automatically added to the objects cache as well. So clear the cache before
108188 // reloading the incidents, otherwise it will panic in object.RestoreObjects().
109189 object .ClearCache ()
110190
191+ // Clear the incidents for the same reasons as above.
192+ currentIncidentsMu .Lock ()
193+ currentIncidents = make (map [* object.Object ]* Incident )
194+ currentIncidentsMu .Unlock ()
195+
111196 // The incident loading process may hang due to unknown bugs or semaphore lock waits.
112197 // Therefore, give it maximum time of 10s to finish normally, otherwise give up and fail.
113198 ctx , cancelFunc := context .WithDeadline (ctx , time .Now ().Add (10 * time .Second ))
114199 defer cancelFunc ()
115200
116- err := LoadOpenIncidents (ctx , db , logger , & config. RuntimeConfig {} )
201+ err := LoadOpenIncidents (ctx , db , logger , runtimeConfig )
117202 require .NoError (t , err , "failed to load not recovered incidents" )
118203
119204 incidents := GetCurrentIncidents ()
@@ -141,38 +226,45 @@ func assertIncidents(ctx context.Context, db *database.DB, t *testing.T, testDat
141226 }
142227}
143228
144- // makeIncident returns a fully initialised recovered/un-recovered incident.
229+ // makeIncident creates a new incident by processing the given event and returns the resulting incident object .
145230//
146- // This will firstly create and synchronise a new object from a freshly generated dummy event with distinct
147- // tags and name, and ensures that no error is returned, otherwise it will cause the entire test suite to fail.
148- // Once the object has been successfully synchronised, an incident is created and synced with the database.
149- func makeIncident (ctx context.Context , db * database.DB , t * testing.T , sourceID int64 , recovered bool ) * Incident {
231+ // The incident is guaranteed to be fully initialized and ready for assertions but might be nil if it's immediately closed.
232+ func makeIncident (db * database.DB , logs * logging.Logging , runtimeConfig * config.RuntimeConfig , t * testing.T , ev * event.Event ) * Incident {
233+ require .NoError (t , ProcessEvent (t .Context (), db , logs , runtimeConfig , ev ))
234+ return GetCurrent (db , object .Get (db , ev ), logs .GetChildLogger ("incident" ), runtimeConfig , false )
235+ }
236+
237+ // makeEvent returns a fully initialized event based on the given parameters.
238+ func makeEvent (t * testing.T , sourceID int64 , opts ... eventOption ) * event.Event {
150239 ev := & event.Event {
151- Time : time.Time {} ,
240+ Time : time .Now (). Add ( - 2 * time . Hour ). Truncate ( time . Second ) ,
152241 SourceId : sourceID ,
153- Event : baseEv.Event {
154- Name : testutils .MakeRandomString (t ),
155- Tags : map [string ]string { // Always generate unique object tags not to produce same object ID!
156- "host" : testutils .MakeRandomString (t ),
157- "service" : testutils .MakeRandomString (t ),
158- },
159- },
242+ Event : baseEv.Event {Name : testutils .MakeRandomString (t )},
243+ }
244+ for _ , opt := range opts {
245+ opt (ev )
246+ }
247+ if ev .Tags == nil {
248+ ev .Tags = map [string ]string { // Always generate unique object tags not to produce same object ID!
249+ "host" : testutils .MakeRandomString (t ),
250+ "service" : testutils .MakeRandomString (t ),
251+ }
160252 }
161253
162- i := NewIncident (db , object .Get (db , ev ), & config.RuntimeConfig {}, nil )
163- i .StartedAt = types .UnixMilli (time .Now ().Add (- 2 * time .Hour ).Truncate (time .Second ))
164- i .Severity = baseEv .SeverityCrit
165- if recovered {
166- i .Severity = baseEv .SeverityOK
167- i .RecoveredAt = types .UnixMilli (time .Now ())
254+ if ev .IsMuted () {
255+ ev .MutedReason = "You're gonna have a bad time!"
168256 }
257+ require .NoError (t , ev .Validate (), "failed to validate event" )
258+ return ev
259+ }
169260
170- require .NoError (t , db .ExecTx (ctx , nil , func (ctx context.Context , tx * sqlx.Tx ) error {
171- if err := i .Object .SyncFromEvent (ctx , tx , ev ); err != nil {
172- return err
173- }
174- return i .Sync (ctx , tx )
175- }))
261+ // eventOption is a functional option type for modifying an event.
262+ type eventOption func (* event.Event )
176263
177- return i
264+ func withIncident () eventOption { return func (ev * event.Event ) { ev .Incident = types .MakeBool (true ) } }
265+ func withClose () eventOption { return func (ev * event.Event ) { ev .Close = types .MakeBool (true ) } }
266+ func withMuted (v bool ) eventOption { return func (ev * event.Event ) { ev .Muted = types .MakeBool (v ) } }
267+ func withTags (tags map [string ]string ) eventOption { return func (ev * event.Event ) { ev .Tags = tags } }
268+ func withSeverity (sev baseEv.Severity ) eventOption {
269+ return func (ev * event.Event ) { ev .Severity = sev }
178270}
0 commit comments