go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luci_notify/notify/notify_test.go (about) 1 // Copyright 2017 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package notify 16 17 import ( 18 "bytes" 19 "compress/gzip" 20 "context" 21 "io" 22 "sort" 23 "testing" 24 25 buildbucketpb "go.chromium.org/luci/buildbucket/proto" 26 "go.chromium.org/luci/common/clock" 27 "go.chromium.org/luci/common/clock/testclock" 28 "go.chromium.org/luci/common/errors" 29 "go.chromium.org/luci/common/logging" 30 "go.chromium.org/luci/common/logging/gologger" 31 "go.chromium.org/luci/gae/impl/memory" 32 "go.chromium.org/luci/gae/service/datastore" 33 "go.chromium.org/luci/server/caching" 34 35 notifypb "go.chromium.org/luci/luci_notify/api/config" 36 "go.chromium.org/luci/luci_notify/common" 37 "go.chromium.org/luci/luci_notify/config" 38 39 . "github.com/smartystreets/goconvey/convey" 40 ) 41 42 func TestNotify(t *testing.T) { 43 t.Parallel() 44 45 Convey("ShouldNotify", t, func() { 46 n := ¬ifypb.Notification{} 47 n.OnOccurrence = []buildbucketpb.Status{} 48 n.OnNewStatus = []buildbucketpb.Status{} 49 50 const ( 51 unspecified = buildbucketpb.Status_STATUS_UNSPECIFIED 52 success = buildbucketpb.Status_SUCCESS 53 failure = buildbucketpb.Status_FAILURE 54 infraFailure = buildbucketpb.Status_INFRA_FAILURE 55 ) 56 57 successfulBuild := &buildbucketpb.Build{Status: success} 58 failedBuild := &buildbucketpb.Build{Status: failure} 59 infraFailedBuild := &buildbucketpb.Build{Status: infraFailure} 60 61 // Helper wrapper which discards the steps and just returns the bool. 62 s := func(oldStatus buildbucketpb.Status, newBuild *buildbucketpb.Build) bool { 63 should, _ := ShouldNotify(context.Background(), n, oldStatus, newBuild) 64 return should 65 } 66 67 Convey("Success", func() { 68 n.OnOccurrence = append(n.OnOccurrence, success) 69 70 So(s(unspecified, successfulBuild), ShouldBeTrue) 71 So(s(unspecified, failedBuild), ShouldBeFalse) 72 So(s(unspecified, infraFailedBuild), ShouldBeFalse) 73 So(s(failure, failedBuild), ShouldBeFalse) 74 So(s(infraFailure, infraFailedBuild), ShouldBeFalse) 75 So(s(success, successfulBuild), ShouldBeTrue) 76 }) 77 78 Convey("Failure", func() { 79 n.OnOccurrence = append(n.OnOccurrence, failure) 80 81 So(s(unspecified, successfulBuild), ShouldBeFalse) 82 So(s(unspecified, failedBuild), ShouldBeTrue) 83 So(s(unspecified, infraFailedBuild), ShouldBeFalse) 84 So(s(failure, failedBuild), ShouldBeTrue) 85 So(s(infraFailure, infraFailedBuild), ShouldBeFalse) 86 So(s(success, successfulBuild), ShouldBeFalse) 87 }) 88 89 Convey("InfraFailure", func() { 90 n.OnOccurrence = append(n.OnOccurrence, infraFailure) 91 92 So(s(unspecified, successfulBuild), ShouldBeFalse) 93 So(s(unspecified, failedBuild), ShouldBeFalse) 94 So(s(unspecified, infraFailedBuild), ShouldBeTrue) 95 So(s(failure, failedBuild), ShouldBeFalse) 96 So(s(infraFailure, infraFailedBuild), ShouldBeTrue) 97 So(s(success, successfulBuild), ShouldBeFalse) 98 }) 99 100 Convey("Failure and InfraFailure", func() { 101 n.OnOccurrence = append(n.OnOccurrence, failure, infraFailure) 102 103 So(s(unspecified, successfulBuild), ShouldBeFalse) 104 So(s(unspecified, failedBuild), ShouldBeTrue) 105 So(s(unspecified, infraFailedBuild), ShouldBeTrue) 106 So(s(failure, failedBuild), ShouldBeTrue) 107 So(s(infraFailure, infraFailedBuild), ShouldBeTrue) 108 So(s(success, successfulBuild), ShouldBeFalse) 109 }) 110 111 Convey("New Failure", func() { 112 n.OnNewStatus = append(n.OnNewStatus, failure) 113 114 So(s(success, successfulBuild), ShouldBeFalse) 115 So(s(success, failedBuild), ShouldBeTrue) 116 So(s(success, infraFailedBuild), ShouldBeFalse) 117 So(s(failure, successfulBuild), ShouldBeFalse) 118 So(s(failure, failedBuild), ShouldBeFalse) 119 So(s(failure, infraFailedBuild), ShouldBeFalse) 120 So(s(infraFailure, successfulBuild), ShouldBeFalse) 121 So(s(infraFailure, failedBuild), ShouldBeTrue) 122 So(s(infraFailure, infraFailedBuild), ShouldBeFalse) 123 So(s(unspecified, successfulBuild), ShouldBeFalse) 124 So(s(unspecified, failedBuild), ShouldBeFalse) 125 So(s(unspecified, infraFailedBuild), ShouldBeFalse) 126 }) 127 128 Convey("New InfraFailure", func() { 129 n.OnNewStatus = append(n.OnNewStatus, infraFailure) 130 131 So(s(success, successfulBuild), ShouldBeFalse) 132 So(s(success, failedBuild), ShouldBeFalse) 133 So(s(success, infraFailedBuild), ShouldBeTrue) 134 So(s(failure, successfulBuild), ShouldBeFalse) 135 So(s(failure, failedBuild), ShouldBeFalse) 136 So(s(failure, infraFailedBuild), ShouldBeTrue) 137 So(s(infraFailure, successfulBuild), ShouldBeFalse) 138 So(s(infraFailure, failedBuild), ShouldBeFalse) 139 So(s(infraFailure, infraFailedBuild), ShouldBeFalse) 140 So(s(unspecified, successfulBuild), ShouldBeFalse) 141 So(s(unspecified, failedBuild), ShouldBeFalse) 142 So(s(unspecified, infraFailedBuild), ShouldBeFalse) 143 }) 144 145 Convey("New Failure and new InfraFailure", func() { 146 n.OnNewStatus = append(n.OnNewStatus, failure, infraFailure) 147 148 So(s(success, successfulBuild), ShouldBeFalse) 149 So(s(success, failedBuild), ShouldBeTrue) 150 So(s(success, infraFailedBuild), ShouldBeTrue) 151 So(s(failure, successfulBuild), ShouldBeFalse) 152 So(s(failure, failedBuild), ShouldBeFalse) 153 So(s(failure, infraFailedBuild), ShouldBeTrue) 154 So(s(infraFailure, successfulBuild), ShouldBeFalse) 155 So(s(infraFailure, failedBuild), ShouldBeTrue) 156 So(s(infraFailure, infraFailedBuild), ShouldBeFalse) 157 So(s(unspecified, successfulBuild), ShouldBeFalse) 158 So(s(unspecified, failedBuild), ShouldBeFalse) 159 So(s(unspecified, infraFailedBuild), ShouldBeFalse) 160 }) 161 162 Convey("InfraFailure and new Failure and new Success", func() { 163 n.OnOccurrence = append(n.OnOccurrence, infraFailure) 164 n.OnNewStatus = append(n.OnNewStatus, failure, success) 165 166 So(s(success, successfulBuild), ShouldBeFalse) 167 So(s(success, failedBuild), ShouldBeTrue) 168 So(s(success, infraFailedBuild), ShouldBeTrue) 169 So(s(failure, successfulBuild), ShouldBeTrue) 170 So(s(failure, failedBuild), ShouldBeFalse) 171 So(s(failure, infraFailedBuild), ShouldBeTrue) 172 So(s(infraFailure, successfulBuild), ShouldBeTrue) 173 So(s(infraFailure, failedBuild), ShouldBeTrue) 174 So(s(infraFailure, infraFailedBuild), ShouldBeTrue) 175 So(s(unspecified, successfulBuild), ShouldBeFalse) 176 So(s(unspecified, failedBuild), ShouldBeFalse) 177 So(s(unspecified, infraFailedBuild), ShouldBeTrue) 178 }) 179 180 Convey("Failure with step regex", func() { 181 n.OnOccurrence = append(n.OnOccurrence, failure) 182 n.FailedStepRegexp = "yes" 183 n.FailedStepRegexpExclude = "no" 184 185 shouldHaveStep := func(oldStatus buildbucketpb.Status, newBuild *buildbucketpb.Build, stepName string) { 186 should, steps := ShouldNotify(context.Background(), n, oldStatus, newBuild) 187 188 So(should, ShouldBeTrue) 189 So(steps, ShouldHaveLength, 1) 190 So(steps[0].Name, ShouldEqual, stepName) 191 } 192 193 So(s(success, failedBuild), ShouldBeFalse) 194 shouldHaveStep(success, &buildbucketpb.Build{ 195 Status: failure, 196 Steps: []*buildbucketpb.Step{ 197 { 198 Name: "yes", 199 Status: failure, 200 }, 201 }, 202 }, "yes") 203 So(s(success, &buildbucketpb.Build{ 204 Status: failure, 205 Steps: []*buildbucketpb.Step{ 206 { 207 Name: "yes", 208 Status: success, 209 }, 210 }, 211 }), ShouldBeFalse) 212 213 So(s(success, &buildbucketpb.Build{ 214 Status: failure, 215 Steps: []*buildbucketpb.Step{ 216 { 217 Name: "no", 218 Status: failure, 219 }, 220 }, 221 }), ShouldBeFalse) 222 So(s(success, &buildbucketpb.Build{ 223 Status: failure, 224 Steps: []*buildbucketpb.Step{ 225 { 226 Name: "yes", 227 Status: success, 228 }, 229 { 230 Name: "no", 231 Status: failure, 232 }, 233 }, 234 }), ShouldBeFalse) 235 shouldHaveStep(success, &buildbucketpb.Build{ 236 Status: failure, 237 Steps: []*buildbucketpb.Step{ 238 { 239 Name: "yes", 240 Status: failure, 241 }, 242 { 243 Name: "no", 244 Status: failure, 245 }, 246 }, 247 }, "yes") 248 So(s(success, &buildbucketpb.Build{ 249 Status: failure, 250 Steps: []*buildbucketpb.Step{ 251 { 252 Name: "yesno", 253 Status: failure, 254 }, 255 }, 256 }), ShouldBeFalse) 257 shouldHaveStep(success, &buildbucketpb.Build{ 258 Status: failure, 259 Steps: []*buildbucketpb.Step{ 260 { 261 Name: "yesno", 262 Status: failure, 263 }, 264 { 265 Name: "yes", 266 Status: failure, 267 }, 268 }, 269 }, "yes") 270 }) 271 272 Convey("OnSuccess deprecated", func() { 273 n.OnSuccess = true 274 275 So(s(success, successfulBuild), ShouldBeTrue) 276 So(s(success, failedBuild), ShouldBeFalse) 277 So(s(success, infraFailedBuild), ShouldBeFalse) 278 So(s(failure, successfulBuild), ShouldBeTrue) 279 So(s(failure, failedBuild), ShouldBeFalse) 280 So(s(failure, infraFailedBuild), ShouldBeFalse) 281 So(s(infraFailure, successfulBuild), ShouldBeTrue) 282 So(s(infraFailure, failedBuild), ShouldBeFalse) 283 So(s(infraFailure, infraFailedBuild), ShouldBeFalse) 284 So(s(unspecified, successfulBuild), ShouldBeTrue) 285 So(s(unspecified, failedBuild), ShouldBeFalse) 286 So(s(unspecified, infraFailedBuild), ShouldBeFalse) 287 }) 288 289 Convey("OnFailure deprecated", func() { 290 n.OnFailure = true 291 292 So(s(success, successfulBuild), ShouldBeFalse) 293 So(s(success, failedBuild), ShouldBeTrue) 294 So(s(success, infraFailedBuild), ShouldBeFalse) 295 So(s(failure, successfulBuild), ShouldBeFalse) 296 So(s(failure, failedBuild), ShouldBeTrue) 297 So(s(failure, infraFailedBuild), ShouldBeFalse) 298 So(s(infraFailure, successfulBuild), ShouldBeFalse) 299 So(s(infraFailure, failedBuild), ShouldBeTrue) 300 So(s(infraFailure, infraFailedBuild), ShouldBeFalse) 301 So(s(unspecified, successfulBuild), ShouldBeFalse) 302 So(s(unspecified, failedBuild), ShouldBeTrue) 303 So(s(unspecified, infraFailedBuild), ShouldBeFalse) 304 }) 305 306 Convey("OnChange deprecated", func() { 307 n.OnChange = true 308 309 So(s(success, successfulBuild), ShouldBeFalse) 310 So(s(success, failedBuild), ShouldBeTrue) 311 So(s(success, infraFailedBuild), ShouldBeTrue) 312 So(s(failure, successfulBuild), ShouldBeTrue) 313 So(s(failure, failedBuild), ShouldBeFalse) 314 So(s(failure, infraFailedBuild), ShouldBeTrue) 315 So(s(infraFailure, successfulBuild), ShouldBeTrue) 316 So(s(infraFailure, failedBuild), ShouldBeTrue) 317 So(s(infraFailure, infraFailedBuild), ShouldBeFalse) 318 So(s(unspecified, successfulBuild), ShouldBeFalse) 319 So(s(unspecified, failedBuild), ShouldBeFalse) 320 So(s(unspecified, infraFailedBuild), ShouldBeFalse) 321 }) 322 323 Convey("OnNewFailure deprecated", func() { 324 n.OnNewFailure = true 325 326 So(s(success, successfulBuild), ShouldBeFalse) 327 So(s(success, failedBuild), ShouldBeTrue) 328 So(s(success, infraFailedBuild), ShouldBeFalse) 329 So(s(failure, successfulBuild), ShouldBeFalse) 330 So(s(failure, failedBuild), ShouldBeFalse) 331 So(s(failure, infraFailedBuild), ShouldBeFalse) 332 So(s(infraFailure, successfulBuild), ShouldBeFalse) 333 So(s(infraFailure, failedBuild), ShouldBeTrue) 334 So(s(infraFailure, infraFailedBuild), ShouldBeFalse) 335 So(s(unspecified, successfulBuild), ShouldBeFalse) 336 So(s(unspecified, failedBuild), ShouldBeTrue) 337 So(s(unspecified, infraFailedBuild), ShouldBeFalse) 338 }) 339 }) 340 341 Convey("Notify", t, func() { 342 c := memory.Use(context.Background()) 343 c = common.SetAppIDForTest(c, "luci-notify") 344 c = caching.WithEmptyProcessCache(c) 345 c = clock.Set(c, testclock.New(testclock.TestRecentTimeUTC)) 346 c = gologger.StdConfig.Use(c) 347 c = logging.SetLevel(c, logging.Debug) 348 349 build := &Build{ 350 Build: buildbucketpb.Build{ 351 Id: 54, 352 Builder: &buildbucketpb.BuilderID{ 353 Project: "chromium", 354 Bucket: "ci", 355 Builder: "linux-rel", 356 }, 357 Status: buildbucketpb.Status_SUCCESS, 358 }, 359 } 360 361 // Put Project and EmailTemplate entities. 362 project := &config.Project{Name: "chromium", Revision: "deadbeef"} 363 templates := []*config.EmailTemplate{ 364 { 365 ProjectKey: datastore.KeyForObj(c, project), 366 Name: "default", 367 SubjectTextTemplate: "Build {{.Build.Id}} completed", 368 BodyHTMLTemplate: "Build {{.Build.Id}} completed with status {{.Build.Status}}", 369 }, 370 { 371 ProjectKey: datastore.KeyForObj(c, project), 372 Name: "non-default", 373 SubjectTextTemplate: "Build {{.Build.Id}} completed from non-default template", 374 BodyHTMLTemplate: "Build {{.Build.Id}} completed with status {{.Build.Status}} from non-default template", 375 }, 376 { 377 ProjectKey: datastore.KeyForObj(c, project), 378 Name: "with-steps", 379 SubjectTextTemplate: "Subject {{ stepNames .MatchingFailedSteps }}", 380 BodyHTMLTemplate: "Body {{ stepNames .MatchingFailedSteps }}", 381 }, 382 } 383 So(datastore.Put(c, project, templates), ShouldBeNil) 384 datastore.GetTestable(c).CatchupIndexes() 385 386 Convey("createEmailTasks", func() { 387 emailNotify := []EmailNotify{ 388 { 389 Email: "jane@example.com", 390 }, 391 { 392 Email: "john@example.com", 393 }, 394 { 395 Email: "don@example.com", 396 Template: "non-default", 397 }, 398 { 399 Email: "juan@example.com", 400 Template: "with-steps", 401 MatchingSteps: []*buildbucketpb.Step{ 402 { 403 Name: "step name", 404 }, 405 }, 406 }, 407 } 408 409 tasks, err := createEmailTasks(c, emailNotify, ¬ifypb.TemplateInput{ 410 BuildbucketHostname: "buildbucket.example.com", 411 Build: &build.Build, 412 OldStatus: buildbucketpb.Status_SUCCESS, 413 }) 414 So(err, ShouldBeNil) 415 So(tasks, ShouldHaveLength, 4) 416 417 t := tasks["54-default-jane@example.com"] 418 So(t.Recipients, ShouldResemble, []string{"jane@example.com"}) 419 So(t.Subject, ShouldEqual, "Build 54 completed") 420 So(decompress(t.BodyGzip), ShouldEqual, "Build 54 completed with status SUCCESS") 421 422 t = tasks["54-default-john@example.com"] 423 So(t.Recipients, ShouldResemble, []string{"john@example.com"}) 424 So(t.Subject, ShouldEqual, "Build 54 completed") 425 So(decompress(t.BodyGzip), ShouldEqual, "Build 54 completed with status SUCCESS") 426 427 t = tasks["54-non-default-don@example.com"] 428 So(t.Recipients, ShouldResemble, []string{"don@example.com"}) 429 So(t.Subject, ShouldEqual, "Build 54 completed from non-default template") 430 So(decompress(t.BodyGzip), ShouldEqual, "Build 54 completed with status SUCCESS from non-default template") 431 432 t = tasks["54-with-steps-juan@example.com"] 433 So(t.Recipients, ShouldResemble, []string{"juan@example.com"}) 434 So(t.Subject, ShouldEqual, `Subject "step name"`) 435 So(decompress(t.BodyGzip), ShouldEqual, "Body "step name"") 436 }) 437 438 Convey("createEmailTasks with dup notifies", func() { 439 emailNotify := []EmailNotify{ 440 { 441 Email: "jane@example.com", 442 }, 443 { 444 Email: "jane@example.com", 445 }, 446 } 447 tasks, err := createEmailTasks(c, emailNotify, ¬ifypb.TemplateInput{ 448 BuildbucketHostname: "buildbucket.example.com", 449 Build: &build.Build, 450 OldStatus: buildbucketpb.Status_SUCCESS, 451 }) 452 So(err, ShouldBeNil) 453 So(tasks, ShouldHaveLength, 1) 454 }) 455 }) 456 } 457 458 func TestComputeRecipients(t *testing.T) { 459 Convey("ComputeRecipients", t, func() { 460 c := memory.Use(context.Background()) 461 c = common.SetAppIDForTest(c, "luci-notify") 462 c = caching.WithEmptyProcessCache(c) 463 c = clock.Set(c, testclock.New(testclock.TestRecentTimeUTC)) 464 c = gologger.StdConfig.Use(c) 465 c = logging.SetLevel(c, logging.Debug) 466 467 oncallers := map[string]string{ 468 "https://rota-ng.appspot.com/legacy/sheriff.json": `{ 469 "updated_unix_timestamp": 1582692124, 470 "emails": [ 471 "sheriff1@google.com", 472 "sheriff2@google.com", 473 "sheriff3@google.com", 474 "sheriff4@google.com" 475 ] 476 }`, 477 "https://rota-ng.appspot.com/legacy/sheriff_ios.json": `{ 478 "updated_unix_timestamp": 1582692124, 479 "emails": [ 480 "sheriff5@google.com", 481 "sheriff6@google.com" 482 ] 483 }`, 484 "https://rotations.site/bad.json": "@!(*", 485 } 486 fetch := func(_ context.Context, url string) ([]byte, error) { 487 if s, e := oncallers[url]; e { 488 return []byte(s), nil 489 } else { 490 return []byte(""), errors.New("Key not present") 491 } 492 } 493 494 Convey("ComputeRecipients fetches all sheriffs", func() { 495 n := []ToNotify{ 496 { 497 Notification: ¬ifypb.Notification{ 498 Template: "sheriff_template", 499 Email: ¬ifypb.Notification_Email{ 500 RotationUrls: []string{"https://rota-ng.appspot.com/legacy/sheriff.json"}, 501 }, 502 }, 503 }, 504 { 505 Notification: ¬ifypb.Notification{ 506 Template: "sheriff_ios_template", 507 Email: ¬ifypb.Notification_Email{ 508 RotationUrls: []string{"https://rota-ng.appspot.com/legacy/sheriff_ios.json"}, 509 }, 510 }, 511 }, 512 } 513 emails := computeRecipientsInternal(c, n, nil, nil, fetch) 514 515 // ComputeRecipients is concurrent, hence we have no guarantees as to the order. 516 // So we sort here to ensure a consistent ordering. 517 sort.Slice(emails, func(i, j int) bool { 518 return emails[i].Email < emails[j].Email 519 }) 520 521 So(emails, ShouldResemble, []EmailNotify{ 522 { 523 Email: "sheriff1@google.com", 524 Template: "sheriff_template", 525 }, 526 { 527 Email: "sheriff2@google.com", 528 Template: "sheriff_template", 529 }, 530 { 531 Email: "sheriff3@google.com", 532 Template: "sheriff_template", 533 }, 534 { 535 Email: "sheriff4@google.com", 536 Template: "sheriff_template", 537 }, 538 { 539 Email: "sheriff5@google.com", 540 Template: "sheriff_ios_template", 541 }, 542 { 543 Email: "sheriff6@google.com", 544 Template: "sheriff_ios_template", 545 }, 546 }) 547 }) 548 549 Convey("ComputeRecipients drops missing", func() { 550 n := []ToNotify{ 551 { 552 Notification: ¬ifypb.Notification{ 553 Template: "sheriff_template", 554 Email: ¬ifypb.Notification_Email{ 555 RotationUrls: []string{"https://rota-ng.appspot.com/legacy/sheriff.json"}, 556 }, 557 }, 558 }, 559 { 560 Notification: ¬ifypb.Notification{ 561 Template: "what", 562 Email: ¬ifypb.Notification_Email{ 563 RotationUrls: []string{"https://somerandom.url/huh.json"}, 564 }, 565 }, 566 }, 567 } 568 emails := computeRecipientsInternal(c, n, nil, nil, fetch) 569 570 // ComputeRecipients is concurrent, hence we have no guarantees as to the order. 571 // So we sort here to ensure a consistent ordering. 572 sort.Slice(emails, func(i, j int) bool { 573 return emails[i].Email < emails[j].Email 574 }) 575 576 So(emails, ShouldResemble, []EmailNotify{ 577 { 578 Email: "sheriff1@google.com", 579 Template: "sheriff_template", 580 }, 581 { 582 Email: "sheriff2@google.com", 583 Template: "sheriff_template", 584 }, 585 { 586 Email: "sheriff3@google.com", 587 Template: "sheriff_template", 588 }, 589 { 590 Email: "sheriff4@google.com", 591 Template: "sheriff_template", 592 }, 593 }) 594 }) 595 596 Convey("ComputeRecipients includes static emails", func() { 597 n := []ToNotify{ 598 { 599 Notification: ¬ifypb.Notification{ 600 Template: "sheriff_template", 601 Email: ¬ifypb.Notification_Email{ 602 RotationUrls: []string{"https://rota-ng.appspot.com/legacy/sheriff.json"}, 603 }, 604 }, 605 }, 606 { 607 Notification: ¬ifypb.Notification{ 608 Template: "other_template", 609 Email: ¬ifypb.Notification_Email{ 610 Recipients: []string{"someone@google.com"}, 611 }, 612 }, 613 }, 614 } 615 emails := computeRecipientsInternal(c, n, nil, nil, fetch) 616 617 // ComputeRecipients is concurrent, hence we have no guarantees as to the order. 618 // So we sort here to ensure a consistent ordering. 619 sort.Slice(emails, func(i, j int) bool { 620 return emails[i].Email < emails[j].Email 621 }) 622 623 So(emails, ShouldResemble, []EmailNotify{ 624 { 625 Email: "sheriff1@google.com", 626 Template: "sheriff_template", 627 }, 628 { 629 Email: "sheriff2@google.com", 630 Template: "sheriff_template", 631 }, 632 { 633 Email: "sheriff3@google.com", 634 Template: "sheriff_template", 635 }, 636 { 637 Email: "sheriff4@google.com", 638 Template: "sheriff_template", 639 }, 640 { 641 Email: "someone@google.com", 642 Template: "other_template", 643 }, 644 }) 645 }) 646 647 Convey("ComputeRecipients drops bad JSON", func() { 648 n := []ToNotify{ 649 { 650 Notification: ¬ifypb.Notification{ 651 Template: "sheriff_template", 652 Email: ¬ifypb.Notification_Email{ 653 RotationUrls: []string{"https://rota-ng.appspot.com/legacy/sheriff.json"}, 654 }, 655 }, 656 }, 657 { 658 Notification: ¬ifypb.Notification{ 659 Template: "bad JSON", 660 Email: ¬ifypb.Notification_Email{ 661 RotationUrls: []string{"https://rotations.site/bad.json"}, 662 }, 663 }, 664 }, 665 } 666 emails := computeRecipientsInternal(c, n, nil, nil, fetch) 667 668 // ComputeRecipients is concurrent, hence we have no guarantees as to the order. 669 // So we sort here to ensure a consistent ordering. 670 sort.Slice(emails, func(i, j int) bool { 671 return emails[i].Email < emails[j].Email 672 }) 673 674 So(emails, ShouldResemble, []EmailNotify{ 675 { 676 Email: "sheriff1@google.com", 677 Template: "sheriff_template", 678 }, 679 { 680 Email: "sheriff2@google.com", 681 Template: "sheriff_template", 682 }, 683 { 684 Email: "sheriff3@google.com", 685 Template: "sheriff_template", 686 }, 687 { 688 Email: "sheriff4@google.com", 689 Template: "sheriff_template", 690 }, 691 }) 692 }) 693 694 Convey("ComputeRecipients propagates MatchingSteps", func() { 695 sheriffSteps := []*buildbucketpb.Step{ 696 { 697 Name: "sheriff step", 698 }, 699 } 700 otherSteps := []*buildbucketpb.Step{ 701 { 702 Name: "other step", 703 }, 704 } 705 n := []ToNotify{ 706 { 707 Notification: ¬ifypb.Notification{ 708 Template: "sheriff_template", 709 Email: ¬ifypb.Notification_Email{ 710 RotationUrls: []string{"https://rota-ng.appspot.com/legacy/sheriff.json"}, 711 }, 712 }, 713 MatchingSteps: sheriffSteps, 714 }, 715 { 716 Notification: ¬ifypb.Notification{ 717 Template: "other_template", 718 Email: ¬ifypb.Notification_Email{ 719 Recipients: []string{"someone@google.com"}, 720 }, 721 }, 722 MatchingSteps: otherSteps, 723 }, 724 } 725 emails := computeRecipientsInternal(c, n, nil, nil, fetch) 726 727 // ComputeRecipients is concurrent, hence we have no guarantees as to the order. 728 // So we sort here to ensure a consistent ordering. 729 sort.Slice(emails, func(i, j int) bool { 730 return emails[i].Email < emails[j].Email 731 }) 732 733 So(emails, ShouldResemble, []EmailNotify{ 734 { 735 Email: "sheriff1@google.com", 736 Template: "sheriff_template", 737 MatchingSteps: sheriffSteps, 738 }, 739 { 740 Email: "sheriff2@google.com", 741 Template: "sheriff_template", 742 MatchingSteps: sheriffSteps, 743 }, 744 { 745 Email: "sheriff3@google.com", 746 Template: "sheriff_template", 747 MatchingSteps: sheriffSteps, 748 }, 749 { 750 Email: "sheriff4@google.com", 751 Template: "sheriff_template", 752 MatchingSteps: sheriffSteps, 753 }, 754 { 755 Email: "someone@google.com", 756 Template: "other_template", 757 MatchingSteps: otherSteps, 758 }, 759 }) 760 }) 761 }) 762 } 763 764 func decompress(gzipped []byte) string { 765 r, err := gzip.NewReader(bytes.NewReader(gzipped)) 766 So(err, ShouldBeNil) 767 buf, err := io.ReadAll(r) 768 So(err, ShouldBeNil) 769 return string(buf) 770 }