blob: 6c0f8bfa047828774a9ed954486612f4a8c0520d [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notify
import (
"bytes"
"context"
"fmt"
"sort"
"testing"
"time"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/ptypes"
"go.chromium.org/gae/service/datastore"
"go.chromium.org/gae/service/user"
"go.chromium.org/luci/appengine/gaetesting"
"go.chromium.org/luci/appengine/tq"
"go.chromium.org/luci/appengine/tq/tqtesting"
buildbucketpb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/clock/testclock"
"go.chromium.org/luci/common/logging/memlogger"
gitpb "go.chromium.org/luci/common/proto/git"
apicfg "go.chromium.org/luci/luci_notify/api/config"
"go.chromium.org/luci/luci_notify/config"
"go.chromium.org/luci/luci_notify/internal"
"go.chromium.org/luci/luci_notify/testutil"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func pubsubDummyBuild(builder string, status buildbucketpb.Status, creationTime time.Time, revision string, notifyEmails ...EmailNotify) *Build {
ret := &Build{
Build: buildbucketpb.Build{
Builder: &buildbucketpb.BuilderID{
Project: "chromium",
Bucket: "ci",
Builder: builder,
},
Status: status,
Input: &buildbucketpb.Build_Input{
GitilesCommit: &buildbucketpb.GitilesCommit{
Host: defaultGitilesHost,
Project: defaultGitilesProject,
Id: revision,
},
},
},
EmailNotify: notifyEmails,
}
ret.Build.CreateTime, _ = ptypes.TimestampProto(creationTime)
return ret
}
func TestExtractEmailNotifyValues(t *testing.T) {
Convey(`Test Environment for extractEmailNotifyValues`, t, func() {
extract := func(buildJSONPB string) ([]EmailNotify, error) {
build := &buildbucketpb.Build{}
err := jsonpb.UnmarshalString(buildJSONPB, build)
So(err, ShouldBeNil)
return extractEmailNotifyValues(build, "")
}
Convey(`empty`, func() {
results, err := extract(`{}`)
So(err, ShouldBeNil)
So(results, ShouldHaveLength, 0)
})
Convey(`populated without email_notify`, func() {
results, err := extract(`{
"input": {
"properties": {
"foo": 1
}
}
}`)
So(err, ShouldBeNil)
So(results, ShouldHaveLength, 0)
})
Convey(`single email_notify value in input`, func() {
results, err := extract(`{
"input": {
"properties": {
"email_notify": [{"email": "test@email"}]
}
}
}`)
So(err, ShouldBeNil)
So(results, ShouldResemble, []EmailNotify{
{
Email: "test@email",
Template: "",
},
})
})
Convey(`single email_notify value_with_template`, func() {
results, err := extract(`{
"input": {
"properties": {
"email_notify": [{
"email": "test@email",
"template": "test-template"
}]
}
}
}`)
So(err, ShouldBeNil)
So(results, ShouldResemble, []EmailNotify{
{
Email: "test@email",
Template: "test-template",
},
})
})
Convey(`multiple email_notify values`, func() {
results, err := extract(`{
"input": {
"properties": {
"email_notify": [
{"email": "test@email"},
{"email": "test2@email"}
]
}
}
}`)
So(err, ShouldBeNil)
So(results, ShouldResemble, []EmailNotify{
{
Email: "test@email",
Template: "",
},
{
Email: "test2@email",
Template: "",
},
})
})
Convey(`output takes precedence`, func() {
results, err := extract(`{
"input": {
"properties": {
"email_notify": [
{"email": "test@email"}
]
}
},
"output": {
"properties": {
"email_notify": [
{"email": "test2@email"}
]
}
}
}`)
So(err, ShouldBeNil)
So(results, ShouldResemble, []EmailNotify{
{
Email: "test2@email",
Template: "",
},
})
})
})
}
func TestHandleBuild(t *testing.T) {
t.Parallel()
Convey(`Test Environment for handleBuild`, t, func() {
cfgName := "basic"
cfg, err := testutil.LoadProjectConfig(cfgName)
So(err, ShouldBeNil)
c := gaetesting.TestingContextWithAppID("luci-notify-test")
c = clock.Set(c, testclock.New(time.Now()))
c = memlogger.Use(c)
user.GetTestable(c).Login("noreply@luci-notify-test.appspotmail.com", "", false)
// Add Project and Notifiers to datastore and update indexes.
project := &config.Project{Name: "chromium"}
builders := makeBuilders(c, "chromium", cfg)
So(datastore.Put(c, project, builders), ShouldBeNil)
datastore.GetTestable(c).CatchupIndexes()
oldTime := time.Date(2015, 2, 3, 12, 54, 3, 0, time.UTC)
newTime := time.Date(2015, 2, 3, 12, 58, 7, 0, time.UTC)
dispatcher, tqTestable := createMockTaskQueue(c)
assertTasks := func(build *Build, checkout Checkout, expectedRecipients ...EmailNotify) {
history := mockHistoryFunc(map[string][]*gitpb.Commit{
"chromium/src": testCommits,
"third_party/hello": revTestCommits,
})
// Test handleBuild.
err := handleBuild(c, dispatcher, build, mockCheckoutFunc(checkout), history)
So(err, ShouldBeNil)
// Verify tasks were scheduled.
var actualEmails []string
for _, t := range tqTestable.GetScheduledTasks() {
actualEmails = append(actualEmails, t.Payload.(*internal.EmailTask).Recipients...)
}
var expectedEmails []string
for _, r := range expectedRecipients {
expectedEmails = append(expectedEmails, r.Email)
}
sort.Strings(actualEmails)
sort.Strings(expectedEmails)
So(actualEmails, ShouldResemble, expectedEmails)
}
verifyBuilder := func(build *Build, revision string, checkout Checkout) {
datastore.GetTestable(c).CatchupIndexes()
id := getBuilderID(build)
builder := config.Builder{
ProjectKey: datastore.KeyForObj(c, project),
ID: id,
}
So(datastore.Get(c, &builder), ShouldBeNil)
So(builder.Revision, ShouldResemble, revision)
So(builder.Status, ShouldEqual, build.Status)
expectCommits := checkout.ToGitilesCommits()
So(&builder.GitilesCommits, ShouldResembleProto, &expectCommits)
}
propEmail := EmailNotify{
Email: "property@google.com",
}
successEmail := EmailNotify{
Email: "test-example-success@google.com",
}
failEmail := EmailNotify{
Email: "test-example-failure@google.com",
}
changeEmail := EmailNotify{
Email: "test-example-change@google.com",
}
commit1Email := EmailNotify{
Email: commitEmail1,
}
commit2Email := EmailNotify{
Email: commitEmail2,
}
grepLog := func(substring string) {
buf := new(bytes.Buffer)
_, err := memlogger.Dump(c, buf)
So(err, ShouldBeNil)
So(buf.String(), ShouldContainSubstring, substring)
}
Convey(`no config`, func() {
build := pubsubDummyBuild("not-a-builder", buildbucketpb.Status_FAILURE, oldTime, rev1)
assertTasks(build, nil)
grepLog("No builder")
})
Convey(`no config w/property`, func() {
build := pubsubDummyBuild("not-a-builder", buildbucketpb.Status_FAILURE, oldTime, rev1, propEmail)
assertTasks(build, nil, propEmail)
})
Convey(`no repository in-order`, func() {
build := pubsubDummyBuild("test-builder-no-repo", buildbucketpb.Status_FAILURE, oldTime, rev1)
assertTasks(build, nil, failEmail)
})
Convey(`no repository out-of-order`, func() {
build := pubsubDummyBuild("test-builder-no-repo", buildbucketpb.Status_FAILURE, newTime, rev1)
assertTasks(build, nil, failEmail)
newBuild := pubsubDummyBuild("test-builder-no-repo", buildbucketpb.Status_SUCCESS, oldTime, rev2)
assertTasks(newBuild, nil, failEmail, successEmail)
grepLog("old time")
})
Convey(`no revision`, func() {
build := &Build{
Build: buildbucketpb.Build{
Builder: &buildbucketpb.BuilderID{
Project: "chromium",
Bucket: "ci",
Builder: "test-builder-1",
},
Status: buildbucketpb.Status_SUCCESS,
},
}
assertTasks(build, nil)
grepLog("revision")
})
Convey(`init builder`, func() {
build := pubsubDummyBuild("test-builder-1", buildbucketpb.Status_FAILURE, oldTime, rev1)
assertTasks(build, nil, failEmail)
verifyBuilder(build, rev1, nil)
})
Convey(`init builder w/property`, func() {
build := pubsubDummyBuild("test-builder-1", buildbucketpb.Status_FAILURE, oldTime, rev1, propEmail)
assertTasks(build, nil, failEmail, propEmail)
verifyBuilder(build, rev1, nil)
})
Convey(`repository mismatch`, func() {
build := pubsubDummyBuild("test-builder-1", buildbucketpb.Status_FAILURE, oldTime, rev1, propEmail)
assertTasks(build, nil, failEmail, propEmail)
verifyBuilder(build, rev1, nil)
newBuild := &Build{
Build: buildbucketpb.Build{
Builder: &buildbucketpb.BuilderID{
Project: "chromium",
Bucket: "ci",
Builder: "test-builder-1",
},
Status: buildbucketpb.Status_SUCCESS,
Input: &buildbucketpb.Build_Input{
GitilesCommit: &buildbucketpb.GitilesCommit{
Host: defaultGitilesHost,
Project: "example/src",
Id: rev2,
},
},
},
}
assertTasks(newBuild, nil, failEmail, propEmail)
grepLog("triggered by commit")
})
Convey(`out-of-order revision`, func() {
build := pubsubDummyBuild("test-builder-2", buildbucketpb.Status_SUCCESS, oldTime, rev2)
assertTasks(build, nil, successEmail)
verifyBuilder(build, rev2, nil)
oldRevBuild := pubsubDummyBuild("test-builder-2", buildbucketpb.Status_FAILURE, newTime, rev1)
assertTasks(oldRevBuild, nil, successEmail, failEmail) //no changeEmail
grepLog("old commit")
})
Convey(`revision update`, func() {
build := pubsubDummyBuild("test-builder-3", buildbucketpb.Status_SUCCESS, oldTime, rev1)
assertTasks(build, nil, successEmail)
verifyBuilder(build, rev1, nil)
newBuild := pubsubDummyBuild("test-builder-3", buildbucketpb.Status_FAILURE, newTime, rev2)
newBuild.Id++
assertTasks(newBuild, nil, successEmail, failEmail, changeEmail)
verifyBuilder(newBuild, rev2, nil)
})
Convey(`revision update w/property`, func() {
build := pubsubDummyBuild("test-builder-3", buildbucketpb.Status_SUCCESS, oldTime, rev1, propEmail)
assertTasks(build, nil, successEmail, propEmail)
verifyBuilder(build, rev1, nil)
newBuild := pubsubDummyBuild("test-builder-3", buildbucketpb.Status_FAILURE, newTime, rev2, propEmail)
newBuild.Id++
assertTasks(newBuild, nil, successEmail, propEmail, failEmail, changeEmail, propEmail)
verifyBuilder(newBuild, rev2, nil)
})
Convey(`out-of-order creation time`, func() {
build := pubsubDummyBuild("test-builder-4", buildbucketpb.Status_SUCCESS, newTime, rev1)
build.Id = 2
assertTasks(build, nil, successEmail)
verifyBuilder(build, rev1, nil)
oldBuild := pubsubDummyBuild("test-builder-4", buildbucketpb.Status_FAILURE, oldTime, rev1)
oldBuild.Id = 1
assertTasks(oldBuild, nil, successEmail, failEmail) // no changeEmail
grepLog("old time")
})
checkoutOld := Checkout{
"https://chromium.googlesource.com/chromium/src": rev1,
"https://chromium.googlesource.com/third_party/hello": rev1,
}
checkoutNew := Checkout{
"https://chromium.googlesource.com/chromium/src": rev2,
"https://chromium.googlesource.com/third_party/hello": rev2,
}
testBlamelistConfig := func(builderID string, emails ...EmailNotify) {
build := pubsubDummyBuild(builderID, buildbucketpb.Status_SUCCESS, oldTime, rev1)
assertTasks(build, checkoutOld)
verifyBuilder(build, rev1, checkoutOld)
newBuild := pubsubDummyBuild(builderID, buildbucketpb.Status_FAILURE, newTime, rev2)
newBuild.Id++
assertTasks(newBuild, checkoutNew, emails...)
verifyBuilder(newBuild, rev2, checkoutNew)
}
Convey(`blamelist no whitelist`, func() {
testBlamelistConfig("test-builder-blamelist-1", changeEmail, commit2Email)
})
Convey(`blamelist with whitelist`, func() {
testBlamelistConfig("test-builder-blamelist-2", changeEmail, commit1Email)
})
Convey(`blamelist against last non-empty checkout`, func() {
build := pubsubDummyBuild("test-builder-blamelist-2", buildbucketpb.Status_SUCCESS, oldTime, rev1)
assertTasks(build, checkoutOld)
verifyBuilder(build, rev1, checkoutOld)
newBuild := pubsubDummyBuild("test-builder-blamelist-2", buildbucketpb.Status_FAILURE, newTime, rev2)
newBuild.Id++
assertTasks(newBuild, nil, changeEmail)
verifyBuilder(newBuild, rev2, checkoutOld)
newestTime := time.Date(2017, 2, 3, 12, 59, 9, 0, time.UTC)
newestBuild := pubsubDummyBuild("test-builder-blamelist-2", buildbucketpb.Status_SUCCESS, newestTime, rev2)
newestBuild.Id++
assertTasks(newestBuild, checkoutNew, changeEmail, commit1Email)
verifyBuilder(newestBuild, rev2, checkoutNew)
})
Convey(`blamelist mixed`, func() {
testBlamelistConfig("test-builder-blamelist-3", commit1Email, commit2Email)
})
Convey(`blamelist duplicate`, func() {
testBlamelistConfig("test-builder-blamelist-4", commit2Email, commit2Email, commit2Email)
})
})
}
func makeBuilders(c context.Context, projectID string, cfg *apicfg.ProjectConfig) []*config.Builder {
var builders []*config.Builder
parentKey := datastore.MakeKey(c, "Project", projectID)
for _, cfgNotifier := range cfg.Notifiers {
for _, cfgBuilder := range cfgNotifier.Builders {
builders = append(builders, &config.Builder{
ProjectKey: parentKey,
ID: fmt.Sprintf("%s/%s", cfgBuilder.Bucket, cfgBuilder.Name),
Repository: cfgBuilder.Repository,
Notifications: apicfg.Notifications{
Notifications: cfgNotifier.Notifications,
},
})
}
}
return builders
}
func mockCheckoutFunc(c Checkout) CheckoutFunc {
return func(_ context.Context, _ *Build) (Checkout, error) {
return c, nil
}
}
func createMockTaskQueue(c context.Context) (*tq.Dispatcher, tqtesting.Testable) {
dispatcher := &tq.Dispatcher{}
InitDispatcher(dispatcher)
taskqueue := tqtesting.GetTestable(c, dispatcher)
taskqueue.CreateQueues()
return dispatcher, taskqueue
}