blob: 49a649878fd2bb651878d54327b1a2df68a79088 [file] [log] [blame]
// Copyright 2020 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 backend
import (
"context"
"fmt"
"testing"
"github.com/golang/mock/gomock"
. "github.com/smartystreets/goconvey/convey"
"go.chromium.org/luci/appengine/gaetesting"
"go.chromium.org/luci/auth/identity"
buildbucketpb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/buildbucket/protoutil"
gitpb "go.chromium.org/luci/common/proto/git"
"go.chromium.org/luci/gae/service/datastore"
milopb "go.chromium.org/luci/milo/api/service/v1"
"go.chromium.org/luci/milo/common"
"go.chromium.org/luci/milo/common/model"
"go.chromium.org/luci/milo/git"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/auth/authtest"
)
func TestPrepareQueryBlamelistRequest(t *testing.T) {
t.Parallel()
Convey(`TestPrepareQueryBlamelistRequest`, t, func() {
Convey(`extract commit ID correctly`, func() {
Convey(`when there's no page token`, func() {
startRev, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{
GitilesCommit: &buildbucketpb.GitilesCommit{
Host: "host",
Project: "project/src",
Id: "commit-id",
Ref: "commit-ref",
},
Builder: &buildbucketpb.BuilderID{
Project: "project",
Bucket: "bucket",
Builder: "builder",
},
})
So(err, ShouldBeNil)
// commit ID should take priority.
So(startRev, ShouldEqual, "commit-id")
})
Convey(`when there's no page token or commit ID`, func() {
startRev, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{
GitilesCommit: &buildbucketpb.GitilesCommit{
Host: "host",
Project: "project/src",
Ref: "commit-ref",
},
Builder: &buildbucketpb.BuilderID{
Project: "project",
Bucket: "bucket",
Builder: "builder",
},
})
So(err, ShouldBeNil)
So(startRev, ShouldEqual, "commit-ref")
})
Convey(`when there's a page token`, func() {
startRev, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{
GitilesCommit: &buildbucketpb.GitilesCommit{
Host: "host",
Project: "project/src",
Id: "commit-id-1",
},
Builder: &buildbucketpb.BuilderID{
Project: "project",
Bucket: "bucket",
Builder: "builder",
},
})
So(err, ShouldBeNil)
So(startRev, ShouldEqual, "commit-id-1")
pageToken, err := serializeQueryBlamelistPageToken(&milopb.QueryBlamelistPageToken{
NextCommitId: "commit-id-2",
})
So(err, ShouldBeNil)
nextCommitRev, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{
GitilesCommit: &buildbucketpb.GitilesCommit{
Host: "host",
Project: "project/src",
Id: "commit-id-1",
},
Builder: &buildbucketpb.BuilderID{
Project: "project",
Bucket: "bucket",
Builder: "builder",
},
PageToken: pageToken,
})
So(err, ShouldBeNil)
So(nextCommitRev, ShouldEqual, "commit-id-2")
})
})
Convey(`reject the page token when the page token is invalid`, func() {
_, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{
GitilesCommit: &buildbucketpb.GitilesCommit{
Host: "host",
Project: "project/src",
Id: "commit-id-1",
},
Builder: &buildbucketpb.BuilderID{
Project: "project",
Bucket: "bucket",
Builder: "builder",
},
PageToken: "abc",
})
So(err, ShouldNotBeNil)
})
})
}
func TestQueryBlamelist(t *testing.T) {
t.Parallel()
Convey(`TestQueryBlamelist`, t, func() {
c := gaetesting.TestingContextWithAppID("luci-milo-dev")
datastore.GetTestable(c).Consistent(true)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
gitMock := git.NewMockClient(ctrl)
srv := &MiloInternalService{
GetGitClient: func(c context.Context) (git.Client, error) {
return gitMock, nil
},
}
c = auth.WithState(c, &authtest.FakeState{Identity: "user"})
builder1 := &buildbucketpb.BuilderID{
Project: "fake_project",
Bucket: "fake_bucket",
Builder: "fake_builder1",
}
builder2 := &buildbucketpb.BuilderID{
Project: "fake_project",
Bucket: "fake_bucket",
Builder: "fake_builder2",
}
commits := []*gitpb.Commit{
{Id: "commit8"},
{Id: "commit7"},
{Id: "commit6"},
{Id: "commit5"},
{Id: "commit4"},
{Id: "commit3"},
{Id: "commit2"},
{Id: "commit1"},
}
createFakeBuild := func(builder *buildbucketpb.BuilderID, buildNum int, commitID string, additionalBlamelistPins ...string) *model.BuildSummary {
builderID := common.LegacyBuilderIDString(builder)
buildID := fmt.Sprintf("%s/%d", builderID, buildNum)
buildSet := protoutil.GitilesBuildSet(&buildbucketpb.GitilesCommit{
Host: "fake_gitiles_host",
Project: "fake_gitiles_project",
Id: commitID,
})
blamelistPins := append(additionalBlamelistPins, buildSet)
return &model.BuildSummary{
BuildKey: datastore.MakeKey(c, "build", buildID),
ProjectID: builder.Project,
BuilderID: builderID,
BuildID: buildID,
BuildSet: []string{buildSet},
BlamelistPins: blamelistPins,
}
}
builds := []*model.BuildSummary{
createFakeBuild(builder1, 1, "commit8", protoutil.GitilesBuildSet(&buildbucketpb.GitilesCommit{
Host: "fake_gitiles_host",
Project: "other_fake_gitiles_project",
Id: "commit3",
})),
createFakeBuild(builder2, 1, "commit7"),
createFakeBuild(builder1, 2, "commit5", protoutil.GitilesBuildSet(&buildbucketpb.GitilesCommit{
Host: "fake_gitiles_host",
Project: "other_fake_gitiles_project",
Id: "commit1",
})),
createFakeBuild(builder1, 3, "commit3"),
}
err := datastore.Put(c, builds)
So(err, ShouldBeNil)
err = datastore.Put(c, &common.Project{
ID: "fake_project",
ACL: common.ACL{Identities: []identity.Identity{"user"}},
LogoURL: "https://logo.com",
})
So(err, ShouldBeNil)
Convey(`reject users with no access`, func() {
req := &milopb.QueryBlamelistRequest{
GitilesCommit: &buildbucketpb.GitilesCommit{
Host: "fake_gitiles_host",
Project: "fake_gitiles_project",
Id: "commit1",
},
Builder: &buildbucketpb.BuilderID{
Project: "secret_fake_project",
Bucket: "secret_fake_bucket",
Builder: "secret_fake_builder",
},
PageSize: 2000,
}
_, err := srv.QueryBlamelist(c, req)
So(err, ShouldNotBeNil)
})
Convey(`coerce page_size`, func() {
Convey(`to 1000 if it's greater than 1000`, func() {
req := &milopb.QueryBlamelistRequest{
GitilesCommit: &buildbucketpb.GitilesCommit{
Host: "fake_gitiles_host",
Project: "fake_gitiles_project",
Id: "commit1",
},
Builder: builder1,
PageSize: 2000,
}
gitMock.
EXPECT().
Log(gomock.Any(), req.GitilesCommit.Host, req.GitilesCommit.Project, req.GitilesCommit.Id, &git.LogOptions{Limit: 1001, WithFiles: true}).
Return(commits, nil)
_, err := srv.QueryBlamelist(c, req)
So(err, ShouldBeNil)
})
Convey(`to 100 if it's not set`, func() {
req := &milopb.QueryBlamelistRequest{
GitilesCommit: &buildbucketpb.GitilesCommit{
Host: "fake_gitiles_host",
Project: "fake_gitiles_project",
Id: "commit8",
},
Builder: builder1,
}
gitMock.
EXPECT().
Log(gomock.Any(), req.GitilesCommit.Host, req.GitilesCommit.Project, req.GitilesCommit.Id, &git.LogOptions{Limit: 101, WithFiles: true}).
Return(commits, nil)
_, err := srv.QueryBlamelist(c, req)
So(err, ShouldBeNil)
})
})
Convey(`get all the commits in the blamelist`, func() {
Convey(`in one page`, func() {
Convey(`when we found the previous build`, func() {
req := &milopb.QueryBlamelistRequest{
GitilesCommit: &buildbucketpb.GitilesCommit{
Host: "fake_gitiles_host",
Project: "fake_gitiles_project",
Id: "commit8",
},
Builder: builder1,
}
gitMock.
EXPECT().
Log(gomock.Any(), req.GitilesCommit.Host, req.GitilesCommit.Project, req.GitilesCommit.Id, &git.LogOptions{Limit: 101, WithFiles: true}).
Return(commits, nil)
res, err := srv.QueryBlamelist(c, req)
So(err, ShouldBeNil)
So(res.Commits, ShouldHaveLength, 3)
So(res.Commits[0].Id, ShouldEqual, "commit8")
So(res.Commits[1].Id, ShouldEqual, "commit7")
So(res.Commits[2].Id, ShouldEqual, "commit6")
So(res.PrecedingCommit.Id, ShouldEqual, "commit5")
})
Convey(`when there's no previous build`, func() {
req := &milopb.QueryBlamelistRequest{
GitilesCommit: &buildbucketpb.GitilesCommit{
Host: "fake_gitiles_host",
Project: "fake_gitiles_project",
Id: "commit3",
},
Builder: builder1,
}
gitMock.
EXPECT().
Log(gomock.Any(), req.GitilesCommit.Host, req.GitilesCommit.Project, req.GitilesCommit.Id, &git.LogOptions{Limit: 101, WithFiles: true}).
Return(commits[5:], nil)
res, err := srv.QueryBlamelist(c, req)
So(err, ShouldBeNil)
So(res.Commits, ShouldHaveLength, 3)
So(res.Commits[0].Id, ShouldEqual, "commit3")
So(res.Commits[1].Id, ShouldEqual, "commit2")
So(res.Commits[2].Id, ShouldEqual, "commit1")
So(res.PrecedingCommit, ShouldBeZeroValue)
})
})
Convey(`in multiple pages`, func() {
Convey(`when we found the previous build`, func() {
// Query the first page.
req := &milopb.QueryBlamelistRequest{
GitilesCommit: &buildbucketpb.GitilesCommit{
Host: "fake_gitiles_host",
Project: "fake_gitiles_project",
Id: "commit8",
},
Builder: builder1,
PageSize: 2,
}
gitMock.
EXPECT().
Log(gomock.Any(), req.GitilesCommit.Host, req.GitilesCommit.Project, req.GitilesCommit.Id, &git.LogOptions{Limit: 3, WithFiles: true}).
Return(commits[0:3], nil)
res, err := srv.QueryBlamelist(c, req)
So(err, ShouldBeNil)
So(res.Commits, ShouldHaveLength, 2)
So(res.Commits[0].Id, ShouldEqual, "commit8")
So(res.Commits[1].Id, ShouldEqual, "commit7")
So(res.NextPageToken, ShouldNotBeZeroValue)
So(res.PrecedingCommit.Id, ShouldEqual, "commit6")
// Query the second page.
req = &milopb.QueryBlamelistRequest{
GitilesCommit: &buildbucketpb.GitilesCommit{
Host: "fake_gitiles_host",
Project: "fake_gitiles_project",
Id: "commit8",
},
Builder: builder1,
PageSize: 2,
PageToken: res.NextPageToken,
}
gitMock.
EXPECT().
Log(gomock.Any(), req.GitilesCommit.Host, req.GitilesCommit.Project, "commit6", &git.LogOptions{Limit: 3, WithFiles: true}).
Return(commits[2:5], nil)
res, err = srv.QueryBlamelist(c, req)
So(err, ShouldBeNil)
So(res.Commits, ShouldHaveLength, 1)
So(res.Commits[0].Id, ShouldEqual, "commit6")
So(res.NextPageToken, ShouldBeZeroValue)
So(res.PrecedingCommit.Id, ShouldEqual, "commit5")
})
Convey(`when there's no previous build`, func() {
// Query the first page.
req := &milopb.QueryBlamelistRequest{
GitilesCommit: &buildbucketpb.GitilesCommit{
Host: "fake_gitiles_host",
Project: "fake_gitiles_project",
Id: "commit3",
},
Builder: builder1,
PageSize: 2,
}
gitMock.
EXPECT().
Log(gomock.Any(), req.GitilesCommit.Host, req.GitilesCommit.Project, req.GitilesCommit.Id, &git.LogOptions{Limit: 3, WithFiles: true}).
Return(commits[5:8], nil)
res, err := srv.QueryBlamelist(c, req)
So(err, ShouldBeNil)
So(res.Commits, ShouldHaveLength, 2)
So(res.Commits[0].Id, ShouldEqual, "commit3")
So(res.Commits[1].Id, ShouldEqual, "commit2")
So(res.NextPageToken, ShouldNotBeZeroValue)
So(res.PrecedingCommit.Id, ShouldEqual, "commit1")
// Query the second page.
req = &milopb.QueryBlamelistRequest{
GitilesCommit: &buildbucketpb.GitilesCommit{
Host: "fake_gitiles_host",
Project: "fake_gitiles_project",
Id: "commit3",
},
Builder: builder1,
PageSize: 2,
PageToken: res.NextPageToken,
}
gitMock.
EXPECT().
Log(gomock.Any(), req.GitilesCommit.Host, req.GitilesCommit.Project, "commit1", &git.LogOptions{Limit: 3, WithFiles: true}).
Return(commits[7:], nil)
res, err = srv.QueryBlamelist(c, req)
So(err, ShouldBeNil)
So(res.Commits, ShouldHaveLength, 1)
So(res.Commits[0].Id, ShouldEqual, "commit1")
So(res.NextPageToken, ShouldBeZeroValue)
So(res.PrecedingCommit, ShouldBeZeroValue)
})
})
})
Convey(`get blamelist of other projects`, func() {
req := &milopb.QueryBlamelistRequest{
GitilesCommit: &buildbucketpb.GitilesCommit{
Host: "fake_gitiles_host",
Project: "other_fake_gitiles_project",
Id: "commit3",
},
Builder: builder1,
MultiProjectSupport: true,
}
gitMock.
EXPECT().
Log(gomock.Any(), req.GitilesCommit.Host, req.GitilesCommit.Project, req.GitilesCommit.Id, &git.LogOptions{Limit: 101, WithFiles: true}).
Return(commits[5:], nil)
res, err := srv.QueryBlamelist(c, req)
So(err, ShouldBeNil)
So(res.Commits, ShouldHaveLength, 2)
So(res.Commits[0].Id, ShouldEqual, "commit3")
So(res.Commits[1].Id, ShouldEqual, "commit2")
So(res.PrecedingCommit.Id, ShouldEqual, "commit1")
})
})
}