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 }