package definitions import ( "errors" "testing" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/timeinterval" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" ) func TestValidateRoutes(t *testing.T) { zero := model.Duration(0) type testCase struct { desc string route Route expMsg string } t.Run("valid route", func(t *testing.T) { cases := []testCase{ { desc: "empty", route: Route{}, }, { desc: "simple", route: Route{ Receiver: "foo", GroupByStr: []string{"..."}, }, }, { desc: "nested", route: Route{ Receiver: "foo", GroupByStr: []string{"..."}, Routes: []*Route{ { Receiver: "bar", }, }, }, }, } for _, c := range cases { t.Run(c.desc, func(t *testing.T) { err := c.route.validateChild() require.NoError(t, err) }) } }) t.Run("invalid route", func(t *testing.T) { cases := []testCase{ { desc: "zero group interval", route: Route{ Receiver: "foo", GroupByStr: []string{"..."}, GroupInterval: &zero, }, expMsg: "group_interval cannot be zero", }, { desc: "zero repeat interval", route: Route{ Receiver: "foo", GroupByStr: []string{"..."}, RepeatInterval: &zero, }, expMsg: "repeat_interval cannot be zero", }, { desc: "duplicated label", route: Route{ Receiver: "foo", GroupByStr: []string{ "abc", "abc", }, }, expMsg: "duplicated label", }, { desc: "wildcard and non-wildcard label simultaneously", route: Route{ Receiver: "foo", GroupByStr: []string{ "...", "abc", }, }, expMsg: "cannot have wildcard", }, { desc: "valid with nested invalid", route: Route{ Receiver: "foo", GroupByStr: []string{"..."}, Routes: []*Route{ { GroupByStr: []string{ "abc", "abc", }, }, }, }, expMsg: "duplicated label", }, } for _, c := range cases { t.Run(c.desc, func(t *testing.T) { err := c.route.validateChild() require.Error(t, err) require.Contains(t, err.Error(), c.expMsg) }) } }) t.Run("route validator normalizes group_by", func(t *testing.T) { t.Run("when grouping normally", func(t *testing.T) { route := Route{ Receiver: "foo", GroupByStr: []string{"abc", "def"}, } _ = route.validateChild() require.False(t, route.GroupByAll) require.Equal(t, []model.LabelName{"abc", "def"}, route.GroupBy) }) t.Run("when grouping by wildcard, nil", func(t *testing.T) { route := Route{ Receiver: "foo", GroupByStr: []string{"..."}, } _ = route.validateChild() require.True(t, route.GroupByAll) require.Nil(t, route.GroupBy) }) t.Run("idempotently", func(t *testing.T) { route := Route{ Receiver: "foo", GroupByStr: []string{"abc", "def"}, } err := route.validateChild() require.NoError(t, err) err = route.validateChild() require.NoError(t, err) require.False(t, route.GroupByAll) require.Equal(t, []model.LabelName{"abc", "def"}, route.GroupBy) }) }) t.Run("valid root route", func(t *testing.T) { cases := []testCase{ { desc: "simple", route: Route{ Receiver: "foo", GroupByStr: []string{"..."}, }, }, } for _, c := range cases { t.Run(c.desc, func(t *testing.T) { err := c.route.Validate() require.NoError(t, err) }) } }) t.Run("invalid root route", func(t *testing.T) { cases := []testCase{ { desc: "no receiver", route: Route{ GroupByStr: []string{"..."}, }, expMsg: "must specify a default receiver", }, { desc: "exact matchers present", route: Route{ Receiver: "foo", GroupByStr: []string{"..."}, Match: map[string]string{ "abc": "def", }, }, expMsg: "must not have any matchers", }, { desc: "regex matchers present", route: Route{ Receiver: "foo", GroupByStr: []string{"..."}, Match: map[string]string{ "abc": "def", }, }, expMsg: "must not have any matchers", }, { desc: "mute time intervals present", route: Route{ Receiver: "foo", GroupByStr: []string{"..."}, MuteTimeIntervals: []string{"10"}, }, expMsg: "must not have any mute time intervals", }, { desc: "validation error that is not specific to root", route: Route{ Receiver: "foo", GroupByStr: []string{"abc", "abc"}, }, expMsg: "duplicated label", }, { desc: "nested validation error that is not specific to root", route: Route{ Receiver: "foo", Routes: []*Route{ { GroupByStr: []string{"abc", "abc"}, }, }, }, expMsg: "duplicated label", }, } for _, c := range cases { t.Run(c.desc, func(t *testing.T) { err := c.route.Validate() require.Error(t, err) require.Contains(t, err.Error(), c.expMsg) }) } }) } func TestValidateMuteTimeInterval(t *testing.T) { type testCase struct { desc string mti MuteTimeInterval expMsg string } t.Run("valid interval", func(t *testing.T) { cases := []testCase{ { desc: "nil intervals", mti: MuteTimeInterval{ MuteTimeInterval: config.MuteTimeInterval{ Name: "interval", }, }, }, { desc: "empty intervals", mti: MuteTimeInterval{ MuteTimeInterval: config.MuteTimeInterval{ Name: "interval", TimeIntervals: []timeinterval.TimeInterval{}, }, }, }, { desc: "blank interval", mti: MuteTimeInterval{ MuteTimeInterval: config.MuteTimeInterval{ Name: "interval", TimeIntervals: []timeinterval.TimeInterval{ {}, }, }, }, }, { desc: "simple", mti: MuteTimeInterval{ MuteTimeInterval: config.MuteTimeInterval{ Name: "interval", TimeIntervals: []timeinterval.TimeInterval{ { Weekdays: []timeinterval.WeekdayRange{ { InclusiveRange: timeinterval.InclusiveRange{ Begin: 1, End: 2, }, }, }, }, }, }, }, }, } for _, c := range cases { t.Run(c.desc, func(t *testing.T) { err := c.mti.Validate() require.NoError(t, err) }) } }) t.Run("invalid interval", func(t *testing.T) { cases := []testCase{ { desc: "empty", mti: MuteTimeInterval{}, expMsg: "missing name", }, { desc: "empty", mti: MuteTimeInterval{ MuteTimeInterval: config.MuteTimeInterval{ Name: "interval", TimeIntervals: []timeinterval.TimeInterval{ { Weekdays: []timeinterval.WeekdayRange{ { InclusiveRange: timeinterval.InclusiveRange{ Begin: -1, End: 7, }, }, }, }, }, }, }, expMsg: "unable to convert -1 into weekday", }, } for _, c := range cases { t.Run(c.desc, func(t *testing.T) { err := c.mti.Validate() require.ErrorContains(t, err, c.expMsg) }) } }) } func TestValidateNotificationTemplates(t *testing.T) { tc := []struct { name string template NotificationTemplate expContent string expError error }{ { name: "Same template name as definition", template: NotificationTemplate{ Name: "Same name as definition", Template: `{{ define "Same name as definition" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}`, Provenance: "test", }, expContent: `{{ define "Same name as definition" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}`, expError: nil, }, { name: "Different template name than definition", template: NotificationTemplate{ Name: "Different name than definition", Template: `{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}`, Provenance: "test", }, expContent: `{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}`, expError: nil, }, { name: "Fix template - missing both {{ define }} and {{ end }}", template: NotificationTemplate{ Name: "Alert Instance Template", Template: `Firing: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}`, Provenance: "test", }, expContent: "{{ define \"Alert Instance Template\" }}\n Firing: {{ .Labels.alertname }}\\nSilence: {{ .SilenceURL }}\n{{ end }}", expError: nil, }, { name: "Multiple definitions", template: NotificationTemplate{ Name: "Alert Instance Template", Template: `{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}{{ define "Alert Instance Template2" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}`, Provenance: "test", }, expContent: `{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}{{ define "Alert Instance Template2" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}`, expError: nil, }, { name: "Malformed template - missing {{ define }}", template: NotificationTemplate{ Name: "Alert Instance Template", Template: `\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}`, Provenance: "test", }, expError: errors.New("invalid template: template: Alert Instance Template:3: unexpected {{end}}"), }, { name: "Malformed template - missing {{ end }}", template: NotificationTemplate{ Name: "Alert Instance Template", Template: `{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n`, Provenance: "test", }, expError: errors.New("invalid template: template: Alert Instance Template:1: unexpected EOF"), }, { name: "Malformed template - multiple definitions duplicate name", template: NotificationTemplate{ Name: "Alert Instance Template", Template: `{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}\n{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}`, Provenance: "test", }, expError: errors.New("invalid template: template: Different name than definition:1: template: multiple definition of template \"Alert Instance Template\""), }, { // This is fine as long as the template name is different from the definition, it just ignores the extra text. name: "Extra text outside definition block - different template name and definition", template: NotificationTemplate{ Name: "Different name than definition", Template: `{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}[what is this?]`, Provenance: "test", }, expContent: `{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}[what is this?]`, expError: nil, }, { // This is NOT fine as the template name is the same as the definition. // GO template parser will treat it as if it's wrapped in {{ define "Alert Instance Template" }}, thus creating a duplicate definition. name: "Extra text outside definition block - same template name and definition", template: NotificationTemplate{ Name: "Alert Instance Template", Template: `{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}[what is this?]`, Provenance: "test", }, expError: errors.New("invalid template: template: Alert Instance Template:1: template: multiple definition of template \"Alert Instance Template\""), }, } for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { err := tt.template.Validate() if tt.expError == nil { require.NoError(t, err) } else { require.Error(t, err) return } require.Equal(t, tt.expContent, tt.template.Template) }) } }