go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/rpc/search_builds_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/genproto/protobuf/field_mask"
    22  	"google.golang.org/protobuf/proto"
    23  	"google.golang.org/protobuf/types/known/structpb"
    24  
    25  	"go.chromium.org/luci/auth/identity"
    26  	"go.chromium.org/luci/common/logging/memlogger"
    27  	"go.chromium.org/luci/gae/impl/memory"
    28  	"go.chromium.org/luci/gae/service/datastore"
    29  	"go.chromium.org/luci/server/auth"
    30  	"go.chromium.org/luci/server/auth/authtest"
    31  
    32  	bb "go.chromium.org/luci/buildbucket"
    33  	"go.chromium.org/luci/buildbucket/appengine/model"
    34  	"go.chromium.org/luci/buildbucket/appengine/rpc/testutil"
    35  	"go.chromium.org/luci/buildbucket/bbperms"
    36  	pb "go.chromium.org/luci/buildbucket/proto"
    37  
    38  	. "github.com/smartystreets/goconvey/convey"
    39  	. "go.chromium.org/luci/common/testing/assertions"
    40  )
    41  
    42  func TestValidateSearchBuilds(t *testing.T) {
    43  	t.Parallel()
    44  
    45  	Convey("validateChange", t, func() {
    46  		Convey("nil", func() {
    47  			err := validateChange(nil)
    48  			So(err, ShouldErrLike, "host is required")
    49  		})
    50  
    51  		Convey("empty", func() {
    52  			ch := &pb.GerritChange{}
    53  			err := validateChange(ch)
    54  			So(err, ShouldErrLike, "host is required")
    55  		})
    56  
    57  		Convey("change", func() {
    58  			ch := &pb.GerritChange{
    59  				Host: "host",
    60  			}
    61  			err := validateChange(ch)
    62  			So(err, ShouldErrLike, "change is required")
    63  		})
    64  
    65  		Convey("patchset", func() {
    66  			ch := &pb.GerritChange{
    67  				Host:   "host",
    68  				Change: 1,
    69  			}
    70  			err := validateChange(ch)
    71  			So(err, ShouldErrLike, "patchset is required")
    72  		})
    73  
    74  		Convey("valid", func() {
    75  			ch := &pb.GerritChange{
    76  				Host:     "host",
    77  				Change:   1,
    78  				Patchset: 1,
    79  			}
    80  			err := validateChange(ch)
    81  			So(err, ShouldBeNil)
    82  		})
    83  	})
    84  
    85  	Convey("validatePredicate", t, func() {
    86  		Convey("nil", func() {
    87  			err := validatePredicate(nil)
    88  			So(err, ShouldBeNil)
    89  		})
    90  
    91  		Convey("empty", func() {
    92  			pr := &pb.BuildPredicate{}
    93  			err := validatePredicate(pr)
    94  			So(err, ShouldBeNil)
    95  		})
    96  
    97  		Convey("mutual exclusion", func() {
    98  			pr := &pb.BuildPredicate{
    99  				Build:      &pb.BuildRange{},
   100  				CreateTime: &pb.TimeRange{},
   101  			}
   102  			err := validatePredicate(pr)
   103  			So(err, ShouldErrLike, "build is mutually exclusive with create_time")
   104  		})
   105  
   106  		Convey("builder id", func() {
   107  			Convey("no project", func() {
   108  				pr := &pb.BuildPredicate{
   109  					Builder: &pb.BuilderID{Bucket: "bucket"},
   110  				}
   111  				err := validatePredicate(pr)
   112  				So(err, ShouldErrLike, `builder: project must match "^[a-z0-9\\-_]+$"`)
   113  			})
   114  			Convey("only project and builder", func() {
   115  				pr := &pb.BuildPredicate{
   116  					Builder: &pb.BuilderID{
   117  						Project: "project",
   118  						Builder: "builder",
   119  					},
   120  				}
   121  				err := validatePredicate(pr)
   122  				So(err, ShouldErrLike, "builder: bucket is required")
   123  			})
   124  		})
   125  
   126  		Convey("experiments", func() {
   127  			Convey("empty", func() {
   128  				pr := &pb.BuildPredicate{
   129  					Experiments: []string{""},
   130  				}
   131  				err := validatePredicate(pr)
   132  				So(err, ShouldErrLike, `too short (expected [+-]$experiment_name)`)
   133  			})
   134  			Convey("bang", func() {
   135  				pr := &pb.BuildPredicate{
   136  					Experiments: []string{"!something"},
   137  				}
   138  				err := validatePredicate(pr)
   139  				So(err, ShouldErrLike, `first character must be + or -`)
   140  			})
   141  			Convey("canary conflict", func() {
   142  				pr := &pb.BuildPredicate{
   143  					Experiments: []string{"+" + bb.ExperimentBBCanarySoftware},
   144  					Canary:      pb.Trinary_YES,
   145  				}
   146  				err := validatePredicate(pr)
   147  				So(err, ShouldErrLike,
   148  					`cannot specify "luci.buildbucket.canary_software" and canary in the same predicate`)
   149  			})
   150  			Convey("duplicate (bad)", func() {
   151  				pr := &pb.BuildPredicate{
   152  					Experiments: []string{
   153  						"+" + bb.ExperimentBBCanarySoftware,
   154  						"-" + bb.ExperimentBBCanarySoftware,
   155  					},
   156  				}
   157  				err := validatePredicate(pr)
   158  				So(err, ShouldErrLike,
   159  					`"luci.buildbucket.canary_software" has both inclusive and exclusive filter`)
   160  			})
   161  
   162  			Convey("ok", func() {
   163  				pr := &pb.BuildPredicate{
   164  					Experiments: []string{
   165  						"+" + bb.ExperimentBBCanarySoftware,
   166  						"+" + bb.ExperimentNonProduction,
   167  					},
   168  				}
   169  				So(validatePredicate(pr), ShouldBeNil)
   170  			})
   171  			Convey("duplicate (ok)", func() {
   172  				pr := &pb.BuildPredicate{
   173  					Experiments: []string{
   174  						"+" + bb.ExperimentBBCanarySoftware,
   175  						"+" + bb.ExperimentBBCanarySoftware,
   176  					},
   177  				}
   178  				So(validatePredicate(pr), ShouldBeNil)
   179  			})
   180  		})
   181  
   182  		Convey("descendant_of and child_of mutual exclusion", func() {
   183  			pr := &pb.BuildPredicate{
   184  				DescendantOf: 1,
   185  				ChildOf:      1,
   186  			}
   187  			err := validatePredicate(pr)
   188  			So(err, ShouldErrLike, "descendant_of is mutually exclusive with child_of")
   189  		})
   190  	})
   191  
   192  	Convey("validatePageToken", t, func() {
   193  		Convey("empty token", func() {
   194  			err := validatePageToken("")
   195  			So(err, ShouldBeNil)
   196  		})
   197  
   198  		Convey("invalid page token", func() {
   199  			err := validatePageToken("abc")
   200  			So(err, ShouldErrLike, "invalid page_token")
   201  		})
   202  
   203  		Convey("valid page token", func() {
   204  			err := validatePageToken("id>123")
   205  			So(err, ShouldBeNil)
   206  		})
   207  	})
   208  
   209  	Convey("validateSearch", t, func() {
   210  		Convey("nil", func() {
   211  			err := validateSearch(nil)
   212  			So(err, ShouldBeNil)
   213  		})
   214  
   215  		Convey("empty", func() {
   216  			req := &pb.SearchBuildsRequest{}
   217  			err := validateSearch(req)
   218  			So(err, ShouldBeNil)
   219  		})
   220  
   221  		Convey("page size", func() {
   222  			Convey("negative", func() {
   223  				req := &pb.SearchBuildsRequest{
   224  					PageSize: -1,
   225  				}
   226  				err := validateSearch(req)
   227  				So(err, ShouldErrLike, "page_size cannot be negative")
   228  			})
   229  
   230  			Convey("zero", func() {
   231  				req := &pb.SearchBuildsRequest{
   232  					PageSize: 0,
   233  				}
   234  				err := validateSearch(req)
   235  				So(err, ShouldBeNil)
   236  			})
   237  
   238  			Convey("positive", func() {
   239  				req := &pb.SearchBuildsRequest{
   240  					PageSize: 1,
   241  				}
   242  				err := validateSearch(req)
   243  				So(err, ShouldBeNil)
   244  			})
   245  		})
   246  	})
   247  }
   248  
   249  func TestSearchBuilds(t *testing.T) {
   250  	t.Parallel()
   251  
   252  	const userID = identity.Identity("user:user@example.com")
   253  
   254  	Convey("search builds", t, func() {
   255  		srv := &Builds{}
   256  		ctx := memory.Use(context.Background())
   257  		ctx = memlogger.Use(ctx)
   258  		ctx = auth.WithState(ctx, &authtest.FakeState{
   259  			Identity: userID,
   260  			FakeDB: authtest.NewFakeDB(
   261  				authtest.MockPermission(userID, "project:bucket", bbperms.BuildersList),
   262  				authtest.MockPermission(userID, "project:bucket", bbperms.BuildsList),
   263  				authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet),
   264  			),
   265  		})
   266  		datastore.GetTestable(ctx).AutoIndex(true)
   267  		datastore.GetTestable(ctx).Consistent(true)
   268  		testutil.PutBucket(ctx, "project", "bucket", nil)
   269  		So(datastore.Put(ctx, &model.Build{
   270  			Proto: &pb.Build{
   271  				Id: 1,
   272  				Builder: &pb.BuilderID{
   273  					Project: "project",
   274  					Bucket:  "bucket",
   275  					Builder: "builder",
   276  				},
   277  				SummaryMarkdown: "foo summary",
   278  				Input: &pb.Build_Input{
   279  					GerritChanges: []*pb.GerritChange{
   280  						{Host: "h1"},
   281  						{Host: "h2"},
   282  					},
   283  				},
   284  			},
   285  			BucketID:  "project/bucket",
   286  			BuilderID: "project/bucket/builder",
   287  			Tags:      []string{"k1:v1", "k2:v2"},
   288  		}), ShouldBeNil)
   289  		So(datastore.Put(ctx, &model.Build{
   290  			Proto: &pb.Build{
   291  				Id: 2,
   292  				Builder: &pb.BuilderID{
   293  					Project: "project",
   294  					Bucket:  "bucket",
   295  					Builder: "builder2",
   296  				},
   297  			},
   298  			BucketID:  "project/bucket",
   299  			BuilderID: "project/bucket/builder2",
   300  		}), ShouldBeNil)
   301  		Convey("query search on Builds", func() {
   302  			req := &pb.SearchBuildsRequest{
   303  				Predicate: &pb.BuildPredicate{
   304  					Builder: &pb.BuilderID{
   305  						Project: "project",
   306  						Bucket:  "bucket",
   307  						Builder: "builder",
   308  					},
   309  					Tags: []*pb.StringPair{
   310  						{Key: "k1", Value: "v1"},
   311  						{Key: "k2", Value: "v2"},
   312  					},
   313  				},
   314  			}
   315  			rsp, err := srv.SearchBuilds(ctx, req)
   316  			So(err, ShouldBeNil)
   317  			expectedRsp := &pb.SearchBuildsResponse{
   318  				Builds: []*pb.Build{
   319  					{
   320  						Id: 1,
   321  						Builder: &pb.BuilderID{
   322  							Project: "project",
   323  							Bucket:  "bucket",
   324  							Builder: "builder",
   325  						},
   326  						Input: &pb.Build_Input{
   327  							GerritChanges: []*pb.GerritChange{
   328  								{Host: "h1"},
   329  								{Host: "h2"},
   330  							},
   331  						},
   332  					},
   333  				},
   334  			}
   335  			So(rsp, ShouldResembleProto, expectedRsp)
   336  		})
   337  
   338  		Convey("search builds with field masks", func() {
   339  			b := &model.Build{
   340  				ID: 1,
   341  			}
   342  			key := datastore.KeyForObj(ctx, b)
   343  			So(datastore.Put(ctx, &model.BuildInfra{
   344  				Build: key,
   345  				Proto: &pb.BuildInfra{
   346  					Buildbucket: &pb.BuildInfra_Buildbucket{
   347  						Hostname: "example.com",
   348  					},
   349  				},
   350  			}), ShouldBeNil)
   351  			So(datastore.Put(ctx, &model.BuildInputProperties{
   352  				Build: key,
   353  				Proto: &structpb.Struct{
   354  					Fields: map[string]*structpb.Value{
   355  						"input": {
   356  							Kind: &structpb.Value_StringValue{
   357  								StringValue: "input value",
   358  							},
   359  						},
   360  					},
   361  				},
   362  			}), ShouldBeNil)
   363  
   364  			req := &pb.SearchBuildsRequest{
   365  				Fields: &field_mask.FieldMask{
   366  					Paths: []string{"builds.*.id", "builds.*.input", "builds.*.infra"},
   367  				},
   368  			}
   369  			rsp, err := srv.SearchBuilds(ctx, req)
   370  			So(err, ShouldBeNil)
   371  			expectedRsp := &pb.SearchBuildsResponse{
   372  				Builds: []*pb.Build{
   373  					{
   374  						Id: 1,
   375  						Input: &pb.Build_Input{
   376  							GerritChanges: []*pb.GerritChange{
   377  								{Host: "h1"},
   378  								{Host: "h2"},
   379  							},
   380  							Properties: &structpb.Struct{
   381  								Fields: map[string]*structpb.Value{
   382  									"input": {
   383  										Kind: &structpb.Value_StringValue{
   384  											StringValue: "input value",
   385  										},
   386  									},
   387  								},
   388  							},
   389  						},
   390  						Infra: &pb.BuildInfra{
   391  							Buildbucket: &pb.BuildInfra_Buildbucket{
   392  								Hostname: "example.com",
   393  							},
   394  						},
   395  					},
   396  					{
   397  						Id:    2,
   398  						Input: &pb.Build_Input{},
   399  					},
   400  				},
   401  			}
   402  			So(rsp, ShouldResembleProto, expectedRsp)
   403  		})
   404  
   405  		Convey("search builds with limited access", func() {
   406  			key := datastore.KeyForObj(ctx, &model.Build{ID: 1})
   407  			s, err := proto.Marshal(&pb.Build{
   408  				Steps: []*pb.Step{
   409  					{
   410  						Name: "step",
   411  					},
   412  				},
   413  			})
   414  			So(err, ShouldBeNil)
   415  			So(datastore.Put(ctx, &model.BuildSteps{
   416  				Build:    key,
   417  				Bytes:    s,
   418  				IsZipped: false,
   419  			}), ShouldBeNil)
   420  			So(datastore.Put(ctx, &model.BuildInfra{
   421  				Build: key,
   422  				Proto: &pb.BuildInfra{
   423  					Buildbucket: &pb.BuildInfra_Buildbucket{
   424  						Hostname: "example.com",
   425  					},
   426  					Resultdb: &pb.BuildInfra_ResultDB{
   427  						Hostname:   "rdb.example.com",
   428  						Invocation: "bb-12345",
   429  					},
   430  				},
   431  			}), ShouldBeNil)
   432  			So(datastore.Put(ctx, &model.BuildInputProperties{
   433  				Build: key,
   434  				Proto: &structpb.Struct{
   435  					Fields: map[string]*structpb.Value{
   436  						"input": {
   437  							Kind: &structpb.Value_StringValue{
   438  								StringValue: "input value",
   439  							},
   440  						},
   441  					},
   442  				},
   443  			}), ShouldBeNil)
   444  			So(datastore.Put(ctx, &model.BuildOutputProperties{
   445  				Build: key,
   446  				Proto: &structpb.Struct{
   447  					Fields: map[string]*structpb.Value{
   448  						"output": {
   449  							Kind: &structpb.Value_StringValue{
   450  								StringValue: "output value",
   451  							},
   452  						},
   453  					},
   454  				},
   455  			}), ShouldBeNil)
   456  
   457  			req := &pb.SearchBuildsRequest{
   458  				Mask: &pb.BuildMask{
   459  					AllFields: true,
   460  				},
   461  			}
   462  
   463  			Convey("BuildsGetLimited only", func() {
   464  				ctx = auth.WithState(ctx, &authtest.FakeState{
   465  					Identity: userID,
   466  					FakeDB: authtest.NewFakeDB(
   467  						authtest.MockPermission(userID, "project:bucket", bbperms.BuildersList),
   468  						authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGetLimited),
   469  					),
   470  				})
   471  
   472  				rsp, err := srv.SearchBuilds(ctx, req)
   473  				So(err, ShouldBeNil)
   474  				expectedRsp := &pb.SearchBuildsResponse{
   475  					Builds: []*pb.Build{
   476  						{
   477  							Id: 1,
   478  							Builder: &pb.BuilderID{
   479  								Project: "project",
   480  								Bucket:  "bucket",
   481  								Builder: "builder",
   482  							},
   483  							Input: &pb.Build_Input{
   484  								GerritChanges: []*pb.GerritChange{
   485  									{Host: "h1"},
   486  									{Host: "h2"},
   487  								},
   488  							},
   489  							Infra: &pb.BuildInfra{
   490  								Resultdb: &pb.BuildInfra_ResultDB{
   491  									Hostname:   "rdb.example.com",
   492  									Invocation: "bb-12345",
   493  								},
   494  							},
   495  						},
   496  						{
   497  							Id: 2,
   498  							Builder: &pb.BuilderID{
   499  								Project: "project",
   500  								Bucket:  "bucket",
   501  								Builder: "builder2",
   502  							},
   503  							Input: &pb.Build_Input{},
   504  						},
   505  					},
   506  				}
   507  				So(rsp, ShouldResembleProto, expectedRsp)
   508  			})
   509  
   510  			Convey("BuildsList only", func() {
   511  				ctx = auth.WithState(ctx, &authtest.FakeState{
   512  					Identity: userID,
   513  					FakeDB: authtest.NewFakeDB(
   514  						authtest.MockPermission(userID, "project:bucket", bbperms.BuildersList),
   515  						authtest.MockPermission(userID, "project:bucket", bbperms.BuildsList),
   516  					),
   517  				})
   518  
   519  				rsp, err := srv.SearchBuilds(ctx, req)
   520  				So(err, ShouldBeNil)
   521  				expectedRsp := &pb.SearchBuildsResponse{
   522  					Builds: []*pb.Build{
   523  						{
   524  							Id: 1,
   525  						},
   526  						{
   527  							Id: 2,
   528  						},
   529  					},
   530  				}
   531  				So(rsp, ShouldResembleProto, expectedRsp)
   532  			})
   533  		})
   534  	})
   535  }