github.com/mongodb/grip@v0.0.0-20240213223901-f906268d82b9/send/send_test.go (about)

     1  package send
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"math/rand"
    10  	"net/http"
    11  	"os"
    12  	"path/filepath"
    13  	"runtime"
    14  	"strings"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/mongodb/grip/level"
    19  	"github.com/mongodb/grip/message"
    20  	"github.com/stretchr/testify/assert"
    21  	"github.com/stretchr/testify/suite"
    22  )
    23  
    24  type SenderSuite struct {
    25  	senders map[string]Sender
    26  	rand    *rand.Rand
    27  	tempDir string
    28  	suite.Suite
    29  }
    30  
    31  func TestSenderSuite(t *testing.T) {
    32  	suite.Run(t, new(SenderSuite))
    33  }
    34  
    35  func (s *SenderSuite) SetupSuite() {
    36  	var err error
    37  	s.rand = rand.New(rand.NewSource(time.Now().Unix()))
    38  	s.tempDir, err = ioutil.TempDir("", "sender-test-")
    39  	s.Require().NoError(err)
    40  }
    41  
    42  func (s *SenderSuite) SetupTest() {
    43  	s.Require().NoError(os.MkdirAll(s.tempDir, 0766))
    44  
    45  	l := LevelInfo{level.Info, level.Notice}
    46  	s.senders = map[string]Sender{
    47  		"slack": &slackJournal{Base: NewBase("slack")},
    48  		"xmpp":  &xmppLogger{Base: NewBase("xmpp")},
    49  		"buildlogger": &buildlogger{
    50  			Base: NewBase("buildlogger"),
    51  			conf: &BuildloggerConfig{Local: MakeNative()},
    52  		},
    53  	}
    54  
    55  	internal := MakeInternalLogger()
    56  	internal.name = "internal"
    57  	internal.output = make(chan *InternalMessage)
    58  	s.senders["internal"] = internal
    59  
    60  	native, err := NewNativeLogger("native", l)
    61  	s.Require().NoError(err)
    62  	s.senders["native"] = native
    63  
    64  	s.senders["writer"] = NewWriterSender(native)
    65  
    66  	var plain, plainerr, plainfile Sender
    67  	plain, err = NewPlainLogger("plain", l)
    68  	s.Require().NoError(err)
    69  	s.senders["plain"] = plain
    70  
    71  	plainerr, err = NewPlainErrorLogger("plain.err", l)
    72  	s.Require().NoError(err)
    73  	s.senders["plain.err"] = plainerr
    74  
    75  	plainfile, err = NewPlainFileLogger("plain.file", filepath.Join(s.tempDir, "plain.file"), l)
    76  	s.Require().NoError(err)
    77  	s.senders["plain.file"] = plainfile
    78  
    79  	var asyncOne, asyncTwo Sender
    80  	asyncOne, err = NewNativeLogger("async-one", l)
    81  	s.Require().NoError(err)
    82  	asyncTwo, err = NewNativeLogger("async-two", l)
    83  	s.Require().NoError(err)
    84  	s.senders["async"] = NewAsyncGroupSender(context.Background(), 16, asyncOne, asyncTwo)
    85  
    86  	nativeErr, err := NewErrorLogger("error", l)
    87  	s.Require().NoError(err)
    88  	s.senders["error"] = nativeErr
    89  
    90  	nativeFile, err := NewFileLogger("native-file", filepath.Join(s.tempDir, "file"), l)
    91  	s.Require().NoError(err)
    92  	s.senders["native-file"] = nativeFile
    93  
    94  	callsite, err := NewCallSiteConsoleLogger("callsite", 1, l)
    95  	s.Require().NoError(err)
    96  	s.senders["callsite"] = callsite
    97  
    98  	callsiteFile, err := NewCallSiteFileLogger("callsite", filepath.Join(s.tempDir, "cs"), 1, l)
    99  	s.Require().NoError(err)
   100  	s.senders["callsite-file"] = callsiteFile
   101  
   102  	stream, err := NewStreamLogger("stream", &bytes.Buffer{}, l)
   103  	s.Require().NoError(err)
   104  	s.senders["stream"] = stream
   105  
   106  	jsons, err := NewJSONConsoleLogger("json", LevelInfo{level.Info, level.Notice})
   107  	s.Require().NoError(err)
   108  	s.senders["json"] = jsons
   109  
   110  	jsonf, err := NewJSONFileLogger("json", filepath.Join(s.tempDir, "js"), l)
   111  	s.Require().NoError(err)
   112  	s.senders["json"] = jsonf
   113  
   114  	var sender Sender
   115  	multiSenders := []Sender{}
   116  	for i := 0; i < 4; i++ {
   117  		sender, err = NewNativeLogger(fmt.Sprintf("native-%d", i), l)
   118  		s.Require().NoError(err)
   119  		multiSenders = append(multiSenders, sender)
   120  	}
   121  
   122  	multi, err := NewMultiSender("multi", l, multiSenders)
   123  	s.Require().NoError(err)
   124  	s.senders["multi"] = multi
   125  
   126  	slackMocked, err := NewSlackLogger(&SlackOptions{
   127  		client:   &slackClientMock{},
   128  		Hostname: "testhost",
   129  		Channel:  "#test",
   130  		Name:     "smoke",
   131  	}, "slack", LevelInfo{level.Info, level.Notice})
   132  	s.Require().NoError(err)
   133  	s.senders["slack-mocked"] = slackMocked
   134  
   135  	xmppMocked, err := NewXMPPLogger("xmpp", "target",
   136  		XMPPConnectionInfo{client: &xmppClientMock{}},
   137  		LevelInfo{level.Info, level.Notice})
   138  	s.Require().NoError(err)
   139  	s.senders["xmpp-mocked"] = xmppMocked
   140  
   141  	bufferedInternal, err := NewNativeLogger("buffered", l)
   142  	s.Require().NoError(err)
   143  	s.senders["buffered"], err = NewBufferedSender(context.Background(), bufferedInternal, BufferedSenderOptions{FlushInterval: minFlushInterval, BufferSize: 1})
   144  	s.Require().NoError(err)
   145  
   146  	bufferedAsyncInternal, err := NewNativeLogger("buffered-async", l)
   147  	s.Require().NoError(err)
   148  	opts := BufferedAsyncSenderOptions{}
   149  	opts.FlushInterval = minFlushInterval
   150  	opts.BufferSize = 1
   151  	s.senders["buffered-async"], err = NewBufferedAsyncSender(
   152  		context.Background(),
   153  		bufferedAsyncInternal,
   154  		opts,
   155  	)
   156  	s.Require().NoError(err)
   157  
   158  	s.senders["github"], err = NewGithubIssuesLogger("gh", &GithubOptions{})
   159  	s.Require().NoError(err)
   160  
   161  	s.senders["github-comment"], err = NewGithubCommentLogger("ghcomment", 100, &GithubOptions{})
   162  	s.Require().NoError(err)
   163  
   164  	s.senders["github-status"], err = NewGithubStatusLogger("ghstatus", &GithubOptions{}, "master")
   165  	s.Require().NoError(err)
   166  
   167  	s.senders["gh-mocked"] = &githubLogger{
   168  		Base: NewBase("gh-mocked"),
   169  		opts: &GithubOptions{},
   170  		gh:   &githubClientMock{},
   171  	}
   172  	s.NoError(s.senders["gh-mocked"].SetFormatter(MakeDefaultFormatter()))
   173  
   174  	s.senders["gh-comment-mocked"] = &githubCommentLogger{
   175  		Base:  NewBase("gh-mocked"),
   176  		opts:  &GithubOptions{},
   177  		gh:    &githubClientMock{},
   178  		issue: 200,
   179  	}
   180  	s.NoError(s.senders["gh-comment-mocked"].SetFormatter(MakeDefaultFormatter()))
   181  
   182  	s.senders["gh-status-mocked"] = &githubStatusMessageLogger{
   183  		Base: NewBase("gh-status-mocked"),
   184  		opts: &GithubOptions{},
   185  		gh:   &githubClientMock{},
   186  		ref:  "master",
   187  	}
   188  	s.NoError(s.senders["gh-status-mocked"].SetFormatter(MakeDefaultFormatter()))
   189  
   190  	annotatingBase, err := NewNativeLogger("async-one", l)
   191  	s.Require().NoError(err)
   192  	s.senders["annotating"] = NewAnnotatingSender(annotatingBase, map[string]interface{}{
   193  		"one":    1,
   194  		"true":   true,
   195  		"string": "string",
   196  	})
   197  
   198  	for _, size := range []int{1, 100, 10000, 1000000} {
   199  		name := fmt.Sprintf("inmemory-%d", size)
   200  		s.senders[name], err = NewInMemorySender(name, l, size)
   201  		s.Require().NoError(err)
   202  		s.NoError(s.senders[name].SetFormatter(MakeDefaultFormatter()))
   203  	}
   204  }
   205  
   206  func (s *SenderSuite) TearDownTest() {
   207  	_ = s.senders["buffered-async"].Close()
   208  
   209  	if runtime.GOOS == "windows" {
   210  		_ = s.senders["native-file"].Close()
   211  		_ = s.senders["callsite-file"].Close()
   212  		_ = s.senders["json"].Close()
   213  		_ = s.senders["plain.file"].Close()
   214  	}
   215  	s.Require().NoError(os.RemoveAll(s.tempDir))
   216  }
   217  
   218  func (s *SenderSuite) functionalMockSenders() map[string]Sender {
   219  	out := map[string]Sender{}
   220  	for t, sender := range s.senders {
   221  		if t == "slack" || t == "internal" || t == "xmpp" || t == "buildlogger" {
   222  			continue
   223  		} else if strings.HasPrefix(t, "github") {
   224  			continue
   225  
   226  		} else {
   227  			out[t] = sender
   228  		}
   229  	}
   230  	return out
   231  }
   232  
   233  func (s *SenderSuite) TearDownSuite() {
   234  	s.NoError(s.senders["internal"].Close())
   235  }
   236  
   237  func (s *SenderSuite) TestSenderImplementsInterface() {
   238  	// this actually won't catch the error; the compiler will in
   239  	// the fixtures, but either way we need to make sure that the
   240  	// tests actually enforce this.
   241  	for name, sender := range s.senders {
   242  		s.Implements((*Sender)(nil), sender, name)
   243  	}
   244  }
   245  
   246  const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()"
   247  
   248  func randomString(n int, r *rand.Rand) string {
   249  	b := make([]byte, n)
   250  	for i := range b {
   251  		b[i] = letters[r.Int63()%int64(len(letters))]
   252  	}
   253  	return string(b)
   254  }
   255  
   256  func (s *SenderSuite) TestNameSetterRoundTrip() {
   257  	for n, sender := range s.senders {
   258  		for i := 0; i < 100; i++ {
   259  			name := randomString(12, s.rand)
   260  			s.NotEqual(sender.Name(), name, n)
   261  			sender.SetName(name)
   262  			s.Equal(sender.Name(), name, n)
   263  		}
   264  	}
   265  }
   266  
   267  func (s *SenderSuite) TestLevelSetterRejectsInvalidSettings() {
   268  	levels := []LevelInfo{
   269  		{level.Invalid, level.Invalid},
   270  		{level.Priority(-10), level.Priority(-1)},
   271  		{level.Debug, level.Priority(-1)},
   272  		{level.Priority(800), level.Priority(-2)},
   273  	}
   274  
   275  	for n, sender := range s.senders {
   276  		if n == "async" {
   277  			// the async sender doesn't meaningfully have
   278  			// its own level because it passes this down
   279  			// to its constituent senders.
   280  			continue
   281  		}
   282  
   283  		s.NoError(sender.SetLevel(LevelInfo{level.Debug, level.Alert}))
   284  		for _, l := range levels {
   285  			s.True(sender.Level().Valid(), n)
   286  			s.False(l.Valid(), n)
   287  			s.Error(sender.SetLevel(l), n)
   288  			s.True(sender.Level().Valid(), n)
   289  			s.NotEqual(sender.Level(), l, n)
   290  		}
   291  
   292  	}
   293  }
   294  
   295  func (s *SenderSuite) TestCloserShouldUsuallyNoop() {
   296  	for t, sender := range s.senders {
   297  		s.NoError(sender.Close(), t)
   298  	}
   299  }
   300  
   301  func (s *SenderSuite) TestBasicNoopSendTest() {
   302  	for _, sender := range s.functionalMockSenders() {
   303  		for i := -10; i <= 110; i += 5 {
   304  			m := message.NewDefaultMessage(level.Priority(i), "hello world! "+randomString(10, s.rand))
   305  			sender.Send(m)
   306  		}
   307  	}
   308  }
   309  
   310  func TestBaseConstructor(t *testing.T) {
   311  	assert := assert.New(t)
   312  
   313  	sink, err := NewInternalLogger("sink", LevelInfo{level.Debug, level.Debug})
   314  	assert.NoError(err)
   315  	handler := ErrorHandlerFromSender(sink)
   316  	assert.Equal(0, sink.Len())
   317  	assert.False(sink.HasMessage())
   318  
   319  	for _, n := range []string{"logger", "grip", "sender"} {
   320  		made := MakeBase(n, func() {}, func() error { return nil })
   321  		newed := NewBase(n)
   322  		assert.Equal(made.name, newed.name)
   323  		assert.Equal(made.level, newed.level)
   324  		assert.Equal(made.closer(), newed.closer())
   325  
   326  		for _, s := range []*Base{made, newed} {
   327  			assert.Error(s.SetFormatter(nil))
   328  			assert.Error(s.SetErrorHandler(nil))
   329  			assert.NoError(s.SetErrorHandler(handler))
   330  			s.ErrorHandler()(errors.New("failed"), message.NewString("fated"))
   331  		}
   332  	}
   333  
   334  	assert.Equal(6, sink.Len())
   335  	assert.True(sink.HasMessage())
   336  }
   337  
   338  func (s *SenderSuite) TestGithubIssuesLogger() {
   339  	sender := s.senders["gh-mocked"].(*githubLogger)
   340  	client := sender.gh.(*githubClientMock)
   341  
   342  	for _, test := range []struct {
   343  		name    string
   344  		setup   func()
   345  		m       message.Composer
   346  		eh      ErrorHandler
   347  		numSent int
   348  	}{
   349  		{
   350  			name:  "FailedSend",
   351  			setup: func() { client.failSend = true },
   352  			m:     message.NewString("hi"),
   353  			eh: func(err error, _ message.Composer) {
   354  				s.Contains(err.Error(), "failed to create issue")
   355  			},
   356  		},
   357  		{
   358  			name: "Non200StatusCode",
   359  			setup: func() {
   360  				client.failSend = false
   361  				client.httpStatusCode = http.StatusInternalServerError
   362  			},
   363  			m: message.NewString("hi"),
   364  			eh: func(err error, _ message.Composer) {
   365  				s.Contains(err.Error(), "received HTTP status")
   366  			},
   367  			numSent: 1,
   368  		},
   369  		{
   370  			name: "SuccessfulSend",
   371  			setup: func() {
   372  				client.failSend = false
   373  				client.httpStatusCode = http.StatusOK
   374  			},
   375  			m: message.NewString("hi"),
   376  			eh: func(err error, m message.Composer) {
   377  				s.Fail("Got error, but shouldn't have: %s for composer: %s", err.Error(), m.String())
   378  			},
   379  			numSent: 1,
   380  		},
   381  		{
   382  			name: "InvalidMessage",
   383  			setup: func() {
   384  				client.failSend = false
   385  				client.httpStatusCode = http.StatusOK
   386  			},
   387  			m: message.NewString(""),
   388  			eh: func(err error, m message.Composer) {
   389  				s.Fail("Got error, but shouldn't have: %s for composer: %s", err.Error(), m.String())
   390  			},
   391  		},
   392  	} {
   393  		s.Run(test.name, func() {
   394  			if test.setup != nil {
   395  				test.setup()
   396  			}
   397  			s.Require().NoError(sender.SetErrorHandler(test.eh))
   398  			prevNumSent := client.numSent
   399  
   400  			sender.Send(test.m)
   401  			s.Equal(prevNumSent+test.numSent, client.numSent)
   402  		})
   403  	}
   404  }
   405  
   406  func (s *SenderSuite) TestGithubStatusLogger() {
   407  	sender := s.senders["gh-status-mocked"].(*githubStatusMessageLogger)
   408  	client := sender.gh.(*githubClientMock)
   409  
   410  	for _, test := range []struct {
   411  		name     string
   412  		setup    func()
   413  		m        message.Composer
   414  		eh       ErrorHandler
   415  		numSent  int
   416  		lastRepo string
   417  	}{
   418  		{
   419  			name:  "FailedSend",
   420  			setup: func() { client.failSend = true },
   421  			m:     message.NewGithubStatusMessage(level.Info, "example", message.GithubStatePending, "https://example.com/hi", "description"),
   422  			eh: func(err error, _ message.Composer) {
   423  				s.Contains(err.Error(), "failed to create status")
   424  			},
   425  		},
   426  		{
   427  			name: "Non200StatusCode",
   428  			setup: func() {
   429  				client.failSend = false
   430  				client.httpStatusCode = http.StatusInternalServerError
   431  			},
   432  			m: message.NewGithubStatusMessage(level.Info, "example", message.GithubStatePending, "https://example.com/hi", "description"),
   433  			eh: func(err error, _ message.Composer) {
   434  				s.Contains(err.Error(), "received HTTP status")
   435  			},
   436  			numSent: 1,
   437  		},
   438  		{
   439  			name: "SuccessfulSend",
   440  			setup: func() {
   441  				client.failSend = false
   442  				client.httpStatusCode = http.StatusOK
   443  			},
   444  			m: message.NewGithubStatusMessage(level.Info, "example", message.GithubStatePending, "https://example.com/hi", "description"),
   445  			eh: func(err error, m message.Composer) {
   446  				s.Fail("Got error, but shouldn't have: %s for composer: %s", err.Error(), m.String())
   447  			},
   448  			numSent: 1,
   449  		},
   450  		{
   451  			name: "SuccessfulSendWithRepoConstructor",
   452  			setup: func() {
   453  				client.failSend = false
   454  				client.httpStatusCode = http.StatusOK
   455  			},
   456  			m: message.NewGithubStatusMessageWithRepo(level.Info, message.GithubStatus{
   457  				Owner:       "somewhere",
   458  				Repo:        "over",
   459  				Ref:         "therainbow",
   460  				Context:     "example",
   461  				State:       message.GithubStatePending,
   462  				URL:         "https://example.com/hi",
   463  				Description: "description",
   464  			}),
   465  			eh: func(err error, m message.Composer) {
   466  				s.Fail("Got error, but shouldn't have: %s for composer: %s", err.Error(), m.String())
   467  			},
   468  			numSent:  1,
   469  			lastRepo: "somewhere/over@therainbow",
   470  		},
   471  		{
   472  			name: "InvalidMessage",
   473  			setup: func() {
   474  				client.failSend = false
   475  				client.httpStatusCode = http.StatusOK
   476  			},
   477  
   478  			m: message.NewGithubStatusMessage(level.Info, "", message.GithubStatePending, "https://example.com/hi", "description"),
   479  			eh: func(err error, m message.Composer) {
   480  				s.Fail("Got error, but shouldn't have: %s for composer: %s", err.Error(), m.String())
   481  			},
   482  		},
   483  	} {
   484  		s.Run(test.name, func() {
   485  			if test.setup != nil {
   486  				test.setup()
   487  			}
   488  			s.Require().NoError(sender.SetErrorHandler(test.eh))
   489  			prevNumSent := client.numSent
   490  
   491  			sender.Send(test.m)
   492  			s.Equal(prevNumSent+test.numSent, client.numSent)
   493  			if test.lastRepo != "" {
   494  				s.Equal(test.lastRepo, client.lastRepo)
   495  			}
   496  		})
   497  	}
   498  }
   499  
   500  func (s *SenderSuite) TestGithubCommentLogger() {
   501  	sender := s.senders["gh-comment-mocked"].(*githubCommentLogger)
   502  	client := sender.gh.(*githubClientMock)
   503  
   504  	for _, test := range []struct {
   505  		name    string
   506  		setup   func()
   507  		m       message.Composer
   508  		eh      ErrorHandler
   509  		numSent int
   510  	}{
   511  		{
   512  			name:  "FailedSend",
   513  			setup: func() { client.failSend = true },
   514  			m:     message.NewString("hi"),
   515  			eh: func(err error, _ message.Composer) {
   516  				s.Contains(err.Error(), "failed to create comment")
   517  			},
   518  		},
   519  		{
   520  			name: "Non200StatusCode",
   521  			setup: func() {
   522  				client.failSend = false
   523  				client.httpStatusCode = http.StatusInternalServerError
   524  			},
   525  			m: message.NewString("hi"),
   526  			eh: func(err error, _ message.Composer) {
   527  				s.Contains(err.Error(), "received HTTP status")
   528  			},
   529  			numSent: 1,
   530  		},
   531  		{
   532  			name: "SuccessfulSend",
   533  			setup: func() {
   534  				client.failSend = false
   535  				client.httpStatusCode = http.StatusOK
   536  			},
   537  			m: message.NewString("hi"),
   538  			eh: func(err error, m message.Composer) {
   539  				s.Fail("Got error, but shouldn't have: %s for composer: %s", err.Error(), m.String())
   540  			},
   541  			numSent: 1,
   542  		},
   543  		{
   544  			name: "InvalidMessage",
   545  			setup: func() {
   546  				client.failSend = false
   547  				client.httpStatusCode = http.StatusOK
   548  			},
   549  			m: message.NewString(""),
   550  			eh: func(err error, m message.Composer) {
   551  				s.Fail("Got error, but shouldn't have: %s for composer: %s", err.Error(), m.String())
   552  			},
   553  		},
   554  	} {
   555  		s.Run(test.name, func() {
   556  			if test.setup != nil {
   557  				test.setup()
   558  			}
   559  			s.Require().NoError(sender.SetErrorHandler(test.eh))
   560  			prevNumSent := client.numSent
   561  
   562  			sender.Send(test.m)
   563  			s.Equal(prevNumSent+test.numSent, client.numSent)
   564  		})
   565  	}
   566  }