go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/internal/git/log_test.go (about) 1 // Copyright 2018 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 git 16 17 import ( 18 "context" 19 "encoding/hex" 20 "testing" 21 22 "github.com/alicebob/miniredis/v2" 23 "github.com/golang/mock/gomock" 24 "github.com/gomodule/redigo/redis" 25 "go.chromium.org/luci/auth/identity" 26 "go.chromium.org/luci/common/proto" 27 gitpb "go.chromium.org/luci/common/proto/git" 28 gitilespb "go.chromium.org/luci/common/proto/gitiles" 29 "go.chromium.org/luci/common/proto/gitiles/mock_gitiles" 30 "go.chromium.org/luci/gae/impl/memory" 31 "go.chromium.org/luci/milo/internal/git/gitacls" 32 configpb "go.chromium.org/luci/milo/proto/config" 33 "go.chromium.org/luci/server/auth" 34 "go.chromium.org/luci/server/auth/authtest" 35 "go.chromium.org/luci/server/redisconn" 36 37 . "github.com/smartystreets/goconvey/convey" 38 . "go.chromium.org/luci/common/testing/assertions" 39 ) 40 41 func TestLog(t *testing.T) { 42 t.Parallel() 43 44 Convey("Log", t, func() { 45 c := memory.Use(context.Background()) 46 47 // Set up a test redis server. 48 s, err := miniredis.Run() 49 So(err, ShouldBeNil) 50 defer s.Close() 51 c = redisconn.UsePool(c, &redis.Pool{ 52 Dial: func() (redis.Conn, error) { 53 return redis.Dial("tcp", s.Addr()) 54 }, 55 }) 56 57 ctl := gomock.NewController(t) 58 defer ctl.Finish() 59 gitilesMock := mock_gitiles.NewMockGitilesClient(ctl) 60 61 host := "limited.googlesource.com" 62 acls, err := gitacls.FromConfig(c, []*configpb.Settings_SourceAcls{ 63 {Hosts: []string{host}, Readers: []string{"allowed@example.com"}}, 64 }) 65 So(err, ShouldBeNil) 66 impl := implementation{mockGitiles: gitilesMock, acls: acls} 67 c = Use(c, &impl) 68 cAllowed := auth.WithState(c, &authtest.FakeState{Identity: "user:allowed@example.com"}) 69 cDenied := auth.WithState(c, &authtest.FakeState{Identity: identity.AnonymousIdentity}) 70 71 fakeCommits := make([]*gitpb.Commit, 255) 72 commitID := make([]byte, 20) 73 commitID[0] = 255 74 for i := range fakeCommits { 75 fakeCommits[i] = &gitpb.Commit{Id: hex.EncodeToString(commitID)} 76 if i > 0 { 77 fakeCommits[i-1].Parents = []string{fakeCommits[i].Id} 78 } 79 80 commitID[0]-- 81 } 82 83 Convey("cold cache", func() { 84 Convey("ACLs respected", func() { 85 _, err := impl.Log(cDenied, host, "project", "refs/heads/main", &LogOptions{Limit: 50}) 86 So(err.Error(), ShouldContainSubstring, "not logged in") 87 }) 88 89 req := &gitilespb.LogRequest{ 90 Project: "project", 91 Committish: "refs/heads/main", 92 PageSize: 100, 93 } 94 res := &gitilespb.LogResponse{ 95 Log: fakeCommits[1:101], // return 100 commits 96 } 97 gitilesMock.EXPECT().Log(gomock.Any(), proto.MatcherEqual(req)).Return(res, nil) 98 99 commits, err := impl.Log(cAllowed, host, "project", "refs/heads/main", &LogOptions{Limit: 100}) 100 So(err, ShouldBeNil) 101 So(commits, ShouldResemble, res.Log) 102 103 // Now that we have something in cache, call Log with cached commits. 104 // gitiles.Log was already called maximum number of times, which is 1, 105 // so another call with cause a test failure. 106 107 Convey("ACLs respected even with cache", func() { 108 _, err := impl.Log(cDenied, host, "project", "refs/heads/main", &LogOptions{Limit: 50}) 109 So(err.Error(), ShouldContainSubstring, "not logged in") 110 }) 111 112 Convey("with exactly one last commit not in cache", func() { 113 req2 := &gitilespb.LogRequest{ 114 Project: "project", 115 Committish: fakeCommits[100].Id, 116 PageSize: 100, // we always fetch 100 117 } 118 res2 := &gitilespb.LogResponse{ 119 Log: fakeCommits[100:200], 120 } 121 gitilesMock.EXPECT().Log(gomock.Any(), proto.MatcherEqual(req2)).Return(res2, nil) 122 commits, err := impl.Log(cAllowed, host, "project", "refs/heads/main", &LogOptions{Limit: 101}) 123 So(err, ShouldBeNil) 124 So(commits, ShouldResembleProto, fakeCommits[1:102]) 125 }) 126 127 Convey("with exactly the proceeding commit not in cache", func() { 128 req2 := &gitilespb.LogRequest{ 129 Project: "project", 130 Committish: fakeCommits[51].Id, 131 PageSize: 100, // we always fetch 100 132 } 133 res2 := &gitilespb.LogResponse{ 134 Log: fakeCommits[51:150], 135 } 136 gitilesMock.EXPECT().Log(gomock.Any(), proto.MatcherEqual(req2)).Return(res2, nil).Times(0) 137 commits, err := impl.Log(cAllowed, host, "project", fakeCommits[51].Id, &LogOptions{Limit: 50}) 138 So(err, ShouldBeNil) 139 So(commits, ShouldResembleProto, fakeCommits[51:101]) 140 }) 141 142 Convey("with ref in cache", func() { 143 commits, err := impl.Log(cAllowed, host, "project", "refs/heads/main", &LogOptions{Limit: 50}) 144 So(err, ShouldBeNil) 145 So(commits, ShouldResembleProto, res.Log[:50]) 146 }) 147 148 Convey("with top commit in cache", func() { 149 commits, err := impl.Log(cAllowed, host, "project", fakeCommits[1].Id, &LogOptions{Limit: 50}) 150 So(err, ShouldBeNil) 151 So(commits, ShouldResembleProto, res.Log[:50]) 152 }) 153 154 Convey("with ancestor commit in cache", func() { 155 commits, err := impl.Log(cAllowed, host, "project", fakeCommits[2].Id, &LogOptions{Limit: 50}) 156 So(err, ShouldBeNil) 157 So(commits, ShouldResembleProto, res.Log[1:51]) 158 }) 159 160 Convey("with second ancestor commit in cache", func() { 161 commits, err := impl.Log(cAllowed, host, "project", fakeCommits[3].Id, &LogOptions{Limit: 50}) 162 So(err, ShouldBeNil) 163 So(commits, ShouldResembleProto, res.Log[2:52]) 164 }) 165 166 Convey("min is honored", func() { 167 req2 := &gitilespb.LogRequest{ 168 Project: "project", 169 Committish: fakeCommits[2].Id, 170 PageSize: 100, 171 } 172 res2 := &gitilespb.LogResponse{ 173 Log: fakeCommits[2:102], 174 } 175 gitilesMock.EXPECT().Log(gomock.Any(), proto.MatcherEqual(req2)).Return(res2, nil) 176 177 commits, err := impl.Log(cAllowed, host, "project", fakeCommits[2].Id, &LogOptions{Limit: 100}) 178 So(err, ShouldBeNil) 179 So(commits, ShouldHaveLength, 100) 180 So(commits, ShouldResembleProto, res2.Log) 181 }) 182 183 Convey("request of item not in cache", func() { 184 req2 := &gitilespb.LogRequest{ 185 Project: "project", 186 Committish: fakeCommits[101].Id, 187 PageSize: 100, 188 } 189 res2 := &gitilespb.LogResponse{ 190 Log: fakeCommits[101:201], 191 } 192 gitilesMock.EXPECT().Log(gomock.Any(), proto.MatcherEqual(req2)).Return(res2, nil) 193 commits, err := impl.Log(cAllowed, host, "project", fakeCommits[101].Id, &LogOptions{Limit: 50}) 194 So(err, ShouldBeNil) 195 So(commits, ShouldHaveLength, 50) 196 So(commits, ShouldResemble, res2.Log[:50]) 197 }) 198 199 Convey("do not update cache entries that have more info", func() { 200 refCacheKey := (&logReq{ 201 host: host, 202 project: "project", 203 }).mkCacheKey(c, "refs/heads/main") 204 205 conn, err := redisconn.Get(c) 206 So(err, ShouldBeNil) 207 defer conn.Close() 208 _, err = conn.Do("DEL", refCacheKey) 209 So(err, ShouldBeNil) 210 211 req2 := &gitilespb.LogRequest{ 212 Project: "project", 213 Committish: "refs/heads/main", 214 PageSize: 100, 215 } 216 res2 := &gitilespb.LogResponse{ 217 Log: fakeCommits[:100], 218 } 219 gitilesMock.EXPECT().Log(gomock.Any(), proto.MatcherEqual(req2)).Return(res2, nil) 220 commits, err := impl.Log(cAllowed, host, "project", "refs/heads/main", &LogOptions{Limit: 50}) 221 So(err, ShouldBeNil) 222 So(commits, ShouldResemble, res2.Log[:50]) 223 }) 224 }) 225 Convey("paging", func() { 226 req1 := &gitilespb.LogRequest{ 227 Project: "project", 228 Committish: "refs/heads/main", 229 PageSize: 100, 230 } 231 res1 := &gitilespb.LogResponse{ 232 Log: fakeCommits[:100], 233 } 234 req2 := &gitilespb.LogRequest{ 235 Project: "project", 236 Committish: res1.Log[len(res1.Log)-1].Id, 237 PageSize: 100, // we always fetch 100 238 } 239 res2 := &gitilespb.LogResponse{ 240 Log: fakeCommits[99:199], 241 } 242 gitilesMock.EXPECT().Log(gomock.Any(), proto.MatcherEqual(req1)).Return(res1, nil) 243 gitilesMock.EXPECT().Log(gomock.Any(), proto.MatcherEqual(req2)).Return(res2, nil) 244 245 commits, err := impl.Log(cAllowed, host, "project", "refs/heads/main", &LogOptions{Limit: 150}) 246 So(err, ShouldBeNil) 247 So(commits, ShouldResemble, fakeCommits[:150]) 248 }) 249 }) 250 }