|
| 1 | +package events |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "github.com/icinga/icinga-go-library/logging" |
| 6 | + "github.com/icinga/icinga-go-library/types" |
| 7 | + "github.com/icinga/icinga-notifications/internal/config" |
| 8 | + "github.com/icinga/icinga-notifications/internal/daemon" |
| 9 | + "github.com/icinga/icinga-notifications/internal/event" |
| 10 | + "github.com/icinga/icinga-notifications/internal/incident" |
| 11 | + "github.com/icinga/icinga-notifications/internal/object" |
| 12 | + "github.com/icinga/icinga-notifications/internal/testutils" |
| 13 | + "github.com/icinga/icinga-notifications/internal/utils" |
| 14 | + "github.com/jmoiron/sqlx" |
| 15 | + "github.com/stretchr/testify/assert" |
| 16 | + "github.com/stretchr/testify/require" |
| 17 | + "go.uber.org/zap/zapcore" |
| 18 | + "go.uber.org/zap/zaptest" |
| 19 | + "testing" |
| 20 | + "time" |
| 21 | +) |
| 22 | + |
| 23 | +func TestProcess(t *testing.T) { |
| 24 | + ctx := context.Background() |
| 25 | + db := testutils.GetTestDB(ctx, t) |
| 26 | + |
| 27 | + require.NoError(t, daemon.InitTestConfig(), "mocking daemon.Config should not fail") |
| 28 | + |
| 29 | + // Insert a dummy source for our test cases! |
| 30 | + source := config.Source{Type: "notifications", Name: "Icinga Notifications", Icinga2InsecureTLS: types.Bool{Bool: false, Valid: true}} |
| 31 | + source.ChangedAt = types.UnixMilli(time.Now()) |
| 32 | + source.Deleted = types.Bool{Bool: false, Valid: true} |
| 33 | + |
| 34 | + err := utils.RunInTx(ctx, db, func(tx *sqlx.Tx) error { |
| 35 | + id, err := utils.InsertAndFetchId(ctx, tx, utils.BuildInsertStmtWithout(db, source, "id"), source) |
| 36 | + require.NoError(t, err, "populating source table should not fail") |
| 37 | + |
| 38 | + source.ID = id |
| 39 | + return nil |
| 40 | + }) |
| 41 | + require.NoError(t, err, "utils.RunInTx() should not fail") |
| 42 | + |
| 43 | + logs, err := logging.NewLogging("events-router", zapcore.DebugLevel, "console", nil, time.Hour) |
| 44 | + require.NoError(t, err, "logging initialisation should not fail") |
| 45 | + |
| 46 | + runtimeConfig := new(config.RuntimeConfig) |
| 47 | + |
| 48 | + t.Run("InvalidEvents", func(t *testing.T) { |
| 49 | + assert.Nil(t, Process(ctx, db, logs, runtimeConfig, makeEvent(t, source.ID, event.TypeState, event.SeverityNone))) |
| 50 | + assert.ErrorIs(t, Process(ctx, db, logs, runtimeConfig, makeEvent(t, source.ID, event.TypeState, event.SeverityOK)), event.ErrSuperfluousStateChange) |
| 51 | + assert.ErrorIs(t, Process(ctx, db, logs, runtimeConfig, makeEvent(t, source.ID, event.TypeAcknowledgementSet, event.SeverityOK)), event.ErrSuperfluousStateChange) |
| 52 | + assert.ErrorIs(t, Process(ctx, db, logs, runtimeConfig, makeEvent(t, source.ID, event.TypeAcknowledgementCleared, event.SeverityOK)), event.ErrSuperfluousStateChange) |
| 53 | + }) |
| 54 | + |
| 55 | + t.Run("StateChangeEvents", func(t *testing.T) { |
| 56 | + states := map[string]*event.Event{ |
| 57 | + "crit": makeEvent(t, source.ID, event.TypeState, event.SeverityCrit), |
| 58 | + "warn": makeEvent(t, source.ID, event.TypeState, event.SeverityWarning), |
| 59 | + "err": makeEvent(t, source.ID, event.TypeState, event.SeverityErr), |
| 60 | + "alert": makeEvent(t, source.ID, event.TypeState, event.SeverityAlert), |
| 61 | + } |
| 62 | + |
| 63 | + for severity, ev := range states { |
| 64 | + assert.NoErrorf(t, Process(ctx, db, logs, runtimeConfig, ev), "state event with severity %q should open an incident", severity) |
| 65 | + assert.ErrorIsf(t, Process(ctx, db, logs, runtimeConfig, ev), event.ErrSuperfluousStateChange, |
| 66 | + "superfluous state event %q should be ignored", severity) |
| 67 | + |
| 68 | + obj := object.GetFromCache(object.ID(source.ID, ev.Tags)) |
| 69 | + require.NotNil(t, obj, "there should be a cached object") |
| 70 | + |
| 71 | + i, err := incident.GetCurrent(ctx, db, obj, logs.GetLogger(), runtimeConfig, false) |
| 72 | + require.NoError(t, err, "retrieving current incident should not fail") |
| 73 | + require.NotNil(t, i, "there should be a cached incident") |
| 74 | + assert.Equal(t, ev.Severity, i.Severity, "severities should be equal") |
| 75 | + } |
| 76 | + |
| 77 | + reloadIncidents := func(ctx context.Context) { |
| 78 | + object.ClearCache() |
| 79 | + |
| 80 | + // Remove all existing incidents from the cache, as they are indexed with the |
| 81 | + // pointer of their object, which is going to change! |
| 82 | + for _, i := range incident.GetCurrentIncidents() { |
| 83 | + incident.RemoveCurrent(i.Object) |
| 84 | + } |
| 85 | + |
| 86 | + // The incident loading process may hang due to unknown bugs or semaphore lock waits. |
| 87 | + // Therefore, give it maximum time of 10s to finish normally, otherwise give up and fail. |
| 88 | + ctx, cancelFunc := context.WithDeadline(ctx, time.Now().Add(10*time.Second)) |
| 89 | + defer cancelFunc() |
| 90 | + |
| 91 | + err := incident.LoadOpenIncidents(ctx, db, logging.NewLogger(zaptest.NewLogger(t).Sugar(), time.Hour), runtimeConfig) |
| 92 | + require.NoError(t, err, "loading active incidents should not fail") |
| 93 | + } |
| 94 | + reloadIncidents(ctx) |
| 95 | + |
| 96 | + for severity, ev := range states { |
| 97 | + obj, err := object.FromEvent(ctx, db, ev) |
| 98 | + assert.NoError(t, err) |
| 99 | + |
| 100 | + i, err := incident.GetCurrent(ctx, db, obj, logs.GetLogger(), runtimeConfig, false) |
| 101 | + assert.NoErrorf(t, err, "incident for event severity %q should be in cache", severity) |
| 102 | + |
| 103 | + assert.Equal(t, obj, i.Object, "incident and event object should be the same") |
| 104 | + assert.Equal(t, i.Severity, ev.Severity, "incident and event severity should be the same") |
| 105 | + } |
| 106 | + |
| 107 | + // Recover the incidents |
| 108 | + for _, ev := range states { |
| 109 | + ev.Time = time.Now() |
| 110 | + ev.Severity = event.SeverityOK |
| 111 | + |
| 112 | + assert.NoErrorf(t, Process(ctx, db, logs, runtimeConfig, ev), "state event with severity %q should close an incident", "ok") |
| 113 | + } |
| 114 | + reloadIncidents(ctx) |
| 115 | + assert.Len(t, incident.GetCurrentIncidents(), 0, "there should be no cached incidents") |
| 116 | + }) |
| 117 | + |
| 118 | + t.Run("NonStateEvents", func(t *testing.T) { |
| 119 | + events := []*event.Event{ |
| 120 | + makeEvent(t, source.ID, event.TypeDowntimeStart, event.SeverityNone), |
| 121 | + makeEvent(t, source.ID, event.TypeDowntimeEnd, event.SeverityNone), |
| 122 | + makeEvent(t, source.ID, event.TypeDowntimeRemoved, event.SeverityNone), |
| 123 | + makeEvent(t, source.ID, event.TypeCustom, event.SeverityNone), |
| 124 | + makeEvent(t, source.ID, event.TypeFlappingStart, event.SeverityNone), |
| 125 | + makeEvent(t, source.ID, event.TypeFlappingEnd, event.SeverityNone), |
| 126 | + } |
| 127 | + |
| 128 | + for _, ev := range events { |
| 129 | + assert.NoErrorf(t, Process(ctx, db, logs, runtimeConfig, ev), "processing non-state event %q should not fail", ev.Type) |
| 130 | + assert.Lenf(t, incident.GetCurrentIncidents(), 0, "non-state event %q should not open an incident", ev.Type) |
| 131 | + require.NotNil(t, object.GetFromCache(object.ID(source.ID, ev.Tags)), "there should be a cached object") |
| 132 | + } |
| 133 | + }) |
| 134 | +} |
| 135 | + |
| 136 | +// makeEvent creates a fully initialised event.Event of the given type and severity. |
| 137 | +func makeEvent(t *testing.T, sourceID int64, typ string, severity event.Severity) *event.Event { |
| 138 | + return &event.Event{ |
| 139 | + SourceId: sourceID, |
| 140 | + Name: testutils.MakeRandomString(t), |
| 141 | + URL: "https://localhost/icingaweb2/icingadb", |
| 142 | + Type: typ, |
| 143 | + Time: time.Now(), |
| 144 | + Severity: severity, |
| 145 | + Username: "icingaadmin", |
| 146 | + Message: "You will contract a rare disease :(", |
| 147 | + Tags: map[string]string{ |
| 148 | + "Host": testutils.MakeRandomString(t), |
| 149 | + "Service": testutils.MakeRandomString(t), |
| 150 | + }, |
| 151 | + ExtraTags: map[string]string{ |
| 152 | + "hostgroup/database-server": "", |
| 153 | + "servicegroup/webserver": "", |
| 154 | + }, |
| 155 | + } |
| 156 | +} |
0 commit comments