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  }