go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/rpc/v0/search_runs_test.go (about)

     1  // Copyright 2022 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  	"testing"
    19  	"time"
    20  
    21  	"go.chromium.org/luci/common/clock/testclock"
    22  	"go.chromium.org/luci/gae/service/datastore"
    23  	"go.chromium.org/luci/server/auth"
    24  	"go.chromium.org/luci/server/auth/authtest"
    25  
    26  	cfgpb "go.chromium.org/luci/cv/api/config/v2"
    27  	apiv0pb "go.chromium.org/luci/cv/api/v0"
    28  	"go.chromium.org/luci/cv/internal/acls"
    29  	"go.chromium.org/luci/cv/internal/changelist"
    30  	"go.chromium.org/luci/cv/internal/common"
    31  	"go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest"
    32  	"go.chromium.org/luci/cv/internal/cvtesting"
    33  	"go.chromium.org/luci/cv/internal/run"
    34  
    35  	. "github.com/smartystreets/goconvey/convey"
    36  	. "go.chromium.org/luci/common/testing/assertions"
    37  )
    38  
    39  func TestSearchRuns(t *testing.T) {
    40  	t.Parallel()
    41  
    42  	Convey("SearchRuns", t, func() {
    43  		ct := cvtesting.Test{}
    44  		ctx, cancel := ct.SetUp(t)
    45  		defer cancel()
    46  
    47  		srv := RunsServer{}
    48  
    49  		const projectName = "prj"
    50  
    51  		prjcfgtest.Create(ctx, projectName, &cfgpb.Config{
    52  			// TODO(crbug/1233963): remove once non-legacy ACLs are implemented.
    53  			CqStatusHost: "chromium-cq-status.appspot.com",
    54  			ConfigGroups: []*cfgpb.ConfigGroup{{Name: "first"}},
    55  		})
    56  
    57  		ctx = auth.WithState(ctx, &authtest.FakeState{
    58  			Identity:       "user:admin@example.com",
    59  			IdentityGroups: []string{acls.V0APIAllowGroup},
    60  		})
    61  
    62  		Convey("without access", func() {
    63  			ctx = auth.WithState(ctx, &authtest.FakeState{
    64  				Identity: "anonymous:anonymous",
    65  			})
    66  			_, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
    67  				Predicate: &apiv0pb.RunPredicate{Project: projectName},
    68  			})
    69  			So(err, ShouldBeRPCPermissionDenied)
    70  		})
    71  
    72  		Convey("with no predicate", func() {
    73  			_, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{})
    74  			So(err, ShouldBeRPCInvalidArgument)
    75  
    76  			_, err = srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{PageSize: 50})
    77  			So(err, ShouldBeRPCInvalidArgument)
    78  		})
    79  
    80  		Convey("with a page size that is too large", func() {
    81  			_, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{PageSize: maxPageSize + 1})
    82  			So(err, ShouldBeRPCInvalidArgument)
    83  		})
    84  
    85  		Convey("with no project", func() {
    86  			_, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
    87  				Predicate: &apiv0pb.RunPredicate{},
    88  			})
    89  			So(err, ShouldBeRPCInvalidArgument)
    90  		})
    91  
    92  		Convey("with nonexistent project", func() {
    93  			resp, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
    94  				Predicate: &apiv0pb.RunPredicate{Project: "bogus"},
    95  			})
    96  			So(err, ShouldBeNil)
    97  			So(resp.Runs, ShouldBeEmpty)
    98  			So(resp.NextPageToken, ShouldBeEmpty)
    99  		})
   100  
   101  		Convey("with no runs", func() {
   102  			resp, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
   103  				Predicate: &apiv0pb.RunPredicate{Project: projectName},
   104  			})
   105  			So(err, ShouldBeNil)
   106  			So(resp.Runs, ShouldBeEmpty)
   107  			So(resp.NextPageToken, ShouldBeEmpty)
   108  		})
   109  
   110  		// Add example data for tests below.
   111  		gHost := "r-review.example.com"
   112  		epoch := testclock.TestRecentTimeUTC.Truncate(time.Millisecond)
   113  
   114  		// putRun puts a Run and RunCLs in datastore and returns the Run.
   115  		putRun := func(proj string, delay time.Duration, cls ...*changelist.CL) *run.Run {
   116  			createdAt := epoch.Add(delay)
   117  			// As long as each RunID used in a given test is unique, RunID
   118  			// details don't matter. So practically, each Run must have a
   119  			// different create time or different set of CLs.
   120  			var clsDigest []byte
   121  			for _, cl := range cls {
   122  				clsDigest = append(clsDigest, []byte(cl.ExternalID)...)
   123  			}
   124  			runID := common.MakeRunID(proj, createdAt, 1, clsDigest)
   125  			clids := make(common.CLIDs, len(cls))
   126  			for i, cl := range cls {
   127  				clids[i] = common.CLID(cl.ID)
   128  			}
   129  			r := &run.Run{
   130  				ID:         runID,
   131  				Status:     run.Status_SUCCEEDED,
   132  				CLs:        clids,
   133  				CreateTime: createdAt,
   134  				StartTime:  createdAt.Add(time.Second),
   135  				UpdateTime: createdAt.Add(time.Minute),
   136  				EndTime:    createdAt.Add(time.Hour),
   137  				Owner:      "user:foo@example.org",
   138  			}
   139  			So(datastore.Put(ctx, r), ShouldBeNil)
   140  			for _, cl := range cls {
   141  				So(datastore.Put(ctx, &run.RunCL{
   142  					Run:        datastore.MakeKey(ctx, common.RunKind, string(runID)),
   143  					ID:         cl.ID,
   144  					IndexedID:  cl.ID,
   145  					ExternalID: cl.ExternalID,
   146  					Detail:     &changelist.Snapshot{Patchset: 1},
   147  				}), ShouldBeNil)
   148  			}
   149  			return r
   150  		}
   151  
   152  		Convey("with matching Runs, project-only predicate", func() {
   153  			cl1 := changelist.MustGobID(gHost, 1).MustCreateIfNotExists(ctx)
   154  			cl2 := changelist.MustGobID(gHost, 2).MustCreateIfNotExists(ctx)
   155  			r1 := putRun(projectName, 1*time.Millisecond, cl1)
   156  			r2 := putRun(projectName, 5*time.Millisecond, cl2)
   157  			resp, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
   158  				Predicate: &apiv0pb.RunPredicate{Project: projectName},
   159  			})
   160  			So(err, ShouldBeNil)
   161  
   162  			// Most recent Run comes first.
   163  			So(respIDs(resp.Runs), ShouldResemble, runIDs(r2, r1))
   164  			So(resp.NextPageToken, ShouldBeEmpty)
   165  		})
   166  
   167  		Convey("paging, project-only predicate", func() {
   168  			cl1 := changelist.MustGobID(gHost, 1).MustCreateIfNotExists(ctx)
   169  			cl2 := changelist.MustGobID(gHost, 2).MustCreateIfNotExists(ctx)
   170  			r1 := putRun(projectName, 1*time.Millisecond, cl1)
   171  			r2 := putRun(projectName, 5*time.Millisecond, cl2)
   172  
   173  			// First request, first page.
   174  			resp, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
   175  				Predicate: &apiv0pb.RunPredicate{Project: projectName},
   176  				PageSize:  1,
   177  			})
   178  			So(err, ShouldBeNil)
   179  			So(respIDs(resp.Runs), ShouldResemble, runIDs(r2))
   180  			So(resp.NextPageToken, ShouldNotBeEmpty)
   181  
   182  			// Second request, second page.
   183  			resp, err = srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
   184  				Predicate: &apiv0pb.RunPredicate{Project: projectName},
   185  				PageSize:  1,
   186  				PageToken: resp.NextPageToken,
   187  			})
   188  			So(err, ShouldBeNil)
   189  			So(respIDs(resp.Runs), ShouldResemble, runIDs(r1))
   190  			So(resp.NextPageToken, ShouldNotBeEmpty)
   191  
   192  			// Third request, no more results.
   193  			resp, err = srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
   194  				Predicate: &apiv0pb.RunPredicate{Project: projectName},
   195  				PageSize:  1,
   196  				PageToken: resp.NextPageToken,
   197  			})
   198  			So(err, ShouldBeNil)
   199  			So(resp.Runs, ShouldBeEmpty)
   200  			So(resp.NextPageToken, ShouldBeEmpty)
   201  		})
   202  
   203  		Convey("with matching Run, single CL predicate", func() {
   204  			cl1 := changelist.MustGobID(gHost, 1).MustCreateIfNotExists(ctx)
   205  			r1 := putRun(projectName, 1*time.Millisecond, cl1)
   206  			resp, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
   207  				Predicate: &apiv0pb.RunPredicate{
   208  					Project: projectName,
   209  					GerritChanges: []*apiv0pb.GerritChange{
   210  						{Host: gHost, Change: 1},
   211  					},
   212  				},
   213  			})
   214  			So(err, ShouldBeNil)
   215  			So(respIDs(resp.Runs), ShouldResemble, runIDs(r1))
   216  		})
   217  
   218  		Convey("with CL predicate that includes patchset", func() {
   219  			cl1 := changelist.MustGobID(gHost, 1).MustCreateIfNotExists(ctx)
   220  			putRun(projectName, 1*time.Millisecond, cl1)
   221  			_, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
   222  				Predicate: &apiv0pb.RunPredicate{
   223  					Project: projectName,
   224  					GerritChanges: []*apiv0pb.GerritChange{
   225  						{Host: gHost, Change: 1, Patchset: 3},
   226  					},
   227  				},
   228  			})
   229  			So(err, ShouldBeRPCInvalidArgument)
   230  		})
   231  
   232  		Convey("with CL predicate and no project given", func() {
   233  			cl1 := changelist.MustGobID(gHost, 1).MustCreateIfNotExists(ctx)
   234  			putRun(projectName, 1*time.Millisecond, cl1)
   235  			_, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
   236  				Predicate: &apiv0pb.RunPredicate{
   237  					GerritChanges: []*apiv0pb.GerritChange{
   238  						{Host: gHost, Change: 1},
   239  					},
   240  				},
   241  			})
   242  			So(err, ShouldBeRPCInvalidArgument)
   243  		})
   244  
   245  		Convey("with no matching Run, CL predicate", func() {
   246  			// No Runs put.
   247  			resp, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
   248  				Predicate: &apiv0pb.RunPredicate{
   249  					Project: projectName,
   250  					GerritChanges: []*apiv0pb.GerritChange{
   251  						{Host: gHost, Change: 1},
   252  					},
   253  				},
   254  			})
   255  			So(err, ShouldBeNil)
   256  			So(resp.Runs, ShouldBeEmpty)
   257  		})
   258  
   259  		Convey("query with multiple CLs returns Run that contains all CLs", func() {
   260  			cl1 := changelist.MustGobID(gHost, 2).MustCreateIfNotExists(ctx)
   261  			cl2 := changelist.MustGobID(gHost, 3).MustCreateIfNotExists(ctx)
   262  			r1 := putRun(projectName, 1*time.Millisecond, cl1, cl2)
   263  			resp, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
   264  				Predicate: &apiv0pb.RunPredicate{
   265  					Project: projectName,
   266  					GerritChanges: []*apiv0pb.GerritChange{
   267  						{
   268  							Host:   gHost,
   269  							Change: 2,
   270  						},
   271  						{
   272  							Host:   gHost,
   273  							Change: 3,
   274  						},
   275  					},
   276  				},
   277  			})
   278  			So(err, ShouldBeNil)
   279  			So(respIDs(resp.Runs), ShouldResemble, runIDs(r1))
   280  		})
   281  
   282  		Convey("query with multiple CLs returns nothing if no single CL contains all CLs", func() {
   283  			cl1 := changelist.MustGobID(gHost, 1).MustCreateIfNotExists(ctx)
   284  			cl2 := changelist.MustGobID(gHost, 2).MustCreateIfNotExists(ctx)
   285  			putRun(projectName, 1*time.Millisecond, cl1)
   286  			putRun(projectName, 5*time.Millisecond, cl2)
   287  			resp, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
   288  				Predicate: &apiv0pb.RunPredicate{
   289  					Project: projectName,
   290  					GerritChanges: []*apiv0pb.GerritChange{
   291  						{
   292  							Host:   gHost,
   293  							Change: 1,
   294  						},
   295  						{
   296  							Host:   gHost,
   297  							Change: 2,
   298  						},
   299  					},
   300  				},
   301  			})
   302  			So(err, ShouldBeNil)
   303  			So(resp.Runs, ShouldBeEmpty)
   304  		})
   305  	})
   306  }
   307  
   308  func respIDs(runs []*apiv0pb.Run) common.RunIDs {
   309  	var ret common.RunIDs
   310  	for _, r := range runs {
   311  		id, err := common.FromPublicRunID(r.Id)
   312  		if err != nil {
   313  			panic(err)
   314  		}
   315  		ret = append(ret, id)
   316  	}
   317  	return ret
   318  }
   319  
   320  func runIDs(runs ...*run.Run) common.RunIDs {
   321  	var ret common.RunIDs
   322  	for _, r := range runs {
   323  		ret = append(ret, r.ID)
   324  	}
   325  	return ret
   326  }