@ -3,342 +3,499 @@ package channels
import (
"context"
"encoding/json"
"errors"
"io"
"mime"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"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"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
)
func TestSlackNotifier ( t * testing . T ) {
tmpl := templateForTests ( t )
fakeImageStore := & fakeImageStore {
Images : [ ] * models . Image {
{
Token : "test-with-url" ,
URL : "https://www.example.com/image.jpg" ,
func TestSlackIncomingWebhook ( t * testing . T ) {
tests := [ ] struct {
name string
alerts [ ] * types . Alert
expectedMessage * slackMessage
expectedError string
settings string
} { {
name : "Message is sent" ,
settings : ` {
"icon_emoji" : ":emoji:" ,
"recipient" : "#test" ,
"url" : "https://example.com/hooks/xxxx"
} ` ,
alerts : [ ] * types . Alert { {
Alert : model . Alert {
Labels : model . LabelSet { "alertname" : "alert1" , "lbl1" : "val1" } ,
Annotations : model . LabelSet { "ann1" : "annv1" } ,
} ,
} ,
}
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
expWebhookURL string
} {
{
name : "Correct config with one alert" ,
settings : ` {
"token" : "1234" ,
"recipient" : "#testchannel" ,
"icon_emoji" : ":emoji:"
} ` ,
alerts : [ ] * types . Alert {
} } ,
expectedMessage : & slackMessage {
Channel : "#test" ,
Username : "Grafana" ,
IconEmoji : ":emoji:" ,
Attachments : [ ] attachment {
{
Alert : model . Alert {
Labels : model . LabelSet { "alertname" : "alert1" , "lbl1" : "val1" } ,
Annotations : model . LabelSet { "ann1" : "annv1" , "__dashboardUid__" : "abcd" , "__panelId__" : "efgh" } ,
} ,
} ,
} ,
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/static/assets/img/fav32.png" ,
Color : "#D63232" ,
Ts : 0 ,
} ,
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\n" ,
Fallback : "[FIRING:1] (val1)" ,
Fields : nil ,
Footer : "Grafana v" + setting . BuildVersion ,
FooterIcon : "https://grafana.com/static/assets/img/fav32.png" ,
Color : "#D63232" ,
} ,
} ,
expMsgError : nil ,
} ,
{
name : "Correct config with webhook " ,
settings : ` {
"url" : "https://webhook.com " ,
"recipient" : "#testchannel " ,
"icon_emoji" : ":emoji: "
} , {
name : "Message is sent with image URL" ,
settings : ` {
"icon_emoji" : ":emoji:" ,
"recipient" : "#test" ,
"url" : "https://example.com/hooks/xxxx"
} ` ,
alerts : [ ] * types . Alert {
{
Alert : model . Alert {
Labels : model . LabelSet { "alertname" : "alert1" , "lbl1" : "val1" } ,
Annotations : model . LabelSet { "ann1" : "annv1" } ,
} ,
} ,
alerts : [ ] * types . Alert { {
Alert : model . Alert {
Labels : model . LabelSet { "alertname" : "alert1" , "lbl1" : "val1" } ,
Annotations : model . LabelSet { "ann1" : "annv1" , "__dashboardUid__" : "abcd" , "__panelId__" : "efgh" , "__alertImageToken__" : "image-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\n " ,
Fallback : "[FIRING:1] (val1) " ,
Fields : nil ,
Footer : "Grafana v" + setting . BuildVersion ,
FooterIcon : "https://grafana.com/static/assets/img/fav32.png" ,
Color : "#D63232 " ,
Ts : 0 ,
} ,
} } ,
expectedMessage : & slackMessage {
Channel : "#test ",
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/static/assets/img/fav32.png ",
Color : "#D63232" ,
ImageURL : "https://www.example.com/test.png" ,
} ,
} ,
expMsgError : nil ,
} ,
{
name : "Image URL in alert appears in slack message " ,
settings : ` {
"token" : "1234 " ,
"recipient" : "#testchannel " ,
"icon_emoji" : ":emoji: "
} , {
name : "Message is sent and image on local disk is ignored " ,
settings : ` {
"icon_emoji" : ":emoji: " ,
"recipient" : "#test" ,
"url" : "https://example.com/hooks/xxxx "
} ` ,
alerts : [ ] * types . Alert {
alerts : [ ] * types . Alert { {
Alert : model . Alert {
Labels : model . LabelSet { "alertname" : "alert1" , "lbl1" : "val1" } ,
Annotations : model . LabelSet { "ann1" : "annv1" , "__dashboardUid__" : "abcd" , "__panelId__" : "efgh" , "__alertImageToken__" : "image-on-disk" } ,
} ,
} } ,
expectedMessage : & slackMessage {
Channel : "#test" ,
Username : "Grafana" ,
IconEmoji : ":emoji:" ,
Attachments : [ ] attachment {
{
Alert : model . Alert {
Labels : model . LabelSet { "alertname" : "alert1" , "lbl1" : "val1" } ,
Annotations : model . LabelSet { "ann1" : "annv1" , "__dashboardUid__" : "abcd" , "__panelId__" : "efgh" , "__alertImageToken__" : "test-with-url" } ,
} ,
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/static/assets/img/fav32.png" ,
Color : "#D63232" ,
} ,
} ,
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/static/assets/img/fav32.png" ,
Color : "#D63232" ,
Ts : 0 ,
ImageURL : "https://www.example.com/image.jpg" ,
} ,
} ,
} }
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
notifier , recorder , err := setupSlackForTests ( t , test . settings )
require . NoError ( t , err )
ctx := context . Background ( )
ctx = notify . WithGroupKey ( ctx , "alertname" )
ctx = notify . WithGroupLabels ( ctx , model . LabelSet { "alertname" : "" } )
ok , err := notifier . Notify ( ctx , test . alerts ... )
if test . expectedError != "" {
assert . EqualError ( t , err , test . expectedError )
assert . False ( t , ok )
} else {
assert . NoError ( t , err )
assert . True ( t , ok )
// When sending a notification to an Incoming Webhook there should a single request.
// This is different from PostMessage where some content, such as images, are sent
// as replies to the original message
require . Len ( t , recorder . requests , 1 )
// Get the request and check that it's sending to the URL of the Incoming Webhook
r := recorder . requests [ 0 ]
assert . Equal ( t , notifier . settings . URL , r . URL . String ( ) )
// Check that the request contains the expected message
b , err := io . ReadAll ( r . Body )
require . NoError ( t , err )
message := slackMessage { }
require . NoError ( t , json . Unmarshal ( b , & message ) )
for i , v := range message . Attachments {
// Need to update the ts as these cannot be set in the test definition
test . expectedMessage . Attachments [ i ] . Ts = v . Ts
}
assert . Equal ( t , * test . expectedMessage , message )
}
} )
}
}
func TestSlackPostMessage ( t * testing . T ) {
tests := [ ] struct {
name string
alerts [ ] * types . Alert
expectedMessage * slackMessage
expectedReplies [ ] interface { } // can contain either slackMessage or map[string]struct{} for multipart/form-data
expectedError string
settings string
} { {
name : "Message is sent" ,
settings : ` {
"icon_emoji" : ":emoji:" ,
"recipient" : "#test" ,
"token" : "1234"
} ` ,
alerts : [ ] * types . Alert { {
Alert : model . Alert {
Labels : model . LabelSet { "alertname" : "alert1" , "lbl1" : "val1" } ,
Annotations : model . LabelSet { "ann1" : "annv1" , "__dashboardUid__" : "abcd" , "__panelId__" : "efgh" } ,
} ,
} } ,
expectedMessage : & slackMessage {
Channel : "#test" ,
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/static/assets/img/fav32.png" ,
Color : "#D63232" ,
} ,
} ,
expMsgError : nil ,
} ,
{
name : "Correct config with multiple alerts and template" ,
settings : ` {
"token" : "1234" ,
"recipient" : "#testchannel" ,
"icon_emoji" : ":emoji:" ,
"title" : "{{ .Alerts.Firing | len }} firing, {{ .Alerts.Resolved | len }} resolved"
} ` ,
alerts : [ ] * types . Alert {
} , {
name : "Message is sent with two firing alerts" ,
settings : ` {
"title" : "{{ .Alerts.Firing | len }} firing, {{ .Alerts.Resolved | len }} resolved" ,
"icon_emoji" : ":emoji:" ,
"recipient" : "#test" ,
"token" : "1234"
} ` ,
alerts : [ ] * types . Alert { {
Alert : model . Alert {
Labels : model . LabelSet { "alertname" : "alert1" , "lbl1" : "val1" } ,
Annotations : model . LabelSet { "ann1" : "annv1" } ,
} ,
} , {
Alert : model . Alert {
Labels : model . LabelSet { "alertname" : "alert1" , "lbl1" : "val2" } ,
Annotations : model . LabelSet { "ann1" : "annv2" } ,
} ,
} } ,
expectedMessage : & slackMessage {
Channel : "#test" ,
Username : "Grafana" ,
IconEmoji : ":emoji:" ,
Attachments : [ ] attachment {
{
Alert : model . Alert {
Labels : model . LabelSet { "alertname" : "alert1" , "lbl1" : "val1" } ,
Annotations : model . LabelSet { "ann1" : "annv1" } ,
} ,
Title : "2 firing, 0 resolved" ,
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\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n" ,
Fallback : "2 firing, 0 resolved" ,
Fields : nil ,
Footer : "Grafana v" + setting . BuildVersion ,
FooterIcon : "https://grafana.com/static/assets/img/fav32.png" ,
Color : "#D63232" ,
} ,
} ,
} ,
} , {
name : "Message is sent and image is uploaded" ,
settings : ` {
"icon_emoji" : ":emoji:" ,
"recipient" : "#test" ,
"token" : "1234"
} ` ,
alerts : [ ] * types . Alert { {
Alert : model . Alert {
Labels : model . LabelSet { "alertname" : "alert1" , "lbl1" : "val1" } ,
Annotations : model . LabelSet { "ann1" : "annv1" , "__dashboardUid__" : "abcd" , "__panelId__" : "efgh" , "__alertImageToken__" : "image-on-disk" } ,
} ,
} } ,
expectedMessage : & slackMessage {
Channel : "#test" ,
Username : "Grafana" ,
IconEmoji : ":emoji:" ,
Attachments : [ ] attachment {
{
Alert : model . Alert {
Labels : model . LabelSet { "alertname" : "alert1" , "lbl1" : "val2" } ,
Annotations : model . LabelSet { "ann1" : "annv2" } ,
} ,
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/static/assets/img/fav32.png" ,
Color : "#D63232" ,
} ,
} ,
expMsg : & slackMessage {
Channel : "#testchannel" ,
Username : "Grafana" ,
IconEmoji : ":emoji:" ,
Attachments : [ ] attachment {
{
Title : "2 firing, 0 resolved" ,
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\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n" ,
Fallback : "2 firing, 0 resolved" ,
Fields : nil ,
Footer : "Grafana v" + setting . BuildVersion ,
FooterIcon : "https://grafana.com/static/assets/img/fav32.png" ,
Color : "#D63232" ,
Ts : 0 ,
} ,
} ,
} ,
expectedReplies : [ ] interface { } {
// check that the following parts are present in the multipart/form-data
map [ string ] struct { } {
"file" : { } ,
"channels" : { } ,
"initial_comment" : { } ,
"thread_ts" : { } ,
} ,
expMsgError : nil ,
} , {
name : "Missing token" ,
settings : ` {
"recipient" : "#testchannel"
} ` ,
expInitError : ` token must be specified when using the Slack chat API ` ,
} , {
name : "Missing recipient" ,
settings : ` {
"token" : "1234"
} ` ,
expInitError : ` recipient must be specified when using the Slack chat API ` ,
} ,
{
name : "Custom endpoint url" ,
settings : ` {
"token" : "1234" ,
"recipient" : "#testchannel" ,
"endpointUrl" : "https://slack-custom.com/api/" ,
"icon_emoji" : ":emoji:"
} ` ,
alerts : [ ] * types . Alert {
{
Alert : model . Alert {
Labels : model . LabelSet { "alertname" : "alert1" , "lbl1" : "val1" } ,
Annotations : model . LabelSet { "ann1" : "annv1" } ,
} ,
} ,
} , {
name : "Message is sent to custom URL" ,
settings : ` {
"icon_emoji" : ":emoji:" ,
"recipient" : "#test" ,
"endpointUrl" : "https://example.com/api" ,
"token" : "1234"
} ` ,
alerts : [ ] * types . Alert { {
Alert : model . Alert {
Labels : model . LabelSet { "alertname" : "alert1" , "lbl1" : "val1" } ,
Annotations : model . LabelSet { "ann1" : "annv1" } ,
} ,
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\n" ,
Fallback : "[FIRING:1] (val1)" ,
Fields : nil ,
Footer : "Grafana v" + setting . BuildVersion ,
FooterIcon : "https://grafana.com/static/assets/img/fav32.png" ,
Color : "#D63232" ,
Ts : 0 ,
} ,
} } ,
expectedMessage : & slackMessage {
Channel : "#test" ,
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\n" ,
Fallback : "[FIRING:1] (val1)" ,
Fields : nil ,
Footer : "Grafana v" + setting . BuildVersion ,
FooterIcon : "https://grafana.com/static/assets/img/fav32.png" ,
Color : "#D63232" ,
} ,
} ,
expMsgError : nil ,
} ,
}
} }
for _ , c := range case s {
t . Run ( c . name , func ( t * testing . T ) {
settingsJSON , err := simplejson . NewJson ( [ ] byte ( c . settings ) )
for _ , test := range test s {
t . Run ( test . name , func ( t * testing . T ) {
notifier , recorder , err := setupSlackForTests ( t , test . settings )
require . NoError ( t , err )
secureSettings := make ( map [ string ] [ ] byte )
secretsService := secretsManager . SetupTestService ( t , fakes . NewFakeSecretsStore ( ) )
decryptFn := secretsService . GetDecryptedValue
notificationService := mockNotificationService ( )
fc := FactoryConfig {
Config : & NotificationChannelConfig {
Name : "slack_testing" ,
Type : "slack" ,
Settings : settingsJSON ,
SecureSettings : secureSettings ,
} ,
ImageStore : fakeImageStore ,
// TODO: allow changing the associated values for different tests.
NotificationService : notificationService ,
DecryptFunc : decryptFn ,
Template : tmpl ,
}
pn , err := buildSlackNotifier ( fc )
if c . expInitError != "" {
require . Error ( t , err )
require . Equal ( t , c . expInitError , err . Error ( ) )
return
}
require . NoError ( t , err )
ctx := context . Background ( )
ctx = notify . WithGroupKey ( ctx , "alertname" )
ctx = notify . WithGroupLabels ( ctx , model . LabelSet { "alertname" : "" } )
body := ""
origSendSlackRequest := sendSlackRequest
t . Cleanup ( func ( ) {
sendSlackRequest = origSendSlackRequest
} )
sendSlackRequest = func ( request * http . Request , log log . Logger ) error {
t . Helper ( )
defer func ( ) {
_ = request . Body . Close ( )
} ( )
url := settingsJSON . Get ( "url" ) . MustString ( )
if len ( url ) == 0 {
endpointUrl := settingsJSON . Get ( "endpointUrl" ) . MustString ( SlackAPIEndpoint )
require . Equal ( t , endpointUrl , request . URL . String ( ) )
}
ok , err := notifier . Notify ( ctx , test . alerts ... )
if test . expectedError != "" {
assert . EqualError ( t , err , test . expectedError )
assert . False ( t , ok )
} else {
assert . NoError ( t , err )
assert . True ( t , ok )
// When sending a notification via PostMessage some content, such as images,
// are sent as replies to the original message
require . Len ( t , recorder . requests , len ( test . expectedReplies ) + 1 )
// Get the request and check that it's sending to the URL
r := recorder . requests [ 0 ]
assert . Equal ( t , notifier . settings . URL , r . URL . String ( ) )
b , err := io . ReadAll ( request . Body )
// Check that the request contains the expected message
b , err := io . ReadAll ( r . Body )
require . NoError ( t , err )
body = string ( b )
return nil
}
ctx := notify . WithGroupKey ( context . Background ( ) , "alertname" )
ctx = notify . WithGroupLabels ( ctx , model . LabelSet { "alertname" : "" } )
ok , err := pn . Notify ( ctx , c . alerts ... )
if c . expMsgError != nil {
require . Error ( t , err )
require . False ( t , ok )
require . Equal ( t , c . expMsgError . Error ( ) , err . Error ( ) )
return
message := slackMessage { }
require . NoError ( t , json . Unmarshal ( b , & message ) )
for i , v := range message . Attachments {
// Need to update the ts as these cannot be set in the test definition
test . expectedMessage . Attachments [ i ] . Ts = v . Ts
}
assert . Equal ( t , * test . expectedMessage , message )
// Check that the replies match expectations
for i := 1 ; i < len ( recorder . requests ) ; i ++ {
r = recorder . requests [ i ]
assert . Equal ( t , "https://slack.com/api/files.upload" , r . URL . String ( ) )
media , params , err := mime . ParseMediaType ( r . Header . Get ( "Content-Type" ) )
require . NoError ( t , err )
if media == "multipart/form-data" {
// Some replies are file uploads, so check the multipart form
checkMultipart ( t , test . expectedReplies [ i - 1 ] . ( map [ string ] struct { } ) , r . Body , params [ "boundary" ] )
} else {
b , err = io . ReadAll ( r . Body )
require . NoError ( t , err )
message = slackMessage { }
require . NoError ( t , json . Unmarshal ( b , & message ) )
assert . Equal ( t , test . expectedReplies [ i - 1 ] , message )
}
}
}
require . True ( t , ok )
require . NoError ( t , err )
} )
}
}
// Getting Ts from actual since that can't be predicted.
var obj slackMessage
require . NoError ( t , json . Unmarshal ( [ ] byte ( body ) , & obj ) )
c . expMsg . Attachments [ 0 ] . Ts = obj . Attachments [ 0 ] . Ts
// slackRequestRecorder is used in tests to record all requests .
type slackRequestRecorder struct {
requests [ ] * http . Request
}
expBody , err := json . Marshal ( c . expMsg )
require . NoError ( t , err )
func ( s * slackRequestRecorder ) fn ( _ context . Context , r * http . Request , _ log . Logger ) ( string , error ) {
s . requests = append ( s . requests , r )
return "" , nil
}
require . JSONEq ( t , string ( expBody ) , body )
// checkMulipart checks that each part is present, but not its contents
func checkMultipart ( t * testing . T , expected map [ string ] struct { } , r io . Reader , boundary string ) {
m := multipart . NewReader ( r , boundary )
visited := make ( map [ string ] struct { } )
for {
part , err := m . NextPart ( )
if errors . Is ( err , io . EOF ) {
break
}
require . NoError ( t , err )
visited [ part . FormName ( ) ] = struct { } { }
}
assert . Equal ( t , expected , visited )
}
// 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 )
func setupSlackForTests ( t * testing . T , settings string ) ( * SlackNotifier , * slackRequestRecorder , error ) {
tmpl := templateForTests ( t )
externalURL , err := url . Parse ( "http://localhost" )
require . NoError ( t , err )
tmpl . ExternalURL = externalURL
f , err := os . Create ( t . TempDir ( ) + "test.png" )
require . NoError ( t , err )
t . Cleanup ( func ( ) {
if err := os . Remove ( f . Name ( ) ) ; err != nil {
t . Logf ( "failed to delete test file: %s" , err )
}
} )
images := & fakeImageStore {
Images : [ ] * models . Image { {
Token : "image-on-disk" ,
Path : f . Name ( ) ,
} , {
Token : "image-with-url" ,
URL : "https://www.example.com/test.png" ,
} } ,
}
settingsJSON , err := simplejson . NewJson ( [ ] byte ( settings ) )
require . NoError ( t , err )
secretsService := secretsManager . SetupTestService ( t , fakes . NewFakeSecretsStore ( ) )
notificationService := mockNotificationService ( )
c := FactoryConfig {
Config : & NotificationChannelConfig {
Name : "slack_testing" ,
Type : "slack" ,
Settings : settingsJSON ,
SecureSettings : make ( map [ string ] [ ] byte ) ,
} ,
ImageStore : images ,
NotificationService : notificationService ,
DecryptFunc : secretsService . GetDecryptedValue ,
Template : tmpl ,
}
sn , err := buildSlackNotifier ( c )
if err != nil {
return nil , nil , err
}
sr := & slackRequestRecorder { }
sn . sendFn = sr . fn
return sn , sr , nil
}
func TestCreateSlackNotifierFromConfig ( t * testing . T ) {
tests := [ ] struct {
name string
settings string
expectedError string
} { {
name : "Missing token" ,
settings : ` {
"recipient" : "#testchannel"
} ` ,
expectedError : "token must be specified when using the Slack chat API" ,
} , {
name : "Missing recipient" ,
settings : ` {
"token" : "1234"
} ` ,
expectedError : "recipient must be specified when using the Slack chat API" ,
} }
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
n , _ , err := setupSlackForTests ( t , test . settings )
if test . expectedError != "" {
assert . Nil ( t , n )
assert . EqualError ( t , err , test . expectedError )
} else {
assert . NotNil ( t , n )
assert . Nil ( t , err )
}
} )
}
}
func TestSendSlackRequest ( t * testing . T ) {
tests := [ ] struct {
name string
slackResponse string
statusCode int
expectError bool
name string
response string
statusCode int
expectError bool
} {
{
name : "Example error" ,
slackResponse : ` {
r esponse: ` {
"ok" : false ,
"error" : "too_many_attachments"
} ` ,
@ -352,7 +509,7 @@ func TestSendSlackRequest(t *testing.T) {
} ,
{
name : "Success case, normal response body" ,
slackR esponse: ` {
r esponse: ` {
"ok" : true ,
"channel" : "C1H9RESGL" ,
"ts" : "1503435956.000247" ,
@ -376,26 +533,27 @@ func TestSendSlackRequest(t *testing.T) {
expectError : false ,
} ,
{
name : "No response body" ,
statusCode : http . StatusOK ,
name : "No response body" ,
statusCode : http . StatusOK ,
expectError : true ,
} ,
{
name : "Success case, unexpected response body" ,
statusCode : http . StatusOK ,
slackResponse : ` { "test": true} ` ,
expectError : fals e ,
name : "Success case, unexpected response body" ,
statusCode : http . StatusOK ,
response : ` { "test": true} ` ,
expectError : tru e,
} ,
{
name : "Success case, ok: true" ,
statusCode : http . StatusOK ,
slackResponse : ` { "ok": true} ` ,
expectError : false ,
name : "Success case, ok: true" ,
statusCode : http . StatusOK ,
response : ` { "ok": true} ` ,
expectError : false ,
} ,
{
name : "200 status code, error in body" ,
statusCode : http . StatusOK ,
slackResponse : ` { "ok": false, "error": "test error"} ` ,
expectError : true ,
name : "200 status code, error in body" ,
statusCode : http . StatusOK ,
response : ` { "ok": false, "error": "test error"} ` ,
expectError : true ,
} ,
}
@ -403,14 +561,14 @@ func TestSendSlackRequest(t *testing.T) {
t . Run ( test . name , func ( tt * testing . T ) {
server := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
w . WriteHeader ( test . statusCode )
_ , err := w . Write ( [ ] byte ( test . slackR esponse) )
_ , err := w . Write ( [ ] byte ( test . r esponse) )
require . NoError ( tt , err )
} ) )
defer server . Close ( )
req , err := http . NewRequest ( http . MethodGet , server . URL , nil )
require . NoError ( tt , err )
err = sendSlackRequest ( req , log . New ( "test" ) )
_ , err = sendSlackRequest ( context . Background ( ) , req , log . New ( "test" ) )
if ! test . expectError {
require . NoError ( tt , err )
} else {