| // Copyright 2018 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 git |
| |
| import ( |
| "context" |
| "encoding/hex" |
| "testing" |
| |
| "github.com/golang/mock/gomock" |
| |
| "go.chromium.org/luci/auth/identity" |
| "go.chromium.org/luci/common/proto" |
| gitpb "go.chromium.org/luci/common/proto/git" |
| gitilespb "go.chromium.org/luci/common/proto/gitiles" |
| "go.chromium.org/luci/common/proto/gitiles/mock_gitiles" |
| "go.chromium.org/luci/gae/impl/memory" |
| "go.chromium.org/luci/gae/service/memcache" |
| "go.chromium.org/luci/milo/api/config" |
| "go.chromium.org/luci/milo/git/gitacls" |
| "go.chromium.org/luci/server/auth" |
| "go.chromium.org/luci/server/auth/authtest" |
| |
| . "github.com/smartystreets/goconvey/convey" |
| . "go.chromium.org/luci/common/testing/assertions" |
| ) |
| |
| func TestLog(t *testing.T) { |
| t.Parallel() |
| |
| Convey("Log", t, func() { |
| c := memory.Use(context.Background()) |
| |
| ctl := gomock.NewController(t) |
| defer ctl.Finish() |
| gitilesMock := mock_gitiles.NewMockGitilesClient(ctl) |
| |
| host := "limited.googlesource.com" |
| acls, err := gitacls.FromConfig(c, []*config.Settings_SourceAcls{ |
| {Hosts: []string{host}, Readers: []string{"allowed@example.com"}}, |
| }) |
| So(err, ShouldBeNil) |
| impl := implementation{mockGitiles: gitilesMock, acls: acls} |
| c = Use(c, &impl) |
| cAllowed := auth.WithState(c, &authtest.FakeState{Identity: "user:allowed@example.com"}) |
| cDenied := auth.WithState(c, &authtest.FakeState{Identity: identity.AnonymousIdentity}) |
| |
| fakeCommits := make([]*gitpb.Commit, 255) |
| commitID := make([]byte, 20) |
| commitID[0] = 255 |
| for i := range fakeCommits { |
| fakeCommits[i] = &gitpb.Commit{Id: hex.EncodeToString(commitID)} |
| if i > 0 { |
| fakeCommits[i-1].Parents = []string{fakeCommits[i].Id} |
| } |
| |
| commitID[0]-- |
| } |
| |
| Convey("cold cache", func() { |
| Convey("ACLs respected", func() { |
| _, err := impl.Log(cDenied, host, "project", "refs/heads/main", &LogOptions{Limit: 50}) |
| So(err.Error(), ShouldContainSubstring, "not logged in") |
| }) |
| |
| req := &gitilespb.LogRequest{ |
| Project: "project", |
| Committish: "refs/heads/main", |
| PageSize: 100, |
| } |
| res := &gitilespb.LogResponse{ |
| Log: fakeCommits[1:101], // return 100 commits |
| } |
| gitilesMock.EXPECT().Log(gomock.Any(), proto.MatcherEqual(req)).Return(res, nil) |
| |
| commits, err := impl.Log(cAllowed, host, "project", "refs/heads/main", &LogOptions{Limit: 100}) |
| So(err, ShouldBeNil) |
| So(commits, ShouldResemble, res.Log) |
| |
| // Now that we have something in cache, call Log with cached commits. |
| // gitiles.Log was already called maximum number of times, which is 1, |
| // so another call with cause a test failure. |
| |
| Convey("ACLs respected even with cache", func() { |
| _, err := impl.Log(cDenied, host, "project", "refs/heads/main", &LogOptions{Limit: 50}) |
| So(err.Error(), ShouldContainSubstring, "not logged in") |
| }) |
| |
| Convey("with exactly one last commit not in cache", func() { |
| req2 := &gitilespb.LogRequest{ |
| Project: "project", |
| Committish: fakeCommits[100].Id, |
| PageSize: 100, // we always fetch 100 |
| } |
| res2 := &gitilespb.LogResponse{ |
| Log: fakeCommits[100:200], |
| } |
| gitilesMock.EXPECT().Log(gomock.Any(), proto.MatcherEqual(req2)).Return(res2, nil) |
| commits, err := impl.Log(cAllowed, host, "project", "refs/heads/main", &LogOptions{Limit: 101}) |
| So(err, ShouldBeNil) |
| So(commits, ShouldResembleProto, fakeCommits[1:102]) |
| }) |
| |
| Convey("with exactly the proceeding commit not in cache", func() { |
| req2 := &gitilespb.LogRequest{ |
| Project: "project", |
| Committish: fakeCommits[51].Id, |
| PageSize: 100, // we always fetch 100 |
| } |
| res2 := &gitilespb.LogResponse{ |
| Log: fakeCommits[51:150], |
| } |
| gitilesMock.EXPECT().Log(gomock.Any(), proto.MatcherEqual(req2)).Return(res2, nil).Times(0) |
| commits, err := impl.Log(cAllowed, host, "project", fakeCommits[51].Id, &LogOptions{Limit: 50}) |
| So(err, ShouldBeNil) |
| So(commits, ShouldResembleProto, fakeCommits[51:101]) |
| }) |
| |
| Convey("with ref in cache", func() { |
| commits, err := impl.Log(cAllowed, host, "project", "refs/heads/main", &LogOptions{Limit: 50}) |
| So(err, ShouldBeNil) |
| So(commits, ShouldResembleProto, res.Log[:50]) |
| }) |
| |
| Convey("with top commit in cache", func() { |
| commits, err := impl.Log(cAllowed, host, "project", fakeCommits[1].Id, &LogOptions{Limit: 50}) |
| So(err, ShouldBeNil) |
| So(commits, ShouldResembleProto, res.Log[:50]) |
| }) |
| |
| Convey("with ancestor commit in cache", func() { |
| commits, err := impl.Log(cAllowed, host, "project", fakeCommits[2].Id, &LogOptions{Limit: 50}) |
| So(err, ShouldBeNil) |
| So(commits, ShouldResembleProto, res.Log[1:51]) |
| }) |
| |
| Convey("with second ancestor commit in cache", func() { |
| commits, err := impl.Log(cAllowed, host, "project", fakeCommits[3].Id, &LogOptions{Limit: 50}) |
| So(err, ShouldBeNil) |
| So(commits, ShouldResembleProto, res.Log[2:52]) |
| }) |
| |
| Convey("min is honored", func() { |
| req2 := &gitilespb.LogRequest{ |
| Project: "project", |
| Committish: fakeCommits[2].Id, |
| PageSize: 100, |
| } |
| res2 := &gitilespb.LogResponse{ |
| Log: fakeCommits[2:102], |
| } |
| gitilesMock.EXPECT().Log(gomock.Any(), proto.MatcherEqual(req2)).Return(res2, nil) |
| |
| commits, err := impl.Log(cAllowed, host, "project", fakeCommits[2].Id, &LogOptions{Limit: 100}) |
| So(err, ShouldBeNil) |
| So(commits, ShouldHaveLength, 100) |
| So(commits, ShouldResembleProto, res2.Log) |
| }) |
| |
| Convey("request of item not in cache", func() { |
| req2 := &gitilespb.LogRequest{ |
| Project: "project", |
| Committish: fakeCommits[101].Id, |
| PageSize: 100, |
| } |
| res2 := &gitilespb.LogResponse{ |
| Log: fakeCommits[101:201], |
| } |
| gitilesMock.EXPECT().Log(gomock.Any(), proto.MatcherEqual(req2)).Return(res2, nil) |
| commits, err := impl.Log(cAllowed, host, "project", fakeCommits[101].Id, &LogOptions{Limit: 50}) |
| So(err, ShouldBeNil) |
| So(commits, ShouldHaveLength, 50) |
| So(commits, ShouldResemble, res2.Log[:50]) |
| }) |
| |
| Convey("do not update cache entries that have more info", func() { |
| refCache := (&logReq{ |
| host: host, |
| project: "project", |
| }).mkCache(c, "refs/heads/main") |
| err = memcache.Delete(c, refCache.Key()) |
| So(err, ShouldBeNil) |
| |
| req2 := &gitilespb.LogRequest{ |
| Project: "project", |
| Committish: "refs/heads/main", |
| PageSize: 100, |
| } |
| res2 := &gitilespb.LogResponse{ |
| Log: fakeCommits[:100], |
| } |
| gitilesMock.EXPECT().Log(gomock.Any(), proto.MatcherEqual(req2)).Return(res2, nil) |
| commits, err := impl.Log(cAllowed, host, "project", "refs/heads/main", &LogOptions{Limit: 50}) |
| So(err, ShouldBeNil) |
| So(commits, ShouldResemble, res2.Log[:50]) |
| }) |
| }) |
| Convey("paging", func() { |
| req1 := &gitilespb.LogRequest{ |
| Project: "project", |
| Committish: "refs/heads/main", |
| PageSize: 100, |
| } |
| res1 := &gitilespb.LogResponse{ |
| Log: fakeCommits[:100], |
| } |
| req2 := &gitilespb.LogRequest{ |
| Project: "project", |
| Committish: res1.Log[len(res1.Log)-1].Id, |
| PageSize: 100, // we always fetch 100 |
| } |
| res2 := &gitilespb.LogResponse{ |
| Log: fakeCommits[99:199], |
| } |
| gitilesMock.EXPECT().Log(gomock.Any(), proto.MatcherEqual(req1)).Return(res1, nil) |
| gitilesMock.EXPECT().Log(gomock.Any(), proto.MatcherEqual(req2)).Return(res2, nil) |
| |
| commits, err := impl.Log(cAllowed, host, "project", "refs/heads/main", &LogOptions{Limit: 150}) |
| So(err, ShouldBeNil) |
| So(commits, ShouldResemble, fakeCommits[:150]) |
| }) |
| }) |
| } |