github.com/goreleaser/goreleaser@v1.25.1/internal/pipe/slack/slack_test.go (about)

     1  package slack
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"testing"
     7  
     8  	"github.com/goreleaser/goreleaser/internal/testctx"
     9  	"github.com/goreleaser/goreleaser/internal/testlib"
    10  	"github.com/goreleaser/goreleaser/internal/yaml"
    11  	"github.com/goreleaser/goreleaser/pkg/config"
    12  	"github.com/slack-go/slack"
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  )
    16  
    17  func TestStringer(t *testing.T) {
    18  	require.Equal(t, "slack", Pipe{}.String())
    19  }
    20  
    21  func TestDefault(t *testing.T) {
    22  	ctx := testctx.New()
    23  	require.NoError(t, Pipe{}.Default(ctx))
    24  	require.Equal(t, defaultMessageTemplate, ctx.Config.Announce.Slack.MessageTemplate)
    25  }
    26  
    27  func TestAnnounceInvalidTemplate(t *testing.T) {
    28  	ctx := testctx.NewWithCfg(config.Project{
    29  		Announce: config.Announce{
    30  			Slack: config.Slack{
    31  				MessageTemplate: "{{ .Foo }",
    32  			},
    33  		},
    34  	})
    35  	testlib.RequireTemplateError(t, Pipe{}.Announce(ctx))
    36  }
    37  
    38  func TestAnnounceWithQuotes(t *testing.T) {
    39  	t.Setenv("SLACK_WEBHOOK", slackTestHook())
    40  	t.Setenv("USER", "bot-mc-botyson")
    41  
    42  	t.Run("with a plain message", func(t *testing.T) {
    43  		ctx := testctx.NewWithCfg(config.Project{
    44  			Announce: config.Announce{
    45  				Slack: config.Slack{
    46  					MessageTemplate: "{{ envOrDefault \"USER\" \"\" }}",
    47  				},
    48  			},
    49  		})
    50  		require.NoError(t, Pipe{}.Announce(ctx))
    51  	})
    52  
    53  	t.Run("with rich text", func(t *testing.T) {
    54  		var project config.Project
    55  		require.NoError(t, yaml.Unmarshal(goodRichSlackConfWithEnv(), &project))
    56  		ctx := testctx.NewWithCfg(project)
    57  		blocks, attachments, err := parseAdvancedFormatting(ctx)
    58  		require.NoError(t, err)
    59  		assert.Len(t, blocks.BlockSet, 2)
    60  
    61  		blocksBody, err := json.Marshal(blocks.BlockSet)
    62  		require.NoError(t, err)
    63  
    64  		assert.Contains(t, string(blocksBody), `The current user is bot-mc-botyson`)
    65  		assert.Contains(t, string(blocksBody), `The current user is bot-mc-botyson\nnewline!`)
    66  
    67  		assert.Len(t, attachments, 1)
    68  		attachmentsBody, err := json.Marshal(attachments)
    69  		require.NoError(t, err)
    70  		assert.Contains(t, string(attachmentsBody), `The current user is bot-mc-botyson\n\nIncluding newlines\n`)
    71  	})
    72  }
    73  
    74  func TestAnnounceMissingEnv(t *testing.T) {
    75  	ctx := testctx.NewWithCfg(config.Project{
    76  		Announce: config.Announce{
    77  			Slack: config.Slack{},
    78  		},
    79  	})
    80  	require.NoError(t, Pipe{}.Default(ctx))
    81  	require.EqualError(t, Pipe{}.Announce(ctx), `slack: env: environment variable "SLACK_WEBHOOK" should not be empty`)
    82  }
    83  
    84  func TestSkip(t *testing.T) {
    85  	t.Run("skip", func(t *testing.T) {
    86  		require.True(t, Pipe{}.Skip(testctx.New()))
    87  	})
    88  
    89  	t.Run("dont skip", func(t *testing.T) {
    90  		ctx := testctx.NewWithCfg(config.Project{
    91  			Announce: config.Announce{
    92  				Slack: config.Slack{
    93  					Enabled: true,
    94  				},
    95  			},
    96  		})
    97  		require.False(t, Pipe{}.Skip(ctx))
    98  	})
    99  }
   100  
   101  const testVersion = "v1.2.3"
   102  
   103  func TestParseRichText(t *testing.T) {
   104  	t.Parallel()
   105  
   106  	t.Run("parse only - full slack config with blocks and attachments", func(t *testing.T) {
   107  		t.Parallel()
   108  		var project config.Project
   109  		require.NoError(t, yaml.Unmarshal(goodRichSlackConf(), &project))
   110  		ctx := testctx.NewWithCfg(project, testctx.WithVersion(testVersion))
   111  		blocks, attachments, err := parseAdvancedFormatting(ctx)
   112  		require.NoError(t, err)
   113  		require.Len(t, blocks.BlockSet, 4)
   114  		require.Len(t, attachments, 2)
   115  	})
   116  
   117  	t.Run("parse only - slack config with bad blocks", func(t *testing.T) {
   118  		t.Parallel()
   119  		var project config.Project
   120  		require.NoError(t, yaml.Unmarshal(badBlocksSlackConf(), &project))
   121  		ctx := testctx.NewWithCfg(project, testctx.WithVersion(testVersion))
   122  		_, _, err := parseAdvancedFormatting(ctx)
   123  		require.Error(t, err)
   124  		require.ErrorContains(t, err, "json")
   125  	})
   126  
   127  	t.Run("parse only - slack config with bad attachments", func(t *testing.T) {
   128  		t.Parallel()
   129  		var project config.Project
   130  		require.NoError(t, yaml.Unmarshal(badAttachmentsSlackConf(), &project))
   131  		ctx := testctx.NewWithCfg(project, testctx.WithVersion(testVersion))
   132  		_, _, err := parseAdvancedFormatting(ctx)
   133  		require.Error(t, err)
   134  		require.ErrorContains(t, err, "json")
   135  	})
   136  }
   137  
   138  func TestRichText(t *testing.T) {
   139  	t.Setenv("SLACK_WEBHOOK", slackTestHook())
   140  
   141  	t.Run("e2e - full slack config with blocks and attachments", func(t *testing.T) {
   142  		t.SkipNow() // requires a valid webhook for integration testing
   143  		var project config.Project
   144  		require.NoError(t, yaml.Unmarshal(goodRichSlackConf(), &project))
   145  		ctx := testctx.NewWithCfg(project, testctx.WithVersion(testVersion))
   146  		require.NoError(t, Pipe{}.Announce(ctx))
   147  	})
   148  
   149  	t.Run("slack config with bad blocks", func(t *testing.T) {
   150  		var project config.Project
   151  		require.NoError(t, yaml.Unmarshal(badBlocksSlackConf(), &project))
   152  		ctx := testctx.NewWithCfg(project, testctx.WithVersion(testVersion))
   153  		err := Pipe{}.Announce(ctx)
   154  		require.Error(t, err)
   155  		require.ErrorContains(t, err, "json")
   156  	})
   157  }
   158  
   159  func TestUnmarshall(t *testing.T) {
   160  	t.Parallel()
   161  
   162  	t.Run("happy unmarshal", func(t *testing.T) {
   163  		t.Parallel()
   164  		ctx := testctx.New(testctx.WithVersion(testVersion))
   165  		var blocks slack.Blocks
   166  		require.NoError(t, unmarshal(ctx, []interface{}{map[string]interface{}{"type": "divider"}}, &blocks))
   167  	})
   168  
   169  	t.Run("unmarshal fails on MarshalJSON", func(t *testing.T) {
   170  		t.Parallel()
   171  		ctx := testctx.New(testctx.WithVersion(testVersion))
   172  		var blocks slack.Blocks
   173  		require.Error(t, unmarshal(ctx, []interface{}{map[string]interface{}{"type": func() {}}}, &blocks))
   174  	})
   175  
   176  	t.Run("unmarshal happy to resolve template", func(t *testing.T) {
   177  		t.Parallel()
   178  		var project config.Project
   179  		require.NoError(t, yaml.Unmarshal(goodTemplateSlackConf(), &project))
   180  		ctx := testctx.NewWithCfg(project, testctx.WithVersion(testVersion))
   181  		var blocks slack.Blocks
   182  		require.NoError(t, unmarshal(ctx, ctx.Config.Announce.Slack.Blocks, &blocks))
   183  		require.Len(t, blocks.BlockSet, 1)
   184  		header, ok := blocks.BlockSet[0].(*slack.HeaderBlock)
   185  		require.True(t, ok)
   186  		require.Contains(t, header.Text.Text, testVersion)
   187  	})
   188  
   189  	t.Run("unmarshal fails on resolve template", func(t *testing.T) {
   190  		t.Parallel()
   191  		var project config.Project
   192  		require.NoError(t, yaml.Unmarshal(badTemplateSlackConf(), &project))
   193  		ctx := testctx.NewWithCfg(project, testctx.WithVersion(testVersion))
   194  		var blocks slack.Blocks
   195  		require.Error(t, unmarshal(ctx, ctx.Config.Announce.Slack.Blocks, &blocks))
   196  	})
   197  }
   198  
   199  func slackTestHook() string {
   200  	// redacted: replace this by a real Slack Web Incoming Hook to test the feature end to end.
   201  	const hook = "https://hooks.slack.com/services/*********/***********/************************"
   202  
   203  	return hook
   204  }
   205  
   206  func goodRichSlackConf() []byte {
   207  	const conf = `
   208  project_name: test
   209  announce:
   210    slack:
   211      enabled: true
   212      message_template: fallback
   213      channel: my_channel
   214      blocks:
   215        - type: header
   216          text:
   217            type: plain_text
   218            text: '{{ .Version }}'
   219        - type: section
   220          text:
   221            type: mrkdwn
   222            text: |
   223              Heading
   224              =======
   225  
   226  			# Other Heading
   227  
   228              *Bold*
   229  			_italic_
   230              ~Strikethrough~
   231  
   232              ## Heading 2
   233              ### Heading 3
   234  			* List item 1
   235  			* List item 2
   236  
   237  			- List item 3
   238  			- List item 4
   239  
   240  			[link](https://example.com)
   241  			<https://example.com|link>
   242  
   243  			:)
   244  
   245  			:star:
   246  
   247  	  - type: divider
   248  	  - type: section
   249          text:
   250            type: mrkdwn
   251            text: |
   252              my release
   253      attachments:
   254          -
   255            title: Release artifacts
   256            color: '#2eb886'
   257  		  text: |
   258              *Helm chart packages*
   259          - fallback: full changelog
   260            color: '#2eb886'
   261            title: Full Change Log
   262            text: |
   263              * this link
   264              * that link
   265  `
   266  
   267  	buf := bytes.NewBufferString(conf)
   268  
   269  	return bytes.ReplaceAll(buf.Bytes(), []byte("\t"), []byte("    "))
   270  }
   271  
   272  func badBlocksSlackConf() []byte {
   273  	const conf = `
   274  project_name: test
   275  announce:
   276    slack:
   277      enabled: true
   278      message_template: fallback
   279      channel: my_channel
   280      blocks:
   281        - type: header
   282  		text: invalid  # <- wrong type for Slack API
   283  `
   284  
   285  	buf := bytes.NewBufferString(conf)
   286  
   287  	return bytes.ReplaceAll(buf.Bytes(), []byte("\t"), []byte("    "))
   288  }
   289  
   290  func badAttachmentsSlackConf() []byte {
   291  	const conf = `
   292  project_name: test
   293  announce:
   294    slack:
   295      enabled: true
   296      message_template: fallback
   297      channel: my_channel
   298      attachments:
   299          -
   300            title:
   301  		   - Release artifacts
   302  		   - wrong # <- title is not an array
   303            color: '#2eb886'
   304  		  text: |
   305              *Helm chart packages*
   306  `
   307  
   308  	buf := bytes.NewBufferString(conf)
   309  
   310  	return bytes.ReplaceAll(buf.Bytes(), []byte("\t"), []byte("    "))
   311  }
   312  
   313  func goodTemplateSlackConf() []byte {
   314  	const conf = `
   315  project_name: test
   316  announce:
   317    slack:
   318      enabled: true
   319  	message_template: '{{ .Version }}'
   320      channel: my_channel
   321      blocks:
   322        - type: header
   323          text:
   324            type: plain_text
   325            text: '{{ .Version }}'
   326  `
   327  
   328  	buf := bytes.NewBufferString(conf)
   329  
   330  	return bytes.ReplaceAll(buf.Bytes(), []byte("\t"), []byte("    "))
   331  }
   332  
   333  func badTemplateSlackConf() []byte {
   334  	const conf = `
   335  project_name: test
   336  announce:
   337    slack:
   338      enabled: true
   339  	message_template: fallback
   340      channel: my_channel
   341      blocks:
   342        - type: header
   343          text:
   344            type: plain_text
   345  		  text: '{{ .Wrong }}'
   346  `
   347  
   348  	buf := bytes.NewBufferString(conf)
   349  
   350  	return bytes.ReplaceAll(buf.Bytes(), []byte("\t"), []byte("    "))
   351  }
   352  
   353  func goodRichSlackConfWithEnv() []byte {
   354  	const conf = `
   355  project_name: test
   356  announce:
   357    slack:
   358      enabled: true
   359      blocks:
   360        - type: header
   361          text:
   362            type: plain_text
   363            text: 'The current user is {{ envOrDefault "USER" "" }}'
   364        - type: header
   365          text:
   366            type: plain_text
   367            text: "The current user is {{ envOrDefault \"USER\" \"\" }}\nnewline!"
   368      attachments:
   369          -
   370            title: Release artifacts
   371            color: '#2eb886'
   372  		  text: |
   373              The current user is {{ envOrDefault "USER" "" }}
   374  
   375  			Including newlines
   376  `
   377  
   378  	buf := bytes.NewBufferString(conf)
   379  
   380  	return bytes.ReplaceAll(buf.Bytes(), []byte("\t"), []byte("    "))
   381  }