go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/rpc/get_build_test.go (about) 1 // Copyright 2020 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 "google.golang.org/protobuf/proto" 22 "google.golang.org/protobuf/types/known/fieldmaskpb" 23 "google.golang.org/protobuf/types/known/structpb" 24 25 "go.chromium.org/luci/auth/identity" 26 "go.chromium.org/luci/gae/impl/memory" 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 "go.chromium.org/luci/buildbucket/appengine/model" 32 "go.chromium.org/luci/buildbucket/appengine/rpc/testutil" 33 "go.chromium.org/luci/buildbucket/bbperms" 34 pb "go.chromium.org/luci/buildbucket/proto" 35 36 . "github.com/smartystreets/goconvey/convey" 37 . "go.chromium.org/luci/common/testing/assertions" 38 ) 39 40 func TestGetBuild(t *testing.T) { 41 t.Parallel() 42 43 const userID = identity.Identity("user:user@example.com") 44 45 Convey("GetBuild", t, func() { 46 srv := &Builds{} 47 ctx := memory.Use(context.Background()) 48 datastore.GetTestable(ctx).AutoIndex(true) 49 datastore.GetTestable(ctx).Consistent(true) 50 51 ctx = auth.WithState(ctx, &authtest.FakeState{ 52 Identity: userID, 53 }) 54 55 Convey("id", func() { 56 Convey("not found", func() { 57 req := &pb.GetBuildRequest{ 58 Id: 1, 59 } 60 rsp, err := srv.GetBuild(ctx, req) 61 So(err, ShouldErrLike, "not found") 62 So(rsp, ShouldBeNil) 63 }) 64 65 Convey("with build entity", func() { 66 testutil.PutBucket(ctx, "project", "bucket", nil) 67 build := &model.Build{ 68 Proto: &pb.Build{ 69 Id: 1, 70 Builder: &pb.BuilderID{ 71 Project: "project", 72 Bucket: "bucket", 73 Builder: "builder", 74 }, 75 Input: &pb.Build_Input{ 76 GerritChanges: []*pb.GerritChange{ 77 {Host: "h1"}, 78 {Host: "h2"}, 79 }, 80 }, 81 CancellationMarkdown: "cancelled", 82 SummaryMarkdown: "summary", 83 }, 84 } 85 So(datastore.Put(ctx, build), ShouldBeNil) 86 key := datastore.KeyForObj(ctx, build) 87 s, err := proto.Marshal(&pb.Build{ 88 Steps: []*pb.Step{ 89 { 90 Name: "step", 91 }, 92 }, 93 }) 94 So(err, ShouldBeNil) 95 So(datastore.Put(ctx, &model.BuildSteps{ 96 Build: key, 97 Bytes: s, 98 IsZipped: false, 99 }), ShouldBeNil) 100 So(datastore.Put(ctx, &model.BuildInfra{ 101 Build: key, 102 Proto: &pb.BuildInfra{ 103 Buildbucket: &pb.BuildInfra_Buildbucket{ 104 Hostname: "example.com", 105 }, 106 Resultdb: &pb.BuildInfra_ResultDB{ 107 Hostname: "rdb.example.com", 108 Invocation: "bb-12345", 109 }, 110 }, 111 }), ShouldBeNil) 112 So(datastore.Put(ctx, &model.BuildInputProperties{ 113 Build: key, 114 Proto: &structpb.Struct{ 115 Fields: map[string]*structpb.Value{ 116 "input": { 117 Kind: &structpb.Value_StringValue{ 118 StringValue: "input value", 119 }, 120 }, 121 }, 122 }, 123 }), ShouldBeNil) 124 So(datastore.Put(ctx, &model.BuildOutputProperties{ 125 Build: key, 126 Proto: &structpb.Struct{ 127 Fields: map[string]*structpb.Value{ 128 "output": { 129 Kind: &structpb.Value_StringValue{ 130 StringValue: "output value", 131 }, 132 }, 133 }, 134 }, 135 }), ShouldBeNil) 136 137 req := &pb.GetBuildRequest{ 138 Id: 1, 139 Mask: &pb.BuildMask{ 140 AllFields: true, 141 }, 142 } 143 144 Convey("permission denied", func() { 145 rsp, err := srv.GetBuild(ctx, req) 146 So(err, ShouldErrLike, "not found") 147 So(rsp, ShouldBeNil) 148 }) 149 150 Convey("permission denied if user only has BuildsList permission", func() { 151 ctx = auth.WithState(ctx, &authtest.FakeState{ 152 Identity: userID, 153 FakeDB: authtest.NewFakeDB( 154 authtest.MockPermission(userID, "project:bucket", bbperms.BuildersList), 155 authtest.MockPermission(userID, "project:bucket", bbperms.BuildsList), 156 ), 157 }) 158 rsp, err := srv.GetBuild(ctx, req) 159 So(err, ShouldErrLike, "not found") 160 So(rsp, ShouldBeNil) 161 }) 162 163 Convey("found with BuildsGetLimited permission only", func() { 164 ctx = auth.WithState(ctx, &authtest.FakeState{ 165 Identity: userID, 166 FakeDB: authtest.NewFakeDB( 167 authtest.MockPermission(userID, "project:bucket", bbperms.BuildersList), 168 authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGetLimited), 169 ), 170 }) 171 rsp, err := srv.GetBuild(ctx, req) 172 So(err, ShouldBeNil) 173 So(rsp, ShouldResembleProto, &pb.Build{ 174 Id: 1, 175 Builder: &pb.BuilderID{ 176 Project: "project", 177 Bucket: "bucket", 178 Builder: "builder", 179 }, 180 Input: &pb.Build_Input{ 181 GerritChanges: []*pb.GerritChange{ 182 {Host: "h1"}, 183 {Host: "h2"}, 184 }, 185 }, 186 Infra: &pb.BuildInfra{ 187 Resultdb: &pb.BuildInfra_ResultDB{ 188 Hostname: "rdb.example.com", 189 Invocation: "bb-12345", 190 }, 191 }, 192 }) 193 }) 194 195 Convey("found", func() { 196 ctx = auth.WithState(ctx, &authtest.FakeState{ 197 Identity: userID, 198 FakeDB: authtest.NewFakeDB( 199 authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet), 200 ), 201 }) 202 rsp, err := srv.GetBuild(ctx, req) 203 So(err, ShouldBeNil) 204 So(rsp, ShouldResembleProto, &pb.Build{ 205 Id: 1, 206 Builder: &pb.BuilderID{ 207 Project: "project", 208 Bucket: "bucket", 209 Builder: "builder", 210 }, 211 Input: &pb.Build_Input{ 212 Properties: &structpb.Struct{ 213 Fields: map[string]*structpb.Value{ 214 "input": { 215 Kind: &structpb.Value_StringValue{ 216 StringValue: "input value", 217 }, 218 }, 219 }, 220 }, 221 GerritChanges: []*pb.GerritChange{ 222 {Host: "h1"}, 223 {Host: "h2"}, 224 }, 225 }, 226 Output: &pb.Build_Output{ 227 Properties: &structpb.Struct{ 228 Fields: map[string]*structpb.Value{ 229 "output": { 230 Kind: &structpb.Value_StringValue{ 231 StringValue: "output value", 232 }, 233 }, 234 }, 235 }, 236 }, 237 Infra: &pb.BuildInfra{ 238 Buildbucket: &pb.BuildInfra_Buildbucket{ 239 Hostname: "example.com", 240 }, 241 Resultdb: &pb.BuildInfra_ResultDB{ 242 Hostname: "rdb.example.com", 243 Invocation: "bb-12345", 244 }, 245 }, 246 Steps: []*pb.Step{ 247 {Name: "step"}, 248 }, 249 CancellationMarkdown: "cancelled", 250 SummaryMarkdown: "summary\ncancelled", 251 }) 252 }) 253 254 Convey("summary", func() { 255 ctx = auth.WithState(ctx, &authtest.FakeState{ 256 Identity: userID, 257 FakeDB: authtest.NewFakeDB( 258 authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet), 259 ), 260 }) 261 req.Mask = &pb.BuildMask{ 262 Fields: &fieldmaskpb.FieldMask{ 263 Paths: []string{ 264 "summary_markdown", 265 }, 266 }, 267 } 268 rsp, err := srv.GetBuild(ctx, req) 269 So(err, ShouldBeNil) 270 So(rsp, ShouldResembleProto, &pb.Build{ 271 SummaryMarkdown: "summary\ncancelled", 272 }) 273 }) 274 }) 275 }) 276 277 Convey("index", func() { 278 So(datastore.Put(ctx, &model.Build{ 279 Proto: &pb.Build{ 280 Id: 1, 281 Builder: &pb.BuilderID{ 282 Project: "project", 283 Bucket: "bucket", 284 Builder: "builder", 285 }, 286 }, 287 BucketID: "project/bucket", 288 Tags: []string{"build_address:luci.project.bucket/builder/1"}, 289 }), ShouldBeNil) 290 291 Convey("error", func() { 292 Convey("incomplete index", func() { 293 So(datastore.Put(ctx, &model.TagIndex{ 294 ID: ":2:build_address:luci.project.bucket/builder/1", 295 Entries: []model.TagIndexEntry{ 296 { 297 BuildID: 1, 298 }, 299 }, 300 Incomplete: true, 301 }), ShouldBeNil) 302 req := &pb.GetBuildRequest{ 303 Builder: &pb.BuilderID{ 304 Project: "project", 305 Bucket: "bucket", 306 Builder: "builder", 307 }, 308 BuildNumber: 1, 309 } 310 rsp, err := srv.GetBuild(ctx, req) 311 So(err, ShouldErrLike, "unexpected incomplete index") 312 So(rsp, ShouldBeNil) 313 }) 314 315 Convey("not found", func() { 316 req := &pb.GetBuildRequest{ 317 Builder: &pb.BuilderID{ 318 Project: "project", 319 Bucket: "bucket", 320 Builder: "builder", 321 }, 322 BuildNumber: 2, 323 } 324 rsp, err := srv.GetBuild(ctx, req) 325 So(err, ShouldErrLike, "not found") 326 So(rsp, ShouldBeNil) 327 }) 328 329 Convey("excessive results", func() { 330 So(datastore.Put(ctx, &model.TagIndex{ 331 ID: ":2:build_address:luci.project.bucket/builder/1", 332 Entries: []model.TagIndexEntry{ 333 { 334 BuildID: 1, 335 BucketID: "proj/bucket", 336 }, 337 { 338 BuildID: 2, 339 BucketID: "proj/bucket", 340 }, 341 }, 342 }), ShouldBeNil) 343 req := &pb.GetBuildRequest{ 344 Builder: &pb.BuilderID{ 345 Project: "project", 346 Bucket: "bucket", 347 Builder: "builder", 348 }, 349 BuildNumber: 1, 350 } 351 rsp, err := srv.GetBuild(ctx, req) 352 So(err, ShouldErrLike, "unexpected number of results") 353 So(rsp, ShouldBeNil) 354 }) 355 }) 356 357 Convey("ok", func() { 358 ctx = auth.WithState(ctx, &authtest.FakeState{ 359 Identity: userID, 360 FakeDB: authtest.NewFakeDB( 361 authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet), 362 ), 363 }) 364 testutil.PutBucket(ctx, "project", "bucket", nil) 365 So(datastore.Put(ctx, &model.TagIndex{ 366 ID: ":2:build_address:luci.project.bucket/builder/1", 367 Entries: []model.TagIndexEntry{ 368 { 369 BuildID: 1, 370 BucketID: "project/bucket", 371 }, 372 }, 373 }), ShouldBeNil) 374 req := &pb.GetBuildRequest{ 375 Builder: &pb.BuilderID{ 376 Project: "project", 377 Bucket: "bucket", 378 Builder: "builder", 379 }, 380 BuildNumber: 1, 381 } 382 rsp, err := srv.GetBuild(ctx, req) 383 So(err, ShouldBeNil) 384 So(rsp, ShouldResembleProto, &pb.Build{ 385 Id: 1, 386 Builder: &pb.BuilderID{ 387 Project: "project", 388 Bucket: "bucket", 389 Builder: "builder", 390 }, 391 Input: &pb.Build_Input{}, 392 }) 393 }) 394 }) 395 396 Convey("led build", func() { 397 testutil.PutBucket(ctx, "project", "bucket", nil) 398 testutil.PutBucket(ctx, "project", "bucket.shadow", nil) 399 build := &model.Build{ 400 Proto: &pb.Build{ 401 Id: 1, 402 Builder: &pb.BuilderID{ 403 Project: "project", 404 Bucket: "bucket.shadow", 405 Builder: "builder", 406 }, 407 Input: &pb.Build_Input{ 408 GerritChanges: []*pb.GerritChange{ 409 {Host: "h1"}, 410 {Host: "h2"}, 411 }, 412 }, 413 CancellationMarkdown: "cancelled", 414 SummaryMarkdown: "summary", 415 }, 416 } 417 So(datastore.Put(ctx, build), ShouldBeNil) 418 key := datastore.KeyForObj(ctx, build) 419 s, err := proto.Marshal(&pb.Build{ 420 Steps: []*pb.Step{ 421 { 422 Name: "step", 423 }, 424 }, 425 }) 426 So(err, ShouldBeNil) 427 So(datastore.Put(ctx, &model.BuildSteps{ 428 Build: key, 429 Bytes: s, 430 IsZipped: false, 431 }), ShouldBeNil) 432 So(datastore.Put(ctx, &model.BuildInfra{ 433 Build: key, 434 Proto: &pb.BuildInfra{ 435 Buildbucket: &pb.BuildInfra_Buildbucket{ 436 Hostname: "example.com", 437 }, 438 Resultdb: &pb.BuildInfra_ResultDB{ 439 Hostname: "rdb.example.com", 440 Invocation: "bb-12345", 441 }, 442 Led: &pb.BuildInfra_Led{ 443 ShadowedBucket: "bucket", 444 }, 445 }, 446 }), ShouldBeNil) 447 So(datastore.Put(ctx, &model.BuildInputProperties{ 448 Build: key, 449 Proto: &structpb.Struct{ 450 Fields: map[string]*structpb.Value{ 451 "input": { 452 Kind: &structpb.Value_StringValue{ 453 StringValue: "input value", 454 }, 455 }, 456 }, 457 }, 458 }), ShouldBeNil) 459 So(datastore.Put(ctx, &model.BuildOutputProperties{ 460 Build: key, 461 Proto: &structpb.Struct{ 462 Fields: map[string]*structpb.Value{ 463 "output": { 464 Kind: &structpb.Value_StringValue{ 465 StringValue: "output value", 466 }, 467 }, 468 }, 469 }, 470 }), ShouldBeNil) 471 472 req := &pb.GetBuildRequest{ 473 Id: 1, 474 Mask: &pb.BuildMask{ 475 AllFields: true, 476 }, 477 } 478 479 Convey("permission denied", func() { 480 rsp, err := srv.GetBuild(ctx, req) 481 So(err, ShouldErrLike, "not found") 482 So(rsp, ShouldBeNil) 483 }) 484 485 Convey("found with permission on shadowed bucket", func() { 486 ctx = auth.WithState(ctx, &authtest.FakeState{ 487 Identity: userID, 488 FakeDB: authtest.NewFakeDB( 489 authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet), 490 ), 491 }) 492 rsp, err := srv.GetBuild(ctx, req) 493 So(err, ShouldBeNil) 494 So(rsp, ShouldResembleProto, &pb.Build{ 495 Id: 1, 496 Builder: &pb.BuilderID{ 497 Project: "project", 498 Bucket: "bucket.shadow", 499 Builder: "builder", 500 }, 501 Input: &pb.Build_Input{ 502 Properties: &structpb.Struct{ 503 Fields: map[string]*structpb.Value{ 504 "input": { 505 Kind: &structpb.Value_StringValue{ 506 StringValue: "input value", 507 }, 508 }, 509 }, 510 }, 511 GerritChanges: []*pb.GerritChange{ 512 {Host: "h1"}, 513 {Host: "h2"}, 514 }, 515 }, 516 Output: &pb.Build_Output{ 517 Properties: &structpb.Struct{ 518 Fields: map[string]*structpb.Value{ 519 "output": { 520 Kind: &structpb.Value_StringValue{ 521 StringValue: "output value", 522 }, 523 }, 524 }, 525 }, 526 }, 527 Infra: &pb.BuildInfra{ 528 Buildbucket: &pb.BuildInfra_Buildbucket{ 529 Hostname: "example.com", 530 }, 531 Resultdb: &pb.BuildInfra_ResultDB{ 532 Hostname: "rdb.example.com", 533 Invocation: "bb-12345", 534 }, 535 Led: &pb.BuildInfra_Led{ 536 ShadowedBucket: "bucket", 537 }, 538 }, 539 Steps: []*pb.Step{ 540 {Name: "step"}, 541 }, 542 CancellationMarkdown: "cancelled", 543 SummaryMarkdown: "summary\ncancelled", 544 }) 545 }) 546 }) 547 }) 548 549 Convey("validateGet", t, func() { 550 Convey("nil", func() { 551 err := validateGet(nil) 552 So(err, ShouldErrLike, "id or (builder and build_number) is required") 553 }) 554 555 Convey("empty", func() { 556 req := &pb.GetBuildRequest{} 557 err := validateGet(req) 558 So(err, ShouldErrLike, "id or (builder and build_number) is required") 559 }) 560 561 Convey("builder", func() { 562 req := &pb.GetBuildRequest{ 563 Builder: &pb.BuilderID{}, 564 } 565 err := validateGet(req) 566 So(err, ShouldErrLike, "id or (builder and build_number) is required") 567 }) 568 569 Convey("build number", func() { 570 req := &pb.GetBuildRequest{ 571 BuildNumber: 1, 572 } 573 err := validateGet(req) 574 So(err, ShouldErrLike, "id or (builder and build_number) is required") 575 }) 576 577 Convey("mutual exclusion", func() { 578 Convey("builder", func() { 579 req := &pb.GetBuildRequest{ 580 Id: 1, 581 Builder: &pb.BuilderID{}, 582 } 583 err := validateGet(req) 584 So(err, ShouldErrLike, "id is mutually exclusive with (builder and build_number)") 585 }) 586 587 Convey("build number", func() { 588 req := &pb.GetBuildRequest{ 589 Id: 1, 590 BuildNumber: 1, 591 } 592 err := validateGet(req) 593 So(err, ShouldErrLike, "id is mutually exclusive with (builder and build_number)") 594 }) 595 }) 596 597 Convey("builder ID", func() { 598 Convey("project", func() { 599 req := &pb.GetBuildRequest{ 600 Builder: &pb.BuilderID{}, 601 BuildNumber: 1, 602 } 603 err := validateGet(req) 604 So(err, ShouldErrLike, "project must match") 605 }) 606 607 Convey("bucket", func() { 608 Convey("empty", func() { 609 req := &pb.GetBuildRequest{ 610 Builder: &pb.BuilderID{ 611 Project: "project", 612 }, 613 BuildNumber: 1, 614 } 615 err := validateGet(req) 616 So(err, ShouldErrLike, "bucket is required") 617 }) 618 619 Convey("v1", func() { 620 req := &pb.GetBuildRequest{ 621 Builder: &pb.BuilderID{ 622 Project: "project", 623 Bucket: "luci.project.bucket", 624 Builder: "builder", 625 }, 626 BuildNumber: 1, 627 } 628 err := validateGet(req) 629 So(err, ShouldErrLike, "invalid use of v1 bucket in v2 API") 630 }) 631 }) 632 633 Convey("builder", func() { 634 req := &pb.GetBuildRequest{ 635 Builder: &pb.BuilderID{ 636 Project: "project", 637 Bucket: "bucket", 638 }, 639 BuildNumber: 1, 640 } 641 err := validateGet(req) 642 So(err, ShouldErrLike, "builder is required") 643 }) 644 }) 645 }) 646 }