go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/rpc/batch_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 "math/rand" 20 "testing" 21 22 "github.com/golang/mock/gomock" 23 spb "google.golang.org/genproto/googleapis/rpc/status" 24 "google.golang.org/protobuf/encoding/protojson" 25 "google.golang.org/protobuf/types/known/fieldmaskpb" 26 "google.golang.org/protobuf/types/known/timestamppb" 27 28 "go.chromium.org/luci/auth/identity" 29 "go.chromium.org/luci/common/clock/testclock" 30 "go.chromium.org/luci/common/data/rand/mathrand" 31 "go.chromium.org/luci/gae/filter/txndefer" 32 "go.chromium.org/luci/gae/impl/memory" 33 "go.chromium.org/luci/gae/service/datastore" 34 "go.chromium.org/luci/server/auth" 35 "go.chromium.org/luci/server/auth/authtest" 36 "go.chromium.org/luci/server/bqlog" 37 "go.chromium.org/luci/server/tq" 38 39 "go.chromium.org/luci/buildbucket/appengine/internal/config" 40 "go.chromium.org/luci/buildbucket/appengine/model" 41 "go.chromium.org/luci/buildbucket/bbperms" 42 pb "go.chromium.org/luci/buildbucket/proto" 43 44 . "github.com/smartystreets/goconvey/convey" 45 . "go.chromium.org/luci/common/testing/assertions" 46 ) 47 48 func TestBatch(t *testing.T) { 49 t.Parallel() 50 51 const userID = identity.Identity("user:caller@example.com") 52 53 Convey("Batch", t, func() { 54 ctl := gomock.NewController(t) 55 defer ctl.Finish() 56 srv := &Builds{} 57 ctx, _ := tq.TestingContext(txndefer.FilterRDS(memory.Use(context.Background())), nil) 58 ctx = mathrand.Set(ctx, rand.New(rand.NewSource(0))) 59 datastore.GetTestable(ctx).AutoIndex(true) 60 datastore.GetTestable(ctx).Consistent(true) 61 62 So(config.SetTestSettingsCfg(ctx, &pb.SettingsCfg{}), ShouldBeNil) 63 64 b := &bqlog.Bundler{ 65 CloudProject: "project", 66 Dataset: "dataset", 67 } 68 ctx = withBundler(ctx, b) 69 b.RegisterSink(bqlog.Sink{ 70 Prototype: &pb.PRPCRequestLog{}, 71 Table: "table", 72 }) 73 b.Start(ctx, &bqlog.FakeBigQueryWriter{}) 74 defer b.Shutdown(ctx) 75 76 ctx = auth.WithState(ctx, &authtest.FakeState{ 77 Identity: userID, 78 FakeDB: authtest.NewFakeDB( 79 authtest.MockPermission(userID, "project:bucket", bbperms.BuildersGet), 80 authtest.MockPermission(userID, "project:bucket", bbperms.BuildersList), 81 authtest.MockPermission(userID, "project:bucket", bbperms.BuildsAdd), 82 authtest.MockPermission(userID, "project:bucket", bbperms.BuildsCancel), 83 authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet), 84 authtest.MockPermission(userID, "project:bucket", bbperms.BuildsList), 85 ), 86 }) 87 So(datastore.Put( 88 ctx, 89 &model.Bucket{ 90 ID: "bucket", 91 Parent: model.ProjectKey(ctx, "project"), 92 Proto: &pb.Bucket{}, 93 }, 94 &model.Bucket{ 95 ID: "bucket1", 96 Parent: model.ProjectKey(ctx, "project"), 97 Proto: &pb.Bucket{ 98 Shadow: "bucket1", 99 }, 100 }, 101 &model.Build{ 102 Proto: &pb.Build{ 103 Id: 1, 104 Builder: &pb.BuilderID{ 105 Project: "project", 106 Bucket: "bucket", 107 Builder: "builder1", 108 }, 109 }, 110 }, 111 &model.Build{ 112 Proto: &pb.Build{ 113 Id: 2, 114 Builder: &pb.BuilderID{ 115 Project: "project", 116 Bucket: "bucket", 117 Builder: "builder2", 118 }, 119 }, 120 }), ShouldBeNil) 121 122 Convey("empty", func() { 123 req := &pb.BatchRequest{ 124 Requests: []*pb.BatchRequest_Request{}, 125 } 126 res, err := srv.Batch(ctx, req) 127 So(err, ShouldBeNil) 128 So(res, ShouldResembleProto, &pb.BatchResponse{}) 129 130 req = &pb.BatchRequest{ 131 Requests: []*pb.BatchRequest_Request{{}}, 132 } 133 res, err = srv.Batch(ctx, req) 134 So(err, ShouldNotBeNil) 135 So(res, ShouldBeNil) 136 So(err, ShouldErrLike, "request includes an unsupported type") 137 }) 138 139 Convey("error", func() { 140 req := &pb.BatchRequest{ 141 Requests: []*pb.BatchRequest_Request{ 142 {Request: &pb.BatchRequest_Request_GetBuild{ 143 GetBuild: &pb.GetBuildRequest{BuildNumber: 1}, 144 }}, 145 }, 146 } 147 res, err := srv.Batch(ctx, req) 148 expectedRes := &pb.BatchResponse{ 149 Responses: []*pb.BatchResponse_Response{ 150 {Response: &pb.BatchResponse_Response_Error{ 151 Error: &spb.Status{ 152 Code: 3, 153 Message: "bad request: one of id or (builder and build_number) is required", 154 }, 155 }}, 156 }, 157 } 158 So(err, ShouldBeNil) 159 So(res, ShouldResembleProto, expectedRes) 160 }) 161 162 Convey("getBuildStatus req", func() { 163 bs := &model.BuildStatus{ 164 Build: datastore.MakeKey(ctx, "Build", 500), 165 BuildAddress: "project/bucket/builder/b500", 166 Status: pb.Status_SCHEDULED, 167 } 168 So(datastore.Put(ctx, bs), ShouldBeNil) 169 req := &pb.BatchRequest{ 170 Requests: []*pb.BatchRequest_Request{ 171 {Request: &pb.BatchRequest_Request_GetBuildStatus{ 172 GetBuildStatus: &pb.GetBuildStatusRequest{Id: 1}, 173 }}, 174 {Request: &pb.BatchRequest_Request_GetBuildStatus{ 175 GetBuildStatus: &pb.GetBuildStatusRequest{Id: 500}, 176 }}, 177 }, 178 } 179 res, err := srv.Batch(ctx, req) 180 expectedRes := &pb.BatchResponse{ 181 Responses: []*pb.BatchResponse_Response{ 182 {Response: &pb.BatchResponse_Response_GetBuildStatus{ 183 GetBuildStatus: &pb.Build{ 184 Id: 1, 185 Status: pb.Status_STATUS_UNSPECIFIED, 186 }, 187 }}, 188 {Response: &pb.BatchResponse_Response_GetBuildStatus{ 189 GetBuildStatus: &pb.Build{ 190 Id: 500, 191 Status: pb.Status_SCHEDULED, 192 }, 193 }}, 194 }, 195 } 196 So(err, ShouldBeNil) 197 So(res, ShouldResembleProto, expectedRes) 198 }) 199 200 Convey("getBuild req", func() { 201 req := &pb.BatchRequest{ 202 Requests: []*pb.BatchRequest_Request{ 203 {Request: &pb.BatchRequest_Request_GetBuild{ 204 GetBuild: &pb.GetBuildRequest{Id: 1}, 205 }}, 206 }, 207 } 208 res, err := srv.Batch(ctx, req) 209 expectedRes := &pb.BatchResponse{ 210 Responses: []*pb.BatchResponse_Response{ 211 {Response: &pb.BatchResponse_Response_GetBuild{ 212 GetBuild: &pb.Build{ 213 Id: 1, 214 Builder: &pb.BuilderID{ 215 Project: "project", 216 Bucket: "bucket", 217 Builder: "builder1", 218 }, 219 Input: &pb.Build_Input{}, 220 }, 221 }}, 222 }, 223 } 224 So(err, ShouldBeNil) 225 So(res, ShouldResembleProto, expectedRes) 226 }) 227 228 Convey("searchBuilds req", func() { 229 req := &pb.BatchRequest{ 230 Requests: []*pb.BatchRequest_Request{ 231 {Request: &pb.BatchRequest_Request_SearchBuilds{ 232 SearchBuilds: &pb.SearchBuildsRequest{}, 233 }}, 234 }, 235 } 236 res, err := srv.Batch(ctx, req) 237 expectedRes := &pb.BatchResponse{ 238 Responses: []*pb.BatchResponse_Response{ 239 {Response: &pb.BatchResponse_Response_SearchBuilds{ 240 SearchBuilds: &pb.SearchBuildsResponse{ 241 Builds: []*pb.Build{ 242 {Id: 1, 243 Builder: &pb.BuilderID{ 244 Project: "project", 245 Bucket: "bucket", 246 Builder: "builder1", 247 }, 248 Input: &pb.Build_Input{}, 249 }, 250 {Id: 2, 251 Builder: &pb.BuilderID{ 252 Project: "project", 253 Bucket: "bucket", 254 Builder: "builder2", 255 }, 256 Input: &pb.Build_Input{}, 257 }, 258 }, 259 }, 260 }}, 261 }, 262 } 263 So(err, ShouldBeNil) 264 So(res, ShouldResembleProto, expectedRes) 265 }) 266 267 Convey("get and search reqs", func() { 268 req := &pb.BatchRequest{ 269 Requests: []*pb.BatchRequest_Request{ 270 {Request: &pb.BatchRequest_Request_GetBuild{ 271 GetBuild: &pb.GetBuildRequest{Id: 1}, 272 }}, 273 {Request: &pb.BatchRequest_Request_SearchBuilds{ 274 SearchBuilds: &pb.SearchBuildsRequest{}, 275 }}, 276 {Request: &pb.BatchRequest_Request_GetBuild{ 277 GetBuild: &pb.GetBuildRequest{Id: 2}, 278 }}, 279 }, 280 } 281 res, err := srv.Batch(ctx, req) 282 build1 := &pb.Build{ 283 Id: 1, 284 Builder: &pb.BuilderID{ 285 Project: "project", 286 Bucket: "bucket", 287 Builder: "builder1", 288 }, 289 Input: &pb.Build_Input{}, 290 } 291 build2 := &pb.Build{ 292 Id: 2, 293 Builder: &pb.BuilderID{ 294 Project: "project", 295 Bucket: "bucket", 296 Builder: "builder2", 297 }, 298 Input: &pb.Build_Input{}, 299 } 300 expectedRes := &pb.BatchResponse{ 301 Responses: []*pb.BatchResponse_Response{ 302 {Response: &pb.BatchResponse_Response_GetBuild{ 303 GetBuild: build1, 304 }}, 305 {Response: &pb.BatchResponse_Response_SearchBuilds{ 306 SearchBuilds: &pb.SearchBuildsResponse{ 307 Builds: []*pb.Build{build1, build2}, 308 }, 309 }}, 310 {Response: &pb.BatchResponse_Response_GetBuild{ 311 GetBuild: build2, 312 }}, 313 }, 314 } 315 So(err, ShouldBeNil) 316 So(res, ShouldResembleProto, expectedRes) 317 }) 318 319 Convey("schedule req", func() { 320 req := &pb.BatchRequest{ 321 Requests: []*pb.BatchRequest_Request{ 322 {Request: &pb.BatchRequest_Request_ScheduleBuild{ 323 ScheduleBuild: &pb.ScheduleBuildRequest{}, 324 }}, 325 }, 326 } 327 res, err := srv.Batch(ctx, req) 328 expectedRes := &pb.BatchResponse{ 329 Responses: []*pb.BatchResponse_Response{ 330 {Response: &pb.BatchResponse_Response_Error{ 331 Error: &spb.Status{ 332 Code: 3, 333 Message: "bad request: builder or template_build_id is required", 334 }, 335 }}, 336 }, 337 } 338 So(err, ShouldBeNil) 339 So(res, ShouldResembleProto, expectedRes) 340 }) 341 342 Convey("schedule batch", func() { 343 req := &pb.BatchRequest{ 344 Requests: []*pb.BatchRequest_Request{ 345 {Request: &pb.BatchRequest_Request_ScheduleBuild{ 346 ScheduleBuild: &pb.ScheduleBuildRequest{}, 347 }}, 348 {Request: &pb.BatchRequest_Request_ScheduleBuild{ 349 ScheduleBuild: &pb.ScheduleBuildRequest{ 350 Builder: &pb.BuilderID{ 351 Project: "project", 352 }, 353 }, 354 }}, 355 {Request: &pb.BatchRequest_Request_ScheduleBuild{ 356 ScheduleBuild: &pb.ScheduleBuildRequest{ 357 Builder: &pb.BuilderID{ 358 Project: "project", 359 Bucket: "bucket", 360 }, 361 }, 362 }}, 363 {Request: &pb.BatchRequest_Request_ScheduleBuild{ 364 ScheduleBuild: &pb.ScheduleBuildRequest{ 365 Builder: &pb.BuilderID{ 366 Project: "project", 367 Bucket: "bucket", 368 Builder: "builder", 369 }, 370 ShadowInput: &pb.ScheduleBuildRequest_ShadowInput{}, 371 }, 372 }}, 373 {Request: &pb.BatchRequest_Request_ScheduleBuild{ 374 ScheduleBuild: &pb.ScheduleBuildRequest{ 375 Builder: &pb.BuilderID{ 376 Project: "project", 377 Bucket: "bucket1", 378 Builder: "builder", 379 }, 380 ShadowInput: &pb.ScheduleBuildRequest_ShadowInput{}, 381 }, 382 }}, 383 }, 384 } 385 res, err := srv.Batch(ctx, req) 386 expectedRes := &pb.BatchResponse{ 387 Responses: []*pb.BatchResponse_Response{ 388 {Response: &pb.BatchResponse_Response_Error{ 389 Error: &spb.Status{ 390 Code: 3, 391 Message: "bad request: builder or template_build_id is required", 392 }, 393 }}, 394 {Response: &pb.BatchResponse_Response_Error{ 395 Error: &spb.Status{ 396 Code: 3, 397 Message: "bad request: builder: bucket is required", 398 }, 399 }}, 400 {Response: &pb.BatchResponse_Response_Error{ 401 Error: &spb.Status{ 402 Code: 3, 403 Message: "bad request: builder: builder is required", 404 }, 405 }}, 406 {Response: &pb.BatchResponse_Response_Error{ 407 Error: &spb.Status{ 408 Code: 3, 409 Message: "bad request: scheduling a shadow build in the original bucket is not allowed", 410 }, 411 }}, 412 {Response: &pb.BatchResponse_Response_Error{ 413 Error: &spb.Status{ 414 Code: 3, 415 Message: "bad request: scheduling a shadow build in the original bucket is not allowed", 416 }, 417 }}, 418 }, 419 } 420 So(err, ShouldBeNil) 421 So(res, ShouldResembleProto, expectedRes) 422 }) 423 424 Convey("cancel req", func() { 425 now := testclock.TestRecentTimeLocal 426 ctx, _ = testclock.UseTime(ctx, now) 427 req := &pb.BatchRequest{ 428 Requests: []*pb.BatchRequest_Request{ 429 {Request: &pb.BatchRequest_Request_CancelBuild{ 430 CancelBuild: &pb.CancelBuildRequest{ 431 Id: 1, 432 SummaryMarkdown: "summary", 433 Mask: &pb.BuildMask{ 434 Fields: &fieldmaskpb.FieldMask{ 435 Paths: []string{ 436 "id", 437 "builder", 438 "update_time", 439 "cancel_time", 440 "status", 441 "cancellation_markdown", 442 }, 443 }, 444 }, 445 }, 446 }}, 447 }, 448 } 449 res, err := srv.Batch(ctx, req) 450 expectedRes := &pb.BatchResponse{ 451 Responses: []*pb.BatchResponse_Response{ 452 {Response: &pb.BatchResponse_Response_CancelBuild{ 453 CancelBuild: &pb.Build{ 454 Id: 1, 455 Builder: &pb.BuilderID{ 456 Project: "project", 457 Bucket: "bucket", 458 Builder: "builder1", 459 }, 460 UpdateTime: timestamppb.New(now), 461 CancelTime: timestamppb.New(now), 462 CancellationMarkdown: "summary", 463 }, 464 }}, 465 }, 466 } 467 So(err, ShouldBeNil) 468 So(res, ShouldResembleProto, expectedRes) 469 }) 470 471 Convey("get, schedule, search, cancel and get_build_status in req", func() { 472 req := &pb.BatchRequest{} 473 err := protojson.Unmarshal([]byte(`{ 474 "requests": [ 475 {"getBuild": {"id": "1"}}, 476 {"scheduleBuild": {}}, 477 {"searchBuilds": {}}, 478 {"cancelBuild": {}}, 479 {"getBuildStatus": {"id": "1"}} 480 ]}`), req) 481 So(err, ShouldBeNil) 482 expectedPyReq := &pb.BatchRequest{} 483 err = protojson.Unmarshal([]byte(`{ 484 "requests": [ 485 {"scheduleBuild": {}} 486 ]}`), expectedPyReq) 487 So(err, ShouldBeNil) 488 actualRes, err := srv.Batch(ctx, req) 489 build1 := &pb.Build{ 490 Id: 1, 491 Builder: &pb.BuilderID{ 492 Project: "project", 493 Bucket: "bucket", 494 Builder: "builder1", 495 }, 496 Input: &pb.Build_Input{}, 497 } 498 build2 := &pb.Build{ 499 Id: 2, 500 Builder: &pb.BuilderID{ 501 Project: "project", 502 Bucket: "bucket", 503 Builder: "builder2", 504 }, 505 Input: &pb.Build_Input{}, 506 } 507 expectedRes := &pb.BatchResponse{ 508 Responses: []*pb.BatchResponse_Response{ 509 {Response: &pb.BatchResponse_Response_GetBuild{ 510 GetBuild: build1, 511 }}, 512 {Response: &pb.BatchResponse_Response_Error{ 513 Error: &spb.Status{ 514 Code: 3, 515 Message: "bad request: builder or template_build_id is required", 516 }, 517 }}, 518 {Response: &pb.BatchResponse_Response_SearchBuilds{ 519 SearchBuilds: &pb.SearchBuildsResponse{ 520 Builds: []*pb.Build{build1, build2}, 521 }, 522 }}, 523 {Response: &pb.BatchResponse_Response_Error{ 524 Error: &spb.Status{ 525 Code: 3, 526 Message: "bad request: id is required", 527 }, 528 }}, 529 {Response: &pb.BatchResponse_Response_GetBuildStatus{ 530 GetBuildStatus: &pb.Build{ 531 Id: 1, 532 Status: pb.Status_STATUS_UNSPECIFIED, 533 }, 534 }}, 535 }, 536 } 537 So(err, ShouldBeNil) 538 So(actualRes, ShouldResembleProto, expectedRes) 539 }) 540 541 Convey("exceed max read reqs amount", func() { 542 req := &pb.BatchRequest{} 543 for i := 0; i < readReqsSizeLimit+1; i++ { 544 req.Requests = append(req.Requests, &pb.BatchRequest_Request{Request: &pb.BatchRequest_Request_GetBuild{}}) 545 } 546 _, err := srv.Batch(ctx, req) 547 So(err, ShouldErrLike, "the maximum allowed read request count in Batch is 1000.") 548 }) 549 550 Convey("exceed max write reqs amount", func() { 551 req := &pb.BatchRequest{} 552 for i := 0; i < writeReqsSizeLimit+1; i++ { 553 req.Requests = append(req.Requests, &pb.BatchRequest_Request{Request: &pb.BatchRequest_Request_ScheduleBuild{}}) 554 } 555 _, err := srv.Batch(ctx, req) 556 So(err, ShouldErrLike, "the maximum allowed write request count in Batch is 200.") 557 }) 558 }) 559 }