go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/rpc/v0/get_cl_run_info_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 "fmt" 19 "testing" 20 "time" 21 22 "google.golang.org/grpc/codes" 23 "google.golang.org/protobuf/types/known/timestamppb" 24 25 "go.chromium.org/luci/common/clock/testclock" 26 gerritpb "go.chromium.org/luci/common/proto/gerrit" 27 "go.chromium.org/luci/gae/service/datastore" 28 "go.chromium.org/luci/grpc/grpcutil" 29 "go.chromium.org/luci/server/auth" 30 "go.chromium.org/luci/server/auth/authtest" 31 "go.chromium.org/luci/server/gerritauth" 32 33 apiv0pb "go.chromium.org/luci/cv/api/v0" 34 "go.chromium.org/luci/cv/internal/acls" 35 "go.chromium.org/luci/cv/internal/changelist" 36 "go.chromium.org/luci/cv/internal/common" 37 "go.chromium.org/luci/cv/internal/cvtesting" 38 "go.chromium.org/luci/cv/internal/run" 39 40 . "github.com/smartystreets/goconvey/convey" 41 ) 42 43 func TestGetCLRunInfo(t *testing.T) { 44 t.Parallel() 45 46 Convey("GetCLRunInfo", t, func() { 47 ct := cvtesting.Test{} 48 ctx, cancel := ct.SetUp(t) 49 defer cancel() 50 51 gis := GerritIntegrationServer{} 52 53 const host = "chromium" 54 const hostReview = host + "-review.googlesource.com" 55 gc := &apiv0pb.GerritChange{ 56 Host: hostReview, 57 Change: 1, 58 Patchset: 39, 59 } 60 61 ctx = auth.WithState(ctx, &authtest.FakeState{ 62 Identity: "user:admin@example.com", 63 IdentityGroups: []string{acls.V0APIAllowGroup, common.InstantTriggerDogfooderGroup}, 64 UserExtra: &gerritauth.AssertedInfo{ 65 Change: gerritauth.AssertedChange{ 66 Host: host, 67 ChangeNumber: 1, 68 }, 69 User: gerritauth.AssertedUser{ 70 AccountID: 12345, 71 PreferredEmail: "admin@example.com", 72 }, 73 }, 74 }) 75 76 Convey("w/o access", func() { 77 ctx = auth.WithState(ctx, &authtest.FakeState{ 78 Identity: "anonymous:anonymous", 79 }) 80 _, err := gis.GetCLRunInfo(ctx, &apiv0pb.GetCLRunInfoRequest{GerritChange: gc}) 81 So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied) 82 }) 83 84 Convey("w/o access but with JWT", func() { 85 ctx = auth.WithState(ctx, &authtest.FakeState{ 86 Identity: "anonymous:anonymous", 87 UserExtra: &gerritauth.AssertedInfo{ 88 Change: gerritauth.AssertedChange{ 89 Host: host, 90 ChangeNumber: 1, 91 }, 92 User: gerritauth.AssertedUser{ 93 AccountID: 12345, 94 PreferredEmail: "admin@example.com", 95 }, 96 }, 97 }) 98 ct.ResetMockedAuthDB(ctx) 99 ct.AddMember("admin@example.com", common.InstantTriggerDogfooderGroup) 100 _, err := gis.GetCLRunInfo(ctx, &apiv0pb.GetCLRunInfoRequest{GerritChange: gc}) 101 // NotFound because we haven't put anything in the datastore yet. 102 So(grpcutil.Code(err), ShouldEqual, codes.NotFound) 103 }) 104 105 Convey("w/ access but no JWT", func() { 106 ctx = auth.WithState(ctx, &authtest.FakeState{ 107 Identity: "user:admin@example.com", 108 IdentityGroups: []string{acls.V0APIAllowGroup}, 109 }) 110 _, err := gis.GetCLRunInfo(ctx, &apiv0pb.GetCLRunInfoRequest{GerritChange: gc}) 111 // NotFound because we haven't put anything in the datastore yet. 112 So(grpcutil.Code(err), ShouldEqual, codes.NotFound) 113 }) 114 115 Convey("w/ an invalid Gerrit Change", func() { 116 invalidGc := &apiv0pb.GerritChange{ 117 Host: "bad/host.example.com", 118 Change: 1, 119 Patchset: 39, 120 } 121 _, err := gis.GetCLRunInfo(ctx, &apiv0pb.GetCLRunInfoRequest{GerritChange: invalidGc}) 122 So(grpcutil.Code(err), ShouldEqual, codes.InvalidArgument) 123 }) 124 125 Convey("w/ a Valid but missing Gerrit Change", func() { 126 _, err := gis.GetCLRunInfo(ctx, &apiv0pb.GetCLRunInfoRequest{GerritChange: gc}) 127 So(grpcutil.Code(err), ShouldEqual, codes.NotFound) 128 }) 129 130 Convey("w/ JWT change differing from Gerrit Change", func() { 131 ctx = auth.WithState(ctx, &authtest.FakeState{ 132 Identity: "user:admin@example.com", 133 IdentityGroups: []string{acls.V0APIAllowGroup}, 134 UserExtra: &gerritauth.AssertedInfo{ 135 Change: gerritauth.AssertedChange{ 136 Host: "other-host", 137 ChangeNumber: 1, 138 }, 139 }, 140 }) 141 _, err := gis.GetCLRunInfo(ctx, &apiv0pb.GetCLRunInfoRequest{GerritChange: gc}) 142 So(grpcutil.Code(err), ShouldEqual, codes.InvalidArgument) 143 }) 144 145 // Add example data for tests below. 146 147 const owner = "owner@example.com" 148 149 // addRunAndGetRunInfo populates the datastore with an example Run associated with the change 150 // and returns the expected RunInfo. 151 addRunAndGetRunInfo := func(c *apiv0pb.GerritChange) *apiv0pb.GetCLRunInfoResponse_RunInfo { 152 cl := changelist.MustGobID(c.Host, c.Change).MustCreateIfNotExists(ctx) 153 epoch := testclock.TestRecentTimeUTC.Truncate(time.Millisecond) 154 rid := common.RunID("prj/123-deadbeef") 155 r := &run.Run{ 156 ID: rid, 157 Status: run.Status_RUNNING, 158 CreateTime: epoch, 159 StartTime: epoch.Add(time.Second), 160 UpdateTime: epoch.Add(time.Minute), 161 EndTime: epoch.Add(time.Hour), 162 Owner: "user:foo@example.org", 163 CLs: common.MakeCLIDs(int64(cl.ID)), 164 Mode: run.FullRun, 165 RootCL: cl.ID, 166 } 167 rcl := &run.RunCL{ 168 Run: datastore.MakeKey(ctx, common.RunKind, string(r.ID)), 169 ID: cl.ID, IndexedID: cl.ID, 170 ExternalID: cl.ExternalID, 171 Detail: &changelist.Snapshot{ 172 Patchset: 39, 173 }, 174 } 175 So(datastore.Put(ctx, rcl), ShouldBeNil) 176 So(datastore.Put(ctx, r), ShouldBeNil) 177 return &apiv0pb.GetCLRunInfoResponse_RunInfo{ 178 Id: fmt.Sprintf("projects/%s/runs/%s", rid.LUCIProject(), rid.Inner()), 179 CreateTime: timestamppb.New(r.CreateTime), 180 StartTime: timestamppb.New(r.StartTime), 181 Mode: string(r.Mode), 182 OriginChange: c, 183 } 184 } 185 186 // setSnapshot sets a CL's snapshot. 187 setSnapshot := func(cl *changelist.CL, gc *apiv0pb.GerritChange, deps []*changelist.Dep) { 188 cl.Snapshot = &changelist.Snapshot{ 189 Kind: &changelist.Snapshot_Gerrit{ 190 Gerrit: &changelist.Gerrit{ 191 Host: gc.Host, 192 Info: &gerritpb.ChangeInfo{ 193 Owner: &gerritpb.AccountInfo{ 194 Email: owner, 195 }, 196 Number: gc.Change, 197 Status: gerritpb.ChangeStatus_NEW, 198 }, 199 }, 200 }, 201 Patchset: gc.Patchset, 202 Deps: deps, 203 } 204 } 205 206 // putWithDeps stores a GerritChange and its dependencies in datastore. 207 putWithDeps := func(change *apiv0pb.GerritChange, depChanges []*apiv0pb.GerritChange) { 208 // Add deps. 209 deps := make([]*changelist.Dep, len(depChanges)) 210 for i, dc := range depChanges { 211 eid := changelist.MustGobID(dc.Host, dc.Change) 212 depCl := eid.MustCreateIfNotExists(ctx) 213 setSnapshot(depCl, dc, nil) 214 So(datastore.Put(ctx, depCl), ShouldBeNil) 215 deps[i] = &changelist.Dep{ 216 Clid: int64(depCl.ID), 217 } 218 } 219 220 // Add the CL itself. 221 eid := changelist.MustGobID(change.Host, change.Change) 222 cl := eid.MustCreateIfNotExists(ctx) 223 setSnapshot(cl, change, deps) 224 So(datastore.Put(ctx, cl), ShouldBeNil) 225 } 226 227 Convey("DepChangeInfos w/ valid Gerrit Change and no deps", func() { 228 putWithDeps(gc, nil) 229 230 resp, err := gis.GetCLRunInfo(ctx, &apiv0pb.GetCLRunInfoRequest{GerritChange: gc}) 231 So(err, ShouldBeNil) 232 So(resp.DepChangeInfos, ShouldBeEmpty) 233 }) 234 235 Convey("DepChangeInfos w/ valid Gerrit Change and deps", func() { 236 deps := []*apiv0pb.GerritChange{ 237 { 238 Host: hostReview, 239 Change: 2, 240 Patchset: 1, 241 }, 242 { 243 Host: hostReview, 244 Change: 3, 245 Patchset: 1, 246 }, 247 } 248 putWithDeps(gc, deps) 249 // Add an ongoing run to the first dep. 250 runInfo := addRunAndGetRunInfo(deps[0]) 251 252 resp, err := gis.GetCLRunInfo(ctx, &apiv0pb.GetCLRunInfoRequest{GerritChange: gc}) 253 So(err, ShouldBeNil) 254 So(resp.DepChangeInfos, ShouldResemble, []*apiv0pb.GetCLRunInfoResponse_DepChangeInfo{ 255 { 256 GerritChange: deps[0], 257 ChangeOwner: owner, 258 Runs: []*apiv0pb.GetCLRunInfoResponse_RunInfo{runInfo}, 259 }, 260 { 261 GerritChange: deps[1], 262 ChangeOwner: owner, 263 Runs: []*apiv0pb.GetCLRunInfoResponse_RunInfo{}, 264 }, 265 }) 266 267 Convey("skip submitted dep", func() { 268 cl, err := changelist.MustGobID(deps[0].GetHost(), deps[0].GetChange()).Load(ctx) 269 So(err, ShouldBeNil) 270 cl.Snapshot.GetGerrit().GetInfo().Status = gerritpb.ChangeStatus_MERGED 271 So(datastore.Put(ctx, cl), ShouldBeNil) 272 resp, err := gis.GetCLRunInfo(ctx, &apiv0pb.GetCLRunInfoRequest{GerritChange: gc}) 273 So(err, ShouldBeNil) 274 So(resp.DepChangeInfos, ShouldResemble, []*apiv0pb.GetCLRunInfoResponse_DepChangeInfo{ 275 { 276 GerritChange: deps[1], 277 ChangeOwner: owner, 278 Runs: []*apiv0pb.GetCLRunInfoResponse_RunInfo{}, 279 }, 280 }) 281 }) 282 Convey("return empty response for non-dogfooder", func() { 283 ct.ResetMockedAuthDB(ctx) 284 resp, err := gis.GetCLRunInfo(ctx, &apiv0pb.GetCLRunInfoRequest{GerritChange: gc}) 285 So(err, ShouldBeNil) 286 So(resp.GetRunsAsOrigin(), ShouldBeEmpty) 287 So(resp.GetRunsAsDep(), ShouldBeEmpty) 288 So(resp.GetDepChangeInfos(), ShouldBeEmpty) 289 }) 290 Convey("return empty response if user email is missing in jwt", func() { 291 ctx = auth.WithState(ctx, &authtest.FakeState{ 292 Identity: "user:admin@example.com", 293 IdentityGroups: []string{acls.V0APIAllowGroup, common.InstantTriggerDogfooderGroup}, 294 UserExtra: &gerritauth.AssertedInfo{ 295 Change: gerritauth.AssertedChange{ 296 Host: host, 297 ChangeNumber: 1, 298 }, 299 }, 300 }) 301 resp, err := gis.GetCLRunInfo(ctx, &apiv0pb.GetCLRunInfoRequest{GerritChange: gc}) 302 So(err, ShouldBeNil) 303 So(resp.GetRunsAsOrigin(), ShouldBeEmpty) 304 So(resp.GetRunsAsDep(), ShouldBeEmpty) 305 So(resp.GetDepChangeInfos(), ShouldBeEmpty) 306 }) 307 }) 308 }) 309 }