diff --git a/pkg/services/ngalert/notifier/channels/slack_test.go b/pkg/services/ngalert/notifier/channels/slack_test.go index 95e7d3db1ec..82fc387a986 100644 --- a/pkg/services/ngalert/notifier/channels/slack_test.go +++ b/pkg/services/ngalert/notifier/channels/slack_test.go @@ -7,10 +7,12 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" "testing" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/secrets/fakes" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/setting" @@ -24,17 +26,45 @@ import ( func TestSlackNotifier(t *testing.T) { tmpl := templateForTests(t) + // Create our temporary image file. + f, err := os.CreateTemp("", "ngalert-images-example*.png") + if err != nil { + panic("Temp file error!") + } + defer func() { _ = os.Remove(f.Name()) }() + + fakeImageStore := &fakeImageStore{ + Images: []*models.Image{ + { + Token: "test-with-url", + URL: "https://www.example.com/image.jpg", + }, + { + Token: "test-with-path-not-found", + Path: "usr/home/nouser/noway.jpg", + }, + { + Token: "test-with-path-found", + Path: f.Name(), // Has the full path because of how CreateTemp works. + }, + }, + } + + _, _ = f.Write([]byte("test image")) + _ = f.Close() + externalURL, err := url.Parse("http://localhost") require.NoError(t, err) tmpl.ExternalURL = externalURL cases := []struct { - name string - settings string - alerts []*types.Alert - expMsg *slackMessage - expInitError string - expMsgError error + name string + settings string + alerts []*types.Alert + expMsg *slackMessage + expInitError string + expMsgError error + expWebhookURL string }{ { name: "Correct config with one alert", @@ -106,6 +136,114 @@ func TestSlackNotifier(t *testing.T) { }, expMsgError: nil, }, + { + name: "Image URL in alert appears in slack message", + settings: `{ + "token": "1234", + "recipient": "#testchannel", + "icon_emoji": ":emoji:" + }`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh", "__alertScreenshotToken__": "test-with-url"}, + }, + }, + }, + expMsg: &slackMessage{ + Channel: "#testchannel", + Username: "Grafana", + IconEmoji: ":emoji:", + Attachments: []attachment{ + { + Title: "[FIRING:1] (val1)", + TitleLink: "http://localhost/alerting/list", + Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + Fallback: "[FIRING:1] (val1)", + Fields: nil, + Footer: "Grafana v" + setting.BuildVersion, + FooterIcon: "https://grafana.com/assets/img/fav32.png", + Color: "#D63232", + Ts: 0, + ImageURL: "https://www.example.com/image.jpg", + }, + }, + }, + expMsgError: nil, + }, + { + name: "Image URL with path but no file creates message with no error", + settings: `{ + "token": "1234", + "image_upload_url": "https://www.webhook.com", + "recipient": "#testchannel", + "icon_emoji": ":emoji:" + }`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh", "__alertScreenshotToken__": "test-with-path-not-found"}, + }, + }, + }, + expMsg: &slackMessage{ + Channel: "#testchannel", + Username: "Grafana", + IconEmoji: ":emoji:", + Attachments: []attachment{ + { + Title: "[FIRING:1] (val1)", + TitleLink: "http://localhost/alerting/list", + Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + Fallback: "[FIRING:1] (val1)", + Fields: nil, + Footer: "Grafana v" + setting.BuildVersion, + FooterIcon: "https://grafana.com/assets/img/fav32.png", + Color: "#D63232", + Ts: 0, + }, + }, + }, + expMsgError: nil, + }, + { + name: "Image URL with path and file creates message and uploads image", + settings: `{ + "token": "1234", + "recipient": "#testchannel", + "icon_emoji": ":emoji:" + }`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh", "__alertScreenshotToken__": "test-with-path-found"}, + }, + }, + }, + expMsg: &slackMessage{ + Channel: "#testchannel", + Username: "Grafana", + IconEmoji: ":emoji:", + Attachments: []attachment{ + { + Title: "[FIRING:1] (val1)", + TitleLink: "http://localhost/alerting/list", + Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + Fallback: "[FIRING:1] (val1)", + Fields: nil, + Footer: "Grafana v" + setting.BuildVersion, + FooterIcon: "https://grafana.com/assets/img/fav32.png", + Color: "#D63232", + Ts: 0, + }, + }, + }, + expMsgError: nil, + expWebhookURL: SlackImageAPIEndpoint, + }, { name: "Correct config with multiple alerts and template", settings: `{ @@ -196,6 +334,43 @@ func TestSlackNotifier(t *testing.T) { }, expMsgError: nil, }, + { + name: "Custom image upload URL", + settings: `{ + "token": "1234", + "recipient": "#testchannel", + "icon_emoji": ":emoji:", + "imageUploadUrl": "https://custom-domain.upload" + }`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh", "__alertScreenshotToken__": "test-with-path-found"}, + }, + }, + }, + expMsg: &slackMessage{ + Channel: "#testchannel", + Username: "Grafana", + IconEmoji: ":emoji:", + Attachments: []attachment{ + { + Title: "[FIRING:1] (val1)", + TitleLink: "http://localhost/alerting/list", + Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + Fallback: "[FIRING:1] (val1)", + Fields: nil, + Footer: "Grafana v" + setting.BuildVersion, + FooterIcon: "https://grafana.com/assets/img/fav32.png", + Color: "#D63232", + Ts: 0, + }, + }, + }, + expMsgError: nil, + expWebhookURL: "https://custom-domain.upload", + }, } for _, c := range cases { @@ -206,6 +381,8 @@ func TestSlackNotifier(t *testing.T) { secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) decryptFn := secretsService.GetDecryptedValue + notificationService := mockNotificationService() + fc := FactoryConfig{ Config: &NotificationChannelConfig{ Name: "slack_testing", @@ -213,8 +390,9 @@ func TestSlackNotifier(t *testing.T) { Settings: settingsJSON, SecureSettings: secureSettings, }, - ImageStore: &UnavailableImageStore{}, - NotificationService: mockNotificationService(), + ImageStore: fakeImageStore, + // TODO: allow changing the associated values for different tests. + NotificationService: notificationService, DecryptFunc: decryptFn, } @@ -271,6 +449,10 @@ func TestSlackNotifier(t *testing.T) { require.NoError(t, err) require.JSONEq(t, string(expBody), body) + + // If we should have sent to the webhook, the mock notification service + // will have a record of it. + require.Equal(t, c.expWebhookURL, notificationService.Webhook.Url) }) } } diff --git a/pkg/services/ngalert/notifier/channels/testing.go b/pkg/services/ngalert/notifier/channels/testing.go index 3fac1346bc6..9afc3aa6156 100644 --- a/pkg/services/ngalert/notifier/channels/testing.go +++ b/pkg/services/ngalert/notifier/channels/testing.go @@ -5,6 +5,7 @@ import ( "time" "github.com/grafana/grafana/pkg/models" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ) // mockTimeNow replaces function timeNow to return constant time. @@ -48,3 +49,16 @@ func (ns *notificationServiceMock) SendEmailCommandHandler(ctx context.Context, } func mockNotificationService() *notificationServiceMock { return ¬ificationServiceMock{} } + +type fakeImageStore struct { + Images []*ngmodels.Image +} + +func (f *fakeImageStore) GetImage(ctx context.Context, token string) (*ngmodels.Image, error) { + for _, img := range f.Images { + if img.Token == token { + return img, nil + } + } + return nil, ngmodels.ErrImageNotFound +}