From bb973979d28d80b4dd3f2e6acffc090648c9e9cb Mon Sep 17 00:00:00 2001 From: stendler Date: Sat, 5 Oct 2024 02:23:07 +0200 Subject: [PATCH] feat(alerting): add email and click action to ntfy provider (#778) * feat(alerting): add optional email to ntfy provider https://docs.ntfy.sh/publish/#e-mail-notifications * feat(alerting): add optional click action to ntfy provider https://docs.ntfy.sh/publish/#click-action * feat(alerting): add option to disable firebase in ntfy provider https://docs.ntfy.sh/publish/#disable-firebase * feat(alerting): add option to disable message caching in ntfy provider https://docs.ntfy.sh/publish/#message-caching * test(alerting): add buildRequestBody tests for email and click properties * test(alerting): add tests for Send to verify request headers * feat(alerting): refactor to prefix firebase & cache with "disable" This avoids the need for a pointer, as omitting these bools in the config defaults to false and omitting to set these headers will use the server's default - which is enabled on ntfy.sh --- README.md | 20 +++-- alerting/provider/ntfy/ntfy.go | 22 +++++- alerting/provider/ntfy/ntfy_test.go | 113 ++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index bd78b4ec..2bcc324b 100644 --- a/README.md +++ b/README.md @@ -983,14 +983,18 @@ endpoints: #### Configuring Ntfy alerts -| Parameter | Description | Default | -|:------------------------------|:-------------------------------------------------------------------------------------------|:------------------| -| `alerting.ntfy` | Configuration for alerts of type `ntfy` | `{}` | -| `alerting.ntfy.topic` | Topic at which the alert will be sent | Required `""` | -| `alerting.ntfy.url` | The URL of the target server | `https://ntfy.sh` | -| `alerting.ntfy.token` | [Access token](https://docs.ntfy.sh/publish/#access-tokens) for restricted topics | `""` | -| `alerting.ntfy.priority` | The priority of the alert | `3` | -| `alerting.ntfy.default-alert` | Default alert configuration.
See [Setting a default alert](#setting-a-default-alert) | N/A | +| Parameter | Description | Default | +|:---------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------|:------------------| +| `alerting.ntfy` | Configuration for alerts of type `ntfy` | `{}` | +| `alerting.ntfy.topic` | Topic at which the alert will be sent | Required `""` | +| `alerting.ntfy.url` | The URL of the target server | `https://ntfy.sh` | +| `alerting.ntfy.token` | [Access token](https://docs.ntfy.sh/publish/#access-tokens) for restricted topics | `""` | +| `alerting.ntfy.email` | E-mail address for additional e-mail notifications | `""` | +| `alerting.ntfy.click` | Website opened when notification is clicked | `""` | +| `alerting.ntfy.priority` | The priority of the alert | `3` | +| `alerting.ntfy.disable-firebase` | Whether message push delivery via firebase should be disabled. [ntfy.sh defaults to enabled](https://docs.ntfy.sh/publish/#disable-firebase) | `false` | +| `alerting.ntfy.disable-cache` | Whether server side message caching should be disabled. [ntfy.sh defaults to enabled](https://docs.ntfy.sh/publish/#message-caching) | `false` | +| `alerting.ntfy.default-alert` | Default alert configuration.
See [Setting a default alert](#setting-a-default-alert) | N/A | [ntfy](https://github.com/binwiederhier/ntfy) is an amazing project that allows you to subscribe to desktop and mobile notifications, making it an awesome addition to Gatus. diff --git a/alerting/provider/ntfy/ntfy.go b/alerting/provider/ntfy/ntfy.go index 1530c10f..784caf39 100644 --- a/alerting/provider/ntfy/ntfy.go +++ b/alerting/provider/ntfy/ntfy.go @@ -21,10 +21,14 @@ const ( // AlertProvider is the configuration necessary for sending an alert using Slack type AlertProvider struct { - Topic string `yaml:"topic"` - URL string `yaml:"url,omitempty"` // Defaults to DefaultURL - Priority int `yaml:"priority,omitempty"` // Defaults to DefaultPriority - Token string `yaml:"token,omitempty"` // Defaults to "" + Topic string `yaml:"topic"` + URL string `yaml:"url,omitempty"` // Defaults to DefaultURL + Priority int `yaml:"priority,omitempty"` // Defaults to DefaultPriority + Token string `yaml:"token,omitempty"` // Defaults to "" + Email string `yaml:"email,omitempty"` // Defaults to "" + Click string `yaml:"click,omitempty"` // Defaults to "" + DisableFirebase bool `yaml:"disable-firebase,omitempty"` // Defaults to false + DisableCache bool `yaml:"disable-cache,omitempty"` // Defaults to false // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"` @@ -56,6 +60,12 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r if len(provider.Token) > 0 { request.Header.Set("Authorization", "Bearer "+provider.Token) } + if provider.DisableFirebase { + request.Header.Set("Firebase", "no") + } + if provider.DisableCache { + request.Header.Set("Cache", "no") + } response, err := client.GetHTTPClient(nil).Do(request) if err != nil { return err @@ -74,6 +84,8 @@ type Body struct { Message string `json:"message"` Tags []string `json:"tags"` Priority int `json:"priority"` + Email string `json:"email,omitempty"` + Click string `json:"click,omitempty"` } // buildRequestBody builds the request body for the provider @@ -105,6 +117,8 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al Message: message, Tags: []string{tag}, Priority: provider.Priority, + Email: provider.Email, + Click: provider.Click, }) return body } diff --git a/alerting/provider/ntfy/ntfy_test.go b/alerting/provider/ntfy/ntfy_test.go index 3ca3e59d..452533f4 100644 --- a/alerting/provider/ntfy/ntfy_test.go +++ b/alerting/provider/ntfy/ntfy_test.go @@ -2,6 +2,9 @@ package ntfy import ( "encoding/json" + "io" + "net/http" + "net/http/httptest" "testing" "github.com/TwiN/gatus/v5/alerting/alert" @@ -88,6 +91,20 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { Resolved: true, ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\n🟢 [CONNECTED] == true\n🟢 [STATUS] == 200","tags":["white_check_mark"],"priority":2}`, }, + { + Name: "triggered-email", + Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com"}, + Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: false, + ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`, + }, + { + Name: "resolved-email", + Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 2, Email: "test@example.com", Click: "example.com"}, + Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: true, + ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\n🟢 [CONNECTED] == true\n🟢 [STATUS] == 200","tags":["white_check_mark"],"priority":2,"email":"test@example.com","click":"example.com"}`, + }, } for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { @@ -112,3 +129,99 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { }) } } + +func TestAlertProvider_Send(t *testing.T) { + description := "description-1" + scenarios := []struct { + Name string + Provider AlertProvider + Alert alert.Alert + Resolved bool + ExpectedBody string + ExpectedHeaders map[string]string + }{ + { + Name: "triggered", + Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com"}, + Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: false, + ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`, + ExpectedHeaders: map[string]string{ + "Content-Type": "application/json", + }, + }, + { + Name: "no firebase", + Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com", DisableFirebase: true}, + Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: false, + ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`, + ExpectedHeaders: map[string]string{ + "Content-Type": "application/json", + "Firebase": "no", + }, + }, + { + Name: "no cache", + Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com", DisableCache: true}, + Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: false, + ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`, + ExpectedHeaders: map[string]string{ + "Content-Type": "application/json", + "Cache": "no", + }, + }, + { + Name: "neither firebase & cache", + Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com", DisableFirebase: true, DisableCache: true}, + Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: false, + ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`, + ExpectedHeaders: map[string]string{ + "Content-Type": "application/json", + "Firebase": "no", + "Cache": "no", + }, + }, + } + for _, scenario := range scenarios { + t.Run(scenario.Name, func(t *testing.T) { + // Start a local HTTP server + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + // Test request parameters + for header, value := range scenario.ExpectedHeaders { + if value != req.Header.Get(header) { + t.Errorf("expected: %s, got: %s", value, req.Header.Get(header)) + } + } + body, _ := io.ReadAll(req.Body) + if string(body) != scenario.ExpectedBody { + t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body) + } + // Send response to be tested + rw.Write([]byte(`OK`)) + })) + // Close the server when test finishes + defer server.Close() + + scenario.Provider.URL = server.URL + err := scenario.Provider.Send( + &endpoint.Endpoint{Name: "endpoint-name"}, + &scenario.Alert, + &endpoint.Result{ + ConditionResults: []*endpoint.ConditionResult{ + {Condition: "[CONNECTED] == true", Success: scenario.Resolved}, + {Condition: "[STATUS] == 200", Success: scenario.Resolved}, + }, + }, + scenario.Resolved, + ) + if err != nil { + t.Error("Encountered an error on Send: ", err) + } + + }) + } + +}