From 21f62f362fe167b370a7d7a61da7ad37233690fd Mon Sep 17 00:00:00 2001 From: Kevin Woblick Date: Sun, 29 Jan 2023 23:32:16 +0100 Subject: [PATCH] feat(alerting): Add Pushover provider (#405) * Add a new Pushover provider (#129) - Adds new provider named Pushover with corresponding tests - Adds Pushover as a provider to the configuration and adjusts test accordingly - Adds Pushover to alerting_test.go, provider.go and type.go - Updates the readme with configuration details * Correct import order * Fix some missing pushover references * Apply suggestions from code review * Rename application-key to application-token for Pushover --------- Co-authored-by: TwiN --- README.md | 36 +++- alerting/alert/type.go | 3 + alerting/config.go | 4 + alerting/provider/provider.go | 2 + alerting/provider/pushover/pushover.go | 106 ++++++++++++ alerting/provider/pushover/pushover_test.go | 174 ++++++++++++++++++++ config/config.go | 1 + config/config_test.go | 89 +++++++++- watchdog/alerting_test.go | 47 ++++++ 9 files changed, 454 insertions(+), 8 deletions(-) create mode 100644 alerting/provider/pushover/pushover.go create mode 100644 alerting/provider/pushover/pushover_test.go diff --git a/README.md b/README.md index d8b59e0e..6845559d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Gatus is a developer-oriented health dashboard that gives you the ability to monitor your services using HTTP, ICMP, TCP, and even DNS queries as well as evaluate the result of said queries by using a list of conditions on values like the status code, the response time, the certificate expiration, the body and many others. The icing on top is that each of these health -checks can be paired with alerting via Slack, PagerDuty, Discord, Twilio and more. +checks can be paired with alerting via Slack, PagerDuty, Pushover, Discord, Twilio and more. I personally deploy it in my Kubernetes cluster and let it monitor the status of my core applications: https://status.twin.sh/ @@ -56,6 +56,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga - [Configuring Ntfy alerts](#configuring-ntfy-alerts) - [Configuring Opsgenie alerts](#configuring-opsgenie-alerts) - [Configuring PagerDuty alerts](#configuring-pagerduty-alerts) + - [Configuring Pushover alerts](#configuring-pushover-alerts) - [Configuring Slack alerts](#configuring-slack-alerts) - [Configuring Teams alerts](#configuring-teams-alerts) - [Configuring Telegram alerts](#configuring-telegram-alerts) @@ -411,6 +412,7 @@ ignored. | `alerting.ntfy` | Configuration for alerts of type `ntfy`.
See [Configuring Ntfy alerts](#configuring-ntfy-alerts). | `{}` | | `alerting.opsgenie` | Configuration for alerts of type `opsgenie`.
See [Configuring Opsgenie alerts](#configuring-opsgenie-alerts). | `{}` | | `alerting.pagerduty` | Configuration for alerts of type `pagerduty`.
See [Configuring PagerDuty alerts](#configuring-pagerduty-alerts). | `{}` | +| `alerting.pushover` | Configuration for alerts of type `pushover`.
See [Configuring Pushover alerts](#configuring-pushover-alerts). | `{}` | | `alerting.slack` | Configuration for alerts of type `slack`.
See [Configuring Slack alerts](#configuring-slack-alerts). | `{}` | | `alerting.teams` | Configuration for alerts of type `teams`.
See [Configuring Teams alerts](#configuring-teams-alerts). | `{}` | | `alerting.telegram` | Configuration for alerts of type `telegram`.
See [Configuring Telegram alerts](#configuring-telegram-alerts). | `{}` | @@ -791,6 +793,38 @@ endpoints: ``` +#### Configuring Pushover alerts +| Parameter | Description | Default | +|:---------------------------------------|:-------------------------------------------------------------------------------------------|:-----------------------------| +| `alerting.pushover` | Configuration for alerts of type `pushover` | `{}` | +| `alerting.pushover.application-token` | Pushover Application token | `""` | +| `alerting.pushover.user-key` | User or group key | `""` | +| `alerting.pushover.title` | A fixed title for all messages sent via Pushover | Name of your App in Pushover | +| `alerting.pushover.priority` | Priority of all messages, ranging from -2 (very low) to 2 (Emergency) | `0` | +| `alerting.pushover.default-alert` | Default alert configuration.
See [Setting a default alert](#setting-a-default-alert) | N/A | + +```yaml +alerting: + pushover: + application-token: "******************************" + user-key: "******************************" + +endpoints: + - name: website + url: "https://twin.sh/health" + interval: 30s + conditions: + - "[STATUS] == 200" + - "[BODY].status == UP" + - "[RESPONSE_TIME] < 300" + alerts: + - type: pushover + failure-threshold: 3 + success-threshold: 5 + send-on-resolved: true + description: "healthcheck failed" +``` + #### Configuring Slack alerts | Parameter | Description | Default | |:------------------------------------------|:-------------------------------------------------------------------------------------------|:--------------| diff --git a/alerting/alert/type.go b/alerting/alert/type.go index 8ccf7faa..8ace3a2b 100644 --- a/alerting/alert/type.go +++ b/alerting/alert/type.go @@ -38,6 +38,9 @@ const ( // TypePagerDuty is the Type for the pagerduty alerting provider TypePagerDuty Type = "pagerduty" + // TypePushover is the Type for the pushover alerting provider + TypePushover Type = "pushover" + // TypeSlack is the Type for the slack alerting provider TypeSlack Type = "slack" diff --git a/alerting/config.go b/alerting/config.go index 18bc0ad6..e9b5c5ba 100644 --- a/alerting/config.go +++ b/alerting/config.go @@ -18,6 +18,7 @@ import ( "github.com/TwiN/gatus/v5/alerting/provider/ntfy" "github.com/TwiN/gatus/v5/alerting/provider/opsgenie" "github.com/TwiN/gatus/v5/alerting/provider/pagerduty" + "github.com/TwiN/gatus/v5/alerting/provider/pushover" "github.com/TwiN/gatus/v5/alerting/provider/slack" "github.com/TwiN/gatus/v5/alerting/provider/teams" "github.com/TwiN/gatus/v5/alerting/provider/telegram" @@ -59,6 +60,9 @@ type Config struct { // PagerDuty is the configuration for the pagerduty alerting provider PagerDuty *pagerduty.AlertProvider `yaml:"pagerduty,omitempty"` + // Pushover is the configuration for the pushover alerting provider + Pushover *pushover.AlertProvider `yaml:"pushover,omitempty"` + // Slack is the configuration for the slack alerting provider Slack *slack.AlertProvider `yaml:"slack,omitempty"` diff --git a/alerting/provider/provider.go b/alerting/provider/provider.go index 851c8d8c..2f5ec850 100644 --- a/alerting/provider/provider.go +++ b/alerting/provider/provider.go @@ -13,6 +13,7 @@ import ( "github.com/TwiN/gatus/v5/alerting/provider/ntfy" "github.com/TwiN/gatus/v5/alerting/provider/opsgenie" "github.com/TwiN/gatus/v5/alerting/provider/pagerduty" + "github.com/TwiN/gatus/v5/alerting/provider/pushover" "github.com/TwiN/gatus/v5/alerting/provider/slack" "github.com/TwiN/gatus/v5/alerting/provider/teams" "github.com/TwiN/gatus/v5/alerting/provider/telegram" @@ -67,6 +68,7 @@ var ( _ AlertProvider = (*ntfy.AlertProvider)(nil) _ AlertProvider = (*opsgenie.AlertProvider)(nil) _ AlertProvider = (*pagerduty.AlertProvider)(nil) + _ AlertProvider = (*pushover.AlertProvider)(nil) _ AlertProvider = (*slack.AlertProvider)(nil) _ AlertProvider = (*teams.AlertProvider)(nil) _ AlertProvider = (*telegram.AlertProvider)(nil) diff --git a/alerting/provider/pushover/pushover.go b/alerting/provider/pushover/pushover.go new file mode 100644 index 00000000..331cec9c --- /dev/null +++ b/alerting/provider/pushover/pushover.go @@ -0,0 +1,106 @@ +package pushover + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/TwiN/gatus/v5/alerting/alert" + "github.com/TwiN/gatus/v5/client" + "github.com/TwiN/gatus/v5/core" +) + +const ( + restAPIURL = "https://api.pushover.net/1/messages.json" + defaultPriority = 0 +) + +// AlertProvider is the configuration necessary for sending an alert using Pushover +type AlertProvider struct { + // Key used to authenticate the application sending + // See "Your Applications" on the dashboard, or add a new one: https://pushover.net/apps/build + ApplicationToken string `yaml:"application-token"` + + // Key of the user or group the messages should be sent to + UserKey string `yaml:"user-key"` + + // The title of your message, likely the application name + // default: the name of your application in Pushover + Title string `yaml:"title,omitempty"` + + // Priority of all messages, ranging from -2 (very low) to 2 (Emergency) + // default: 0 + Priority int `yaml:"priority,omitempty"` + + // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type + DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"` +} + +// IsValid returns whether the provider's configuration is valid +func (provider *AlertProvider) IsValid() bool { + if provider.Priority == 0 { + provider.Priority = defaultPriority + } + return len(provider.ApplicationToken) == 30 && len(provider.UserKey) == 30 && provider.Priority >= -2 && provider.Priority <= 2 +} + +// Send an alert using the provider +// Reference doc for pushover: https://pushover.net/api +func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { + buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved)) + request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer) + if err != nil { + return err + } + request.Header.Set("Content-Type", "application/json") + response, err := client.GetHTTPClient(nil).Do(request) + if err != nil { + return err + } + defer response.Body.Close() + if response.StatusCode > 399 { + body, _ := io.ReadAll(response.Body) + return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body)) + } + return err +} + +type Body struct { + Token string `json:"token"` + User string `json:"user"` + Title string `json:"title,omitempty"` + Message string `json:"message"` + Priority int `json:"priority"` +} + +// buildRequestBody builds the request body for the provider +func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { + var message string + if resolved { + message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription()) + } else { + message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription()) + } + body, _ := json.Marshal(Body{ + Token: provider.ApplicationToken, + User: provider.UserKey, + Title: provider.Title, + Message: message, + Priority: provider.priority(), + }) + return body +} + +func (provider *AlertProvider) priority() int { + if provider.Priority == 0 { + return defaultPriority + } + return provider.Priority +} + +// GetDefaultAlert returns the provider's default alert configuration +func (provider AlertProvider) GetDefaultAlert() *alert.Alert { + return provider.DefaultAlert +} diff --git a/alerting/provider/pushover/pushover_test.go b/alerting/provider/pushover/pushover_test.go new file mode 100644 index 00000000..719808ab --- /dev/null +++ b/alerting/provider/pushover/pushover_test.go @@ -0,0 +1,174 @@ +package pushover + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/TwiN/gatus/v5/alerting/alert" + "github.com/TwiN/gatus/v5/client" + "github.com/TwiN/gatus/v5/core" + "github.com/TwiN/gatus/v5/test" +) + +func TestPushoverAlertProvider_IsValid(t *testing.T) { + invalidProvider := AlertProvider{} + if invalidProvider.IsValid() { + t.Error("provider shouldn't have been valid") + } + validProvider := AlertProvider{ + ApplicationToken: "aTokenWithLengthOf30characters", + UserKey: "aTokenWithLengthOf30characters", + Title: "Gatus Notification", + Priority: 1, + } + if !validProvider.IsValid() { + t.Error("provider should've been valid") + } +} + +func TestPushoverAlertProvider_IsInvalid(t *testing.T) { + invalidProvider := AlertProvider{ + ApplicationToken: "aTokenWithLengthOfMoreThan30characters", + UserKey: "aTokenWithLengthOfMoreThan30characters", + Priority: 5, + } + if invalidProvider.IsValid() { + t.Error("provider should've been invalid") + } +} + +func TestAlertProvider_Send(t *testing.T) { + defer client.InjectHTTPClient(nil) + firstDescription := "description-1" + secondDescription := "description-2" + scenarios := []struct { + Name string + Provider AlertProvider + Alert alert.Alert + Resolved bool + MockRoundTripper test.MockRoundTripper + ExpectedError bool + }{ + { + Name: "triggered", + Provider: AlertProvider{}, + Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: false, + MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response { + return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody} + }), + ExpectedError: false, + }, + { + Name: "triggered-error", + Provider: AlertProvider{}, + Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: false, + MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response { + return &http.Response{StatusCode: http.StatusInternalServerError, Body: http.NoBody} + }), + ExpectedError: true, + }, + { + Name: "resolved", + Provider: AlertProvider{}, + Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: true, + MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response { + return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody} + }), + ExpectedError: false, + }, + { + Name: "resolved-error", + Provider: AlertProvider{}, + Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: true, + MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response { + return &http.Response{StatusCode: http.StatusInternalServerError, Body: http.NoBody} + }), + ExpectedError: true, + }, + } + for _, scenario := range scenarios { + t.Run(scenario.Name, func(t *testing.T) { + client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper}) + err := scenario.Provider.Send( + &core.Endpoint{Name: "endpoint-name"}, + &scenario.Alert, + &core.Result{ + ConditionResults: []*core.ConditionResult{ + {Condition: "[CONNECTED] == true", Success: scenario.Resolved}, + {Condition: "[STATUS] == 200", Success: scenario.Resolved}, + }, + }, + scenario.Resolved, + ) + if scenario.ExpectedError && err == nil { + t.Error("expected error, got none") + } + if !scenario.ExpectedError && err != nil { + t.Error("expected no error, got", err.Error()) + } + }) + } +} + +func TestAlertProvider_buildRequestBody(t *testing.T) { + firstDescription := "description-1" + secondDescription := "description-2" + scenarios := []struct { + Name string + Provider AlertProvider + Alert alert.Alert + Resolved bool + ExpectedBody string + }{ + { + Name: "triggered", + Provider: AlertProvider{ApplicationToken: "TokenWithLengthOf30Characters1", UserKey: "TokenWithLengthOf30Characters4"}, + Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: false, + ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters1\",\"user\":\"TokenWithLengthOf30Characters4\",\"message\":\"TRIGGERED: endpoint-name - description-1\",\"priority\":0}", + }, + { + Name: "resolved", + Provider: AlertProvider{ApplicationToken: "TokenWithLengthOf30Characters2", UserKey: "TokenWithLengthOf30Characters5", Title: "Gatus Notifications", Priority: 2}, + Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: true, + ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters2\",\"user\":\"TokenWithLengthOf30Characters5\",\"title\":\"Gatus Notifications\",\"message\":\"RESOLVED: endpoint-name - description-2\",\"priority\":2}", + }, + } + for _, scenario := range scenarios { + t.Run(scenario.Name, func(t *testing.T) { + body := scenario.Provider.buildRequestBody( + &core.Endpoint{Name: "endpoint-name"}, + &scenario.Alert, + &core.Result{ + ConditionResults: []*core.ConditionResult{ + {Condition: "[CONNECTED] == true", Success: scenario.Resolved}, + {Condition: "[STATUS] == 200", Success: scenario.Resolved}, + }, + }, + scenario.Resolved, + ) + if string(body) != scenario.ExpectedBody { + t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body) + } + out := make(map[string]interface{}) + if err := json.Unmarshal([]byte(body), &out); err != nil { + t.Error("expected body to be valid JSON, got error:", err.Error()) + } + }) + } +} + +func TestAlertProvider_GetDefaultAlert(t *testing.T) { + if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil { + t.Error("expected default alert to be not nil") + } + if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil { + t.Error("expected default alert to be nil") + } +} diff --git a/config/config.go b/config/config.go index f6b8490d..48bc7a2e 100644 --- a/config/config.go +++ b/config/config.go @@ -352,6 +352,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E alert.TypeNtfy, alert.TypeOpsgenie, alert.TypePagerDuty, + alert.TypePushover, alert.TypeSlack, alert.TypeTeams, alert.TypeTelegram, diff --git a/config/config_test.go b/config/config_test.go index 9f541e2f..1ab465c4 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -22,6 +22,7 @@ import ( "github.com/TwiN/gatus/v5/alerting/provider/ntfy" "github.com/TwiN/gatus/v5/alerting/provider/opsgenie" "github.com/TwiN/gatus/v5/alerting/provider/pagerduty" + "github.com/TwiN/gatus/v5/alerting/provider/pushover" "github.com/TwiN/gatus/v5/alerting/provider/slack" "github.com/TwiN/gatus/v5/alerting/provider/teams" "github.com/TwiN/gatus/v5/alerting/provider/telegram" @@ -684,6 +685,9 @@ alerting: webhook-url: "http://example.org" pagerduty: integration-key: "00000000000000000000000000000000" + pushover: + application-token: "000000000000000000000000000000" + user-key: "000000000000000000000000000000" mattermost: webhook-url: "http://example.com" client: @@ -723,6 +727,7 @@ endpoints: failure-threshold: 12 success-threshold: 15 - type: teams + - type: pushover conditions: - "[STATUS] == 200" `)) @@ -749,8 +754,8 @@ endpoints: if config.Endpoints[0].Interval != 60*time.Second { t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second) } - if len(config.Endpoints[0].Alerts) != 8 { - t.Fatal("There should've been 8 alerts configured") + if len(config.Endpoints[0].Alerts) != 9 { + t.Fatal("There should've been 9 alerts configured") } if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack { @@ -850,6 +855,13 @@ endpoints: if config.Endpoints[0].Alerts[7].SuccessThreshold != 2 { t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[7].SuccessThreshold) } + + if config.Endpoints[0].Alerts[8].Type != alert.TypePushover { + t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypePushover, config.Endpoints[0].Alerts[8].Type) + } + if !config.Endpoints[0].Alerts[8].IsEnabled() { + t.Error("The alert should've been enabled") + } } func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlert(t *testing.T) { @@ -874,6 +886,14 @@ alerting: description: default description failure-threshold: 7 success-threshold: 5 + pushover: + application-token: "000000000000000000000000000000" + user-key: "000000000000000000000000000000" + default-alert: + enabled: true + description: default description + failure-threshold: 5 + success-threshold: 3 mattermost: webhook-url: "http://example.com" default-alert: @@ -917,6 +937,7 @@ endpoints: - type: telegram - type: twilio - type: teams + - type: pushover conditions: - "[STATUS] == 200" `)) @@ -953,6 +974,19 @@ endpoints: t.Errorf("PagerDuty integration key should've been %s, but was %s", "00000000000000000000000000000000", config.Alerting.PagerDuty.IntegrationKey) } + if config.Alerting.Pushover == nil || !config.Alerting.Pushover.IsValid() { + t.Fatal("Pushover alerting config should've been valid") + } + if config.Alerting.Pushover.GetDefaultAlert() == nil { + t.Fatal("Pushover.GetDefaultAlert() shouldn't have returned nil") + } + if config.Alerting.Pushover.ApplicationToken != "000000000000000000000000000000" { + t.Errorf("Pushover application token should've been %s, but was %s", "000000000000000000000000000000", config.Alerting.Pushover.ApplicationToken) + } + if config.Alerting.Pushover.UserKey != "000000000000000000000000000000" { + t.Errorf("Pushover user key should've been %s, but was %s", "000000000000000000000000000000", config.Alerting.Pushover.UserKey) + } + if config.Alerting.Mattermost == nil || !config.Alerting.Mattermost.IsValid() { t.Fatal("Mattermost alerting config should've been valid") } @@ -1026,8 +1060,8 @@ endpoints: if config.Endpoints[0].Interval != 60*time.Second { t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second) } - if len(config.Endpoints[0].Alerts) != 8 { - t.Fatal("There should've been 8 alerts configured") + if len(config.Endpoints[0].Alerts) != 9 { + t.Fatal("There should've been 9 alerts configured") } if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack { @@ -1131,6 +1165,19 @@ endpoints: t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[7].SuccessThreshold) } + if config.Endpoints[0].Alerts[8].Type != alert.TypePushover { + t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypePushover, config.Endpoints[0].Alerts[8].Type) + } + if !config.Endpoints[0].Alerts[8].IsEnabled() { + t.Error("The alert should've been enabled") + } + if config.Endpoints[0].Alerts[8].FailureThreshold != 5 { + t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[8].FailureThreshold) + } + if config.Endpoints[0].Alerts[8].SuccessThreshold != 3 { + t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[8].SuccessThreshold) + } + } func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlertAndMultipleAlertsOfSameTypeWithOverriddenParameters(t *testing.T) { @@ -1238,6 +1285,32 @@ endpoints: t.Fatal("PagerDuty alerting config should've been set to nil, because its IsValid() method returned false and therefore alerting.Config.SetAlertingProviderToNil() should've been called") } } +func TestParseAndValidateConfigBytesWithInvalidPushoverAlertingConfig(t *testing.T) { + config, err := parseAndValidateConfigBytes([]byte(` +alerting: + pushover: + application-token: "INVALID_TOKEN" +endpoints: + - name: website + url: https://twin.sh/health + alerts: + - type: pushover + conditions: + - "[STATUS] == 200" +`)) + if err != nil { + t.Error("expected no error, got", err.Error()) + } + if config == nil { + t.Fatal("Config shouldn't have been nil") + } + if config.Alerting == nil { + t.Fatal("config.Alerting shouldn't have been nil") + } + if config.Alerting.Pushover != nil { + t.Fatal("Pushover alerting config should've been set to nil, because its IsValid() method returned false and therefore alerting.Config.SetAlertingProviderToNil() should've been called") + } +} func TestParseAndValidateConfigBytesWithCustomAlertingConfig(t *testing.T) { config, err := parseAndValidateConfigBytes([]byte(` @@ -1266,7 +1339,7 @@ endpoints: t.Fatal("config.Alerting shouldn't have been nil") } if config.Alerting.Custom == nil { - t.Fatal("PagerDuty alerting config shouldn't have been nil") + t.Fatal("Custom alerting config shouldn't have been nil") } if !config.Alerting.Custom.IsValid() { t.Fatal("Custom alerting config should've been valid") @@ -1311,7 +1384,7 @@ endpoints: t.Fatal("config.Alerting shouldn't have been nil") } if config.Alerting.Custom == nil { - t.Fatal("PagerDuty alerting config shouldn't have been nil") + t.Fatal("Custom alerting config shouldn't have been nil") } if !config.Alerting.Custom.IsValid() { t.Fatal("Custom alerting config should've been valid") @@ -1351,7 +1424,7 @@ endpoints: t.Fatal("config.Alerting shouldn't have been nil") } if config.Alerting.Custom == nil { - t.Fatal("PagerDuty alerting config shouldn't have been nil") + t.Fatal("Custom alerting config shouldn't have been nil") } if !config.Alerting.Custom.IsValid() { t.Fatal("Custom alerting config should've been valid") @@ -1481,6 +1554,7 @@ func TestGetAlertingProviderByAlertType(t *testing.T) { Ntfy: &ntfy.AlertProvider{}, Opsgenie: &opsgenie.AlertProvider{}, PagerDuty: &pagerduty.AlertProvider{}, + Pushover: &pushover.AlertProvider{}, Slack: &slack.AlertProvider{}, Telegram: &telegram.AlertProvider{}, Twilio: &twilio.AlertProvider{}, @@ -1501,6 +1575,7 @@ func TestGetAlertingProviderByAlertType(t *testing.T) { {alertType: alert.TypeNtfy, expected: alertingConfig.Ntfy}, {alertType: alert.TypeOpsgenie, expected: alertingConfig.Opsgenie}, {alertType: alert.TypePagerDuty, expected: alertingConfig.PagerDuty}, + {alertType: alert.TypePushover, expected: alertingConfig.Pushover}, {alertType: alert.TypeSlack, expected: alertingConfig.Slack}, {alertType: alert.TypeTelegram, expected: alertingConfig.Telegram}, {alertType: alert.TypeTwilio, expected: alertingConfig.Twilio}, diff --git a/watchdog/alerting_test.go b/watchdog/alerting_test.go index 2281f836..88dd10a1 100644 --- a/watchdog/alerting_test.go +++ b/watchdog/alerting_test.go @@ -13,6 +13,7 @@ import ( "github.com/TwiN/gatus/v5/alerting/provider/mattermost" "github.com/TwiN/gatus/v5/alerting/provider/messagebird" "github.com/TwiN/gatus/v5/alerting/provider/pagerduty" + "github.com/TwiN/gatus/v5/alerting/provider/pushover" "github.com/TwiN/gatus/v5/alerting/provider/slack" "github.com/TwiN/gatus/v5/alerting/provider/teams" "github.com/TwiN/gatus/v5/alerting/provider/telegram" @@ -203,6 +204,42 @@ func TestHandleAlertingWhenTriggeredAlertIsResolvedPagerDuty(t *testing.T) { verify(t, endpoint, 0, 1, false, "The alert should've been resolved") } +func TestHandleAlertingWhenTriggeredAlertIsResolvedPushover(t *testing.T) { + _ = os.Setenv("MOCK_ALERT_PROVIDER", "true") + defer os.Clearenv() + + cfg := &config.Config{ + Debug: true, + Alerting: &alerting.Config{ + Pushover: &pushover.AlertProvider{ + ApplicationToken: "000000000000000000000000000000", + UserKey: "000000000000000000000000000000", + }, + }, + } + enabled := true + endpoint := &core.Endpoint{ + URL: "https://example.com", + Alerts: []*alert.Alert{ + { + Type: alert.TypePushover, + Enabled: &enabled, + FailureThreshold: 1, + SuccessThreshold: 1, + SendOnResolved: &enabled, + Triggered: false, + }, + }, + NumberOfFailuresInARow: 0, + } + + HandleAlerting(endpoint, &core.Result{Success: false}, cfg.Alerting, cfg.Debug) + verify(t, endpoint, 1, 0, true, "") + + HandleAlerting(endpoint, &core.Result{Success: true}, cfg.Alerting, cfg.Debug) + verify(t, endpoint, 0, 1, false, "The alert should've been resolved") +} + func TestHandleAlertingWithProviderThatReturnsAnError(t *testing.T) { _ = os.Setenv("MOCK_ALERT_PROVIDER", "true") defer os.Clearenv() @@ -273,6 +310,16 @@ func TestHandleAlertingWithProviderThatReturnsAnError(t *testing.T) { }, }, }, + { + Name: "pushover", + AlertType: alert.TypePushover, + AlertingConfig: &alerting.Config{ + Pushover: &pushover.AlertProvider{ + ApplicationToken: "000000000000000000000000000000", + UserKey: "000000000000000000000000000000", + }, + }, + }, { Name: "slack", AlertType: alert.TypeSlack,