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

     1  // Copyright 2021 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 admin
    16  
    17  import (
    18  	"context"
    19  	"encoding/hex"
    20  	"fmt"
    21  	"testing"
    22  	"time"
    23  
    24  	"google.golang.org/protobuf/proto"
    25  	"google.golang.org/protobuf/types/known/timestamppb"
    26  
    27  	"go.chromium.org/luci/gae/service/datastore"
    28  	"go.chromium.org/luci/server/auth"
    29  	"go.chromium.org/luci/server/auth/authtest"
    30  
    31  	cfgpb "go.chromium.org/luci/cv/api/config/v2"
    32  	"go.chromium.org/luci/cv/internal/changelist"
    33  	"go.chromium.org/luci/cv/internal/common"
    34  	"go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest"
    35  	"go.chromium.org/luci/cv/internal/cvtesting"
    36  	"go.chromium.org/luci/cv/internal/gerrit/poller"
    37  	"go.chromium.org/luci/cv/internal/prjmanager"
    38  	"go.chromium.org/luci/cv/internal/prjmanager/prjpb"
    39  	adminpb "go.chromium.org/luci/cv/internal/rpc/admin/api"
    40  	"go.chromium.org/luci/cv/internal/run"
    41  	"go.chromium.org/luci/cv/internal/run/eventpb"
    42  
    43  	. "github.com/smartystreets/goconvey/convey"
    44  
    45  	"go.chromium.org/luci/common/clock/testclock"
    46  	"go.chromium.org/luci/common/data/stringset"
    47  	. "go.chromium.org/luci/common/testing/assertions"
    48  )
    49  
    50  func TestGetProject(t *testing.T) {
    51  	t.Parallel()
    52  
    53  	Convey("GetProject works", t, func() {
    54  		ct := cvtesting.Test{}
    55  		ctx, cancel := ct.SetUp(t)
    56  		defer cancel()
    57  
    58  		const lProject = "luci"
    59  		a := AdminServer{}
    60  
    61  		Convey("without access", func() {
    62  			ctx = auth.WithState(ctx, &authtest.FakeState{
    63  				Identity: "anonymous:anonymous",
    64  			})
    65  			_, err := a.GetProject(ctx, &adminpb.GetProjectRequest{Project: lProject})
    66  			So(err, ShouldBeRPCPermissionDenied)
    67  		})
    68  
    69  		Convey("with access", func() {
    70  			ctx = auth.WithState(ctx, &authtest.FakeState{
    71  				Identity:       "user:admin@example.com",
    72  				IdentityGroups: []string{allowGroup},
    73  			})
    74  			Convey("not exists", func() {
    75  				_, err := a.GetProject(ctx, &adminpb.GetProjectRequest{Project: lProject})
    76  				So(err, ShouldBeRPCNotFound)
    77  			})
    78  		})
    79  	})
    80  }
    81  
    82  func TestGetProjectLogs(t *testing.T) {
    83  	t.Parallel()
    84  
    85  	Convey("GetProjectLogs works", t, func() {
    86  		ct := cvtesting.Test{}
    87  		ctx, cancel := ct.SetUp(t)
    88  		defer cancel()
    89  
    90  		const lProject = "luci"
    91  		a := AdminServer{}
    92  
    93  		Convey("without access", func() {
    94  			ctx = auth.WithState(ctx, &authtest.FakeState{
    95  				Identity: "anonymous:anonymous",
    96  			})
    97  			_, err := a.GetProjectLogs(ctx, &adminpb.GetProjectLogsRequest{Project: lProject})
    98  			So(err, ShouldBeRPCPermissionDenied)
    99  		})
   100  
   101  		Convey("with access", func() {
   102  			ctx = auth.WithState(ctx, &authtest.FakeState{
   103  				Identity:       "user:admin@example.com",
   104  				IdentityGroups: []string{allowGroup},
   105  			})
   106  			Convey("nothing", func() {
   107  				resp, err := a.GetProjectLogs(ctx, &adminpb.GetProjectLogsRequest{Project: lProject})
   108  				So(err, ShouldBeNil)
   109  				So(resp.GetLogs(), ShouldHaveLength, 0)
   110  			})
   111  		})
   112  	})
   113  }
   114  
   115  func TestGetRun(t *testing.T) {
   116  	t.Parallel()
   117  
   118  	Convey("GetRun works", t, func() {
   119  		ct := cvtesting.Test{}
   120  		ctx, cancel := ct.SetUp(t)
   121  		defer cancel()
   122  
   123  		const rid = "proj/123-deadbeef"
   124  		So(datastore.Put(ctx, &run.Run{ID: rid}), ShouldBeNil)
   125  
   126  		a := AdminServer{}
   127  
   128  		Convey("without access", func() {
   129  			ctx = auth.WithState(ctx, &authtest.FakeState{
   130  				Identity: "anonymous:anonymous",
   131  			})
   132  			_, err := a.GetRun(ctx, &adminpb.GetRunRequest{Run: rid})
   133  			So(err, ShouldBeRPCPermissionDenied)
   134  		})
   135  
   136  		Convey("with access", func() {
   137  			ctx = auth.WithState(ctx, &authtest.FakeState{
   138  				Identity:       "user:admin@example.com",
   139  				IdentityGroups: []string{allowGroup},
   140  			})
   141  			Convey("not exists", func() {
   142  				_, err := a.GetRun(ctx, &adminpb.GetRunRequest{Run: rid + "cafe"})
   143  				So(err, ShouldBeRPCNotFound)
   144  			})
   145  			Convey("exists", func() {
   146  				_, err := a.GetRun(ctx, &adminpb.GetRunRequest{Run: rid})
   147  				So(err, ShouldBeRPCOK)
   148  			})
   149  		})
   150  	})
   151  }
   152  
   153  func TestGetCL(t *testing.T) {
   154  	t.Parallel()
   155  
   156  	Convey("GetCL works", t, func() {
   157  		ct := cvtesting.Test{}
   158  		ctx, cancel := ct.SetUp(t)
   159  		defer cancel()
   160  
   161  		a := AdminServer{}
   162  
   163  		Convey("without access", func() {
   164  			ctx = auth.WithState(ctx, &authtest.FakeState{
   165  				Identity: "anonymous:anonymous",
   166  			})
   167  			_, err := a.GetCL(ctx, &adminpb.GetCLRequest{Id: 123})
   168  			So(err, ShouldBeRPCPermissionDenied)
   169  		})
   170  
   171  		Convey("with access", func() {
   172  			ctx = auth.WithState(ctx, &authtest.FakeState{
   173  				Identity:       "user:admin@example.com",
   174  				IdentityGroups: []string{allowGroup},
   175  			})
   176  			So(datastore.Put(ctx, &changelist.CL{ID: 123, ExternalID: changelist.MustGobID("x-review", 44)}), ShouldBeNil)
   177  			resp, err := a.GetCL(ctx, &adminpb.GetCLRequest{Id: 123})
   178  			So(err, ShouldBeNil)
   179  			So(resp.GetExternalId(), ShouldEqual, "gerrit/x-review/44")
   180  		})
   181  	})
   182  }
   183  
   184  func TestGetPoller(t *testing.T) {
   185  	t.Parallel()
   186  
   187  	Convey("GetPoller works", t, func() {
   188  		ct := cvtesting.Test{}
   189  		ctx, cancel := ct.SetUp(t)
   190  		defer cancel()
   191  
   192  		const lProject = "luci"
   193  		a := AdminServer{}
   194  
   195  		Convey("without access", func() {
   196  			ctx = auth.WithState(ctx, &authtest.FakeState{
   197  				Identity: "anonymous:anonymous",
   198  			})
   199  			_, err := a.GetPoller(ctx, &adminpb.GetPollerRequest{Project: lProject})
   200  			So(err, ShouldBeRPCPermissionDenied)
   201  		})
   202  
   203  		Convey("with access", func() {
   204  			ctx = auth.WithState(ctx, &authtest.FakeState{
   205  				Identity:       "user:admin@example.com",
   206  				IdentityGroups: []string{allowGroup},
   207  			})
   208  			now := ct.Clock.Now().UTC().Truncate(time.Second)
   209  			So(datastore.Put(ctx, &poller.State{
   210  				LuciProject: lProject,
   211  				UpdateTime:  now,
   212  			}), ShouldBeNil)
   213  			resp, err := a.GetPoller(ctx, &adminpb.GetPollerRequest{Project: lProject})
   214  			So(err, ShouldBeNil)
   215  			So(resp.GetUpdateTime().AsTime(), ShouldResemble, now)
   216  		})
   217  	})
   218  }
   219  
   220  func TestSearchRuns(t *testing.T) {
   221  	t.Parallel()
   222  
   223  	Convey("SearchRuns works", t, func() {
   224  		ct := cvtesting.Test{}
   225  		ctx, cancel := ct.SetUp(t)
   226  		defer cancel()
   227  
   228  		const lProject = "proj"
   229  		const earlierID = lProject + "/124-earlier-has-higher-number"
   230  		const laterID = lProject + "/123-later-has-lower-number"
   231  		const diffProjectID = "diff/333-foo-bar"
   232  
   233  		idsOf := func(resp *adminpb.RunsResponse) []string {
   234  			out := make([]string, len(resp.GetRuns()))
   235  			for i, r := range resp.GetRuns() {
   236  				out[i] = r.GetId()
   237  			}
   238  			return out
   239  		}
   240  
   241  		assertOrdered := func(resp *adminpb.RunsResponse) {
   242  			for i := 1; i < len(resp.GetRuns()); i++ {
   243  				curr := resp.GetRuns()[i]
   244  				prev := resp.GetRuns()[i-1]
   245  
   246  				currID := common.RunID(curr.GetId())
   247  				prevID := common.RunID(prev.GetId())
   248  				So(prevID, ShouldNotResemble, currID)
   249  
   250  				currTS := curr.GetCreateTime().AsTime()
   251  				prevTS := prev.GetCreateTime().AsTime()
   252  				if !prevTS.Equal(currTS) {
   253  					So(prevTS, ShouldHappenAfter, currTS)
   254  					continue
   255  				}
   256  				// Same TS.
   257  
   258  				if prevID.LUCIProject() != currID.LUCIProject() {
   259  					So(prevID.LUCIProject(), ShouldBeLessThan, currID.LUCIProject())
   260  					continue
   261  				}
   262  				// Same LUCI project.
   263  				So(prevID, ShouldBeLessThan, currID)
   264  			}
   265  		}
   266  
   267  		a := AdminServer{}
   268  
   269  		fetchAll := func(ctx context.Context, origReq *adminpb.SearchRunsRequest) *adminpb.RunsResponse {
   270  			var out *adminpb.RunsResponse
   271  			// Allow orig request to have page token already.
   272  			nextPageToken := origReq.GetPageToken()
   273  			for out == nil || nextPageToken != "" {
   274  				req := proto.Clone(origReq).(*adminpb.SearchRunsRequest)
   275  				req.PageToken = nextPageToken
   276  				resp, err := a.SearchRuns(ctx, req)
   277  				So(err, ShouldBeNil)
   278  				assertOrdered(resp)
   279  				if out == nil {
   280  					out = resp
   281  				} else {
   282  					out.Runs = append(out.Runs, resp.GetRuns()...)
   283  				}
   284  				nextPageToken = resp.GetNextPageToken()
   285  			}
   286  			return out
   287  		}
   288  
   289  		Convey("without access", func() {
   290  			ctx = auth.WithState(ctx, &authtest.FakeState{
   291  				Identity: "anonymous:anonymous",
   292  			})
   293  			_, err := a.SearchRuns(ctx, &adminpb.SearchRunsRequest{Project: lProject})
   294  			So(err, ShouldBeRPCPermissionDenied)
   295  		})
   296  
   297  		Convey("with access", func() {
   298  			ctx = auth.WithState(ctx, &authtest.FakeState{
   299  				Identity:       "user:admin@example.com",
   300  				IdentityGroups: []string{allowGroup},
   301  			})
   302  			Convey("no runs exist", func() {
   303  				resp, err := a.SearchRuns(ctx, &adminpb.SearchRunsRequest{Project: lProject})
   304  				So(err, ShouldBeNil)
   305  				So(resp.GetRuns(), ShouldHaveLength, 0)
   306  			})
   307  			Convey("two runs of the same project", func() {
   308  				So(datastore.Put(ctx, &run.Run{ID: earlierID}, &run.Run{ID: laterID}), ShouldBeNil)
   309  				resp, err := a.SearchRuns(ctx, &adminpb.SearchRunsRequest{Project: lProject})
   310  				So(err, ShouldBeNil)
   311  				So(idsOf(resp), ShouldResemble, []string{laterID, earlierID})
   312  
   313  				Convey("with page size exactly 2", func() {
   314  					req := &adminpb.SearchRunsRequest{Project: lProject, PageSize: 2}
   315  					resp1, err := a.SearchRuns(ctx, req)
   316  					So(err, ShouldBeNil)
   317  					So(idsOf(resp1), ShouldResemble, []string{laterID, earlierID})
   318  
   319  					req.PageToken = resp1.GetNextPageToken()
   320  					resp2, err := a.SearchRuns(ctx, req)
   321  					So(err, ShouldBeNil)
   322  					So(idsOf(resp2), ShouldBeEmpty)
   323  				})
   324  
   325  				Convey("with page size of 1", func() {
   326  					req := &adminpb.SearchRunsRequest{Project: lProject, PageSize: 1}
   327  					resp1, err := a.SearchRuns(ctx, req)
   328  					So(err, ShouldBeNil)
   329  					So(idsOf(resp1), ShouldResemble, []string{laterID})
   330  
   331  					req.PageToken = resp1.GetNextPageToken()
   332  					resp2, err := a.SearchRuns(ctx, req)
   333  					So(err, ShouldBeNil)
   334  					So(idsOf(resp2), ShouldResemble, []string{earlierID})
   335  
   336  					req.PageToken = resp2.GetNextPageToken()
   337  					resp3, err := a.SearchRuns(ctx, req)
   338  					So(err, ShouldBeNil)
   339  					So(idsOf(resp3), ShouldBeEmpty)
   340  					So(resp3.GetNextPageToken(), ShouldBeEmpty)
   341  				})
   342  			})
   343  
   344  			Convey("filtering", func() {
   345  				const gHost = "r-review.example.com"
   346  				cl1 := changelist.MustGobID(gHost, 1).MustCreateIfNotExists(ctx)
   347  				cl2 := changelist.MustGobID(gHost, 2).MustCreateIfNotExists(ctx)
   348  
   349  				So(datastore.Put(ctx,
   350  					&run.Run{
   351  						ID:     earlierID,
   352  						Status: run.Status_CANCELLED,
   353  						CLs:    common.MakeCLIDs(1, 2),
   354  					},
   355  					&run.RunCL{Run: datastore.MakeKey(ctx, common.RunKind, earlierID), ID: cl1.ID, IndexedID: cl1.ID},
   356  					&run.RunCL{Run: datastore.MakeKey(ctx, common.RunKind, earlierID), ID: cl2.ID, IndexedID: cl2.ID},
   357  
   358  					&run.Run{
   359  						ID:     laterID,
   360  						Status: run.Status_RUNNING,
   361  						CLs:    common.MakeCLIDs(1),
   362  					},
   363  					&run.RunCL{Run: datastore.MakeKey(ctx, common.RunKind, laterID), ID: cl1.ID, IndexedID: cl1.ID},
   364  				), ShouldBeNil)
   365  
   366  				Convey("exact", func() {
   367  					resp, err := a.SearchRuns(ctx, &adminpb.SearchRunsRequest{
   368  						Project: lProject,
   369  						Status:  run.Status_CANCELLED,
   370  					})
   371  					So(err, ShouldBeNil)
   372  					So(idsOf(resp), ShouldResemble, []string{earlierID})
   373  				})
   374  
   375  				Convey("ended", func() {
   376  					resp, err := a.SearchRuns(ctx, &adminpb.SearchRunsRequest{
   377  						Project: lProject,
   378  						Status:  run.Status_ENDED_MASK,
   379  					})
   380  					So(err, ShouldBeNil)
   381  					So(idsOf(resp), ShouldResemble, []string{earlierID})
   382  				})
   383  
   384  				Convey("with CL", func() {
   385  					resp, err := a.SearchRuns(ctx, &adminpb.SearchRunsRequest{
   386  						Cl: &adminpb.GetCLRequest{ExternalId: string(cl2.ExternalID)},
   387  					})
   388  					So(err, ShouldBeNil)
   389  					So(idsOf(resp), ShouldResemble, []string{earlierID})
   390  				})
   391  
   392  				Convey("with CL and run status", func() {
   393  					resp, err := a.SearchRuns(ctx, &adminpb.SearchRunsRequest{
   394  						Cl:     &adminpb.GetCLRequest{ExternalId: string(cl1.ExternalID)},
   395  						Status: run.Status_ENDED_MASK,
   396  					})
   397  					So(err, ShouldBeNil)
   398  					So(idsOf(resp), ShouldResemble, []string{earlierID})
   399  				})
   400  
   401  				Convey("with CL + paging", func() {
   402  					req := &adminpb.SearchRunsRequest{
   403  						Cl:       &adminpb.GetCLRequest{ExternalId: string(cl1.ExternalID)},
   404  						PageSize: 1,
   405  					}
   406  					total := fetchAll(ctx, req)
   407  					So(idsOf(total), ShouldResemble, []string{laterID, earlierID})
   408  				})
   409  
   410  				Convey("with CL across projects and paging", func() {
   411  					// Make CL1 included in 3 runs: diffProjectID, laterID, earlierID.
   412  					So(datastore.Put(ctx,
   413  						&run.Run{
   414  							ID:     diffProjectID,
   415  							Status: run.Status_RUNNING,
   416  							CLs:    common.MakeCLIDs(1),
   417  						},
   418  						&run.RunCL{Run: datastore.MakeKey(ctx, common.RunKind, diffProjectID), ID: cl1.ID, IndexedID: cl1.ID},
   419  					), ShouldBeNil)
   420  
   421  					req := &adminpb.SearchRunsRequest{
   422  						Cl:       &adminpb.GetCLRequest{ExternalId: string(cl1.ExternalID)},
   423  						PageSize: 2,
   424  					}
   425  					total := fetchAll(ctx, req)
   426  					So(idsOf(total), ShouldResemble, []string{diffProjectID, laterID, earlierID})
   427  				})
   428  
   429  				Convey("with CL and project and paging", func() {
   430  					// Make CL1 included in 3 runs: diffProjectID, laterID, earlierID.
   431  					So(datastore.Put(ctx,
   432  						&run.Run{
   433  							ID:     diffProjectID,
   434  							Status: run.Status_RUNNING,
   435  							CLs:    common.MakeCLIDs(1),
   436  						},
   437  						&run.RunCL{Run: datastore.MakeKey(ctx, common.RunKind, diffProjectID), ID: cl1.ID, IndexedID: cl1.ID},
   438  					), ShouldBeNil)
   439  
   440  					req := &adminpb.SearchRunsRequest{
   441  						Cl:       &adminpb.GetCLRequest{ExternalId: string(cl1.ExternalID)},
   442  						Project:  lProject,
   443  						PageSize: 1,
   444  					}
   445  					total := fetchAll(ctx, req)
   446  					So(idsOf(total), ShouldResemble, []string{laterID, earlierID})
   447  				})
   448  			})
   449  
   450  			Convey("runs aross all projects", func() {
   451  				// Choose epoch such that inverseTS of Run ID has zeros at the end for
   452  				// ease of debugging.
   453  				epoch := testclock.TestRecentTimeUTC.Truncate(time.Millisecond).Add(498490844 * time.Millisecond)
   454  
   455  				makeRun := func(project, createdAfter, remainder int) *run.Run {
   456  					remBytes, err := hex.DecodeString(fmt.Sprintf("%02d", remainder))
   457  					if err != nil {
   458  						panic(err)
   459  					}
   460  					createTime := epoch.Add(time.Duration(createdAfter) * time.Millisecond)
   461  					id := common.MakeRunID(
   462  						fmt.Sprintf("p%02d", project),
   463  						createTime,
   464  						1,
   465  						remBytes,
   466  					)
   467  					return &run.Run{ID: id, CreateTime: createTime}
   468  				}
   469  
   470  				placeRuns := func(runs ...*run.Run) []string {
   471  					ids := make([]string, len(runs))
   472  					So(datastore.Put(ctx, runs), ShouldBeNil)
   473  					projects := stringset.New(10)
   474  					for i, r := range runs {
   475  						projects.Add(r.ID.LUCIProject())
   476  						ids[i] = string(r.ID)
   477  					}
   478  					for p := range projects {
   479  						prjcfgtest.Create(ctx, p, &cfgpb.Config{})
   480  					}
   481  					return ids
   482  				}
   483  
   484  				Convey("just one project", func() {
   485  					expIDs := placeRuns(
   486  						// project, creationDelay, hash.
   487  						makeRun(1, 90, 11),
   488  						makeRun(1, 80, 12),
   489  						makeRun(1, 70, 13),
   490  						makeRun(1, 70, 14),
   491  						makeRun(1, 70, 15),
   492  						makeRun(1, 60, 11),
   493  						makeRun(1, 60, 12),
   494  					)
   495  					Convey("without paging", func() {
   496  						resp, err := a.SearchRuns(ctx, &adminpb.SearchRunsRequest{PageSize: 128})
   497  						So(err, ShouldBeNil)
   498  						assertOrdered(resp)
   499  						So(idsOf(resp), ShouldResemble, expIDs)
   500  					})
   501  					Convey("with paging", func() {
   502  						total := fetchAll(ctx, &adminpb.SearchRunsRequest{PageSize: 2})
   503  						assertOrdered(total)
   504  						So(idsOf(total), ShouldResemble, expIDs)
   505  					})
   506  				})
   507  
   508  				Convey("two projects with overlapping timestaps", func() {
   509  					expIDs := placeRuns(
   510  						// project, creationDelay, hash.
   511  						makeRun(1, 90, 11),
   512  						makeRun(1, 80, 12), // same creationDelay, but smaller project
   513  						makeRun(2, 80, 12),
   514  						makeRun(2, 80, 13), // later hash
   515  						makeRun(2, 70, 13),
   516  						makeRun(1, 60, 12),
   517  					)
   518  					Convey("without paging", func() {
   519  						resp, err := a.SearchRuns(ctx, &adminpb.SearchRunsRequest{PageSize: 128})
   520  						So(err, ShouldBeNil)
   521  						assertOrdered(resp)
   522  						So(idsOf(resp), ShouldResemble, expIDs)
   523  					})
   524  					Convey("with paging", func() {
   525  						total := fetchAll(ctx, &adminpb.SearchRunsRequest{PageSize: 1})
   526  						assertOrdered(total)
   527  						So(idsOf(total), ShouldResemble, expIDs)
   528  					})
   529  				})
   530  
   531  				Convey("large scale", func() {
   532  					var runs []*run.Run
   533  					for p := 50; p < 60; p++ {
   534  						// Distribute # of Runs unevenly across projects.
   535  						for c := p - 49; c > 0; c-- {
   536  							// Create some Runs with the same start timestamp.
   537  							for r := 90; r <= 90+c%3; r++ {
   538  								runs = append(runs, makeRun(p, c, r))
   539  							}
   540  						}
   541  					}
   542  					placeRuns(runs...)
   543  
   544  					Convey("without paging", func() {
   545  						So(len(runs), ShouldBeLessThan, 128)
   546  						resp, err := a.SearchRuns(ctx, &adminpb.SearchRunsRequest{PageSize: 128})
   547  						So(err, ShouldBeNil)
   548  						assertOrdered(resp)
   549  						So(resp.GetRuns(), ShouldHaveLength, len(runs))
   550  					})
   551  					Convey("with paging", func() {
   552  						total := fetchAll(ctx, &adminpb.SearchRunsRequest{PageSize: 8})
   553  						assertOrdered(total)
   554  						So(total.GetRuns(), ShouldHaveLength, len(runs))
   555  					})
   556  				})
   557  			})
   558  		})
   559  	})
   560  }
   561  
   562  func TestDeleteProjectEvents(t *testing.T) {
   563  	t.Parallel()
   564  
   565  	Convey("DeleteProjectEvents works", t, func() {
   566  		ct := cvtesting.Test{}
   567  		ctx, cancel := ct.SetUp(t)
   568  		defer cancel()
   569  
   570  		const lProject = "luci"
   571  		a := AdminServer{}
   572  
   573  		Convey("without access", func() {
   574  			ctx = auth.WithState(ctx, &authtest.FakeState{
   575  				Identity: "anonymous:anonymous",
   576  			})
   577  			_, err := a.DeleteProjectEvents(ctx, &adminpb.DeleteProjectEventsRequest{Project: lProject, Limit: 10})
   578  			So(err, ShouldBeRPCPermissionDenied)
   579  		})
   580  
   581  		Convey("with access", func() {
   582  			ctx = auth.WithState(ctx, &authtest.FakeState{
   583  				Identity:       "user:admin@example.com",
   584  				IdentityGroups: []string{allowGroup},
   585  			})
   586  			pm := prjmanager.NewNotifier(ct.TQDispatcher)
   587  
   588  			So(pm.Poke(ctx, lProject), ShouldBeNil)
   589  			So(pm.UpdateConfig(ctx, lProject), ShouldBeNil)
   590  			So(pm.Poke(ctx, lProject), ShouldBeNil)
   591  
   592  			Convey("All", func() {
   593  				resp, err := a.DeleteProjectEvents(ctx, &adminpb.DeleteProjectEventsRequest{Project: lProject, Limit: 10})
   594  				So(err, ShouldBeNil)
   595  				So(resp.GetEvents(), ShouldResemble, map[string]int64{"*prjpb.Event_Poke": 2, "*prjpb.Event_NewConfig": 1})
   596  			})
   597  
   598  			Convey("Limited", func() {
   599  				resp, err := a.DeleteProjectEvents(ctx, &adminpb.DeleteProjectEventsRequest{Project: lProject, Limit: 2})
   600  				So(err, ShouldBeNil)
   601  				sum := int64(0)
   602  				for _, v := range resp.GetEvents() {
   603  					sum += v
   604  				}
   605  				So(sum, ShouldEqual, 2)
   606  			})
   607  		})
   608  	})
   609  }
   610  
   611  func TestRefreshProjectCLs(t *testing.T) {
   612  	t.Parallel()
   613  
   614  	Convey("RefreshProjectCLs works", t, func() {
   615  		ct := cvtesting.Test{}
   616  		ctx, cancel := ct.SetUp(t)
   617  		defer cancel()
   618  
   619  		const lProject = "luci"
   620  		a := AdminServer{
   621  			clUpdater:  changelist.NewUpdater(ct.TQDispatcher, nil),
   622  			pmNotifier: prjmanager.NewNotifier(ct.TQDispatcher),
   623  		}
   624  
   625  		Convey("without access", func() {
   626  			ctx = auth.WithState(ctx, &authtest.FakeState{
   627  				Identity: "anonymous:anonymous",
   628  			})
   629  			_, err := a.RefreshProjectCLs(ctx, &adminpb.RefreshProjectCLsRequest{Project: lProject})
   630  			So(err, ShouldBeRPCPermissionDenied)
   631  		})
   632  
   633  		Convey("with access", func() {
   634  			ctx = auth.WithState(ctx, &authtest.FakeState{
   635  				Identity:       "user:admin@example.com",
   636  				IdentityGroups: []string{allowGroup},
   637  			})
   638  
   639  			So(datastore.Put(ctx, &prjmanager.Project{
   640  				ID: lProject,
   641  				State: &prjpb.PState{
   642  					Pcls: []*prjpb.PCL{
   643  						{Clid: 1},
   644  					},
   645  				},
   646  			}), ShouldBeNil)
   647  			cl := changelist.CL{
   648  				ID:         1,
   649  				EVersion:   4,
   650  				ExternalID: changelist.MustGobID("x-review.example.com", 55),
   651  			}
   652  			So(datastore.Put(ctx, &cl), ShouldBeNil)
   653  
   654  			resp, err := a.RefreshProjectCLs(ctx, &adminpb.RefreshProjectCLsRequest{Project: lProject})
   655  			So(err, ShouldBeNil)
   656  			So(resp.GetClVersions(), ShouldResemble, map[int64]int64{1: 4})
   657  			scheduledIDs := stringset.New(1)
   658  			for _, p := range ct.TQ.Tasks().Payloads() {
   659  				if t, ok := p.(*changelist.UpdateCLTask); ok {
   660  					scheduledIDs.Add(t.GetExternalId())
   661  				}
   662  			}
   663  			So(scheduledIDs.ToSortedSlice(), ShouldResemble, []string{string(cl.ExternalID)})
   664  		})
   665  	})
   666  }
   667  
   668  func TestSendProjectEvent(t *testing.T) {
   669  	t.Parallel()
   670  
   671  	Convey("SendProjectEvent works", t, func() {
   672  		ct := cvtesting.Test{}
   673  		ctx, cancel := ct.SetUp(t)
   674  		defer cancel()
   675  
   676  		const lProject = "luci"
   677  		a := AdminServer{
   678  			pmNotifier: prjmanager.NewNotifier(ct.TQDispatcher),
   679  		}
   680  
   681  		Convey("without access", func() {
   682  			ctx = auth.WithState(ctx, &authtest.FakeState{
   683  				Identity: "anonymous:anonymous",
   684  			})
   685  			_, err := a.SendProjectEvent(ctx, &adminpb.SendProjectEventRequest{Project: lProject})
   686  			So(err, ShouldBeRPCPermissionDenied)
   687  		})
   688  
   689  		Convey("with access", func() {
   690  			ctx = auth.WithState(ctx, &authtest.FakeState{
   691  				Identity:       "user:admin@example.com",
   692  				IdentityGroups: []string{allowGroup},
   693  			})
   694  			Convey("not exists", func() {
   695  				_, err := a.SendProjectEvent(ctx, &adminpb.SendProjectEventRequest{
   696  					Project: lProject,
   697  					Event:   &prjpb.Event{Event: &prjpb.Event_Poke{Poke: &prjpb.Poke{}}},
   698  				})
   699  				So(err, ShouldBeRPCNotFound)
   700  			})
   701  		})
   702  	})
   703  }
   704  
   705  func TestSendRunEvent(t *testing.T) {
   706  	t.Parallel()
   707  
   708  	Convey("SendRunEvent works", t, func() {
   709  		ct := cvtesting.Test{}
   710  		ctx, cancel := ct.SetUp(t)
   711  		defer cancel()
   712  
   713  		const rid = "proj/123-deadbeef"
   714  		a := AdminServer{
   715  			runNotifier: run.NewNotifier(ct.TQDispatcher),
   716  		}
   717  
   718  		Convey("without access", func() {
   719  			ctx = auth.WithState(ctx, &authtest.FakeState{
   720  				Identity: "anonymous:anonymous",
   721  			})
   722  			_, err := a.SendRunEvent(ctx, &adminpb.SendRunEventRequest{Run: rid})
   723  			So(err, ShouldBeRPCPermissionDenied)
   724  		})
   725  
   726  		Convey("with access", func() {
   727  			ctx = auth.WithState(ctx, &authtest.FakeState{
   728  				Identity:       "user:admin@example.com",
   729  				IdentityGroups: []string{allowGroup},
   730  			})
   731  			Convey("not exists", func() {
   732  				_, err := a.SendRunEvent(ctx, &adminpb.SendRunEventRequest{
   733  					Run:   rid,
   734  					Event: &eventpb.Event{Event: &eventpb.Event_Poke{Poke: &eventpb.Poke{}}},
   735  				})
   736  				So(err, ShouldBeRPCNotFound)
   737  			})
   738  		})
   739  	})
   740  }
   741  
   742  func TestScheduleTask(t *testing.T) {
   743  	t.Parallel()
   744  
   745  	Convey("ScheduleTask works", t, func() {
   746  		ct := cvtesting.Test{}
   747  		ctx, cancel := ct.SetUp(t)
   748  		defer cancel()
   749  
   750  		const lProject = "infra"
   751  		a := AdminServer{
   752  			tqDispatcher: ct.TQDispatcher,
   753  			pmNotifier:   prjmanager.NewNotifier(ct.TQDispatcher),
   754  		}
   755  		req := &adminpb.ScheduleTaskRequest{
   756  			DeduplicationKey: "key",
   757  			ManageProject: &prjpb.ManageProjectTask{
   758  				Eta:         timestamppb.New(ct.Clock.Now()),
   759  				LuciProject: lProject,
   760  			},
   761  		}
   762  		reqTrans := &adminpb.ScheduleTaskRequest{
   763  			KickManageProject: &prjpb.KickManageProjectTask{
   764  				Eta:         timestamppb.New(ct.Clock.Now()),
   765  				LuciProject: lProject,
   766  			},
   767  		}
   768  
   769  		Convey("without access", func() {
   770  			ctx = auth.WithState(ctx, &authtest.FakeState{
   771  				Identity: "anonymous:anonymous",
   772  			})
   773  			_, err := a.ScheduleTask(ctx, req)
   774  			So(err, ShouldBeRPCPermissionDenied)
   775  		})
   776  
   777  		Convey("with access", func() {
   778  			ctx = auth.WithState(ctx, &authtest.FakeState{
   779  				Identity:       "user:admin@example.com",
   780  				IdentityGroups: []string{allowGroup},
   781  			})
   782  			Convey("OK", func() {
   783  				Convey("Non-Transactional", func() {
   784  					_, err := a.ScheduleTask(ctx, req)
   785  					So(err, ShouldBeNil)
   786  					So(ct.TQ.Tasks().Payloads(), ShouldResembleProto, []proto.Message{
   787  						req.GetManageProject(),
   788  					})
   789  				})
   790  				Convey("Transactional", func() {
   791  					_, err := a.ScheduleTask(ctx, reqTrans)
   792  					So(err, ShouldBeNil)
   793  					So(ct.TQ.Tasks().Payloads(), ShouldResembleProto, []proto.Message{
   794  						reqTrans.GetKickManageProject(),
   795  					})
   796  				})
   797  			})
   798  			Convey("InvalidArgument", func() {
   799  				Convey("Missing payload", func() {
   800  					req.ManageProject = nil
   801  					_, err := a.ScheduleTask(ctx, req)
   802  					So(err, ShouldBeRPCInvalidArgument, "none given")
   803  				})
   804  				Convey("Two payloads", func() {
   805  					req.KickManageProject = reqTrans.GetKickManageProject()
   806  					_, err := a.ScheduleTask(ctx, req)
   807  					So(err, ShouldBeRPCInvalidArgument, "but 2+ given")
   808  				})
   809  				Convey("Trans + DeduplicationKey is not allwoed", func() {
   810  					reqTrans.DeduplicationKey = "beef"
   811  					_, err := a.ScheduleTask(ctx, reqTrans)
   812  					So(err, ShouldBeRPCInvalidArgument, `"KickManageProjectTask" is transactional`)
   813  				})
   814  			})
   815  		})
   816  	})
   817  }