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 }