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  }