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 := &notifypb.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, &notifypb.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, &notifypb.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: &notifypb.Notification{
   498  						Template: "sheriff_template",
   499  						Email: &notifypb.Notification_Email{
   500  							RotationUrls: []string{"https://rota-ng.appspot.com/legacy/sheriff.json"},
   501  						},
   502  					},
   503  				},
   504  				{
   505  					Notification: &notifypb.Notification{
   506  						Template: "sheriff_ios_template",
   507  						Email: &notifypb.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: &notifypb.Notification{
   553  						Template: "sheriff_template",
   554  						Email: &notifypb.Notification_Email{
   555  							RotationUrls: []string{"https://rota-ng.appspot.com/legacy/sheriff.json"},
   556  						},
   557  					},
   558  				},
   559  				{
   560  					Notification: &notifypb.Notification{
   561  						Template: "what",
   562  						Email: &notifypb.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: &notifypb.Notification{
   600  						Template: "sheriff_template",
   601  						Email: &notifypb.Notification_Email{
   602  							RotationUrls: []string{"https://rota-ng.appspot.com/legacy/sheriff.json"},
   603  						},
   604  					},
   605  				},
   606  				{
   607  					Notification: &notifypb.Notification{
   608  						Template: "other_template",
   609  						Email: &notifypb.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: &notifypb.Notification{
   651  						Template: "sheriff_template",
   652  						Email: &notifypb.Notification_Email{
   653  							RotationUrls: []string{"https://rota-ng.appspot.com/legacy/sheriff.json"},
   654  						},
   655  					},
   656  				},
   657  				{
   658  					Notification: &notifypb.Notification{
   659  						Template: "bad JSON",
   660  						Email: &notifypb.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: &notifypb.Notification{
   708  						Template: "sheriff_template",
   709  						Email: &notifypb.Notification_Email{
   710  							RotationUrls: []string{"https://rota-ng.appspot.com/legacy/sheriff.json"},
   711  						},
   712  					},
   713  					MatchingSteps: sheriffSteps,
   714  				},
   715  				{
   716  					Notification: &notifypb.Notification{
   717  						Template: "other_template",
   718  						Email: &notifypb.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  }