go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/rpc/query_blamelist_test.go (about)

     1  // Copyright 2020 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 rpc
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"testing"
    21  
    22  	"github.com/golang/mock/gomock"
    23  
    24  	. "github.com/smartystreets/goconvey/convey"
    25  	"go.chromium.org/luci/appengine/gaetesting"
    26  	"go.chromium.org/luci/auth/identity"
    27  	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
    28  	"go.chromium.org/luci/buildbucket/protoutil"
    29  	gitpb "go.chromium.org/luci/common/proto/git"
    30  	"go.chromium.org/luci/common/proto/gitiles"
    31  	"go.chromium.org/luci/common/proto/gitiles/mock_gitiles"
    32  	. "go.chromium.org/luci/common/testing/assertions"
    33  	"go.chromium.org/luci/gae/service/datastore"
    34  	"go.chromium.org/luci/milo/internal/model"
    35  	"go.chromium.org/luci/milo/internal/projectconfig"
    36  	"go.chromium.org/luci/milo/internal/utils"
    37  	milopb "go.chromium.org/luci/milo/proto/v1"
    38  	"go.chromium.org/luci/server/auth"
    39  	"go.chromium.org/luci/server/auth/authtest"
    40  )
    41  
    42  func TestPrepareQueryBlamelistRequest(t *testing.T) {
    43  	t.Parallel()
    44  	Convey(`TestPrepareQueryBlamelistRequest`, t, func() {
    45  		Convey(`extract commit ID correctly`, func() {
    46  			Convey(`when there's no page token`, func() {
    47  				startRev, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{
    48  					GitilesCommit: &buildbucketpb.GitilesCommit{
    49  						Host:    "host",
    50  						Project: "project/src",
    51  						Id:      "commit-id",
    52  						Ref:     "commit-ref",
    53  					},
    54  					Builder: &buildbucketpb.BuilderID{
    55  						Project: "project",
    56  						Bucket:  "bucket",
    57  						Builder: "builder",
    58  					},
    59  				})
    60  				So(err, ShouldBeNil)
    61  				// commit ID should take priority.
    62  				So(startRev, ShouldEqual, "commit-id")
    63  			})
    64  
    65  			Convey(`when there's no page token or commit ID`, func() {
    66  				startRev, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{
    67  					GitilesCommit: &buildbucketpb.GitilesCommit{
    68  						Host:    "host",
    69  						Project: "project/src",
    70  						Ref:     "commit-ref",
    71  					},
    72  					Builder: &buildbucketpb.BuilderID{
    73  						Project: "project",
    74  						Bucket:  "bucket",
    75  						Builder: "builder",
    76  					},
    77  				})
    78  				So(err, ShouldBeNil)
    79  				So(startRev, ShouldEqual, "commit-ref")
    80  			})
    81  
    82  			Convey(`when there's a page token`, func() {
    83  				startRev, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{
    84  					GitilesCommit: &buildbucketpb.GitilesCommit{
    85  						Host:    "host",
    86  						Project: "project/src",
    87  						Id:      "commit-id-1",
    88  					},
    89  					Builder: &buildbucketpb.BuilderID{
    90  						Project: "project",
    91  						Bucket:  "bucket",
    92  						Builder: "builder",
    93  					},
    94  				})
    95  				So(err, ShouldBeNil)
    96  				So(startRev, ShouldEqual, "commit-id-1")
    97  
    98  				pageToken, err := serializeQueryBlamelistPageToken(&milopb.QueryBlamelistPageToken{
    99  					NextCommitId: "commit-id-2",
   100  				})
   101  				So(err, ShouldBeNil)
   102  
   103  				nextCommitRev, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{
   104  					GitilesCommit: &buildbucketpb.GitilesCommit{
   105  						Host:    "host",
   106  						Project: "project/src",
   107  						Id:      "commit-id-1",
   108  					},
   109  					Builder: &buildbucketpb.BuilderID{
   110  						Project: "project",
   111  						Bucket:  "bucket",
   112  						Builder: "builder",
   113  					},
   114  					PageToken: pageToken,
   115  				})
   116  				So(err, ShouldBeNil)
   117  				So(nextCommitRev, ShouldEqual, "commit-id-2")
   118  			})
   119  		})
   120  
   121  		Convey(`reject the page token when the page token is invalid`, func() {
   122  			_, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{
   123  				GitilesCommit: &buildbucketpb.GitilesCommit{
   124  					Host:    "host",
   125  					Project: "project/src",
   126  					Id:      "commit-id-1",
   127  				},
   128  				Builder: &buildbucketpb.BuilderID{
   129  					Project: "project",
   130  					Bucket:  "bucket",
   131  					Builder: "builder",
   132  				},
   133  				PageToken: "abc",
   134  			})
   135  			So(err, ShouldNotBeNil)
   136  		})
   137  
   138  		Convey("no builder", func() {
   139  			_, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{
   140  				GitilesCommit: &buildbucketpb.GitilesCommit{
   141  					Host:    "host",
   142  					Project: "project/src",
   143  					Id:      "commit-id",
   144  					Ref:     "commit-ref",
   145  				},
   146  			})
   147  			So(err, ShouldErrLike, "builder: project must match")
   148  		})
   149  
   150  		Convey("invalid builder", func() {
   151  			_, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{
   152  				GitilesCommit: &buildbucketpb.GitilesCommit{
   153  					Host:    "host",
   154  					Project: "project/src",
   155  					Id:      "commit-id",
   156  					Ref:     "commit-ref",
   157  				},
   158  				Builder: &buildbucketpb.BuilderID{
   159  					Project: "fake_proj",
   160  					Bucket:  "fake[]ucket",
   161  					Builder: "fake_/uilder1",
   162  				},
   163  			})
   164  			So(err, ShouldErrLike, "builder: bucket must match")
   165  		})
   166  	})
   167  }
   168  
   169  func TestQueryBlamelist(t *testing.T) {
   170  	t.Parallel()
   171  	Convey(`TestQueryBlamelist`, t, func() {
   172  		c := gaetesting.TestingContextWithAppID("luci-milo-dev")
   173  		datastore.GetTestable(c).Consistent(true)
   174  		ctrl := gomock.NewController(t)
   175  		defer ctrl.Finish()
   176  		gitMock := mock_gitiles.NewMockGitilesClient(ctrl)
   177  		srv := &MiloInternalService{
   178  			GetGitilesClient: func(c context.Context, host string, as auth.RPCAuthorityKind) (gitiles.GitilesClient, error) {
   179  				return gitMock, nil
   180  			},
   181  		}
   182  		c = auth.WithState(c, &authtest.FakeState{Identity: "user"})
   183  
   184  		builder1 := &buildbucketpb.BuilderID{
   185  			Project: "fake_project",
   186  			Bucket:  "fake_bucket",
   187  			Builder: "fake_builder1",
   188  		}
   189  		builder2 := &buildbucketpb.BuilderID{
   190  			Project: "fake_project",
   191  			Bucket:  "fake_bucket",
   192  			Builder: "fake_builder2",
   193  		}
   194  
   195  		commits := []*gitpb.Commit{
   196  			{Id: "commit8"},
   197  			{Id: "commit7"},
   198  			{Id: "commit6"},
   199  			{Id: "commit5"},
   200  			{Id: "commit4"},
   201  			{Id: "commit3"},
   202  			{Id: "commit2"},
   203  			{Id: "commit1"},
   204  		}
   205  
   206  		createFakeBuild := func(builder *buildbucketpb.BuilderID, buildNum int, commitID string, additionalBlamelistPins ...string) *model.BuildSummary {
   207  			builderID := utils.LegacyBuilderIDString(builder)
   208  			buildID := fmt.Sprintf("%s/%d", builderID, buildNum)
   209  			buildSet := protoutil.GitilesBuildSet(&buildbucketpb.GitilesCommit{
   210  				Host:    "fake_gitiles_host",
   211  				Project: "fake_gitiles_project",
   212  				Id:      commitID,
   213  			})
   214  			blamelistPins := append(additionalBlamelistPins, buildSet)
   215  			return &model.BuildSummary{
   216  				BuildKey:      datastore.MakeKey(c, "build", buildID),
   217  				ProjectID:     builder.Project,
   218  				BuilderID:     builderID,
   219  				BuildID:       buildID,
   220  				BuildSet:      []string{buildSet},
   221  				BlamelistPins: blamelistPins,
   222  			}
   223  		}
   224  
   225  		builds := []*model.BuildSummary{
   226  			createFakeBuild(builder1, 1, "commit8", protoutil.GitilesBuildSet(&buildbucketpb.GitilesCommit{
   227  				Host:    "fake_gitiles_host",
   228  				Project: "other_fake_gitiles_project",
   229  				Id:      "commit3",
   230  			})),
   231  			createFakeBuild(builder2, 1, "commit7"),
   232  			createFakeBuild(builder1, 2, "commit5", protoutil.GitilesBuildSet(&buildbucketpb.GitilesCommit{
   233  				Host:    "fake_gitiles_host",
   234  				Project: "other_fake_gitiles_project",
   235  				Id:      "commit1",
   236  			})),
   237  			createFakeBuild(builder1, 3, "commit3"),
   238  		}
   239  
   240  		err := datastore.Put(c, builds)
   241  		So(err, ShouldBeNil)
   242  
   243  		err = datastore.Put(c, &projectconfig.Project{
   244  			ID:      "fake_project",
   245  			ACL:     projectconfig.ACL{Identities: []identity.Identity{"user"}},
   246  			LogoURL: "https://logo.com",
   247  		})
   248  		So(err, ShouldBeNil)
   249  
   250  		Convey(`reject users with no access`, func() {
   251  			req := &milopb.QueryBlamelistRequest{
   252  				GitilesCommit: &buildbucketpb.GitilesCommit{
   253  					Host:    "fake_gitiles_host",
   254  					Project: "fake_gitiles_project",
   255  					Id:      "commit1",
   256  				},
   257  				Builder: &buildbucketpb.BuilderID{
   258  					Project: "secret_fake_project",
   259  					Bucket:  "secret_fake_bucket",
   260  					Builder: "secret_fake_builder",
   261  				},
   262  				PageSize: 2000,
   263  			}
   264  			_, err := srv.QueryBlamelist(c, req)
   265  			So(err, ShouldNotBeNil)
   266  		})
   267  
   268  		Convey(`coerce page_size`, func() {
   269  			Convey(`to 1000 if it's greater than 1000`, func() {
   270  				req := &milopb.QueryBlamelistRequest{
   271  					GitilesCommit: &buildbucketpb.GitilesCommit{
   272  						Host:    "fake_gitiles_host",
   273  						Project: "fake_gitiles_project",
   274  						Id:      "commit1",
   275  					},
   276  					Builder:  builder1,
   277  					PageSize: 2000,
   278  				}
   279  				gitMock.
   280  					EXPECT().
   281  					Log(gomock.Any(), &gitiles.LogRequest{
   282  						Project:    "fake_gitiles_project",
   283  						Committish: "commit1",
   284  						PageSize:   1001,
   285  						TreeDiff:   true,
   286  					}).
   287  					Return(&gitiles.LogResponse{
   288  						Log: commits,
   289  					}, nil)
   290  
   291  				_, err := srv.QueryBlamelist(c, req)
   292  				So(err, ShouldBeNil)
   293  			})
   294  
   295  			Convey(`to 100 if it's not set`, func() {
   296  				req := &milopb.QueryBlamelistRequest{
   297  					GitilesCommit: &buildbucketpb.GitilesCommit{
   298  						Host:    "fake_gitiles_host",
   299  						Project: "fake_gitiles_project",
   300  						Id:      "commit8",
   301  					},
   302  					Builder: builder1,
   303  				}
   304  				gitMock.
   305  					EXPECT().
   306  					Log(gomock.Any(), &gitiles.LogRequest{
   307  						Project:    req.GitilesCommit.Project,
   308  						Committish: req.GitilesCommit.Id,
   309  						PageSize:   101,
   310  						TreeDiff:   true,
   311  					}).
   312  					Return(&gitiles.LogResponse{
   313  						Log: commits,
   314  					}, nil)
   315  
   316  				_, err := srv.QueryBlamelist(c, req)
   317  				So(err, ShouldBeNil)
   318  			})
   319  		})
   320  
   321  		Convey(`get all the commits in the blamelist`, func() {
   322  			Convey(`in one page`, func() {
   323  				Convey(`when we found the previous build`, func() {
   324  					req := &milopb.QueryBlamelistRequest{
   325  						GitilesCommit: &buildbucketpb.GitilesCommit{
   326  							Host:    "fake_gitiles_host",
   327  							Project: "fake_gitiles_project",
   328  							Id:      "commit8",
   329  						},
   330  						Builder: builder1,
   331  					}
   332  					gitMock.
   333  						EXPECT().
   334  						Log(gomock.Any(), &gitiles.LogRequest{
   335  							Project:    req.GitilesCommit.Project,
   336  							Committish: req.GitilesCommit.Id,
   337  							PageSize:   101,
   338  							TreeDiff:   true,
   339  						}).
   340  						Return(&gitiles.LogResponse{
   341  							Log: commits,
   342  						}, nil)
   343  
   344  					res, err := srv.QueryBlamelist(c, req)
   345  					So(err, ShouldBeNil)
   346  					So(res.Commits, ShouldHaveLength, 3)
   347  					So(res.Commits[0].Id, ShouldEqual, "commit8")
   348  					So(res.Commits[1].Id, ShouldEqual, "commit7")
   349  					So(res.Commits[2].Id, ShouldEqual, "commit6")
   350  					So(res.PrecedingCommit.Id, ShouldEqual, "commit5")
   351  				})
   352  
   353  				Convey(`when there's no previous build`, func() {
   354  					req := &milopb.QueryBlamelistRequest{
   355  						GitilesCommit: &buildbucketpb.GitilesCommit{
   356  							Host:    "fake_gitiles_host",
   357  							Project: "fake_gitiles_project",
   358  							Id:      "commit3",
   359  						},
   360  						Builder: builder1,
   361  					}
   362  					gitMock.
   363  						EXPECT().
   364  						Log(gomock.Any(), &gitiles.LogRequest{
   365  							Project:    req.GitilesCommit.Project,
   366  							Committish: req.GitilesCommit.Id,
   367  							PageSize:   101,
   368  							TreeDiff:   true,
   369  						}).
   370  						Return(&gitiles.LogResponse{
   371  							Log: commits[5:],
   372  						}, nil)
   373  
   374  					res, err := srv.QueryBlamelist(c, req)
   375  					So(err, ShouldBeNil)
   376  					So(res.Commits, ShouldHaveLength, 3)
   377  					So(res.Commits[0].Id, ShouldEqual, "commit3")
   378  					So(res.Commits[1].Id, ShouldEqual, "commit2")
   379  					So(res.Commits[2].Id, ShouldEqual, "commit1")
   380  					So(res.PrecedingCommit, ShouldBeZeroValue)
   381  				})
   382  			})
   383  
   384  			Convey(`in multiple pages`, func() {
   385  				Convey(`when we found the previous build`, func() {
   386  					// Query the first page.
   387  					req := &milopb.QueryBlamelistRequest{
   388  						GitilesCommit: &buildbucketpb.GitilesCommit{
   389  							Host:    "fake_gitiles_host",
   390  							Project: "fake_gitiles_project",
   391  							Id:      "commit8",
   392  						},
   393  						Builder:  builder1,
   394  						PageSize: 2,
   395  					}
   396  
   397  					gitMock.
   398  						EXPECT().
   399  						Log(gomock.Any(), &gitiles.LogRequest{
   400  							Project:    req.GitilesCommit.Project,
   401  							Committish: req.GitilesCommit.Id,
   402  							PageSize:   3,
   403  							TreeDiff:   true,
   404  						}).
   405  						Return(&gitiles.LogResponse{
   406  							Log: commits[0:3],
   407  						}, nil)
   408  
   409  					res, err := srv.QueryBlamelist(c, req)
   410  					So(err, ShouldBeNil)
   411  					So(res.Commits, ShouldHaveLength, 2)
   412  					So(res.Commits[0].Id, ShouldEqual, "commit8")
   413  					So(res.Commits[1].Id, ShouldEqual, "commit7")
   414  					So(res.NextPageToken, ShouldNotBeZeroValue)
   415  					So(res.PrecedingCommit.Id, ShouldEqual, "commit6")
   416  
   417  					// Query the second page.
   418  					req = &milopb.QueryBlamelistRequest{
   419  						GitilesCommit: &buildbucketpb.GitilesCommit{
   420  							Host:    "fake_gitiles_host",
   421  							Project: "fake_gitiles_project",
   422  							Id:      "commit8",
   423  						},
   424  						Builder:   builder1,
   425  						PageSize:  2,
   426  						PageToken: res.NextPageToken,
   427  					}
   428  
   429  					gitMock.
   430  						EXPECT().
   431  						Log(gomock.Any(), &gitiles.LogRequest{
   432  							Project:    req.GitilesCommit.Project,
   433  							Committish: "commit6",
   434  							PageSize:   3,
   435  							TreeDiff:   true,
   436  						}).
   437  						Return(&gitiles.LogResponse{
   438  							Log: commits[2:5],
   439  						}, nil)
   440  
   441  					res, err = srv.QueryBlamelist(c, req)
   442  					So(err, ShouldBeNil)
   443  					So(res.Commits, ShouldHaveLength, 1)
   444  					So(res.Commits[0].Id, ShouldEqual, "commit6")
   445  					So(res.NextPageToken, ShouldBeZeroValue)
   446  					So(res.PrecedingCommit.Id, ShouldEqual, "commit5")
   447  				})
   448  
   449  				Convey(`when there's no previous build`, func() {
   450  					// Query the first page.
   451  					req := &milopb.QueryBlamelistRequest{
   452  						GitilesCommit: &buildbucketpb.GitilesCommit{
   453  							Host:    "fake_gitiles_host",
   454  							Project: "fake_gitiles_project",
   455  							Id:      "commit3",
   456  						},
   457  						Builder:  builder1,
   458  						PageSize: 2,
   459  					}
   460  
   461  					gitMock.
   462  						EXPECT().
   463  						Log(gomock.Any(), &gitiles.LogRequest{
   464  							Project:    req.GitilesCommit.Project,
   465  							Committish: req.GitilesCommit.Id,
   466  							PageSize:   3,
   467  							TreeDiff:   true,
   468  						}).
   469  						Return(&gitiles.LogResponse{
   470  							Log: commits[5:8],
   471  						}, nil)
   472  
   473  					res, err := srv.QueryBlamelist(c, req)
   474  					So(err, ShouldBeNil)
   475  					So(res.Commits, ShouldHaveLength, 2)
   476  					So(res.Commits[0].Id, ShouldEqual, "commit3")
   477  					So(res.Commits[1].Id, ShouldEqual, "commit2")
   478  					So(res.NextPageToken, ShouldNotBeZeroValue)
   479  					So(res.PrecedingCommit.Id, ShouldEqual, "commit1")
   480  
   481  					// Query the second page.
   482  					req = &milopb.QueryBlamelistRequest{
   483  						GitilesCommit: &buildbucketpb.GitilesCommit{
   484  							Host:    "fake_gitiles_host",
   485  							Project: "fake_gitiles_project",
   486  							Id:      "commit3",
   487  						},
   488  						Builder:   builder1,
   489  						PageSize:  2,
   490  						PageToken: res.NextPageToken,
   491  					}
   492  
   493  					gitMock.
   494  						EXPECT().
   495  						Log(gomock.Any(), &gitiles.LogRequest{
   496  							Project:    req.GitilesCommit.Project,
   497  							Committish: "commit1",
   498  							PageSize:   3,
   499  							TreeDiff:   true,
   500  						}).
   501  						Return(&gitiles.LogResponse{
   502  							Log: commits[7:],
   503  						}, nil)
   504  
   505  					res, err = srv.QueryBlamelist(c, req)
   506  					So(err, ShouldBeNil)
   507  					So(res.Commits, ShouldHaveLength, 1)
   508  					So(res.Commits[0].Id, ShouldEqual, "commit1")
   509  					So(res.NextPageToken, ShouldBeZeroValue)
   510  					So(res.PrecedingCommit, ShouldBeZeroValue)
   511  				})
   512  			})
   513  		})
   514  
   515  		Convey(`get blamelist of other projects`, func() {
   516  			req := &milopb.QueryBlamelistRequest{
   517  				GitilesCommit: &buildbucketpb.GitilesCommit{
   518  					Host:    "fake_gitiles_host",
   519  					Project: "other_fake_gitiles_project",
   520  					Id:      "commit3",
   521  				},
   522  				Builder: builder1,
   523  			}
   524  			gitMock.
   525  				EXPECT().
   526  				Log(gomock.Any(), &gitiles.LogRequest{
   527  					Project:    req.GitilesCommit.Project,
   528  					Committish: req.GitilesCommit.Id,
   529  					PageSize:   101,
   530  					TreeDiff:   true,
   531  				}).
   532  				Return(&gitiles.LogResponse{
   533  					Log: commits[5:],
   534  				}, nil)
   535  
   536  			res, err := srv.QueryBlamelist(c, req)
   537  			So(err, ShouldBeNil)
   538  			So(res.Commits, ShouldHaveLength, 2)
   539  			So(res.Commits[0].Id, ShouldEqual, "commit3")
   540  			So(res.Commits[1].Id, ShouldEqual, "commit2")
   541  			So(res.PrecedingCommit.Id, ShouldEqual, "commit1")
   542  		})
   543  	})
   544  }