blob: 64cde3c9503d8e3778fc1b89cf20ecb5d36685bf [file] [log] [blame]
// Copyright 2021 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package admin
import (
"context"
"encoding/hex"
"fmt"
"testing"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/grpc/grpcutil"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/auth/authtest"
cfgpb "go.chromium.org/luci/cv/api/config/v2"
"go.chromium.org/luci/cv/internal/changelist"
"go.chromium.org/luci/cv/internal/common"
"go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest"
"go.chromium.org/luci/cv/internal/cvtesting"
"go.chromium.org/luci/cv/internal/gerrit/poller"
"go.chromium.org/luci/cv/internal/gerrit/updater"
"go.chromium.org/luci/cv/internal/gerrit/updater/updatertest"
"go.chromium.org/luci/cv/internal/prjmanager"
"go.chromium.org/luci/cv/internal/prjmanager/prjpb"
adminpb "go.chromium.org/luci/cv/internal/rpc/admin/api"
"go.chromium.org/luci/cv/internal/run"
"go.chromium.org/luci/cv/internal/run/eventpb"
. "github.com/smartystreets/goconvey/convey"
"go.chromium.org/luci/common/clock/testclock"
"go.chromium.org/luci/common/data/stringset"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestParseGerritURL(t *testing.T) {
t.Parallel()
Convey("parseGerritURL works", t, func() {
eid, err := parseGerritURL("https://crrev.com/i/12/34")
So(err, ShouldBeNil)
So(string(eid), ShouldEqual, "gerrit/chrome-internal-review.googlesource.com/12")
eid, err = parseGerritURL("https://crrev.com/c/12")
So(err, ShouldBeNil)
So(string(eid), ShouldEqual, "gerrit/chromium-review.googlesource.com/12")
eid, err = parseGerritURL("https://chromium-review.googlesource.com/1541677")
So(err, ShouldBeNil)
So(string(eid), ShouldEqual, "gerrit/chromium-review.googlesource.com/1541677")
eid, err = parseGerritURL("https://pdfium-review.googlesource.com/c/33")
So(err, ShouldBeNil)
So(string(eid), ShouldEqual, "gerrit/pdfium-review.googlesource.com/33")
eid, err = parseGerritURL("https://chromium-review.googlesource.com/#/c/infra/luci/luci-go/+/1541677/7")
So(err, ShouldBeNil)
So(string(eid), ShouldEqual, "gerrit/chromium-review.googlesource.com/1541677")
eid, err = parseGerritURL("https://chromium-review.googlesource.com/c/infra/luci/luci-go/+/2652967")
So(err, ShouldBeNil)
So(string(eid), ShouldEqual, "gerrit/chromium-review.googlesource.com/2652967")
eid, err = parseGerritURL("chromium-review.googlesource.com/2652967")
So(err, ShouldBeNil)
So(string(eid), ShouldEqual, "gerrit/chromium-review.googlesource.com/2652967")
})
}
func TestGetProject(t *testing.T) {
t.Parallel()
Convey("GetProject works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
const lProject = "luci"
d := AdminServer{}
Convey("without access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "anonymous:anonymous",
})
_, err := d.GetProject(ctx, &adminpb.GetProjectRequest{Project: lProject})
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
})
Convey("with access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "user:admin@example.com",
IdentityGroups: []string{allowGroup},
})
Convey("not exists", func() {
_, err := d.GetProject(ctx, &adminpb.GetProjectRequest{Project: lProject})
So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
})
})
})
}
func TestGetProjectLogs(t *testing.T) {
t.Parallel()
Convey("GetProjectLogs works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
const lProject = "luci"
d := AdminServer{}
Convey("without access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "anonymous:anonymous",
})
_, err := d.GetProjectLogs(ctx, &adminpb.GetProjectLogsRequest{Project: lProject})
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
})
Convey("with access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "user:admin@example.com",
IdentityGroups: []string{allowGroup},
})
Convey("nothing", func() {
resp, err := d.GetProjectLogs(ctx, &adminpb.GetProjectLogsRequest{Project: lProject})
So(err, ShouldBeNil)
So(resp.GetLogs(), ShouldHaveLength, 0)
})
})
})
}
func TestGetRun(t *testing.T) {
t.Parallel()
Convey("GetRun works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
const rid = "proj/123-deadbeef"
So(datastore.Put(ctx, &run.Run{ID: rid}), ShouldBeNil)
d := AdminServer{}
Convey("without access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "anonymous:anonymous",
})
_, err := d.GetRun(ctx, &adminpb.GetRunRequest{Run: rid})
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
Convey("HACK(crbug/1245864): loading `infra` Runs is allowed to everyone", func() {
const infraID = "infra/123-deadbeef"
So(datastore.Put(ctx, &run.Run{ID: infraID}), ShouldBeNil)
_, err := d.GetRun(ctx, &adminpb.GetRunRequest{Run: infraID})
So(grpcutil.Code(err), ShouldEqual, codes.OK)
})
})
Convey("with access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "user:admin@example.com",
IdentityGroups: []string{allowGroup},
})
Convey("not exists", func() {
_, err := d.GetRun(ctx, &adminpb.GetRunRequest{Run: rid + "cafe"})
So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
})
Convey("exists", func() {
_, err := d.GetRun(ctx, &adminpb.GetRunRequest{Run: rid})
So(grpcutil.Code(err), ShouldEqual, codes.OK)
})
})
})
}
func TestGetCL(t *testing.T) {
t.Parallel()
Convey("GetCL works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
d := AdminServer{}
Convey("without access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "anonymous:anonymous",
})
_, err := d.GetCL(ctx, &adminpb.GetCLRequest{Id: 123})
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
})
Convey("with access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "user:admin@example.com",
IdentityGroups: []string{allowGroup},
})
So(datastore.Put(ctx, &changelist.CL{ID: 123, ExternalID: changelist.MustGobID("x-review", 44)}), ShouldBeNil)
resp, err := d.GetCL(ctx, &adminpb.GetCLRequest{Id: 123})
So(err, ShouldBeNil)
So(resp.GetExternalId(), ShouldEqual, "gerrit/x-review/44")
})
})
}
func TestGetPoller(t *testing.T) {
t.Parallel()
Convey("GetPoller works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
const lProject = "luci"
d := AdminServer{}
Convey("without access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "anonymous:anonymous",
})
_, err := d.GetPoller(ctx, &adminpb.GetPollerRequest{Project: lProject})
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
})
Convey("with access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "user:admin@example.com",
IdentityGroups: []string{allowGroup},
})
now := ct.Clock.Now().UTC().Truncate(time.Second)
So(datastore.Put(ctx, &poller.State{
LuciProject: lProject,
UpdateTime: now,
}), ShouldBeNil)
resp, err := d.GetPoller(ctx, &adminpb.GetPollerRequest{Project: lProject})
So(err, ShouldBeNil)
So(resp.GetUpdateTime().AsTime(), ShouldResemble, now)
})
})
}
func TestSearchRuns(t *testing.T) {
t.Parallel()
Convey("SearchRuns works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
const lProject = "proj"
const earlierID = lProject + "/124-earlier-has-higher-number"
const laterID = lProject + "/123-later-has-lower-number"
const diffProjectID = "diff/333-foo-bar"
idsOf := func(resp *adminpb.RunsResponse) []string {
out := make([]string, len(resp.GetRuns()))
for i, r := range resp.GetRuns() {
out[i] = r.GetId()
}
return out
}
assertOrdered := func(resp *adminpb.RunsResponse) {
for i := 1; i < len(resp.GetRuns()); i++ {
curr := resp.GetRuns()[i]
prev := resp.GetRuns()[i-1]
currID := common.RunID(curr.GetId())
prevID := common.RunID(prev.GetId())
So(prevID, ShouldNotResemble, currID)
currTS := curr.GetCreateTime().AsTime()
prevTS := prev.GetCreateTime().AsTime()
if !prevTS.Equal(currTS) {
So(prevTS, ShouldHappenAfter, currTS)
continue
}
// Same TS.
if prevID.LUCIProject() != currID.LUCIProject() {
So(prevID.LUCIProject(), ShouldBeLessThan, currID.LUCIProject())
continue
}
// Same LUCI project.
So(prevID, ShouldBeLessThan, currID)
}
}
d := AdminServer{}
fetchAll := func(ctx context.Context, origReq *adminpb.SearchRunsRequest) *adminpb.RunsResponse {
var out *adminpb.RunsResponse
// Allow orig request to have page token already.
nextPageToken := origReq.GetPageToken()
for out == nil || nextPageToken != "" {
req := proto.Clone(origReq).(*adminpb.SearchRunsRequest)
req.PageToken = nextPageToken
resp, err := d.SearchRuns(ctx, req)
So(err, ShouldBeNil)
assertOrdered(resp)
if out == nil {
out = resp
} else {
out.Runs = append(out.Runs, resp.GetRuns()...)
}
nextPageToken = resp.GetNextPageToken()
}
return out
}
Convey("without access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "anonymous:anonymous",
})
_, err := d.SearchRuns(ctx, &adminpb.SearchRunsRequest{Project: lProject})
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
})
Convey("with access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "user:admin@example.com",
IdentityGroups: []string{allowGroup},
})
Convey("no runs exist", func() {
resp, err := d.SearchRuns(ctx, &adminpb.SearchRunsRequest{Project: lProject})
So(err, ShouldBeNil)
So(resp.GetRuns(), ShouldHaveLength, 0)
})
Convey("two runs of the same project", func() {
So(datastore.Put(ctx, &run.Run{ID: earlierID}, &run.Run{ID: laterID}), ShouldBeNil)
resp, err := d.SearchRuns(ctx, &adminpb.SearchRunsRequest{Project: lProject})
So(err, ShouldBeNil)
So(idsOf(resp), ShouldResemble, []string{laterID, earlierID})
Convey("with page size exactly 2", func() {
req := &adminpb.SearchRunsRequest{Project: lProject, PageSize: 2}
resp1, err := d.SearchRuns(ctx, req)
So(err, ShouldBeNil)
So(idsOf(resp1), ShouldResemble, []string{laterID, earlierID})
req.PageToken = resp1.GetNextPageToken()
resp2, err := d.SearchRuns(ctx, req)
So(err, ShouldBeNil)
So(idsOf(resp2), ShouldBeEmpty)
})
Convey("with page size of 1", func() {
req := &adminpb.SearchRunsRequest{Project: lProject, PageSize: 1}
resp1, err := d.SearchRuns(ctx, req)
So(err, ShouldBeNil)
So(idsOf(resp1), ShouldResemble, []string{laterID})
req.PageToken = resp1.GetNextPageToken()
resp2, err := d.SearchRuns(ctx, req)
So(err, ShouldBeNil)
So(idsOf(resp2), ShouldResemble, []string{earlierID})
req.PageToken = resp2.GetNextPageToken()
resp3, err := d.SearchRuns(ctx, req)
So(err, ShouldBeNil)
So(idsOf(resp3), ShouldBeEmpty)
So(resp3.GetNextPageToken(), ShouldBeEmpty)
})
})
Convey("filtering", func() {
const gHost = "r-review.example.com"
cl1 := changelist.MustGobID(gHost, 1).MustCreateIfNotExists(ctx)
cl2 := changelist.MustGobID(gHost, 2).MustCreateIfNotExists(ctx)
So(datastore.Put(ctx,
&run.Run{
ID: earlierID,
Status: run.Status_CANCELLED,
CLs: common.MakeCLIDs(1, 2),
},
&run.RunCL{Run: datastore.MakeKey(ctx, run.RunKind, earlierID), ID: cl1.ID, IndexedID: cl1.ID},
&run.RunCL{Run: datastore.MakeKey(ctx, run.RunKind, earlierID), ID: cl2.ID, IndexedID: cl2.ID},
&run.Run{
ID: laterID,
Status: run.Status_RUNNING,
CLs: common.MakeCLIDs(1),
},
&run.RunCL{Run: datastore.MakeKey(ctx, run.RunKind, laterID), ID: cl1.ID, IndexedID: cl1.ID},
), ShouldBeNil)
Convey("exact", func() {
resp, err := d.SearchRuns(ctx, &adminpb.SearchRunsRequest{
Project: lProject,
Status: run.Status_CANCELLED,
})
So(err, ShouldBeNil)
So(idsOf(resp), ShouldResemble, []string{earlierID})
})
Convey("ended", func() {
resp, err := d.SearchRuns(ctx, &adminpb.SearchRunsRequest{
Project: lProject,
Status: run.Status_ENDED_MASK,
})
So(err, ShouldBeNil)
So(idsOf(resp), ShouldResemble, []string{earlierID})
})
Convey("with CL", func() {
resp, err := d.SearchRuns(ctx, &adminpb.SearchRunsRequest{
Cl: &adminpb.GetCLRequest{ExternalId: string(cl2.ExternalID)},
})
So(err, ShouldBeNil)
So(idsOf(resp), ShouldResemble, []string{earlierID})
})
Convey("with CL and run status", func() {
resp, err := d.SearchRuns(ctx, &adminpb.SearchRunsRequest{
Cl: &adminpb.GetCLRequest{ExternalId: string(cl1.ExternalID)},
Status: run.Status_ENDED_MASK,
})
So(err, ShouldBeNil)
So(idsOf(resp), ShouldResemble, []string{earlierID})
})
Convey("with CL + paging", func() {
req := &adminpb.SearchRunsRequest{
Cl: &adminpb.GetCLRequest{ExternalId: string(cl1.ExternalID)},
PageSize: 1,
}
total := fetchAll(ctx, req)
So(idsOf(total), ShouldResemble, []string{laterID, earlierID})
})
Convey("with CL across projects and paging", func() {
// Make CL1 included in 3 runs: diffProjectID, laterID, earlierID.
So(datastore.Put(ctx,
&run.Run{
ID: diffProjectID,
Status: run.Status_RUNNING,
CLs: common.MakeCLIDs(1),
},
&run.RunCL{Run: datastore.MakeKey(ctx, run.RunKind, diffProjectID), ID: cl1.ID, IndexedID: cl1.ID},
), ShouldBeNil)
req := &adminpb.SearchRunsRequest{
Cl: &adminpb.GetCLRequest{ExternalId: string(cl1.ExternalID)},
PageSize: 2,
}
total := fetchAll(ctx, req)
So(idsOf(total), ShouldResemble, []string{diffProjectID, laterID, earlierID})
})
Convey("with CL and project and paging", func() {
// Make CL1 included in 3 runs: diffProjectID, laterID, earlierID.
So(datastore.Put(ctx,
&run.Run{
ID: diffProjectID,
Status: run.Status_RUNNING,
CLs: common.MakeCLIDs(1),
},
&run.RunCL{Run: datastore.MakeKey(ctx, run.RunKind, diffProjectID), ID: cl1.ID, IndexedID: cl1.ID},
), ShouldBeNil)
req := &adminpb.SearchRunsRequest{
Cl: &adminpb.GetCLRequest{ExternalId: string(cl1.ExternalID)},
Project: lProject,
PageSize: 1,
}
total := fetchAll(ctx, req)
So(idsOf(total), ShouldResemble, []string{laterID, earlierID})
})
})
Convey("runs aross all projects", func() {
// Choose epoch such that inverseTS of Run ID has zeros at the end for
// ease of debugging.
epoch := testclock.TestRecentTimeUTC.Truncate(time.Millisecond).Add(498490844 * time.Millisecond)
makeRun := func(project, createdAfter, remainder int) *run.Run {
remBytes, err := hex.DecodeString(fmt.Sprintf("%02d", remainder))
if err != nil {
panic(err)
}
createTime := epoch.Add(time.Duration(createdAfter) * time.Millisecond)
id := common.MakeRunID(
fmt.Sprintf("p%02d", project),
createTime,
1,
remBytes,
)
return &run.Run{ID: id, CreateTime: createTime}
}
placeRuns := func(runs ...*run.Run) []string {
ids := make([]string, len(runs))
So(datastore.Put(ctx, runs), ShouldBeNil)
projects := stringset.New(10)
for i, r := range runs {
projects.Add(r.ID.LUCIProject())
ids[i] = string(r.ID)
}
for p := range projects {
prjcfgtest.Create(ctx, p, &cfgpb.Config{})
}
return ids
}
Convey("just one project", func() {
expIDs := placeRuns(
// project, creationDelay, hash.
makeRun(1, 90, 11),
makeRun(1, 80, 12),
makeRun(1, 70, 13),
makeRun(1, 70, 14),
makeRun(1, 70, 15),
makeRun(1, 60, 11),
makeRun(1, 60, 12),
)
Convey("without paging", func() {
resp, err := d.SearchRuns(ctx, &adminpb.SearchRunsRequest{PageSize: 128})
So(err, ShouldBeNil)
assertOrdered(resp)
So(idsOf(resp), ShouldResemble, expIDs)
})
Convey("with paging", func() {
total := fetchAll(ctx, &adminpb.SearchRunsRequest{PageSize: 2})
assertOrdered(total)
So(idsOf(total), ShouldResemble, expIDs)
})
})
Convey("two projects with overlapping timestaps", func() {
expIDs := placeRuns(
// project, creationDelay, hash.
makeRun(1, 90, 11),
makeRun(1, 80, 12), // same creationDelay, but smaller project
makeRun(2, 80, 12),
makeRun(2, 80, 13), // later hash
makeRun(2, 70, 13),
makeRun(1, 60, 12),
)
Convey("without paging", func() {
resp, err := d.SearchRuns(ctx, &adminpb.SearchRunsRequest{PageSize: 128})
So(err, ShouldBeNil)
assertOrdered(resp)
So(idsOf(resp), ShouldResemble, expIDs)
})
Convey("with paging", func() {
total := fetchAll(ctx, &adminpb.SearchRunsRequest{PageSize: 1})
assertOrdered(total)
So(idsOf(total), ShouldResemble, expIDs)
})
})
Convey("large scale", func() {
var runs []*run.Run
for p := 50; p < 60; p++ {
// Distribute # of Runs unevenly across projects.
for c := p - 49; c > 0; c-- {
// Create some Runs with the same start timestamp.
for r := 90; r <= 90+c%3; r++ {
runs = append(runs, makeRun(p, c, r))
}
}
}
placeRuns(runs...)
Convey("without paging", func() {
So(len(runs), ShouldBeLessThan, 128)
resp, err := d.SearchRuns(ctx, &adminpb.SearchRunsRequest{PageSize: 128})
So(err, ShouldBeNil)
assertOrdered(resp)
So(resp.GetRuns(), ShouldHaveLength, len(runs))
})
Convey("with paging", func() {
total := fetchAll(ctx, &adminpb.SearchRunsRequest{PageSize: 8})
assertOrdered(total)
So(total.GetRuns(), ShouldHaveLength, len(runs))
})
})
})
})
})
}
func TestDeleteProjectEvents(t *testing.T) {
t.Parallel()
Convey("DeleteProjectEvents works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
const lProject = "luci"
d := AdminServer{}
Convey("without access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "anonymous:anonymous",
})
_, err := d.DeleteProjectEvents(ctx, &adminpb.DeleteProjectEventsRequest{Project: lProject, Limit: 10})
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
})
Convey("with access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "user:admin@example.com",
IdentityGroups: []string{allowGroup},
})
pm := prjmanager.NewNotifier(ct.TQDispatcher)
So(pm.NotifyCLUpdated(ctx, lProject, common.CLID(1), 1), ShouldBeNil)
So(pm.NotifyCLUpdated(ctx, lProject, common.CLID(2), 1), ShouldBeNil)
So(pm.UpdateConfig(ctx, lProject), ShouldBeNil)
Convey("All", func() {
resp, err := d.DeleteProjectEvents(ctx, &adminpb.DeleteProjectEventsRequest{Project: lProject, Limit: 10})
So(err, ShouldBeNil)
So(resp.GetEvents(), ShouldResemble, map[string]int64{"*prjpb.Event_ClUpdated": 2, "*prjpb.Event_NewConfig": 1})
})
Convey("Limited", func() {
resp, err := d.DeleteProjectEvents(ctx, &adminpb.DeleteProjectEventsRequest{Project: lProject, Limit: 2})
So(err, ShouldBeNil)
sum := int64(0)
for _, v := range resp.GetEvents() {
sum += v
}
So(sum, ShouldEqual, 2)
})
})
})
}
func TestRefreshProjectCLs(t *testing.T) {
t.Parallel()
Convey("RefreshProjectCLs works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
const lProject = "luci"
d := AdminServer{
GerritUpdater: updater.New(ct.TQDispatcher, ct.GFactory(), nil),
PMNotifier: prjmanager.NewNotifier(ct.TQDispatcher),
}
Convey("without access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "anonymous:anonymous",
})
_, err := d.RefreshProjectCLs(ctx, &adminpb.RefreshProjectCLsRequest{Project: lProject})
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
})
Convey("with access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "user:admin@example.com",
IdentityGroups: []string{allowGroup},
})
So(datastore.Put(ctx, &prjmanager.Project{
ID: lProject,
State: &prjpb.PState{
Pcls: []*prjpb.PCL{
{Clid: 1},
},
},
}), ShouldBeNil)
So(datastore.Put(ctx, &changelist.CL{
ID: 1,
EVersion: 4,
ExternalID: changelist.MustGobID("x-review.example.com", 55),
}), ShouldBeNil)
resp, err := d.RefreshProjectCLs(ctx, &adminpb.RefreshProjectCLsRequest{Project: lProject})
So(err, ShouldBeNil)
So(resp.GetClVersions(), ShouldResemble, map[int64]int64{1: 4})
So(updatertest.ChangeNumbers(ct.TQ.Tasks()), ShouldResemble, []int64{55})
})
})
}
func TestSendProjectEvent(t *testing.T) {
t.Parallel()
Convey("SendProjectEvent works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
const lProject = "luci"
d := AdminServer{
PMNotifier: prjmanager.NewNotifier(ct.TQDispatcher),
}
Convey("without access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "anonymous:anonymous",
})
_, err := d.SendProjectEvent(ctx, &adminpb.SendProjectEventRequest{Project: lProject})
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
})
Convey("with access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "user:admin@example.com",
IdentityGroups: []string{allowGroup},
})
Convey("not exists", func() {
_, err := d.SendProjectEvent(ctx, &adminpb.SendProjectEventRequest{
Project: lProject,
Event: &prjpb.Event{Event: &prjpb.Event_Poke{Poke: &prjpb.Poke{}}},
})
So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
})
})
})
}
func TestSendRunEvent(t *testing.T) {
t.Parallel()
Convey("SendRunEvent works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
const rid = "proj/123-deadbeef"
d := AdminServer{
RunNotifier: run.NewNotifier(ct.TQDispatcher),
}
Convey("without access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "anonymous:anonymous",
})
_, err := d.SendRunEvent(ctx, &adminpb.SendRunEventRequest{Run: rid})
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
})
Convey("with access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "user:admin@example.com",
IdentityGroups: []string{allowGroup},
})
Convey("not exists", func() {
_, err := d.SendRunEvent(ctx, &adminpb.SendRunEventRequest{
Run: rid,
Event: &eventpb.Event{Event: &eventpb.Event_Poke{Poke: &eventpb.Poke{}}},
})
So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
})
})
})
}
func TestScheduleTask(t *testing.T) {
t.Parallel()
Convey("ScheduleTask works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
const lProject = "infra"
d := AdminServer{
TQDispatcher: ct.TQDispatcher,
PMNotifier: prjmanager.NewNotifier(ct.TQDispatcher),
}
req := &adminpb.ScheduleTaskRequest{
DeduplicationKey: "key",
ManageProject: &prjpb.ManageProjectTask{
Eta: timestamppb.New(ct.Clock.Now()),
LuciProject: lProject,
},
}
reqTrans := &adminpb.ScheduleTaskRequest{
KickManageProject: &prjpb.KickManageProjectTask{
Eta: timestamppb.New(ct.Clock.Now()),
LuciProject: lProject,
},
}
Convey("without access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "anonymous:anonymous",
})
_, err := d.ScheduleTask(ctx, req)
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
})
Convey("with access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "user:admin@example.com",
IdentityGroups: []string{allowGroup},
})
Convey("OK", func() {
Convey("Non-Transactional", func() {
_, err := d.ScheduleTask(ctx, req)
So(err, ShouldBeNil)
So(ct.TQ.Tasks().Payloads(), ShouldResembleProto, []proto.Message{
req.GetManageProject(),
})
})
Convey("Transactional", func() {
_, err := d.ScheduleTask(ctx, reqTrans)
So(err, ShouldBeNil)
So(ct.TQ.Tasks().Payloads(), ShouldResembleProto, []proto.Message{
reqTrans.GetKickManageProject(),
})
})
})
Convey("InvalidArgument", func() {
Convey("Missing payload", func() {
req.ManageProject = nil
_, err := d.ScheduleTask(ctx, req)
So(grpcutil.Code(err), ShouldEqual, codes.InvalidArgument)
So(err, ShouldErrLike, "none given")
})
Convey("Two payloads", func() {
req.KickManageProject = reqTrans.GetKickManageProject()
_, err := d.ScheduleTask(ctx, req)
So(grpcutil.Code(err), ShouldEqual, codes.InvalidArgument)
So(err, ShouldErrLike, "but 2+ given")
})
Convey("Trans + DeduplicationKey is not allwoed", func() {
reqTrans.DeduplicationKey = "beef"
_, err := d.ScheduleTask(ctx, reqTrans)
So(grpcutil.Code(err), ShouldEqual, codes.InvalidArgument)
So(err, ShouldErrLike, `"KickManageProjectTask" is transactional`)
})
})
})
})
}