// TestCheckNotifyUnknownDefault tests the default unknownTemplate. func TestCheckNotifyUnknownDefault(t *testing.T) { s := new(Schedule) nc := make(chan string, 1) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, _ := ioutil.ReadAll(r.Body) nc <- string(b) })) defer ts.Close() u, err := url.Parse(ts.URL) if err != nil { t.Fatal(err) } c, err := conf.New("", fmt.Sprintf(` template t { subject = template } notification n { post = http://%s/ } alert a { template = t critNotification = n crit = 1 } `, u.Host)) if err != nil { t.Fatal(err) } c.StateFile = "" err = s.Init(c) if err != nil { t.Fatal(err) } r := &RunHistory{ Events: map[expr.AlertKey]*Event{ expr.NewAlertKey("a", opentsdb.TagSet{"h": "x"}): {Status: StUnknown}, expr.NewAlertKey("a", opentsdb.TagSet{"h": "y"}): {Status: StUnknown}, }, } s.RunHistory(r) s.CheckNotifications() gotExpected := false Loop: for { select { case r := <-nc: if r == "a: 2 unknown alerts" { gotExpected = true } else { t.Fatalf("unexpected: %v", r) } // TODO: remove this silly timeout-based test case <-time.After(time.Second): break Loop } } if !gotExpected { t.Errorf("didn't get expected result") } }
func buildConfig(r *http.Request) (c *conf.Conf, a *conf.Alert, hash string, err error) { config, err := ioutil.ReadAll(r.Body) if err != nil { return nil, nil, "", err } c, err = conf.New("Test Config", string(config)) if err != nil { return nil, nil, "", err } c.StateFile = "" hash, err = sched.DefaultSched.DataAccess.Configs().SaveTempConfig(string(config)) if err != nil { return nil, nil, "", err } alertName := r.FormValue("alert") if alertName == "" { return nil, nil, "", fmt.Errorf("must supply alert to run") } a, ok := c.Alerts[alertName] if !ok { return nil, nil, "", fmt.Errorf("alert %s not found", alertName) } return c, a, hash, nil }
func TestCheckSilence(t *testing.T) { s := new(Schedule) done := make(chan bool, 1) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { done <- true })) defer ts.Close() u, err := url.Parse(ts.URL) if err != nil { t.Fatal(err) } c, err := conf.New("", fmt.Sprintf(` template t { subject = "test" body = "test" } notification n { post = http://%s/ } alert a { template = t warnNotification = n warn = 1 } `, u.Host)) if err != nil { t.Fatal(err) } c.StateFile = "" err = s.Init(c) if err != nil { t.Fatal(err) } _, err = s.AddSilence(time.Now().Add(-time.Hour), time.Now().Add(time.Hour), "a", "", false, true, "", "user", "message") if err != nil { t.Fatal(err) } _, err = s.Check(nil, time.Now(), 0) if err != nil { t.Fatal(err) } s.CheckNotifications() select { case <-done: t.Fatal("silenced notification was sent") case <-time.After(time.Second * 2): // Timeout *probably* means the silence worked } }
func ConfigTest(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) { b, err := ioutil.ReadAll(r.Body) if err != nil { serveError(w, err) return } if len(b) == 0 { serveError(w, fmt.Errorf("empty config")) return } _, err = conf.New("test", string(b)) if err != nil { fmt.Fprintf(w, err.Error()) } }
func TestIncidentIds(t *testing.T) { s := new(Schedule) c, err := conf.New("", ` alert a { crit = 1 } `) if err != nil { t.Fatal(err) } c.StateFile = "" s.Init(c) ak := expr.NewAlertKey("a", nil) r := &RunHistory{ Events: map[expr.AlertKey]*Event{ ak: {Status: StWarning}, }, } expect := func(id uint64) { if s.status[ak].Last().IncidentId != id { t.Fatalf("Expeted incident id %d. Got %d.", id, s.status[ak].Last().IncidentId) } } s.RunHistory(r) expect(1) r.Events[ak].Status = StNormal r.Events[ak].IncidentId = 0 s.RunHistory(r) expect(1) r.Events[ak].Status = StWarning r.Events[ak].IncidentId = 0 s.RunHistory(r) expect(1) r.Events[ak].Status = StNormal r.Events[ak].IncidentId = 0 s.RunHistory(r) err = s.Action("", "", ActionClose, ak) if err != nil { t.Fatal(err) } r.Events[ak].Status = StWarning r.Events[ak].IncidentId = 0 s.RunHistory(r) expect(2) }
// TestCheckCritUnknownEmpty checks that if an alert goes normal -> crit -> // unknown, it's body and subject are empty. This is because we should not // keep around the crit template renders if we are unknown. func TestCheckCritUnknownEmpty(t *testing.T) { s := new(Schedule) c, err := conf.New("", ` template t { subject = 1 body = 2 } alert a { crit = 1 template = t } `) if err != nil { t.Fatal(err) } c.StateFile = "" s.Init(c) ak := expr.NewAlertKey("a", nil) r := &RunHistory{ Events: map[expr.AlertKey]*Event{ ak: {Status: StNormal}, }, } verify := func(empty bool) { st := s.GetStatus(ak) if empty { if st.Body != "" || st.Subject != "" { t.Fatalf("expected empty body and subject") } } else { if st.Body != "<html><head></head><body>2</body></html>" || st.Subject != "1" { t.Fatalf("expected body and subject") } } } s.RunHistory(r) verify(true) r.Events[ak].Status = StCritical s.RunHistory(r) verify(false) r.Events[ak].Status = StUnknown s.RunHistory(r) verify(true) r.Events[ak].Status = StNormal s.RunHistory(r) verify(true) }
func TestCheckNotify(t *testing.T) { s := new(Schedule) nc := make(chan string) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, _ := ioutil.ReadAll(r.Body) nc <- string(b) })) defer ts.Close() u, err := url.Parse(ts.URL) if err != nil { t.Fatal(err) } c, err := conf.New("", fmt.Sprintf(` template t { subject = {{.Last.Status}} } notification n { post = http://%s/ } alert a { template = t warnNotification = n warn = 1 } `, u.Host)) if err != nil { t.Fatal(err) } c.StateFile = "" err = s.Init(c) if err != nil { t.Fatal(err) } _, err = s.Check(nil, time.Now(), 0) if err != nil { t.Fatal(err) } s.CheckNotifications() select { case r := <-nc: if r != "warning" { t.Fatalf("expected warning, got %v", r) } case <-time.After(time.Second): t.Fatal("failed to receive notification before timeout") } }
func TestIncidentIds(t *testing.T) { defer setup()() c, err := conf.New("", ` alert a { crit = 1 } `) if err != nil { t.Fatal(err) } s, _ := initSched(c) ak := models.NewAlertKey("a", nil) r := &RunHistory{ Events: map[models.AlertKey]*models.Event{ ak: {Status: models.StWarning}, }, } expect := func(id int64) { incident, err := s.DataAccess.State().GetLatestIncident(ak) if err != nil { t.Fatal(err) } if incident.Id != id { t.Fatalf("Expeted incident id %d. Got %d.", id, incident.Id) } } s.RunHistory(r) expect(1) r.Events[ak].Status = models.StNormal s.RunHistory(r) expect(1) r.Events[ak].Status = models.StWarning s.RunHistory(r) expect(1) r.Events[ak].Status = models.StNormal s.RunHistory(r) err = s.Action("", "", models.ActionClose, ak) if err != nil { t.Fatal(err) } r.Events[ak].Status = models.StWarning s.RunHistory(r) expect(2) }
func TestActionNotificationTemplates(t *testing.T) { c, err := conf.New("", `hostname = abc`) c.StateFile = "" if err != nil { t.Fatal(err) } s, _ := initSched(c) data := &actionNotificationContext{} data.ActionType = ActionAcknowledge data.Message = "Bad things happened" data.User = "******" data.States = []*State{ { History: []Event{ { Status: StCritical, IncidentId: 224, }, }, Alert: "xyz", Subject: "Critical!!", }, } data.schedule = s buf := &bytes.Buffer{} err = actionNotificationBodyTemplate.Execute(buf, data) if err != nil { t.Fatal(err) } if !strings.Contains(buf.String(), "http://abc/incident?id=224") { t.Fatal("Expected link to incident in body") } buf = &bytes.Buffer{} err = actionNotificationSubjectTemplate.Execute(buf, data) if err != nil { t.Fatal(err) } if !strings.Contains(buf.String(), "Batman Acknowledged") { t.Fatal("Expected name and actionType in subject") } }
func TestDifferentSchedules(t *testing.T) { s := new(Schedule) c, err := conf.New("", ` alert a { crit = 1 runEvery = 3 } alert b { crit = 1 runEvery = 1 } `) if err != nil { t.Fatal(err) } c.StateFile = "" check := func(interval uint64, alerts ...string) { s.Init(c) _, err = s.Check(nil, time.Now(), interval) if err != nil { t.Fatal(err) } if len(alerts) != len(s.status) { t.Errorf("Expected %d statuses, but have %d for interval %d.", len(alerts), len(s.status), interval) } for _, alert := range alerts { if state, ok := s.status[expr.NewAlertKey(alert, nil)]; !ok || state.Status() != StCritical { t.Fatalf("Expected results for alert %s in interval %d.", alert, interval) } } } check(0, "a", "b") check(1, "b") check(2, "b") check(3, "a", "b") }
func TestCheckNotifyLog(t *testing.T) { s := new(Schedule) nc := make(chan string, 1) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, _ := ioutil.ReadAll(r.Body) nc <- string(b) })) defer ts.Close() u, err := url.Parse(ts.URL) if err != nil { t.Fatal(err) } c, err := conf.New("", fmt.Sprintf(` template t { subject = {{.Alert.Name}} } notification n { post = http://%s/ } alert a { template = t critNotification = n crit = 1 } alert b { template = t critNotification = n crit = 1 log = true } `, u.Host)) if err != nil { t.Fatal(err) } c.StateFile = "" err = s.Init(c) if err != nil { t.Fatal(err) } _, err = s.Check(nil, time.Now(), 0) if err != nil { t.Fatal(err) } s.CheckNotifications() gotA := false gotB := false Loop: for { select { case r := <-nc: if r == "a" && !gotA { gotA = true } else if r == "b" && !gotB { gotB = true } else { t.Errorf("unexpected: %v", r) } // TODO: remove this silly timeout-based test case <-time.After(time.Second): break Loop } } if !gotA { t.Errorf("didn't get expected a") } if !gotB { t.Errorf("didn't get expected b") } for ak, st := range s.status { switch ak { case "a{}": if !st.Open { t.Errorf("expected a to be open") } case "b{}": if st.Open { t.Errorf("expected b to be closed") } default: t.Errorf("unexpected alert key %s", ak) } } }
func TestActionNotificationGrouping(t *testing.T) { c, err := conf.New("", ` template t{ subject = 2 } notification n1 { print = true } notification n2{ print = true } notification n3{ print = true runOnActions = true } notification n4{ print = true runOnActions = false } alert a { template = t warnNotification = n1 critNotification = n2 warnNotification = n4 crit = 1 warn = 1 } alert b{ template = t warnNotification = n2 critNotification = n3 crit = 1 warn = 1 } lookup byHost{ entry host=a{ main_contact = n2 } entry host=b{ main_contact = n3 } } alert c{ template = t warnNotification = n1 warnNotification = lookup("byHost", "main_contact") warn = 1 } `) if err != nil { t.Fatal(err) } s, err := initSched(c) if err != nil { t.Fatal(err) } awarn := models.AlertKey("a{host=w}") acrit := models.AlertKey("a{host=c}") bwarn := models.AlertKey("b{host=w}") bcrit := models.AlertKey("b{host=c}") cA := models.AlertKey("c{host=a}") cB := models.AlertKey("c{host=b}") s.status[awarn] = &State{Alert: "a", Group: opentsdb.TagSet{"host": "w"}, History: []Event{{Status: StWarning}}} s.status[acrit] = &State{Alert: "a", Group: opentsdb.TagSet{"host": "c"}, History: []Event{{Status: StCritical}}} s.status[bwarn] = &State{Alert: "b", Group: opentsdb.TagSet{"host": "w"}, History: []Event{{Status: StWarning}}} s.status[bcrit] = &State{Alert: "b", Group: opentsdb.TagSet{"host": "c"}, History: []Event{{Status: StCritical}}} s.status[cA] = &State{Alert: "c", Group: opentsdb.TagSet{"host": "a"}, History: []Event{{Status: StWarning}}} s.status[cB] = &State{Alert: "c", Group: opentsdb.TagSet{"host": "b"}, History: []Event{{Status: StWarning}}} groups := s.groupActionNotifications([]models.AlertKey{awarn, acrit, bwarn, bcrit, cA, cB}) expect := func(not string, aks ...models.AlertKey) { n := c.Notifications[not] actualAks, ok := groups[n] if !ok { t.Fatalf("Notification %s not present in groupings.", not) } if len(actualAks) != len(aks) { t.Fatalf("Count mismatch for grouping %s. %d != %d.", not, len(actualAks), len(aks)) } for i, ak := range aks { if actualAks[i].AlertKey() != ak { t.Fatalf("Alert key mismatch at index %d. %s != %s.", i, actualAks[i].AlertKey(), ak) } } } expect("n1", awarn, cA, cB) expect("n2", acrit, bwarn, cA) expect("n3", bcrit, cB) }
func TestCheckFlapping(t *testing.T) { c, err := conf.New("", ` template t { subject = 1 } notification n { print = true } alert a { warnNotification = n warn = 1 critNotification = n crit = 1 template = t } `) if err != nil { t.Fatal(err) } s, _ := initSched(c) ak := models.NewAlertKey("a", nil) r := &RunHistory{ Events: map[models.AlertKey]*Event{ ak: {Status: StWarning}, }, } hasNots := func() bool { defer func() { s.pendingNotifications = nil }() if len(s.pendingNotifications) != 1 { return false } for k, v := range s.pendingNotifications { if k.Name != "n" || len(v) != 1 || v[0].Alert != "a" { return false } return true } return false } type stateTransition struct { S Status ExpectNots bool } transitions := []stateTransition{ {StWarning, true}, {StNormal, false}, {StWarning, false}, {StNormal, false}, {StCritical, true}, {StWarning, false}, {StCritical, false}, } for i, trans := range transitions { r.Events[ak].Status = trans.S s.RunHistory(r) has := hasNots() if has && !trans.ExpectNots { t.Fatalf("unexpected notifications for transition %d.", i) } else if !has && trans.ExpectNots { t.Fatalf("expected notifications for transition %d.", i) } } r.Events[ak].Status = StNormal s.RunHistory(r) // Close the alert, so it should notify next time. if err := s.Action("", "", ActionClose, ak); err != nil { t.Fatal(err) } r.Events[ak].Status = StWarning s.RunHistory(r) if !hasNots() { t.Fatal("expected notification") } }
func TestCheckNotifyUnknown(t *testing.T) { defer setup()() nc := make(chan string, 1) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, _ := ioutil.ReadAll(r.Body) nc <- string(b) })) defer ts.Close() u, err := url.Parse(ts.URL) if err != nil { t.Fatal(err) } c, err := conf.New("", fmt.Sprintf(` minGroupSize = 2 template t { subject = {{.Name}}: {{.Group | len}} unknown alerts } unknownTemplate = t notification n { post = http://%s/ } alert a { template = t critNotification = n crit = 1 } `, u.Host)) if err != nil { t.Fatal(err) } s, err := initSched(c) if err != nil { t.Fatal(err) } r := &RunHistory{ Events: map[models.AlertKey]*models.Event{ models.NewAlertKey("a", opentsdb.TagSet{"h": "x"}): {Status: models.StUnknown}, models.NewAlertKey("a", opentsdb.TagSet{"h": "y"}): {Status: models.StUnknown}, }, } s.RunHistory(r) s.CheckNotifications() s.sendUnknownNotifications() gotExpected := false Loop: for { select { case r := <-nc: if r == "a: 2 unknown alerts" { gotExpected = true } else { t.Fatalf("unexpected: %v", r) } // TODO: remove this silly timeout-based test case <-time.After(time.Second): break Loop } } if !gotExpected { t.Errorf("didn't get expected result") } }
func TestCheckFlapping(t *testing.T) { defer setup()() c, err := conf.New("", ` template t { subject = 1 } notification n { print = true } alert a { warnNotification = n warn = 1 critNotification = n crit = 1 normNotification = n template = t } `) if err != nil { t.Fatal(err) } s, _ := initSched(c) ak := models.NewAlertKey("a", nil) r := &RunHistory{ Events: map[models.AlertKey]*models.Event{ ak: {Status: models.StWarning}, }, } hasNots := func() bool { defer func() { s.pendingNotifications = nil }() if len(s.pendingNotifications) != 1 { return false } for k, v := range s.pendingNotifications { if k.Name != "n" || len(v) != 1 || v[0].Alert != "a" { return false } return true } return false } type stateTransition struct { S models.Status ExpectNots bool } /** transitions := []stateTransition{ {models.StWarning, true}, {models.StNormal, false}, {models.StWarning, false}, {models.StNormal, false}, {models.StCritical, true}, {models.StWarning, false}, {models.StCritical, false}, } VICTOROPS INTEGRATION: These state Transitions have been commented out and replaced with a test case which matches the behaviour we want to integrate with victorops */ transitions := []stateTransition{ {models.StWarning, true}, {models.StNormal, true}, {models.StWarning, true}, {models.StNormal, true}, {models.StCritical, true}, {models.StWarning, false}, {models.StCritical, false}, {models.StNormal, true}, } for i, trans := range transitions { r.Events[ak].Status = trans.S s.RunHistory(r) has := hasNots() if has && !trans.ExpectNots { t.Fatalf("unexpected notifications for transition %d.", i) } else if !has && trans.ExpectNots { t.Fatalf("expected notifications for transition %d.", i) } } r.Events[ak].Status = models.StNormal s.RunHistory(r) // Close the alert, so it should notify next time. if err := s.Action("", "", models.ActionClose, ak); err != nil { t.Fatal(err) } r.Events[ak].Status = models.StWarning s.RunHistory(r) if !hasNots() { t.Fatal("expected notification") } }
func TestCheckFlapping(t *testing.T) { s := new(Schedule) c, err := conf.New("", ` template t { subject = 1 } notification n { print = true } alert a { warnNotification = n warn = 1 critNotification = n crit = 1 template = t } `) if err != nil { t.Fatal(err) } c.StateFile = "" s.Init(c) ak := expr.NewAlertKey("a", nil) r := &RunHistory{ Events: map[expr.AlertKey]*Event{ ak: {Status: StWarning}, }, } hasNots := func() bool { defer func() { s.notifications = nil }() if len(s.notifications) != 1 { return false } for k, v := range s.notifications { if k.Name != "n" || len(v) != 1 || v[0].Alert != "a" { return false } return true } return false } s.RunHistory(r) if !hasNots() { t.Fatalf("expected notification: %v", s.notifications) } r.Events[ak].Status = StNormal s.RunHistory(r) if hasNots() { t.Fatal("unexpected notification") } r.Events[ak].Status = StWarning s.RunHistory(r) if hasNots() { t.Fatal("unexpected notification") } r.Events[ak].Status = StNormal s.RunHistory(r) if hasNots() { t.Fatal("unexpected notification") } r.Events[ak].Status = StCritical s.RunHistory(r) if !hasNots() { t.Fatal("expected notification") } r.Events[ak].Status = StNormal s.RunHistory(r) if hasNots() { t.Fatal("unexpected notification") } s.RunHistory(r) // Close the alert, so it should notify next time. if err := s.Action("", "", ActionClose, ak); err != nil { t.Fatal(err) } r.Events[ak].Status = StWarning s.RunHistory(r) if !hasNots() { t.Fatal("expected notification") } }
func testSched(t *testing.T, st *schedTest) (s *Schedule) { bosunStartupTime = time.Date(1900, 0, 0, 0, 0, 0, 0, time.UTC) //pretend we've been running for a while. ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req opentsdb.Request if err := json.NewDecoder(r.Body).Decode(&req); err != nil { log.Fatal(err) } var resp opentsdb.ResponseSet for _, rq := range req.Queries { qs := fmt.Sprintf(`q("%s", "%v", "%v")`, rq, req.Start, req.End) q, ok := st.queries[qs] if !ok { t.Errorf("unknown query: %s", qs) return } if q == nil { return // Put nil entry in map to simulate opentsdb error. } resp = append(resp, q...) } if err := json.NewEncoder(w).Encode(&resp); err != nil { log.Fatal(err) } })) defer ts.Close() u, err := url.Parse(ts.URL) if err != nil { t.Fatal(err) } confs := "tsdbHost = " + u.Host + "\n" + st.conf c, err := conf.New("testconf", confs) if err != nil { t.Error(err) t.Logf("conf:\n%s", confs) return } time.Sleep(time.Millisecond * 250) s, _ = initSched(c) for ak, time := range st.touched { s.DataAccess.State().TouchAlertKey(ak, time) } check(s, queryTime) groups, err := s.MarshalGroups(new(miniprofiler.Profile), "") if err != nil { t.Error(err) return } var check func(g *StateGroup) check = func(g *StateGroup) { for _, c := range g.Children { check(c) } if g.AlertKey == "" { return } ss := schedState{string(g.AlertKey), g.Status.String()} v, ok := st.state[ss] if !ok { t.Errorf("unexpected state: %s, %s", g.AlertKey, g.Status) return } if v != g.Active { t.Errorf("bad active: %s, %s", g.AlertKey, g.Status) return } delete(st.state, ss) } for _, v := range groups.Groups.NeedAck { check(v) } for _, v := range groups.Groups.Acknowledged { check(v) } for k := range st.state { t.Errorf("unused state: %s", k) } return s }
func TestActionNotificationGrouping(t *testing.T) { defer setup()() c, err := conf.New("", ` template t{ subject = 2 } notification n1 { print = true } notification n2{ print = true } notification n3{ print = true runOnActions = true } notification n4{ print = true runOnActions = false } alert a { template = t warnNotification = n1 critNotification = n2 warnNotification = n4 crit = 1 warn = 1 } alert b{ template = t warnNotification = n2 critNotification = n3 crit = 1 warn = 1 } lookup byHost{ entry host=a{ main_contact = n2 } entry host=b{ main_contact = n3 } } alert c{ template = t warnNotification = n1 warnNotification = lookup("byHost", "main_contact") warn = 1 } `) if err != nil { t.Fatal(err) } s, err := initSched(c) if err != nil { t.Fatal(err) } awarn := models.AlertKey("a{host=w}") acrit := models.AlertKey("a{host=c}") bwarn := models.AlertKey("b{host=w}") bcrit := models.AlertKey("b{host=c}") cA := models.AlertKey("c{host=a}") cB := models.AlertKey("c{host=b}") da := s.DataAccess.State() da.UpdateIncidentState(&models.IncidentState{AlertKey: awarn, Alert: awarn.Name(), Tags: awarn.Group().Tags(), WorstStatus: models.StWarning, Events: []models.Event{{Status: models.StWarning}}}) da.UpdateIncidentState(&models.IncidentState{AlertKey: acrit, Alert: acrit.Name(), Tags: acrit.Group().Tags(), WorstStatus: models.StCritical, Events: []models.Event{{Status: models.StCritical}}}) da.UpdateIncidentState(&models.IncidentState{AlertKey: bwarn, Alert: bwarn.Name(), Tags: bwarn.Group().Tags(), WorstStatus: models.StWarning, Events: []models.Event{{Status: models.StWarning}}}) da.UpdateIncidentState(&models.IncidentState{AlertKey: bcrit, Alert: bcrit.Name(), Tags: bcrit.Group().Tags(), WorstStatus: models.StCritical, Events: []models.Event{{Status: models.StCritical}}}) da.UpdateIncidentState(&models.IncidentState{AlertKey: cA, Alert: cA.Name(), Tags: cA.Group().Tags(), WorstStatus: models.StWarning, Events: []models.Event{{Status: models.StWarning}}}) da.UpdateIncidentState(&models.IncidentState{AlertKey: cB, Alert: cB.Name(), Tags: cB.Group().Tags(), WorstStatus: models.StWarning, Events: []models.Event{{Status: models.StWarning}}}) groups, err := s.groupActionNotifications([]models.AlertKey{awarn, acrit, bwarn, bcrit, cA, cB}) if err != nil { t.Fatal(err) } expect := func(not string, aks ...models.AlertKey) { n := c.Notifications[not] actualAks, ok := groups[n] if !ok { t.Fatalf("Notification %s not present in groupings.", not) } if len(actualAks) != len(aks) { t.Fatalf("Count mismatch for grouping %s. %d != %d.", not, len(actualAks), len(aks)) } for i, ak := range aks { if actualAks[i].AlertKey != ak { t.Fatalf("Alert key mismatch at index %d. %s != %s.", i, actualAks[i].AlertKey, ak) } } } expect("n1", awarn, cA, cB) expect("n2", acrit, bwarn, cA) expect("n3", bcrit, cB) }