diff --git a/pkg/services/slack_test.go b/pkg/services/slack_test.go index 55b6481c..ebda3df5 100644 --- a/pkg/services/slack_test.go +++ b/pkg/services/slack_test.go @@ -2,6 +2,7 @@ package services import ( "encoding/json" + "fmt" "io" "net/http" "net/http/httptest" @@ -63,6 +64,149 @@ func TestGetTemplater_Slack(t *testing.T) { assert.Equal(t, true, notification.Slack.NotifyBroadcast) } +func TestGetTemplater_Slack_InvalidTemplates(t *testing.T) { + tests := []struct { + name string + notification SlackNotification + }{ + { + name: "Invalid username template", + notification: SlackNotification{ + Username: "{{.foo", + }, + }, + { + name: "Invalid icon template", + notification: SlackNotification{ + Icon: "{{.foo", + }, + }, + { + name: "Invalid attachments template", + notification: SlackNotification{ + Attachments: "{{.foo", + }, + }, + { + name: "Invalid blocks template", + notification: SlackNotification{ + Blocks: "{{.foo", + }, + }, + { + name: "Invalid grouping key template", + notification: SlackNotification{ + GroupingKey: "{{.foo", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := tt.notification.GetTemplater("test", template.FuncMap{}) + assert.Error(t, err) + }) + } +} + +func TestGetTemplater_Slack_NilNotification(t *testing.T) { + n := Notification{ + Slack: &SlackNotification{ + Username: "{{.name}}", + }, + } + + templater, err := n.GetTemplater("", template.FuncMap{}) + assert.NoError(t, err) + + // Test with nil Slack on target notification + var notification Notification + err = templater(¬ification, map[string]interface{}{ + "name": "test", + }) + + assert.NoError(t, err) + assert.NotNil(t, notification.Slack) + assert.Equal(t, "test", notification.Slack.Username) +} + +func TestGetTemplater_Slack_DeliveryPolicy(t *testing.T) { + n := Notification{ + Slack: &SlackNotification{ + Username: "bot", + DeliveryPolicy: slackutil.Update, + }, + } + + templater, err := n.GetTemplater("", template.FuncMap{}) + assert.NoError(t, err) + + var notification Notification + err = templater(¬ification, map[string]interface{}{}) + + assert.NoError(t, err) + assert.Equal(t, slackutil.Update, notification.Slack.DeliveryPolicy) +} + +func TestGetTemplater_Slack_TemplateExecutionError(t *testing.T) { + // Create a FuncMap with the required function + funcMap := template.FuncMap{ + "required": func(msg string, val interface{}) (interface{}, error) { + if val == nil || val == "" { + return nil, fmt.Errorf("%s", msg) + } + return val, nil + }, + } + + tests := []struct { + name string + notification SlackNotification + }{ + { + name: "Username execution error", + notification: SlackNotification{ + Username: "{{.missing | required \"missing is required\"}}", + }, + }, + { + name: "Icon execution error", + notification: SlackNotification{ + Icon: "{{.missing | required \"missing is required\"}}", + }, + }, + { + name: "Attachments execution error", + notification: SlackNotification{ + Attachments: "{{.missing | required \"missing is required\"}}", + }, + }, + { + name: "Blocks execution error", + notification: SlackNotification{ + Blocks: "{{.missing | required \"missing is required\"}}", + }, + }, + { + name: "GroupingKey execution error", + notification: SlackNotification{ + GroupingKey: "{{.missing | required \"missing is required\"}}", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + templater, err := tt.notification.GetTemplater("", funcMap) + assert.NoError(t, err) + + var notification Notification + err = templater(¬ification, map[string]interface{}{}) + assert.Error(t, err) + }) + } +} + func TestBuildMessageOptionsWithNonExistTemplate(t *testing.T) { n := Notification{} @@ -73,6 +217,116 @@ func TestBuildMessageOptionsWithNonExistTemplate(t *testing.T) { assert.Equal(t, slackutil.Post, sn.DeliveryPolicy) } +func TestBuildMessageOptions_IconURL(t *testing.T) { + t.Run("Valid icon URL from notification", func(t *testing.T) { + n := Notification{ + Message: "test", + Slack: &SlackNotification{ + Icon: "https://example.com/icon.png", + }, + } + + _, opts, err := buildMessageOptions(n, SlackOptions{}) + assert.NoError(t, err) + // Should have text + icon_url options + assert.GreaterOrEqual(t, len(opts), 2) + }) + + t.Run("Valid icon URL from options", func(t *testing.T) { + n := Notification{ + Message: "test", + } + + _, opts, err := buildMessageOptions(n, SlackOptions{ + Icon: "http://example.com/icon.png", + }) + assert.NoError(t, err) + assert.GreaterOrEqual(t, len(opts), 2) + }) + + t.Run("Invalid icon - neither emoji nor URL", func(t *testing.T) { + n := Notification{ + Message: "test", + Slack: &SlackNotification{ + Icon: "invalid-icon", + }, + } + + _, opts, err := buildMessageOptions(n, SlackOptions{}) + assert.NoError(t, err) + // Should have text + attachments + blocks (but no icon option because it's invalid) + // Use GreaterOrEqual to make test less fragile to implementation changes + assert.GreaterOrEqual(t, len(opts), 3) + }) +} + +func TestBuildMessageOptions_DisableUnfurl(t *testing.T) { + n := Notification{ + Message: "test", + } + + _, opts, err := buildMessageOptions(n, SlackOptions{ + DisableUnfurl: true, + }) + assert.NoError(t, err) + // Should have text + 2 unfurl options + assert.GreaterOrEqual(t, len(opts), 3) +} + +func TestBuildMessageOptions_InvalidAttachments(t *testing.T) { + n := Notification{ + Message: "test", + Slack: &SlackNotification{ + Attachments: "invalid json", + }, + } + + _, _, err := buildMessageOptions(n, SlackOptions{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to unmarshal attachments") +} + +func TestBuildMessageOptions_InvalidBlocks(t *testing.T) { + n := Notification{ + Message: "test", + Slack: &SlackNotification{ + Blocks: "invalid json", + }, + } + + _, _, err := buildMessageOptions(n, SlackOptions{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to unmarshal blocks") +} + +func TestGetSigningSecret(t *testing.T) { + service := NewSlackService(SlackOptions{ + Token: "test-token", + SigningSecret: "test-signing-secret", + }) + + slackService, ok := service.(*slackService) + assert.True(t, ok) + assert.Equal(t, "test-signing-secret", slackService.GetSigningSecret()) +} + +func TestNewSlackClient_CustomAPIURL(t *testing.T) { + client, err := newSlackClient(SlackOptions{ + Token: "test-token", + ApiURL: "https://custom.slack.com/api/", + }) + assert.NoError(t, err) + assert.NotNil(t, client) +} + +func TestNewSlackClient_DefaultAPIURL(t *testing.T) { + client, err := newSlackClient(SlackOptions{ + Token: "test-token", + }) + assert.NoError(t, err) + assert.NotNil(t, client) +} + type chatResponseFull struct { Channel string `json:"channel"` Timestamp string `json:"ts"` // Regular message timestamp @@ -319,3 +573,22 @@ func TestSlack_SetUsernameAndIcon(t *testing.T) { } }) } + +func TestSlack_SendNotification_WithInvalidJSON(t *testing.T) { + service := NewSlackService(SlackOptions{ + Token: "something-token", + InsecureSkipVerify: true, + }) + + err := service.Send( + Notification{ + Message: "test", + Slack: &SlackNotification{ + Attachments: "invalid json", + }, + }, + Destination{Recipient: "test", Service: "slack"}, + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to unmarshal") +} diff --git a/pkg/templates/service_test.go b/pkg/templates/service_test.go index 48283e9e..a92cf759 100644 --- a/pkg/templates/service_test.go +++ b/pkg/templates/service_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/argoproj/notifications-engine/pkg/services" ) @@ -29,3 +30,180 @@ func TestFormat_Message(t *testing.T) { assert.Equal(t, "hello", notification.Message) } + +func TestNewService_Success(t *testing.T) { + t.Run("Single template", func(t *testing.T) { + svc, err := NewService(map[string]services.Notification{ + "test": { + Message: "{{.foo}}", + }, + }) + require.NoError(t, err) + require.NotNil(t, svc) + assert.Len(t, svc.templaters, 1) + }) + + t.Run("Multiple templates", func(t *testing.T) { + svc, err := NewService(map[string]services.Notification{ + "template1": { + Message: "{{.foo}}", + }, + "template2": { + Message: "{{.bar}}", + }, + "template3": { + Message: "{{.baz}}", + }, + }) + require.NoError(t, err) + require.NotNil(t, svc) + assert.Len(t, svc.templaters, 3) + }) + + t.Run("Empty templates map", func(t *testing.T) { + svc, err := NewService(map[string]services.Notification{}) + require.NoError(t, err) + require.NotNil(t, svc) + assert.Len(t, svc.templaters, 0) + }) + + t.Run("Env and expandenv functions are removed", func(t *testing.T) { + // Test that env and expandenv Sprig functions are removed for security + svc, err := NewService(map[string]services.Notification{ + "test": { + Message: "test", + }, + }) + require.NoError(t, err) + require.NotNil(t, svc) + }) +} + +func TestNewService_InvalidTemplate(t *testing.T) { + t.Run("Invalid template syntax", func(t *testing.T) { + _, err := NewService(map[string]services.Notification{ + "invalid": { + Message: "{{.foo", + }, + }) + assert.Error(t, err) + }) +} + +func TestFormatNotification_Success(t *testing.T) { + t.Run("Single template", func(t *testing.T) { + svc, err := NewService(map[string]services.Notification{ + "greeting": { + Message: "Hello {{.name}}!", + }, + }) + require.NoError(t, err) + + notification, err := svc.FormatNotification(map[string]interface{}{ + "name": "World", + }, "greeting") + + require.NoError(t, err) + assert.Equal(t, "Hello World!", notification.Message) + }) + + t.Run("Multiple templates applied in order", func(t *testing.T) { + svc, err := NewService(map[string]services.Notification{ + "template1": { + Message: "{{.first}}", + }, + "template2": { + Message: "{{.second}}", + }, + }) + require.NoError(t, err) + + // When multiple templates are used, the last one should overwrite + notification, err := svc.FormatNotification(map[string]interface{}{ + "first": "First", + "second": "Second", + }, "template1", "template2") + + require.NoError(t, err) + assert.Equal(t, "Second", notification.Message) + }) + + t.Run("Complex template with sprig functions", func(t *testing.T) { + svc, err := NewService(map[string]services.Notification{ + "complex": { + Message: "{{.name | upper}}", + }, + }) + require.NoError(t, err) + + notification, err := svc.FormatNotification(map[string]interface{}{ + "name": "test", + }, "complex") + + require.NoError(t, err) + assert.Equal(t, "TEST", notification.Message) + }) + + t.Run("Empty template list", func(t *testing.T) { + svc, err := NewService(map[string]services.Notification{ + "test": { + Message: "test", + }, + }) + require.NoError(t, err) + + notification, err := svc.FormatNotification(map[string]interface{}{}) + require.NoError(t, err) + assert.NotNil(t, notification) + assert.Empty(t, notification.Message) + }) +} + +func TestFormatNotification_TemplateNotFound(t *testing.T) { + svc, err := NewService(map[string]services.Notification{ + "existing": { + Message: "test", + }, + }) + require.NoError(t, err) + + notification, err := svc.FormatNotification(map[string]interface{}{}, "nonexistent") + assert.Error(t, err) + assert.Nil(t, notification) + assert.Contains(t, err.Error(), "template 'nonexistent' is not supported") +} + +func TestFormatNotification_TemplateExecutionError(t *testing.T) { + svc, err := NewService(map[string]services.Notification{ + "test": { + Message: "{{fail \"intentional error\"}}", + }, + }) + require.NoError(t, err) + + // This should trigger an error during template execution + notification, err := svc.FormatNotification(map[string]interface{}{ + "other": "value", + }, "test") + + assert.Error(t, err) + assert.Nil(t, notification) +} + +func TestFormatNotification_MultipleTemplatesWithError(t *testing.T) { + svc, err := NewService(map[string]services.Notification{ + "valid": { + Message: "{{.foo}}", + }, + }) + require.NoError(t, err) + + // First template is valid, second doesn't exist + notification, err := svc.FormatNotification(map[string]interface{}{ + "foo": "bar", + }, "valid", "invalid") + + assert.Error(t, err) + assert.Nil(t, notification) + assert.Contains(t, err.Error(), "template 'invalid' is not supported") +}