From ba31b5aec35a598d44b55af68f852064b4327584 Mon Sep 17 00:00:00 2001 From: almostinf Date: Mon, 11 Mar 2024 16:38:50 +0300 Subject: [PATCH 1/4] add populater for webhook --- api/dto/triggers.go | 4 +- datatypes.go | 20 ++- senders/webhook/request.go | 41 ++++++- senders/webhook/request_test.go | 6 +- senders/webhook/webhook.go | 18 ++- templating/templating.go | 55 +++------ templating/templating_test.go | 207 -------------------------------- 7 files changed, 89 insertions(+), 262 deletions(-) delete mode 100644 templating/templating_test.go diff --git a/api/dto/triggers.go b/api/dto/triggers.go index 90cd1714b..af7c8ef8a 100644 --- a/api/dto/triggers.go +++ b/api/dto/triggers.go @@ -382,8 +382,8 @@ func (trigger *Trigger) PopulatedDescription(events moira.NotificationEvents) er return nil } - templatingEvents := moira.NotificationEventsToTemplatingEvents(events) - description, err := templating.Populate(trigger.Name, *trigger.Desc, templatingEvents) + triggerDescriptionPopulater := templating.NewTriggerDescriptionPopulater(trigger.Name, events.ToTemplateEvents()) + description, err := triggerDescriptionPopulater.Populate(*trigger.Desc) if err != nil { return fmt.Errorf("you have an error in your Go template: %v", err) } diff --git a/datatypes.go b/datatypes.go index 0c6e63c46..6265abbaf 100644 --- a/datatypes.go +++ b/datatypes.go @@ -129,8 +129,10 @@ func (event *NotificationEvent) CreateMessage(location *time.Location) string { // NotificationEvents represents slice of NotificationEvent type NotificationEvents []NotificationEvent +// PopulatedDescription populates a description template using provided trigger and events data func (trigger *TriggerData) PopulatedDescription(events NotificationEvents) error { - description, err := templating.Populate(trigger.Name, trigger.Desc, NotificationEventsToTemplatingEvents(events)) + triggerDescriptionPopulater := templating.NewTriggerDescriptionPopulater(trigger.Name, events.ToTemplateEvents()) + description, err := triggerDescriptionPopulater.Populate(trigger.Desc) if err != nil { description = "Your description is using the wrong template. Since we were unable to populate your template with " + "data, we return it so you can parse it.\n\n" + trigger.Desc @@ -141,10 +143,11 @@ func (trigger *TriggerData) PopulatedDescription(events NotificationEvents) erro return err } -func NotificationEventsToTemplatingEvents(events NotificationEvents) []templating.Event { - templatingEvents := make([]templating.Event, 0, len(events)) +// ToTemplateEvents converts a slice of NotificationEvent into a slice of templating.Event +func (events NotificationEvents) ToTemplateEvents() []templating.Event { + templateEvents := make([]templating.Event, 0, len(events)) for _, event := range events { - templatingEvents = append(templatingEvents, templating.Event{ + templateEvents = append(templateEvents, templating.Event{ Metric: event.Metric, MetricElements: strings.Split(event.Metric, "."), Timestamp: event.Timestamp, @@ -153,7 +156,7 @@ func NotificationEventsToTemplatingEvents(events NotificationEvents) []templatin }) } - return templatingEvents + return templateEvents } // TriggerData represents trigger object @@ -199,6 +202,13 @@ type ContactData struct { Team string `json:"team"` } +func (contact *ContactData) ToTemplateContact() *templating.Contact { + return &templating.Contact{ + Type: contact.Type, + Value: contact.Value, + } +} + // SubscriptionData represents user subscription type SubscriptionData struct { Contacts []string `json:"contacts" example:"acd2db98-1659-4a2f-b227-52d71f6e3ba1"` diff --git a/senders/webhook/request.go b/senders/webhook/request.go index 10e0f5ccc..07a8a17f0 100644 --- a/senders/webhook/request.go +++ b/senders/webhook/request.go @@ -4,11 +4,13 @@ import ( "bytes" "context" "encoding/json" + "html" "net/http" "net/url" "strings" "github.com/moira-alert/moira" + "github.com/moira-alert/moira/templating" ) func (sender *Sender) buildRequest(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) (*http.Request, error) { @@ -17,21 +19,26 @@ func (sender *Sender) buildRequest(events moira.NotificationEvents, contact moir String("potentially_dangerous_url", sender.url). Msg("Found potentially dangerous url template, api contact validation is advised") } + requestURL := buildRequestURL(sender.url, trigger, contact) - requestBody, err := buildRequestBody(events, contact, trigger, plots, throttled) + requestBody, err := sender.buildRequestBody(events, contact, trigger, plots, throttled) if err != nil { return nil, err } + request, err := http.NewRequestWithContext(context.Background(), http.MethodPost, requestURL, bytes.NewBuffer(requestBody)) if err != nil { return request, err } + if sender.user != "" && sender.password != "" { request.SetBasicAuth(sender.user, sender.password) } + for k, v := range sender.headers { request.Header.Set(k, v) } + sender.log.Debug(). String("method", request.Method). String("url", request.URL.String()). @@ -41,7 +48,33 @@ func (sender *Sender) buildRequest(events moira.NotificationEvents, contact moir return request, nil } -func buildRequestBody(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) ([]byte, error) { +func (sender *Sender) buildRequestBody( + events moira.NotificationEvents, + contact moira.ContactData, + trigger moira.TriggerData, + plots [][]byte, + throttled bool, +) ([]byte, error) { + if sender.body == "" { + return buildDefaultRequestBody(events, contact, trigger, plots, throttled) + } + + webhookBodyPopulater := templating.NewWebhookBodyPopulater(contact.ToTemplateContact()) + populatedBody, err := webhookBodyPopulater.Populate(sender.body) + if err != nil { + return nil, err + } + + return []byte(html.UnescapeString(populatedBody)), nil +} + +func buildDefaultRequestBody( + events moira.NotificationEvents, + contact moira.ContactData, + trigger moira.TriggerData, + plots [][]byte, + throttled bool, +) ([]byte, error) { encodedFirstPlot := "" encodedPlots := make([]string, 0, len(plots)) for i, plot := range plots { @@ -51,6 +84,7 @@ func buildRequestBody(events moira.NotificationEvents, contact moira.ContactData encodedFirstPlot = encodedPlot } } + requestPayload := payload{ Trigger: toTriggerData(trigger), Events: toEventsData(events), @@ -65,6 +99,7 @@ func buildRequestBody(events moira.NotificationEvents, contact moira.ContactData Plots: encodedPlots, Throttled: throttled, } + return json.Marshal(requestPayload) } @@ -75,6 +110,7 @@ func buildRequestURL(template string, trigger moira.TriggerData, contact moira.C moira.VariableContactType: contact.Type, moira.VariableTriggerID: trigger.ID, } + for k, v := range templateVariables { value := url.PathEscape(v) if k == moira.VariableContactValue && @@ -83,5 +119,6 @@ func buildRequestURL(template string, trigger moira.TriggerData, contact moira.C } template = strings.ReplaceAll(template, k, value) } + return template } diff --git a/senders/webhook/request_test.go b/senders/webhook/request_test.go index 5dbe46341..24a1169e2 100644 --- a/senders/webhook/request_test.go +++ b/senders/webhook/request_test.go @@ -183,17 +183,19 @@ var requestURLTestCases = []requestURLTestCase{ } func TestBuildRequestBody(t *testing.T) { + sender := Sender{} Convey("Payload should be valid", t, func() { Convey("Trigger state change", func() { events, contact, trigger, plot, throttled := testEvents, testContact, testTrigger, testPlot, testThrottled - requestBody, err := buildRequestBody(events, contact, trigger, plot, throttled) + requestBody, err := sender.buildRequestBody(events, contact, trigger, plot, throttled) actual, expected := prepareStrings(string(requestBody), expectedStateChangePayload) So(actual, ShouldEqual, expected) So(err, ShouldBeNil) }) + Convey("Empty notification", func() { events, contact, trigger, plots, throttled := moira.NotificationEvents{}, moira.ContactData{}, moira.TriggerData{}, make([][]byte, 0), false - requestBody, err := buildRequestBody(events, contact, trigger, plots, throttled) + requestBody, err := sender.buildRequestBody(events, contact, trigger, plots, throttled) actual, expected := prepareStrings(string(requestBody), expectedEmptyPayload) So(actual, ShouldEqual, expected) So(actual, ShouldNotContainSubstring, "null") diff --git a/senders/webhook/webhook.go b/senders/webhook/webhook.go index faaf250eb..cdb65a721 100644 --- a/senders/webhook/webhook.go +++ b/senders/webhook/webhook.go @@ -12,15 +12,18 @@ import ( // Structure that represents the Webhook configuration in the YAML file type config struct { - URL string `mapstructure:"url"` - User string `mapstructure:"user"` - Password string `mapstructure:"password"` - Timeout int `mapstructure:"timeout"` + URL string `mapstructure:"url"` + Body string `mapstructure:"body"` + Headers map[string]string `mapstructure:"headers"` + User string `mapstructure:"user"` + Password string `mapstructure:"password"` + Timeout int `mapstructure:"timeout"` } // Sender implements moira sender interface via webhook type Sender struct { url string + body string user string password string headers map[string]string @@ -41,6 +44,8 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca return fmt.Errorf("can not read url from config") } + sender.body = cfg.Body + sender.user, sender.password = cfg.User, cfg.Password sender.headers = map[string]string{ @@ -48,6 +53,10 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca "Content-Type": "application/json", } + for header, value := range cfg.Headers { + sender.headers[header] = value + } + var timeout int if cfg.Timeout != 0 { timeout = cfg.Timeout @@ -60,6 +69,7 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca Timeout: time.Duration(timeout) * time.Second, Transport: &http.Transport{DisableKeepAlives: true}, } + return nil } diff --git a/templating/templating.go b/templating/templating.go index 8b2101922..003c53b15 100644 --- a/templating/templating.go +++ b/templating/templating.go @@ -12,19 +12,6 @@ import ( const eventTimeFormat = "2006-01-02 15:04:05" -type notification struct { - Trigger trigger - Events []Event -} - -type Event struct { - Metric string - MetricElements []string - Timestamp int64 - Value *float64 - State string -} - func date(unixTime int64) string { return time.Unix(unixTime, 0).Format(eventTimeFormat) } @@ -33,18 +20,6 @@ func formatDate(unixTime int64, format string) string { return time.Unix(unixTime, 0).Format(format) } -func (event Event) TimestampDecrease(second int64) int64 { - return event.Timestamp - second -} - -func (event Event) TimestampIncrease(second int64) int64 { - return event.Timestamp + second -} - -type trigger struct { - Name string `json:"name"` -} - func filterKeys(source template.FuncMap, keys []string) template.FuncMap { result := template.FuncMap{} for _, key := range keys { @@ -250,33 +225,33 @@ var sprigFuncMap = filterKeys(sprig.FuncMap(), []string{ "regexSplit", "mustRegexSplit", "regexQuoteMeta", + + // Advanced functions + "uuidv4", }) -func Populate(name, description string, events []Event) (desc string, err error) { +type Populater interface { + Populate(template string) (string, error) +} + +func populate(tmpl string, data any) (populatedTemplate string, err error) { defer func() { if errRecover := recover(); errRecover != nil { - desc = description - err = fmt.Errorf("PANIC in populate: %w, Trigger name: %s, desc: %s, events:%#v", - err, name, description, events) + populatedTemplate = tmpl + err = fmt.Errorf("PANIC in populate: %v, tmpl: %s, provided data: %#v", errRecover, tmpl, data) } }() buffer := bytes.Buffer{} - dataToExecute := notification{ - Trigger: trigger{Name: name}, - Events: events, - } + template := template.New("populate-template").Funcs(sprigFuncMap).Funcs(funcMap) - triggerTemplate := template.New("populate-description").Funcs(sprigFuncMap).Funcs(funcMap) - triggerTemplate, err = triggerTemplate.Parse(description) - if err != nil { - return description, err + if template, err = template.Parse(tmpl); err != nil { + return tmpl, err } - err = triggerTemplate.Execute(&buffer, dataToExecute) - if err != nil { - return description, err + if err = template.Execute(&buffer, data); err != nil { + return tmpl, err } return strings.TrimSpace(buffer.String()), nil diff --git a/templating/templating_test.go b/templating/templating_test.go deleted file mode 100644 index cdc3fe0a5..000000000 --- a/templating/templating_test.go +++ /dev/null @@ -1,207 +0,0 @@ -package templating - -import ( - "fmt" - "testing" - "time" - - . "github.com/smartystreets/goconvey/convey" -) - -func Test_TemplateDescription(t *testing.T) { - Convey("Test templates", t, func() { - var Name = "TestName" - var Desc = "" + - "Trigger name: {{.Trigger.Name}}\n" + - "{{range $v := .Events }}\n" + - "Metric: {{$v.Metric}}\n" + - "MetricElements: {{$v.MetricElements}}\n" + - "Timestamp: {{$v.Timestamp}}\n" + - "Value: {{$v.Value}}\n" + - "State: {{$v.State}}\n" + - "{{end}}\n" + - "https://grafana.yourhost.com/some-dashboard" + - "{{ range $i, $v := .Events }}{{ if ne $i 0 }}&{{ else }}?" + - "{{ end }}var-host={{ $v.Metric }}{{ end }}" - - var testUnixTime = time.Now().Unix() - var events = []Event{{Metric: "1", Timestamp: testUnixTime}, {Metric: "2", Timestamp: testUnixTime}} - - Convey("Test nil data", func() { - expected, err := Populate(Name, Desc, nil) - if err != nil { - println("Error:", err.Error()) - } - So(err, ShouldBeNil) - So(`Trigger name: TestName - -https://grafana.yourhost.com/some-dashboard`, - ShouldResemble, expected) - }) - - Convey("Test data", func() { - expected, err := Populate(Name, Desc, events) - So(err, ShouldBeNil) - So(fmt.Sprintf("Trigger name: TestName\n\nMetric: 1\nMetricElements: []\nTimestamp: %d\nValue: <nil>"+ - "\nState: \n\nMetric: 2\nMetricElements: []\nTimestamp: %d\nValue: <nil>"+ - "\nState: \n\nhttps://grafana.yourhost.com/some-dashboard?var-host=1&var-host=2", testUnixTime, testUnixTime), - ShouldResemble, expected) - }) - - Convey("Test description without templates", func() { - anotherText := "Another text" - Desc = anotherText - - expected, err := Populate(Name, Desc, events) - So(err, ShouldBeNil) - So(anotherText, ShouldEqual, expected) - }) - - Convey("Test method Date", func() { - formatDate := time.Unix(testUnixTime, 0).Format(eventTimeFormat) - actual := fmt.Sprintf("%s | %s |", formatDate, formatDate) - Desc = "{{ range .Events }}{{ date .Timestamp }} | {{ end }}" - - expected, err := Populate(Name, Desc, events) - So(err, ShouldBeNil) - So(actual, ShouldEqual, expected) - }) - - Convey("Test method formatted Date", func() { - formatedDate := time.Unix(testUnixTime, 0).Format("2006-01-02 15:04:05") - actual := fmt.Sprintf("%s | %s |", formatedDate, formatedDate) - Desc = "{{ range .Events }}{{ formatDate .Timestamp \"2006-01-02 15:04:05\" }} | {{ end }}" - - expected, err := Populate(Name, Desc, events) - So(err, ShouldBeNil) - So(actual, ShouldEqual, expected) - }) - - Convey("Test method decrease and increase Date", func() { - var timeOffset int64 = 300 - - Convey("Date increase", func() { - increase := testUnixTime + timeOffset - actual := fmt.Sprintf("%d | %d |", increase, increase) - Desc = fmt.Sprintf("{{ range .Events }}{{ .TimestampIncrease %d }} | {{ end }}", timeOffset) - - expected, err := Populate(Name, Desc, events) - So(err, ShouldBeNil) - So(actual, ShouldEqual, expected) - }) - - Convey("Date decrease", func() { - increase := testUnixTime - timeOffset - actual := fmt.Sprintf("%d | %d |", increase, increase) - Desc = fmt.Sprintf("{{ range .Events }}{{ .TimestampDecrease %d }} | {{ end }}", timeOffset) - - expected, err := Populate(Name, Desc, events) - So(err, ShouldBeNil) - So(actual, ShouldEqual, expected) - }) - }) - - Convey("Bad functions", func() { - var timeOffset int64 = 300 - - Convey("Non-existent function", func() { - Desc = fmt.Sprintf("{{ range .Events }}{{ decrease %d }} | {{ end }}", timeOffset) - - expected, err := Populate(Name, Desc, events) - So(err, ShouldNotBeNil) - So(Desc, ShouldEqual, expected) - }) - - Convey("Non-existent method", func() { - Desc = fmt.Sprintf("{{ range .Events }}{{ .Decrease %d }} | {{ end }}", timeOffset) - - expected, err := Populate(Name, Desc, events) - So(err, ShouldNotBeNil) - So(Desc, ShouldEqual, expected) - }) - - Convey("Bad parameters", func() { - Desc = "{{ date \"bad\" }} " - - expected, err := Populate(Name, Desc, events) - So(err, ShouldNotBeNil) - So(Desc, ShouldEqual, expected) - }) - - Convey("No parameters", func() { - Desc = "{{ date }} " - - expected, err := Populate(Name, Desc, events) - So(err, ShouldNotBeNil) - So(Desc, ShouldEqual, expected) - }) - }) - - Convey("Test strings functions", func() { - Convey("Test replace", func() { - Desc = "{{ stringsReplace \"my.metrics.path\" \".\" \"_\" -1 }} " - expected, err := Populate(Name, Desc, events) - So(err, ShouldBeNil) - So("my_metrics_path", ShouldEqual, expected) - }) - - Convey("Test replace limited to 1", func() { - Desc = "{{ stringsReplace \"my.metrics.path\" \".\" \"_\" 1 }} " - expected, err := Populate(Name, Desc, events) - So(err, ShouldBeNil) - So("my_metrics.path", ShouldEqual, expected) - }) - - Convey("Test trim suffix", func() { - Desc = "{{ stringsTrimSuffix \"my.metrics.path\" \".path\" }} " - expected, err := Populate(Name, Desc, events) - So(err, ShouldBeNil) - So("my.metrics", ShouldEqual, expected) - }) - - Convey("Test trim prefix", func() { - Desc = "{{ stringsTrimPrefix \"my.metrics.path\" \"my.\" }} " - expected, err := Populate(Name, Desc, events) - So(err, ShouldBeNil) - So("metrics.path", ShouldEqual, expected) - }) - - Convey("Test lower case", func() { - Desc = "{{ stringsToLower \"MY.PATH\" }} " - expected, err := Populate(Name, Desc, events) - So(err, ShouldBeNil) - So("my.path", ShouldEqual, expected) - }) - - Convey("Test upper case", func() { - Desc = "{{ stringsToUpper \"my.path\" }} " - expected, err := Populate(Name, Desc, events) - So(err, ShouldBeNil) - So("MY.PATH", ShouldEqual, expected) - }) - }) - - Convey("Test some sprig functions", func() { - Convey("Test upper", func() { - Desc = "{{ \"hello!\" | upper}} " - expected, err := Populate(Name, Desc, events) - So(err, ShouldBeNil) - So("HELLO!", ShouldEqual, expected) - }) - - Convey("Test upper repeat", func() { - Desc = "{{ \"hello!\" | upper | repeat 5 }} " - expected, err := Populate(Name, Desc, events) - So(err, ShouldBeNil) - So("HELLO!HELLO!HELLO!HELLO!HELLO!", ShouldEqual, expected) - }) - - Convey("Test list uniq without", func() { - Desc = "{{ without (list 1 3 3 2 2 2 4 4 4 4 1 | uniq) 4 }} " - expected, err := Populate(Name, Desc, events) - So(err, ShouldBeNil) - So("[1 3 2]", ShouldEqual, expected) - }) - }) - }) -} From 72431eba545a7f1ddb68c8e1108a7e64696cad9c Mon Sep 17 00:00:00 2001 From: almostinf Date: Mon, 11 Mar 2024 16:39:09 +0300 Subject: [PATCH 2/4] add populater for webhook --- templating/trigger.go | 39 +++++++ templating/trigger_test.go | 224 +++++++++++++++++++++++++++++++++++++ templating/webhook.go | 20 ++++ 3 files changed, 283 insertions(+) create mode 100644 templating/trigger.go create mode 100644 templating/trigger_test.go create mode 100644 templating/webhook.go diff --git a/templating/trigger.go b/templating/trigger.go new file mode 100644 index 000000000..aace37c85 --- /dev/null +++ b/templating/trigger.go @@ -0,0 +1,39 @@ +package templating + +type trigger struct { + Name string +} + +type Event struct { + Metric string + MetricElements []string + Timestamp int64 + Value *float64 + State string +} + +func (event Event) TimestampDecrease(second int64) int64 { + return event.Timestamp - second +} + +func (event Event) TimestampIncrease(second int64) int64 { + return event.Timestamp + second +} + +type triggerDescriptionPopulater struct { + Trigger *trigger + Events []Event +} + +func NewTriggerDescriptionPopulater(triggerName string, events []Event) *triggerDescriptionPopulater { + return &triggerDescriptionPopulater{ + Trigger: &trigger{ + Name: triggerName, + }, + Events: events, + } +} + +func (templateData *triggerDescriptionPopulater) Populate(tmpl string) (string, error) { + return populate(tmpl, templateData) +} diff --git a/templating/trigger_test.go b/templating/trigger_test.go new file mode 100644 index 000000000..4d77382d5 --- /dev/null +++ b/templating/trigger_test.go @@ -0,0 +1,224 @@ +package templating + +import ( + "fmt" + "testing" + "time" + + . "github.com/smartystreets/goconvey/convey" +) + +func Test_TemplateDescription(t *testing.T) { + Convey("Test templates", t, func() { + triggerName := "TestName" + template := "" + + "Trigger name: {{.Trigger.Name}}\n" + + "{{range $v := .Events }}\n" + + "Metric: {{$v.Metric}}\n" + + "MetricElements: {{$v.MetricElements}}\n" + + "Timestamp: {{$v.Timestamp}}\n" + + "Value: {{$v.Value}}\n" + + "State: {{$v.State}}\n" + + "{{end}}\n" + + "https://grafana.yourhost.com/some-dashboard" + + "{{ range $i, $v := .Events }}{{ if ne $i 0 }}&{{ else }}?" + + "{{ end }}var-host={{ $v.Metric }}{{ end }}" + + testUnixTime := time.Now().Unix() + events := []Event{ + {Metric: "1", Timestamp: testUnixTime}, + {Metric: "2", Timestamp: testUnixTime}, + } + triggerDescriptionPopulater := NewTriggerDescriptionPopulater(triggerName, events) + + Convey("Test nil data", func() { + triggerDescriptionPopulater = NewTriggerDescriptionPopulater(triggerName, nil) + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldResemble, `Trigger name: TestName + +https://grafana.yourhost.com/some-dashboard`) + }) + + Convey("Test data", func() { + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldResemble, fmt.Sprintf("Trigger name: TestName\n\nMetric: 1\nMetricElements: []\nTimestamp: %d\nValue: <nil>"+ + "\nState: \n\nMetric: 2\nMetricElements: []\nTimestamp: %d\nValue: <nil>"+ + "\nState: \n\nhttps://grafana.yourhost.com/some-dashboard?var-host=1&var-host=2", testUnixTime, testUnixTime)) + }) + + Convey("Test description without templates", func() { + template = "Another text" + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, template) + }) + + Convey("Test method Date", func() { + formatDate := time.Unix(testUnixTime, 0).Format(eventTimeFormat) + expected := fmt.Sprintf("%s | %s |", formatDate, formatDate) + template = "{{ range .Events }}{{ date .Timestamp }} | {{ end }}" + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Test method formatted Date", func() { + formatedDate := time.Unix(testUnixTime, 0).Format("2006-01-02 15:04:05") + expected := fmt.Sprintf("%s | %s |", formatedDate, formatedDate) + template = "{{ range .Events }}{{ formatDate .Timestamp \"2006-01-02 15:04:05\" }} | {{ end }}" + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Test method decrease and increase Date", func() { + var timeOffset int64 = 300 + + Convey("Date increase", func() { + increase := testUnixTime + timeOffset + expected := fmt.Sprintf("%d | %d |", increase, increase) + template = fmt.Sprintf("{{ range .Events }}{{ .TimestampIncrease %d }} | {{ end }}", timeOffset) + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Date decrease", func() { + increase := testUnixTime - timeOffset + expected := fmt.Sprintf("%d | %d |", increase, increase) + template = fmt.Sprintf("{{ range .Events }}{{ .TimestampDecrease %d }} | {{ end }}", timeOffset) + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + }) + + Convey("Bad functions", func() { + var timeOffset int64 = 300 + + Convey("Non-existent function", func() { + template = fmt.Sprintf("{{ range .Events }}{{ decrease %d }} | {{ end }}", timeOffset) + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldNotBeNil) + So(actual, ShouldEqual, template) + }) + + Convey("Non-existent method", func() { + template = fmt.Sprintf("{{ range .Events }}{{ .Decrease %d }} | {{ end }}", timeOffset) + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldNotBeNil) + So(actual, ShouldEqual, template) + }) + + Convey("Bad parameters", func() { + template = "{{ date \"bad\" }} " + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldNotBeNil) + So(actual, ShouldEqual, template) + }) + + Convey("No parameters", func() { + template = "{{ date }} " + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldNotBeNil) + So(actual, ShouldEqual, template) + }) + }) + + Convey("Test strings functions", func() { + Convey("Test replace", func() { + template = "{{ stringsReplace \"my.metrics.path\" \".\" \"_\" -1 }} " + expected := "my_metrics_path" + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Test replace limited to 1", func() { + template = "{{ stringsReplace \"my.metrics.path\" \".\" \"_\" 1 }} " + expected := "my_metrics.path" + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Test trim suffix", func() { + template = "{{ stringsTrimSuffix \"my.metrics.path\" \".path\" }} " + expected := "my.metrics" + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Test trim prefix", func() { + template = "{{ stringsTrimPrefix \"my.metrics.path\" \"my.\" }} " + expected := "metrics.path" + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Test lower case", func() { + template = "{{ stringsToLower \"MY.PATH\" }} " + expected := "my.path" + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Test upper case", func() { + template = "{{ stringsToUpper \"my.path\" }} " + expected := "MY.PATH" + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + }) + + Convey("Test some sprig functions", func() { + Convey("Test upper", func() { + template = "{{ \"hello!\" | upper}} " + expected := "HELLO!" + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Test upper repeat", func() { + template = "{{ \"hello!\" | upper | repeat 5 }} " + expected := "HELLO!HELLO!HELLO!HELLO!HELLO!" + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Test list uniq without", func() { + template = "{{ without (list 1 3 3 2 2 2 4 4 4 4 1 | uniq) 4 }} " + expected := "[1 3 2]" + + actual, err := triggerDescriptionPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + }) + }) +} diff --git a/templating/webhook.go b/templating/webhook.go new file mode 100644 index 000000000..e18911a58 --- /dev/null +++ b/templating/webhook.go @@ -0,0 +1,20 @@ +package templating + +type Contact struct { + Type string + Value string +} + +type webhookBodyPopulater struct { + Contact *Contact +} + +func NewWebhookBodyPopulater(contact *Contact) *webhookBodyPopulater { + return &webhookBodyPopulater{ + Contact: contact, + } +} + +func (templateData *webhookBodyPopulater) Populate(tmpl string) (string, error) { + return populate(tmpl, templateData) +} From c6c44e940acf9c26a0ec5e9c5187e7c3b97d1b9d Mon Sep 17 00:00:00 2001 From: almostinf Date: Thu, 21 Mar 2024 15:10:45 +0300 Subject: [PATCH 3/4] add tests and godocs --- datatypes.go | 1 + go.mod | 2 +- senders/webhook/request_test.go | 47 +++++++++++++++++- senders/webhook/webhook.go | 5 +- senders/webhook/webhook_test.go | 75 ++++++++++++++++++++++++++++- templating/templating.go | 1 + templating/trigger.go | 5 ++ templating/trigger_test.go | 85 --------------------------------- templating/webhook.go | 3 ++ 9 files changed, 134 insertions(+), 90 deletions(-) diff --git a/datatypes.go b/datatypes.go index 229f9c5b0..31e2480f5 100644 --- a/datatypes.go +++ b/datatypes.go @@ -202,6 +202,7 @@ type ContactData struct { Team string `json:"team"` } +// ToTemplateContact converts a ContactData into a template Contact. func (contact *ContactData) ToTemplateContact() *templating.Contact { return &templating.Contact{ Type: contact.Type, diff --git a/go.mod b/go.mod index 553b8ac1a..dcd44d320 100644 --- a/go.mod +++ b/go.mod @@ -96,7 +96,7 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/gomodule/redigo v1.8.9 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.3.0 github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a // indirect diff --git a/senders/webhook/request_test.go b/senders/webhook/request_test.go index 24a1169e2..81fbb57da 100644 --- a/senders/webhook/request_test.go +++ b/senders/webhook/request_test.go @@ -10,7 +10,11 @@ import ( "github.com/moira-alert/moira" ) -const testBadID = "!@#$" +const ( + testBadID = "!@#$" + testContactType = "testType" + testContactValue = "testValue" +) var ( testHost = "https://hostname.domain" @@ -184,7 +188,8 @@ var requestURLTestCases = []requestURLTestCase{ func TestBuildRequestBody(t *testing.T) { sender := Sender{} - Convey("Payload should be valid", t, func() { + + Convey("Test building default request body", t, func() { Convey("Trigger state change", func() { events, contact, trigger, plot, throttled := testEvents, testContact, testTrigger, testPlot, testThrottled requestBody, err := sender.buildRequestBody(events, contact, trigger, plot, throttled) @@ -202,6 +207,44 @@ func TestBuildRequestBody(t *testing.T) { So(err, ShouldBeNil) }) }) + + Convey("Test building custom request body with webhook populater", t, func() { + sender.body = "" + + "Contact.Type: {{ .Contact.Type }}\n" + + "Contact.Value: {{ .Contact.Value }}" + + Convey("With empty contact", func() { + events, contact, trigger, plots, throttled := moira.NotificationEvents{}, moira.ContactData{}, moira.TriggerData{}, make([][]byte, 0), false + + requestBody, err := sender.buildRequestBody(events, contact, trigger, plots, throttled) + So(err, ShouldBeNil) + So(string(requestBody), ShouldResemble, "Contact.Type: \nContact.Value:") + }) + + Convey("With only contact type", func() { + events, contact, trigger, plots, throttled := moira.NotificationEvents{}, moira.ContactData{Type: testContactType}, moira.TriggerData{}, make([][]byte, 0), false + + requestBody, err := sender.buildRequestBody(events, contact, trigger, plots, throttled) + So(err, ShouldBeNil) + So(string(requestBody), ShouldResemble, fmt.Sprintf("Contact.Type: %s\nContact.Value:", testContactType)) + }) + + Convey("With only contact value", func() { + events, contact, trigger, plots, throttled := moira.NotificationEvents{}, moira.ContactData{Value: testContactValue}, moira.TriggerData{}, make([][]byte, 0), false + + requestBody, err := sender.buildRequestBody(events, contact, trigger, plots, throttled) + So(err, ShouldBeNil) + So(string(requestBody), ShouldResemble, fmt.Sprintf("Contact.Type: \nContact.Value: %s", testContactValue)) + }) + + Convey("With full provided data", func() { + events, contact, trigger, plots, throttled := moira.NotificationEvents{}, moira.ContactData{Value: testContactValue, Type: testContactType}, moira.TriggerData{}, make([][]byte, 0), false + + requestBody, err := sender.buildRequestBody(events, contact, trigger, plots, throttled) + So(err, ShouldBeNil) + So(string(requestBody), ShouldResemble, fmt.Sprintf("Contact.Type: %s\nContact.Value: %s", testContactType, testContactValue)) + }) + }) } func TestBuildRequestURL(t *testing.T) { diff --git a/senders/webhook/webhook.go b/senders/webhook/webhook.go index 5957c53cc..3812a8799 100644 --- a/senders/webhook/webhook.go +++ b/senders/webhook/webhook.go @@ -1,6 +1,7 @@ package webhook import ( + "errors" "fmt" "io" "net/http" @@ -10,6 +11,8 @@ import ( "github.com/moira-alert/moira" ) +var ErrMissingURL = errors.New("can not read url from config") + // Structure that represents the Webhook configuration in the YAML file. type config struct { URL string `mapstructure:"url"` @@ -41,7 +44,7 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca sender.url = cfg.URL if sender.url == "" { - return fmt.Errorf("can not read url from config") + return ErrMissingURL } sender.body = cfg.Body diff --git a/senders/webhook/webhook_test.go b/senders/webhook/webhook_test.go index f8b0450a6..07369f11c 100644 --- a/senders/webhook/webhook_test.go +++ b/senders/webhook/webhook_test.go @@ -21,9 +21,82 @@ import ( const ( testUser = "testUser" testPass = "testPass" + testURL = "http://localhost:8080" + testBody = "testBody" + + dateTimeFormat = "15:04 02.01.2006" +) + +var ( + logger, _ = logging.GetLogger("webhook") + location, _ = time.LoadLocation("UTC") + defaultHeaders = map[string]string{ + "User-Agent": "Moira", + "Content-Type": "application/json", + } ) -var logger, _ = logging.GetLogger("webhook") +func TestSender_Init(t *testing.T) { + Convey("Test Init function", t, func() { + Convey("With empty settings", func() { + settings := map[string]interface{}{} + sender := Sender{} + + err := sender.Init(settings, logger, location, dateTimeFormat) + So(err, ShouldResemble, ErrMissingURL) + }) + + Convey("With only url", func() { + settings := map[string]interface{}{ + "url": testURL, + } + sender := Sender{} + + err := sender.Init(settings, logger, location, dateTimeFormat) + So(err, ShouldBeNil) + So(sender, ShouldResemble, Sender{ + url: testURL, + headers: defaultHeaders, + client: &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{DisableKeepAlives: true}, + }, + log: logger, + }) + }) + + Convey("With full provided data", func() { + settings := map[string]interface{}{ + "url": testURL, + "body": testBody, + "user": testUser, + "password": testPass, + "headers": map[string]string{ + "testHeader": "test", + }, + "timeout": 120, + } + sender := Sender{} + expectedHeaders := defaultHeaders + expectedHeaders["testHeader"] = "test" + + err := sender.Init(settings, logger, location, dateTimeFormat) + So(err, ShouldBeNil) + So(sender, ShouldResemble, Sender{ + url: testURL, + body: testBody, + user: testUser, + password: testPass, + headers: expectedHeaders, + client: &http.Client{ + Timeout: 120 * time.Second, + Transport: &http.Transport{DisableKeepAlives: true}, + }, + log: logger, + }) + }) + }) +} func TestSender_SendEvents(t *testing.T) { Convey("Receive test webhook", t, func() { diff --git a/templating/templating.go b/templating/templating.go index 003c53b15..d59c88177 100644 --- a/templating/templating.go +++ b/templating/templating.go @@ -230,6 +230,7 @@ var sprigFuncMap = filterKeys(sprig.FuncMap(), []string{ "uuidv4", }) +// Populater represents an interface for populating a template with data. type Populater interface { Populate(template string) (string, error) } diff --git a/templating/trigger.go b/templating/trigger.go index aace37c85..dc7f485e5 100644 --- a/templating/trigger.go +++ b/templating/trigger.go @@ -4,6 +4,7 @@ type trigger struct { Name string } +// Event represents a template event with fields allowed for use in templates. type Event struct { Metric string MetricElements []string @@ -12,10 +13,12 @@ type Event struct { State string } +// TimestampDecrease decreases the timestamp of the event by the given number of seconds. func (event Event) TimestampDecrease(second int64) int64 { return event.Timestamp - second } +// TimestampIncrease increases the timestamp of the event by the given number of seconds. func (event Event) TimestampIncrease(second int64) int64 { return event.Timestamp + second } @@ -25,6 +28,7 @@ type triggerDescriptionPopulater struct { Events []Event } +// NewTriggerDescriptionPopulater creates a new trigger description populater with the given trigger name and template events. func NewTriggerDescriptionPopulater(triggerName string, events []Event) *triggerDescriptionPopulater { return &triggerDescriptionPopulater{ Trigger: &trigger{ @@ -34,6 +38,7 @@ func NewTriggerDescriptionPopulater(triggerName string, events []Event) *trigger } } +// Populate populates the given template with trigger description data. func (templateData *triggerDescriptionPopulater) Populate(tmpl string) (string, error) { return populate(tmpl, templateData) } diff --git a/templating/trigger_test.go b/templating/trigger_test.go index 4d77382d5..386851e95 100644 --- a/templating/trigger_test.go +++ b/templating/trigger_test.go @@ -135,90 +135,5 @@ https://grafana.yourhost.com/some-dashboard`) So(actual, ShouldEqual, template) }) }) - - Convey("Test strings functions", func() { - Convey("Test replace", func() { - template = "{{ stringsReplace \"my.metrics.path\" \".\" \"_\" -1 }} " - expected := "my_metrics_path" - - actual, err := triggerDescriptionPopulater.Populate(template) - So(err, ShouldBeNil) - So(actual, ShouldEqual, expected) - }) - - Convey("Test replace limited to 1", func() { - template = "{{ stringsReplace \"my.metrics.path\" \".\" \"_\" 1 }} " - expected := "my_metrics.path" - - actual, err := triggerDescriptionPopulater.Populate(template) - So(err, ShouldBeNil) - So(actual, ShouldEqual, expected) - }) - - Convey("Test trim suffix", func() { - template = "{{ stringsTrimSuffix \"my.metrics.path\" \".path\" }} " - expected := "my.metrics" - - actual, err := triggerDescriptionPopulater.Populate(template) - So(err, ShouldBeNil) - So(actual, ShouldEqual, expected) - }) - - Convey("Test trim prefix", func() { - template = "{{ stringsTrimPrefix \"my.metrics.path\" \"my.\" }} " - expected := "metrics.path" - - actual, err := triggerDescriptionPopulater.Populate(template) - So(err, ShouldBeNil) - So(actual, ShouldEqual, expected) - }) - - Convey("Test lower case", func() { - template = "{{ stringsToLower \"MY.PATH\" }} " - expected := "my.path" - - actual, err := triggerDescriptionPopulater.Populate(template) - So(err, ShouldBeNil) - So(actual, ShouldEqual, expected) - }) - - Convey("Test upper case", func() { - template = "{{ stringsToUpper \"my.path\" }} " - expected := "MY.PATH" - - actual, err := triggerDescriptionPopulater.Populate(template) - So(err, ShouldBeNil) - So(actual, ShouldEqual, expected) - }) - }) - - Convey("Test some sprig functions", func() { - Convey("Test upper", func() { - template = "{{ \"hello!\" | upper}} " - expected := "HELLO!" - - actual, err := triggerDescriptionPopulater.Populate(template) - So(err, ShouldBeNil) - So(actual, ShouldEqual, expected) - }) - - Convey("Test upper repeat", func() { - template = "{{ \"hello!\" | upper | repeat 5 }} " - expected := "HELLO!HELLO!HELLO!HELLO!HELLO!" - - actual, err := triggerDescriptionPopulater.Populate(template) - So(err, ShouldBeNil) - So(actual, ShouldEqual, expected) - }) - - Convey("Test list uniq without", func() { - template = "{{ without (list 1 3 3 2 2 2 4 4 4 4 1 | uniq) 4 }} " - expected := "[1 3 2]" - - actual, err := triggerDescriptionPopulater.Populate(template) - So(err, ShouldBeNil) - So(actual, ShouldEqual, expected) - }) - }) }) } diff --git a/templating/webhook.go b/templating/webhook.go index e18911a58..1e955f1aa 100644 --- a/templating/webhook.go +++ b/templating/webhook.go @@ -1,5 +1,6 @@ package templating +// Contact represents a template contact with fields allowed for use in templates. type Contact struct { Type string Value string @@ -9,12 +10,14 @@ type webhookBodyPopulater struct { Contact *Contact } +// NewWebhookBodyPopulater creates a new webhook body populater with provided template contact. func NewWebhookBodyPopulater(contact *Contact) *webhookBodyPopulater { return &webhookBodyPopulater{ Contact: contact, } } +// Populate populates the given template with contact data. func (templateData *webhookBodyPopulater) Populate(tmpl string) (string, error) { return populate(tmpl, templateData) } From 9670501e81f1b17a25e12481f4ac265a5fe278e7 Mon Sep 17 00:00:00 2001 From: almostinf Date: Thu, 21 Mar 2024 15:14:01 +0300 Subject: [PATCH 4/4] add templating and webhook tests --- templating/templating_test.go | 115 ++++++++++++++++++++++++++++++++++ templating/webhook_test.go | 74 ++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 templating/templating_test.go create mode 100644 templating/webhook_test.go diff --git a/templating/templating_test.go b/templating/templating_test.go new file mode 100644 index 000000000..02b4497a9 --- /dev/null +++ b/templating/templating_test.go @@ -0,0 +1,115 @@ +package templating + +import ( + "testing" + + "github.com/google/uuid" + . "github.com/smartystreets/goconvey/convey" +) + +type testPopulater struct{} + +func (testData *testPopulater) Populate(tmpl string) (string, error) { + return populate(tmpl, testData) +} + +func Test_populate(t *testing.T) { + Convey("Test populate function", t, func() { + populater := testPopulater{} + + Convey("Test strings functions", func() { + Convey("Test replace", func() { + template := "{{ stringsReplace \"my.metrics.path\" \".\" \"_\" -1 }} " + expected := "my_metrics_path" + + actual, err := populater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Test replace limited to 1", func() { + template := "{{ stringsReplace \"my.metrics.path\" \".\" \"_\" 1 }} " + expected := "my_metrics.path" + + actual, err := populater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Test trim suffix", func() { + template := "{{ stringsTrimSuffix \"my.metrics.path\" \".path\" }} " + expected := "my.metrics" + + actual, err := populater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Test trim prefix", func() { + template := "{{ stringsTrimPrefix \"my.metrics.path\" \"my.\" }} " + expected := "metrics.path" + + actual, err := populater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Test lower case", func() { + template := "{{ stringsToLower \"MY.PATH\" }} " + expected := "my.path" + + actual, err := populater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Test upper case", func() { + template := "{{ stringsToUpper \"my.path\" }} " + expected := "MY.PATH" + + actual, err := populater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + }) + + Convey("Test some sprig functions", func() { + Convey("Test upper", func() { + template := "{{ \"hello!\" | upper}} " + expected := "HELLO!" + + actual, err := populater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Test upper repeat", func() { + template := "{{ \"hello!\" | upper | repeat 5 }} " + expected := "HELLO!HELLO!HELLO!HELLO!HELLO!" + + actual, err := populater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Test list uniq without", func() { + template := "{{ without (list 1 3 3 2 2 2 4 4 4 4 1 | uniq) 4 }} " + expected := "[1 3 2]" + + actual, err := populater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldEqual, expected) + }) + + Convey("Test uuidv4 function", func() { + template := "{{ uuidv4 }}" + + actual, err := populater.Populate(template) + So(err, ShouldBeNil) + + _, err = uuid.Parse(actual) + So(err, ShouldBeNil) + }) + }) + }) +} diff --git a/templating/webhook_test.go b/templating/webhook_test.go new file mode 100644 index 000000000..f176a7c3f --- /dev/null +++ b/templating/webhook_test.go @@ -0,0 +1,74 @@ +package templating + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func Test_TemplateWebhookBody(t *testing.T) { + Convey("Test webhook body populater", t, func() { + template := "" + + "Contact Type: {{ .Contact.Type }}\n" + + "Contact Value: {{ .Contact.Value }}" + + Convey("Test with nil data", func() { + webhookPopulater := NewWebhookBodyPopulater(nil) + + actual, err := webhookPopulater.Populate(template) + So(err, ShouldNotBeNil) + So(actual, ShouldResemble, template) + }) + + Convey("Test with empty data", func() { + webhookPopulater := NewWebhookBodyPopulater(&Contact{}) + expected := "" + + "Contact Type: \n" + + "Contact Value:" + + actual, err := webhookPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldResemble, expected) + }) + + Convey("Test with empty value", func() { + webhookPopulater := NewWebhookBodyPopulater(&Contact{ + Type: "slack", + }) + expected := "" + + "Contact Type: slack\n" + + "Contact Value:" + + actual, err := webhookPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldResemble, expected) + }) + + Convey("Test with empty type", func() { + webhookPopulater := NewWebhookBodyPopulater(&Contact{ + Value: "#test_channel", + }) + expected := "" + + "Contact Type: \n" + + "Contact Value: #test_channel" + + actual, err := webhookPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldResemble, expected) + }) + + Convey("Test with full template contact info", func() { + webhookPopulater := NewWebhookBodyPopulater(&Contact{ + Type: "slack", + Value: "#test_channel", + }) + expected := "" + + "Contact Type: slack\n" + + "Contact Value: #test_channel" + + actual, err := webhookPopulater.Populate(template) + So(err, ShouldBeNil) + So(actual, ShouldResemble, expected) + }) + }) +}