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  }