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 }