go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/rpc/query_console_snapshots_test.go (about) 1 // Copyright 2023 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 "context" 19 "testing" 20 21 . "github.com/smartystreets/goconvey/convey" 22 "google.golang.org/grpc/codes" 23 24 "go.chromium.org/luci/auth/identity" 25 "go.chromium.org/luci/buildbucket/bbperms" 26 buildbucketpb "go.chromium.org/luci/buildbucket/proto" 27 . "go.chromium.org/luci/common/testing/assertions" 28 "go.chromium.org/luci/gae/impl/memory" 29 "go.chromium.org/luci/gae/service/datastore" 30 "go.chromium.org/luci/grpc/grpcutil" 31 "go.chromium.org/luci/milo/internal/model" 32 "go.chromium.org/luci/milo/internal/model/milostatus" 33 "go.chromium.org/luci/milo/internal/projectconfig" 34 "go.chromium.org/luci/milo/internal/testutils" 35 "go.chromium.org/luci/milo/internal/utils" 36 projectconfigpb "go.chromium.org/luci/milo/proto/projectconfig" 37 milopb "go.chromium.org/luci/milo/proto/v1" 38 "go.chromium.org/luci/server/auth" 39 "go.chromium.org/luci/server/auth/authtest" 40 "go.chromium.org/luci/server/auth/realms" 41 "go.chromium.org/luci/server/secrets" 42 ) 43 44 var projects = []*projectconfig.Project{ 45 { 46 ID: "allowed-project", 47 ACL: projectconfig.ACL{Identities: []identity.Identity{"user"}}, 48 }, 49 { 50 ID: "other-allowed-project", 51 ACL: projectconfig.ACL{Identities: []identity.Identity{"user"}}, 52 }, 53 { 54 ID: "project-with-external-ref", 55 ACL: projectconfig.ACL{Identities: []identity.Identity{"user"}}, 56 }, 57 } 58 59 var consoleDefs = []*projectconfigpb.Console{ 60 { 61 Realm: "allowed-project:@root", 62 Id: "con1", 63 Builders: []*projectconfigpb.Builder{ 64 { 65 Id: &buildbucketpb.BuilderID{ 66 Project: "allowed-project", 67 Bucket: "bucket", 68 Builder: "builder", 69 }, 70 }, 71 { 72 Id: &buildbucketpb.BuilderID{ 73 Project: "other-allowed-project", 74 Bucket: "bucket", 75 Builder: "builder", 76 }, 77 }, 78 { 79 Id: &buildbucketpb.BuilderID{ 80 Project: "forbidden-project", 81 Bucket: "bucket", 82 Builder: "builder", 83 }, 84 }, 85 }, 86 }, 87 { 88 Realm: "allowed-project:@root", 89 Id: "con2", 90 Builders: []*projectconfigpb.Builder{ 91 { 92 Id: &buildbucketpb.BuilderID{ 93 Project: "allowed-project", 94 Bucket: "bucket", 95 Builder: "builder", 96 }, 97 }, 98 { 99 Id: &buildbucketpb.BuilderID{ 100 Project: "allowed-project", 101 Bucket: "bucket", 102 Builder: "new-builder", 103 }, 104 }, 105 }, 106 }, 107 { 108 Realm: "allowed-project:@root", 109 Id: "con3", 110 Builders: []*projectconfigpb.Builder{ 111 { 112 Id: &buildbucketpb.BuilderID{ 113 Project: "other-allowed-project", 114 Bucket: "bucket", 115 Builder: "builder", 116 }, 117 }, 118 }, 119 }, 120 { 121 Realm: "other-allowed-project:@root", 122 Id: "con1", 123 Builders: []*projectconfigpb.Builder{ 124 { 125 Id: &buildbucketpb.BuilderID{ 126 Project: "allowed-project", 127 Bucket: "bucket", 128 Builder: "builder", 129 }, 130 }, 131 }, 132 }, 133 { 134 Realm: "forbidden-project:@root", 135 Id: "con1", 136 Builders: []*projectconfigpb.Builder{ 137 { 138 Id: &buildbucketpb.BuilderID{ 139 Project: "allowed-project", 140 Bucket: "bucket", 141 Builder: "builder", 142 }, 143 }, 144 }, 145 }, 146 { 147 Realm: "project-with-external-ref:@root", 148 Id: "con1", 149 ExternalProject: "allowed-project", 150 ExternalId: "con1", 151 }, 152 { 153 Realm: "project-with-external-ref:@root", 154 Id: "con2", 155 ExternalProject: "forbidden-project", 156 ExternalId: "con1", 157 }, 158 { 159 Realm: "project-with-external-ref:@root", 160 Id: "con3", 161 Builders: []*projectconfigpb.Builder{ 162 { 163 Id: &buildbucketpb.BuilderID{ 164 Project: "project-with-external-ref", 165 Bucket: "bucket", 166 Builder: "builder", 167 }, 168 }, 169 }, 170 }, 171 } 172 173 var builderSummaries = []*model.BuilderSummary{ 174 { 175 BuilderID: "buildbucket/luci.allowed-project.bucket/builder", 176 ProjectID: "allowed-project", 177 LastFinishedBuildID: "buildbucket/111111", 178 LastFinishedStatus: milostatus.InfraFailure, 179 }, 180 { 181 BuilderID: "buildbucket/luci.other-allowed-project.bucket/builder", 182 ProjectID: "other-allowed-project", 183 LastFinishedBuildID: "buildbucket/111112", 184 LastFinishedStatus: milostatus.Success, 185 }, 186 { 187 BuilderID: "buildbucket/luci.forbidden-project.bucket/builder", 188 ProjectID: "forbidden-project", 189 LastFinishedBuildID: "buildbucket/111113", 190 LastFinishedStatus: milostatus.Canceled, 191 }, 192 { 193 BuilderID: "buildbucket/luci.project-with-external-ref.bucket/builder", 194 ProjectID: "project-with-external-ref", 195 LastFinishedBuildID: "buildbucket/111114", 196 LastFinishedStatus: milostatus.Failure, 197 }, 198 } 199 200 var perms = []authtest.RealmPermission{ 201 { 202 Realm: "allowed-project:bucket", 203 Permission: bbperms.BuildsList, 204 }, 205 { 206 Realm: "other-allowed-project:bucket", 207 Permission: bbperms.BuildsList, 208 }, 209 { 210 Realm: "project-with-external-ref:bucket", 211 Permission: bbperms.BuildsList, 212 }, 213 } 214 215 func TestQueryConsoleSnapshots(t *testing.T) { 216 t.Parallel() 217 Convey(`TestQueryConsoleSnapshots`, t, func() { 218 ctx := memory.Use(context.Background()) 219 ctx = testutils.SetUpTestGlobalCache(ctx) 220 ctx = secrets.GeneratePrimaryTinkAEADForTest(ctx) 221 222 datastore.GetTestable(ctx).AutoIndex(true) 223 datastore.GetTestable(ctx).Consistent(true) 224 225 err := datastore.Put(ctx, projects) 226 So(err, ShouldBeNil) 227 228 // Transform & save console defs to datastore. 229 consoles := make([]*projectconfig.Console, 0, len(consoleDefs)) 230 for _, conDef := range consoleDefs { 231 proj, _ := realms.Split(conDef.Realm) 232 conID := projectconfig.ConsoleID{ 233 Project: proj, 234 ID: conDef.Id, 235 } 236 console := conID.SetID(ctx, nil) 237 console.Def = *conDef 238 console.Builders = make([]string, 0, len(conDef.Builders)) 239 for _, builder := range conDef.Builders { 240 legacyID := utils.LegacyBuilderIDString(builder.Id) 241 console.Builders = append(console.Builders, legacyID) 242 } 243 consoles = append(consoles, console) 244 } 245 err = datastore.Put(ctx, consoles) 246 So(err, ShouldBeNil) 247 248 err = datastore.Put(ctx, builderSummaries) 249 So(err, ShouldBeNil) 250 251 srv := &MiloInternalService{} 252 253 Convey(`e2e`, func() { 254 ctx := auth.WithState(ctx, &authtest.FakeState{Identity: "user", IdentityPermissions: perms}) 255 256 res, err := srv.QueryConsoleSnapshots(ctx, &milopb.QueryConsoleSnapshotsRequest{ 257 Predicate: &milopb.ConsolePredicate{ 258 Project: "allowed-project", 259 }, 260 PageSize: 2, 261 }) 262 So(err, ShouldBeNil) 263 So(res.Snapshots, ShouldResembleProto, []*milopb.ConsoleSnapshot{ 264 { 265 Console: &projectconfigpb.Console{ 266 Realm: "allowed-project:@root", 267 Id: "con1", 268 Builders: []*projectconfigpb.Builder{ 269 { 270 Id: &buildbucketpb.BuilderID{ 271 Project: "allowed-project", 272 Bucket: "bucket", 273 Builder: "builder", 274 }, 275 }, 276 { 277 Id: &buildbucketpb.BuilderID{ 278 Project: "other-allowed-project", 279 Bucket: "bucket", 280 Builder: "builder", 281 }, 282 }, 283 }, 284 }, 285 BuilderSnapshots: []*milopb.BuilderSnapshot{ 286 { 287 Builder: &buildbucketpb.BuilderID{ 288 Project: "allowed-project", 289 Bucket: "bucket", 290 Builder: "builder", 291 }, 292 Build: &buildbucketpb.Build{ 293 Id: 111111, 294 Builder: &buildbucketpb.BuilderID{ 295 Project: "allowed-project", 296 Bucket: "bucket", 297 Builder: "builder", 298 }, 299 Status: buildbucketpb.Status_INFRA_FAILURE, 300 }, 301 }, 302 { 303 Builder: &buildbucketpb.BuilderID{ 304 Project: "other-allowed-project", 305 Bucket: "bucket", 306 Builder: "builder", 307 }, 308 Build: &buildbucketpb.Build{ 309 Id: 111112, 310 Builder: &buildbucketpb.BuilderID{ 311 Project: "other-allowed-project", 312 Bucket: "bucket", 313 Builder: "builder", 314 }, 315 Status: buildbucketpb.Status_SUCCESS, 316 }, 317 }, 318 }, 319 }, 320 { 321 Console: &projectconfigpb.Console{ 322 Realm: "allowed-project:@root", 323 Id: "con2", 324 Builders: []*projectconfigpb.Builder{ 325 { 326 Id: &buildbucketpb.BuilderID{ 327 Project: "allowed-project", 328 Bucket: "bucket", 329 Builder: "builder", 330 }, 331 }, 332 { 333 Id: &buildbucketpb.BuilderID{ 334 Project: "allowed-project", 335 Bucket: "bucket", 336 Builder: "new-builder", 337 }, 338 }, 339 }, 340 }, 341 BuilderSnapshots: []*milopb.BuilderSnapshot{ 342 { 343 Builder: &buildbucketpb.BuilderID{ 344 Project: "allowed-project", 345 Bucket: "bucket", 346 Builder: "builder", 347 }, 348 Build: &buildbucketpb.Build{ 349 Id: 111111, 350 Builder: &buildbucketpb.BuilderID{ 351 Project: "allowed-project", 352 Bucket: "bucket", 353 Builder: "builder", 354 }, 355 Status: buildbucketpb.Status_INFRA_FAILURE, 356 }, 357 }, 358 { 359 Builder: &buildbucketpb.BuilderID{ 360 Project: "allowed-project", 361 Bucket: "bucket", 362 Builder: "new-builder", 363 }, 364 Build: nil, 365 }, 366 }, 367 }, 368 }) 369 So(res.NextPageToken, ShouldNotBeEmpty) 370 371 res, err = srv.QueryConsoleSnapshots(ctx, &milopb.QueryConsoleSnapshotsRequest{ 372 Predicate: &milopb.ConsolePredicate{ 373 Project: "allowed-project", 374 }, 375 PageSize: 2, 376 PageToken: res.NextPageToken, 377 }) 378 So(err, ShouldBeNil) 379 So(res.Snapshots, ShouldResembleProto, []*milopb.ConsoleSnapshot{ 380 { 381 Console: &projectconfigpb.Console{ 382 Realm: "allowed-project:@root", 383 Id: "con3", 384 Builders: []*projectconfigpb.Builder{ 385 { 386 Id: &buildbucketpb.BuilderID{ 387 Project: "other-allowed-project", 388 Bucket: "bucket", 389 Builder: "builder", 390 }, 391 }, 392 }, 393 }, 394 BuilderSnapshots: []*milopb.BuilderSnapshot{ 395 { 396 Builder: &buildbucketpb.BuilderID{ 397 Project: "other-allowed-project", 398 Bucket: "bucket", 399 Builder: "builder", 400 }, 401 Build: &buildbucketpb.Build{ 402 Id: 111112, 403 Builder: &buildbucketpb.BuilderID{ 404 Project: "other-allowed-project", 405 Bucket: "bucket", 406 Builder: "builder", 407 }, 408 Status: buildbucketpb.Status_SUCCESS, 409 }, 410 }, 411 }, 412 }, 413 }) 414 So(res.NextPageToken, ShouldBeEmpty) 415 }) 416 417 Convey(`query with builder predicate`, func() { 418 ctx := auth.WithState(ctx, &authtest.FakeState{Identity: "user", IdentityPermissions: perms}) 419 420 res, err := srv.QueryConsoleSnapshots(ctx, &milopb.QueryConsoleSnapshotsRequest{ 421 Predicate: &milopb.ConsolePredicate{ 422 Project: "allowed-project", 423 Builder: &buildbucketpb.BuilderID{ 424 Project: "other-allowed-project", 425 Bucket: "bucket", 426 Builder: "builder", 427 }, 428 }, 429 }) 430 So(err, ShouldBeNil) 431 So(res.Snapshots, ShouldResembleProto, []*milopb.ConsoleSnapshot{ 432 { 433 Console: &projectconfigpb.Console{ 434 Realm: "allowed-project:@root", 435 Id: "con1", 436 Builders: []*projectconfigpb.Builder{ 437 { 438 Id: &buildbucketpb.BuilderID{ 439 Project: "allowed-project", 440 Bucket: "bucket", 441 Builder: "builder", 442 }, 443 }, 444 { 445 Id: &buildbucketpb.BuilderID{ 446 Project: "other-allowed-project", 447 Bucket: "bucket", 448 Builder: "builder", 449 }, 450 }, 451 }, 452 }, 453 BuilderSnapshots: []*milopb.BuilderSnapshot{ 454 { 455 Builder: &buildbucketpb.BuilderID{ 456 Project: "allowed-project", 457 Bucket: "bucket", 458 Builder: "builder", 459 }, 460 Build: &buildbucketpb.Build{ 461 Id: 111111, 462 Builder: &buildbucketpb.BuilderID{ 463 Project: "allowed-project", 464 Bucket: "bucket", 465 Builder: "builder", 466 }, 467 Status: buildbucketpb.Status_INFRA_FAILURE, 468 }, 469 }, 470 { 471 Builder: &buildbucketpb.BuilderID{ 472 Project: "other-allowed-project", 473 Bucket: "bucket", 474 Builder: "builder", 475 }, 476 Build: &buildbucketpb.Build{ 477 Id: 111112, 478 Builder: &buildbucketpb.BuilderID{ 479 Project: "other-allowed-project", 480 Bucket: "bucket", 481 Builder: "builder", 482 }, 483 Status: buildbucketpb.Status_SUCCESS, 484 }, 485 }, 486 }, 487 }, 488 { 489 Console: &projectconfigpb.Console{ 490 Realm: "allowed-project:@root", 491 Id: "con3", 492 Builders: []*projectconfigpb.Builder{ 493 { 494 Id: &buildbucketpb.BuilderID{ 495 Project: "other-allowed-project", 496 Bucket: "bucket", 497 Builder: "builder", 498 }, 499 }, 500 }, 501 }, 502 BuilderSnapshots: []*milopb.BuilderSnapshot{ 503 { 504 Builder: &buildbucketpb.BuilderID{ 505 Project: "other-allowed-project", 506 Bucket: "bucket", 507 Builder: "builder", 508 }, 509 Build: &buildbucketpb.Build{ 510 Id: 111112, 511 Builder: &buildbucketpb.BuilderID{ 512 Project: "other-allowed-project", 513 Bucket: "bucket", 514 Builder: "builder", 515 }, 516 Status: buildbucketpb.Status_SUCCESS, 517 }, 518 }, 519 }, 520 }, 521 }) 522 So(res.NextPageToken, ShouldBeEmpty) 523 }) 524 525 Convey(`query forbidden project`, func() { 526 ctx := auth.WithState(ctx, &authtest.FakeState{Identity: "user", IdentityPermissions: perms}) 527 528 res, err := srv.QueryConsoleSnapshots(ctx, &milopb.QueryConsoleSnapshotsRequest{ 529 Predicate: &milopb.ConsolePredicate{ 530 Project: "forbidden-project", 531 }, 532 }) 533 So(err, ShouldNotBeNil) 534 So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied) 535 So(res, ShouldBeNil) 536 }) 537 538 Convey(`query forbidden project with builder predicate`, func() { 539 ctx := auth.WithState(ctx, &authtest.FakeState{Identity: "user", IdentityPermissions: perms}) 540 541 res, err := srv.QueryConsoleSnapshots(ctx, &milopb.QueryConsoleSnapshotsRequest{ 542 Predicate: &milopb.ConsolePredicate{ 543 Project: "allowed-project", 544 Builder: &buildbucketpb.BuilderID{ 545 Project: "forbidden-project", 546 Bucket: "bucket", 547 Builder: "builder", 548 }, 549 }, 550 }) 551 So(err, ShouldNotBeNil) 552 So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied) 553 So(res, ShouldBeNil) 554 }) 555 556 Convey(`resolve external consoles`, func() { 557 ctx := auth.WithState(ctx, &authtest.FakeState{Identity: "user", IdentityPermissions: perms}) 558 559 res, err := srv.QueryConsoleSnapshots(ctx, &milopb.QueryConsoleSnapshotsRequest{ 560 Predicate: &milopb.ConsolePredicate{ 561 Project: "project-with-external-ref", 562 }, 563 }) 564 So(err, ShouldBeNil) 565 So(res.Snapshots, ShouldResembleProto, []*milopb.ConsoleSnapshot{ 566 { 567 Console: &projectconfigpb.Console{ 568 Realm: "allowed-project:@root", 569 Id: "con1", 570 Builders: []*projectconfigpb.Builder{ 571 { 572 Id: &buildbucketpb.BuilderID{ 573 Project: "allowed-project", 574 Bucket: "bucket", 575 Builder: "builder", 576 }, 577 }, 578 { 579 Id: &buildbucketpb.BuilderID{ 580 Project: "other-allowed-project", 581 Bucket: "bucket", 582 Builder: "builder", 583 }, 584 }, 585 }, 586 }, 587 BuilderSnapshots: []*milopb.BuilderSnapshot{ 588 { 589 Builder: &buildbucketpb.BuilderID{ 590 Project: "allowed-project", 591 Bucket: "bucket", 592 Builder: "builder", 593 }, 594 Build: &buildbucketpb.Build{ 595 Id: 111111, 596 Builder: &buildbucketpb.BuilderID{ 597 Project: "allowed-project", 598 Bucket: "bucket", 599 Builder: "builder", 600 }, 601 Status: buildbucketpb.Status_INFRA_FAILURE, 602 }, 603 }, 604 { 605 Builder: &buildbucketpb.BuilderID{ 606 Project: "other-allowed-project", 607 Bucket: "bucket", 608 Builder: "builder", 609 }, 610 Build: &buildbucketpb.Build{ 611 Id: 111112, 612 Builder: &buildbucketpb.BuilderID{ 613 Project: "other-allowed-project", 614 Bucket: "bucket", 615 Builder: "builder", 616 }, 617 Status: buildbucketpb.Status_SUCCESS, 618 }, 619 }, 620 }, 621 }, 622 { 623 Console: &projectconfigpb.Console{ 624 Realm: "project-with-external-ref:@root", 625 Id: "con3", 626 Builders: []*projectconfigpb.Builder{ 627 { 628 Id: &buildbucketpb.BuilderID{ 629 Project: "project-with-external-ref", 630 Bucket: "bucket", 631 Builder: "builder", 632 }, 633 }, 634 }, 635 }, 636 BuilderSnapshots: []*milopb.BuilderSnapshot{ 637 { 638 Builder: &buildbucketpb.BuilderID{ 639 Project: "project-with-external-ref", 640 Bucket: "bucket", 641 Builder: "builder", 642 }, 643 Build: &buildbucketpb.Build{ 644 Id: 111114, 645 Builder: &buildbucketpb.BuilderID{ 646 Project: "project-with-external-ref", 647 Bucket: "bucket", 648 Builder: "builder", 649 }, 650 Status: buildbucketpb.Status_FAILURE, 651 }, 652 }, 653 }, 654 }, 655 }) 656 So(res.NextPageToken, ShouldBeEmpty) 657 }) 658 }) 659 } 660 661 func TestValidateQueryConsoleSnapshotsQuery(t *testing.T) { 662 t.Parallel() 663 Convey(`TestValidateQueryConsoleSnapshotsRequest`, t, func() { 664 Convey(`negative page size`, func() { 665 err := validateQueryConsoleSnapshotsRequest(&milopb.QueryConsoleSnapshotsRequest{ 666 Predicate: &milopb.ConsolePredicate{ 667 Project: "project", 668 }, 669 PageSize: -1, 670 }) 671 So(err, ShouldNotBeNil) 672 So(err, ShouldErrLike, "page_size can not be negative") 673 }) 674 675 Convey(`missing project`, func() { 676 err := validateQueryConsoleSnapshotsRequest(&milopb.QueryConsoleSnapshotsRequest{ 677 PageSize: 10, 678 }) 679 So(err, ShouldErrLike, "predicate.project is required") 680 }) 681 682 Convey(`valid`, func() { 683 err := validateQueryConsoleSnapshotsRequest(&milopb.QueryConsoleSnapshotsRequest{ 684 Predicate: &milopb.ConsolePredicate{ 685 Project: "project", 686 Builder: &buildbucketpb.BuilderID{ 687 Project: "project", 688 Bucket: "bucket", 689 Builder: "builder", 690 }, 691 }, 692 PageSize: 10, 693 }) 694 So(err, ShouldBeNil) 695 }) 696 697 Convey(`valid with only project`, func() { 698 err := validateQueryConsoleSnapshotsRequest(&milopb.QueryConsoleSnapshotsRequest{ 699 Predicate: &milopb.ConsolePredicate{ 700 Project: "project", 701 }, 702 }) 703 So(err, ShouldBeNil) 704 }) 705 }) 706 }