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  }