go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/rpc/query_console_snapshots_test.go (about)

     1  // Copyright 2023 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  	. "github.com/smartystreets/goconvey/convey"
    22  	"google.golang.org/grpc/codes"
    23  
    24  	"go.chromium.org/luci/auth/identity"
    25  	"go.chromium.org/luci/buildbucket/bbperms"
    26  	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
    27  	. "go.chromium.org/luci/common/testing/assertions"
    28  	"go.chromium.org/luci/gae/impl/memory"
    29  	"go.chromium.org/luci/gae/service/datastore"
    30  	"go.chromium.org/luci/grpc/grpcutil"
    31  	"go.chromium.org/luci/milo/internal/model"
    32  	"go.chromium.org/luci/milo/internal/model/milostatus"
    33  	"go.chromium.org/luci/milo/internal/projectconfig"
    34  	"go.chromium.org/luci/milo/internal/testutils"
    35  	"go.chromium.org/luci/milo/internal/utils"
    36  	projectconfigpb "go.chromium.org/luci/milo/proto/projectconfig"
    37  	milopb "go.chromium.org/luci/milo/proto/v1"
    38  	"go.chromium.org/luci/server/auth"
    39  	"go.chromium.org/luci/server/auth/authtest"
    40  	"go.chromium.org/luci/server/auth/realms"
    41  	"go.chromium.org/luci/server/secrets"
    42  )
    43  
    44  var projects = []*projectconfig.Project{
    45  	{
    46  		ID:  "allowed-project",
    47  		ACL: projectconfig.ACL{Identities: []identity.Identity{"user"}},
    48  	},
    49  	{
    50  		ID:  "other-allowed-project",
    51  		ACL: projectconfig.ACL{Identities: []identity.Identity{"user"}},
    52  	},
    53  	{
    54  		ID:  "project-with-external-ref",
    55  		ACL: projectconfig.ACL{Identities: []identity.Identity{"user"}},
    56  	},
    57  }
    58  
    59  var consoleDefs = []*projectconfigpb.Console{
    60  	{
    61  		Realm: "allowed-project:@root",
    62  		Id:    "con1",
    63  		Builders: []*projectconfigpb.Builder{
    64  			{
    65  				Id: &buildbucketpb.BuilderID{
    66  					Project: "allowed-project",
    67  					Bucket:  "bucket",
    68  					Builder: "builder",
    69  				},
    70  			},
    71  			{
    72  				Id: &buildbucketpb.BuilderID{
    73  					Project: "other-allowed-project",
    74  					Bucket:  "bucket",
    75  					Builder: "builder",
    76  				},
    77  			},
    78  			{
    79  				Id: &buildbucketpb.BuilderID{
    80  					Project: "forbidden-project",
    81  					Bucket:  "bucket",
    82  					Builder: "builder",
    83  				},
    84  			},
    85  		},
    86  	},
    87  	{
    88  		Realm: "allowed-project:@root",
    89  		Id:    "con2",
    90  		Builders: []*projectconfigpb.Builder{
    91  			{
    92  				Id: &buildbucketpb.BuilderID{
    93  					Project: "allowed-project",
    94  					Bucket:  "bucket",
    95  					Builder: "builder",
    96  				},
    97  			},
    98  			{
    99  				Id: &buildbucketpb.BuilderID{
   100  					Project: "allowed-project",
   101  					Bucket:  "bucket",
   102  					Builder: "new-builder",
   103  				},
   104  			},
   105  		},
   106  	},
   107  	{
   108  		Realm: "allowed-project:@root",
   109  		Id:    "con3",
   110  		Builders: []*projectconfigpb.Builder{
   111  			{
   112  				Id: &buildbucketpb.BuilderID{
   113  					Project: "other-allowed-project",
   114  					Bucket:  "bucket",
   115  					Builder: "builder",
   116  				},
   117  			},
   118  		},
   119  	},
   120  	{
   121  		Realm: "other-allowed-project:@root",
   122  		Id:    "con1",
   123  		Builders: []*projectconfigpb.Builder{
   124  			{
   125  				Id: &buildbucketpb.BuilderID{
   126  					Project: "allowed-project",
   127  					Bucket:  "bucket",
   128  					Builder: "builder",
   129  				},
   130  			},
   131  		},
   132  	},
   133  	{
   134  		Realm: "forbidden-project:@root",
   135  		Id:    "con1",
   136  		Builders: []*projectconfigpb.Builder{
   137  			{
   138  				Id: &buildbucketpb.BuilderID{
   139  					Project: "allowed-project",
   140  					Bucket:  "bucket",
   141  					Builder: "builder",
   142  				},
   143  			},
   144  		},
   145  	},
   146  	{
   147  		Realm:           "project-with-external-ref:@root",
   148  		Id:              "con1",
   149  		ExternalProject: "allowed-project",
   150  		ExternalId:      "con1",
   151  	},
   152  	{
   153  		Realm:           "project-with-external-ref:@root",
   154  		Id:              "con2",
   155  		ExternalProject: "forbidden-project",
   156  		ExternalId:      "con1",
   157  	},
   158  	{
   159  		Realm: "project-with-external-ref:@root",
   160  		Id:    "con3",
   161  		Builders: []*projectconfigpb.Builder{
   162  			{
   163  				Id: &buildbucketpb.BuilderID{
   164  					Project: "project-with-external-ref",
   165  					Bucket:  "bucket",
   166  					Builder: "builder",
   167  				},
   168  			},
   169  		},
   170  	},
   171  }
   172  
   173  var builderSummaries = []*model.BuilderSummary{
   174  	{
   175  		BuilderID:           "buildbucket/luci.allowed-project.bucket/builder",
   176  		ProjectID:           "allowed-project",
   177  		LastFinishedBuildID: "buildbucket/111111",
   178  		LastFinishedStatus:  milostatus.InfraFailure,
   179  	},
   180  	{
   181  		BuilderID:           "buildbucket/luci.other-allowed-project.bucket/builder",
   182  		ProjectID:           "other-allowed-project",
   183  		LastFinishedBuildID: "buildbucket/111112",
   184  		LastFinishedStatus:  milostatus.Success,
   185  	},
   186  	{
   187  		BuilderID:           "buildbucket/luci.forbidden-project.bucket/builder",
   188  		ProjectID:           "forbidden-project",
   189  		LastFinishedBuildID: "buildbucket/111113",
   190  		LastFinishedStatus:  milostatus.Canceled,
   191  	},
   192  	{
   193  		BuilderID:           "buildbucket/luci.project-with-external-ref.bucket/builder",
   194  		ProjectID:           "project-with-external-ref",
   195  		LastFinishedBuildID: "buildbucket/111114",
   196  		LastFinishedStatus:  milostatus.Failure,
   197  	},
   198  }
   199  
   200  var perms = []authtest.RealmPermission{
   201  	{
   202  		Realm:      "allowed-project:bucket",
   203  		Permission: bbperms.BuildsList,
   204  	},
   205  	{
   206  		Realm:      "other-allowed-project:bucket",
   207  		Permission: bbperms.BuildsList,
   208  	},
   209  	{
   210  		Realm:      "project-with-external-ref:bucket",
   211  		Permission: bbperms.BuildsList,
   212  	},
   213  }
   214  
   215  func TestQueryConsoleSnapshots(t *testing.T) {
   216  	t.Parallel()
   217  	Convey(`TestQueryConsoleSnapshots`, t, func() {
   218  		ctx := memory.Use(context.Background())
   219  		ctx = testutils.SetUpTestGlobalCache(ctx)
   220  		ctx = secrets.GeneratePrimaryTinkAEADForTest(ctx)
   221  
   222  		datastore.GetTestable(ctx).AutoIndex(true)
   223  		datastore.GetTestable(ctx).Consistent(true)
   224  
   225  		err := datastore.Put(ctx, projects)
   226  		So(err, ShouldBeNil)
   227  
   228  		// Transform & save console defs to datastore.
   229  		consoles := make([]*projectconfig.Console, 0, len(consoleDefs))
   230  		for _, conDef := range consoleDefs {
   231  			proj, _ := realms.Split(conDef.Realm)
   232  			conID := projectconfig.ConsoleID{
   233  				Project: proj,
   234  				ID:      conDef.Id,
   235  			}
   236  			console := conID.SetID(ctx, nil)
   237  			console.Def = *conDef
   238  			console.Builders = make([]string, 0, len(conDef.Builders))
   239  			for _, builder := range conDef.Builders {
   240  				legacyID := utils.LegacyBuilderIDString(builder.Id)
   241  				console.Builders = append(console.Builders, legacyID)
   242  			}
   243  			consoles = append(consoles, console)
   244  		}
   245  		err = datastore.Put(ctx, consoles)
   246  		So(err, ShouldBeNil)
   247  
   248  		err = datastore.Put(ctx, builderSummaries)
   249  		So(err, ShouldBeNil)
   250  
   251  		srv := &MiloInternalService{}
   252  
   253  		Convey(`e2e`, func() {
   254  			ctx := auth.WithState(ctx, &authtest.FakeState{Identity: "user", IdentityPermissions: perms})
   255  
   256  			res, err := srv.QueryConsoleSnapshots(ctx, &milopb.QueryConsoleSnapshotsRequest{
   257  				Predicate: &milopb.ConsolePredicate{
   258  					Project: "allowed-project",
   259  				},
   260  				PageSize: 2,
   261  			})
   262  			So(err, ShouldBeNil)
   263  			So(res.Snapshots, ShouldResembleProto, []*milopb.ConsoleSnapshot{
   264  				{
   265  					Console: &projectconfigpb.Console{
   266  						Realm: "allowed-project:@root",
   267  						Id:    "con1",
   268  						Builders: []*projectconfigpb.Builder{
   269  							{
   270  								Id: &buildbucketpb.BuilderID{
   271  									Project: "allowed-project",
   272  									Bucket:  "bucket",
   273  									Builder: "builder",
   274  								},
   275  							},
   276  							{
   277  								Id: &buildbucketpb.BuilderID{
   278  									Project: "other-allowed-project",
   279  									Bucket:  "bucket",
   280  									Builder: "builder",
   281  								},
   282  							},
   283  						},
   284  					},
   285  					BuilderSnapshots: []*milopb.BuilderSnapshot{
   286  						{
   287  							Builder: &buildbucketpb.BuilderID{
   288  								Project: "allowed-project",
   289  								Bucket:  "bucket",
   290  								Builder: "builder",
   291  							},
   292  							Build: &buildbucketpb.Build{
   293  								Id: 111111,
   294  								Builder: &buildbucketpb.BuilderID{
   295  									Project: "allowed-project",
   296  									Bucket:  "bucket",
   297  									Builder: "builder",
   298  								},
   299  								Status: buildbucketpb.Status_INFRA_FAILURE,
   300  							},
   301  						},
   302  						{
   303  							Builder: &buildbucketpb.BuilderID{
   304  								Project: "other-allowed-project",
   305  								Bucket:  "bucket",
   306  								Builder: "builder",
   307  							},
   308  							Build: &buildbucketpb.Build{
   309  								Id: 111112,
   310  								Builder: &buildbucketpb.BuilderID{
   311  									Project: "other-allowed-project",
   312  									Bucket:  "bucket",
   313  									Builder: "builder",
   314  								},
   315  								Status: buildbucketpb.Status_SUCCESS,
   316  							},
   317  						},
   318  					},
   319  				},
   320  				{
   321  					Console: &projectconfigpb.Console{
   322  						Realm: "allowed-project:@root",
   323  						Id:    "con2",
   324  						Builders: []*projectconfigpb.Builder{
   325  							{
   326  								Id: &buildbucketpb.BuilderID{
   327  									Project: "allowed-project",
   328  									Bucket:  "bucket",
   329  									Builder: "builder",
   330  								},
   331  							},
   332  							{
   333  								Id: &buildbucketpb.BuilderID{
   334  									Project: "allowed-project",
   335  									Bucket:  "bucket",
   336  									Builder: "new-builder",
   337  								},
   338  							},
   339  						},
   340  					},
   341  					BuilderSnapshots: []*milopb.BuilderSnapshot{
   342  						{
   343  							Builder: &buildbucketpb.BuilderID{
   344  								Project: "allowed-project",
   345  								Bucket:  "bucket",
   346  								Builder: "builder",
   347  							},
   348  							Build: &buildbucketpb.Build{
   349  								Id: 111111,
   350  								Builder: &buildbucketpb.BuilderID{
   351  									Project: "allowed-project",
   352  									Bucket:  "bucket",
   353  									Builder: "builder",
   354  								},
   355  								Status: buildbucketpb.Status_INFRA_FAILURE,
   356  							},
   357  						},
   358  						{
   359  							Builder: &buildbucketpb.BuilderID{
   360  								Project: "allowed-project",
   361  								Bucket:  "bucket",
   362  								Builder: "new-builder",
   363  							},
   364  							Build: nil,
   365  						},
   366  					},
   367  				},
   368  			})
   369  			So(res.NextPageToken, ShouldNotBeEmpty)
   370  
   371  			res, err = srv.QueryConsoleSnapshots(ctx, &milopb.QueryConsoleSnapshotsRequest{
   372  				Predicate: &milopb.ConsolePredicate{
   373  					Project: "allowed-project",
   374  				},
   375  				PageSize:  2,
   376  				PageToken: res.NextPageToken,
   377  			})
   378  			So(err, ShouldBeNil)
   379  			So(res.Snapshots, ShouldResembleProto, []*milopb.ConsoleSnapshot{
   380  				{
   381  					Console: &projectconfigpb.Console{
   382  						Realm: "allowed-project:@root",
   383  						Id:    "con3",
   384  						Builders: []*projectconfigpb.Builder{
   385  							{
   386  								Id: &buildbucketpb.BuilderID{
   387  									Project: "other-allowed-project",
   388  									Bucket:  "bucket",
   389  									Builder: "builder",
   390  								},
   391  							},
   392  						},
   393  					},
   394  					BuilderSnapshots: []*milopb.BuilderSnapshot{
   395  						{
   396  							Builder: &buildbucketpb.BuilderID{
   397  								Project: "other-allowed-project",
   398  								Bucket:  "bucket",
   399  								Builder: "builder",
   400  							},
   401  							Build: &buildbucketpb.Build{
   402  								Id: 111112,
   403  								Builder: &buildbucketpb.BuilderID{
   404  									Project: "other-allowed-project",
   405  									Bucket:  "bucket",
   406  									Builder: "builder",
   407  								},
   408  								Status: buildbucketpb.Status_SUCCESS,
   409  							},
   410  						},
   411  					},
   412  				},
   413  			})
   414  			So(res.NextPageToken, ShouldBeEmpty)
   415  		})
   416  
   417  		Convey(`query with builder predicate`, func() {
   418  			ctx := auth.WithState(ctx, &authtest.FakeState{Identity: "user", IdentityPermissions: perms})
   419  
   420  			res, err := srv.QueryConsoleSnapshots(ctx, &milopb.QueryConsoleSnapshotsRequest{
   421  				Predicate: &milopb.ConsolePredicate{
   422  					Project: "allowed-project",
   423  					Builder: &buildbucketpb.BuilderID{
   424  						Project: "other-allowed-project",
   425  						Bucket:  "bucket",
   426  						Builder: "builder",
   427  					},
   428  				},
   429  			})
   430  			So(err, ShouldBeNil)
   431  			So(res.Snapshots, ShouldResembleProto, []*milopb.ConsoleSnapshot{
   432  				{
   433  					Console: &projectconfigpb.Console{
   434  						Realm: "allowed-project:@root",
   435  						Id:    "con1",
   436  						Builders: []*projectconfigpb.Builder{
   437  							{
   438  								Id: &buildbucketpb.BuilderID{
   439  									Project: "allowed-project",
   440  									Bucket:  "bucket",
   441  									Builder: "builder",
   442  								},
   443  							},
   444  							{
   445  								Id: &buildbucketpb.BuilderID{
   446  									Project: "other-allowed-project",
   447  									Bucket:  "bucket",
   448  									Builder: "builder",
   449  								},
   450  							},
   451  						},
   452  					},
   453  					BuilderSnapshots: []*milopb.BuilderSnapshot{
   454  						{
   455  							Builder: &buildbucketpb.BuilderID{
   456  								Project: "allowed-project",
   457  								Bucket:  "bucket",
   458  								Builder: "builder",
   459  							},
   460  							Build: &buildbucketpb.Build{
   461  								Id: 111111,
   462  								Builder: &buildbucketpb.BuilderID{
   463  									Project: "allowed-project",
   464  									Bucket:  "bucket",
   465  									Builder: "builder",
   466  								},
   467  								Status: buildbucketpb.Status_INFRA_FAILURE,
   468  							},
   469  						},
   470  						{
   471  							Builder: &buildbucketpb.BuilderID{
   472  								Project: "other-allowed-project",
   473  								Bucket:  "bucket",
   474  								Builder: "builder",
   475  							},
   476  							Build: &buildbucketpb.Build{
   477  								Id: 111112,
   478  								Builder: &buildbucketpb.BuilderID{
   479  									Project: "other-allowed-project",
   480  									Bucket:  "bucket",
   481  									Builder: "builder",
   482  								},
   483  								Status: buildbucketpb.Status_SUCCESS,
   484  							},
   485  						},
   486  					},
   487  				},
   488  				{
   489  					Console: &projectconfigpb.Console{
   490  						Realm: "allowed-project:@root",
   491  						Id:    "con3",
   492  						Builders: []*projectconfigpb.Builder{
   493  							{
   494  								Id: &buildbucketpb.BuilderID{
   495  									Project: "other-allowed-project",
   496  									Bucket:  "bucket",
   497  									Builder: "builder",
   498  								},
   499  							},
   500  						},
   501  					},
   502  					BuilderSnapshots: []*milopb.BuilderSnapshot{
   503  						{
   504  							Builder: &buildbucketpb.BuilderID{
   505  								Project: "other-allowed-project",
   506  								Bucket:  "bucket",
   507  								Builder: "builder",
   508  							},
   509  							Build: &buildbucketpb.Build{
   510  								Id: 111112,
   511  								Builder: &buildbucketpb.BuilderID{
   512  									Project: "other-allowed-project",
   513  									Bucket:  "bucket",
   514  									Builder: "builder",
   515  								},
   516  								Status: buildbucketpb.Status_SUCCESS,
   517  							},
   518  						},
   519  					},
   520  				},
   521  			})
   522  			So(res.NextPageToken, ShouldBeEmpty)
   523  		})
   524  
   525  		Convey(`query forbidden project`, func() {
   526  			ctx := auth.WithState(ctx, &authtest.FakeState{Identity: "user", IdentityPermissions: perms})
   527  
   528  			res, err := srv.QueryConsoleSnapshots(ctx, &milopb.QueryConsoleSnapshotsRequest{
   529  				Predicate: &milopb.ConsolePredicate{
   530  					Project: "forbidden-project",
   531  				},
   532  			})
   533  			So(err, ShouldNotBeNil)
   534  			So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
   535  			So(res, ShouldBeNil)
   536  		})
   537  
   538  		Convey(`query forbidden project with builder predicate`, func() {
   539  			ctx := auth.WithState(ctx, &authtest.FakeState{Identity: "user", IdentityPermissions: perms})
   540  
   541  			res, err := srv.QueryConsoleSnapshots(ctx, &milopb.QueryConsoleSnapshotsRequest{
   542  				Predicate: &milopb.ConsolePredicate{
   543  					Project: "allowed-project",
   544  					Builder: &buildbucketpb.BuilderID{
   545  						Project: "forbidden-project",
   546  						Bucket:  "bucket",
   547  						Builder: "builder",
   548  					},
   549  				},
   550  			})
   551  			So(err, ShouldNotBeNil)
   552  			So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
   553  			So(res, ShouldBeNil)
   554  		})
   555  
   556  		Convey(`resolve external consoles`, func() {
   557  			ctx := auth.WithState(ctx, &authtest.FakeState{Identity: "user", IdentityPermissions: perms})
   558  
   559  			res, err := srv.QueryConsoleSnapshots(ctx, &milopb.QueryConsoleSnapshotsRequest{
   560  				Predicate: &milopb.ConsolePredicate{
   561  					Project: "project-with-external-ref",
   562  				},
   563  			})
   564  			So(err, ShouldBeNil)
   565  			So(res.Snapshots, ShouldResembleProto, []*milopb.ConsoleSnapshot{
   566  				{
   567  					Console: &projectconfigpb.Console{
   568  						Realm: "allowed-project:@root",
   569  						Id:    "con1",
   570  						Builders: []*projectconfigpb.Builder{
   571  							{
   572  								Id: &buildbucketpb.BuilderID{
   573  									Project: "allowed-project",
   574  									Bucket:  "bucket",
   575  									Builder: "builder",
   576  								},
   577  							},
   578  							{
   579  								Id: &buildbucketpb.BuilderID{
   580  									Project: "other-allowed-project",
   581  									Bucket:  "bucket",
   582  									Builder: "builder",
   583  								},
   584  							},
   585  						},
   586  					},
   587  					BuilderSnapshots: []*milopb.BuilderSnapshot{
   588  						{
   589  							Builder: &buildbucketpb.BuilderID{
   590  								Project: "allowed-project",
   591  								Bucket:  "bucket",
   592  								Builder: "builder",
   593  							},
   594  							Build: &buildbucketpb.Build{
   595  								Id: 111111,
   596  								Builder: &buildbucketpb.BuilderID{
   597  									Project: "allowed-project",
   598  									Bucket:  "bucket",
   599  									Builder: "builder",
   600  								},
   601  								Status: buildbucketpb.Status_INFRA_FAILURE,
   602  							},
   603  						},
   604  						{
   605  							Builder: &buildbucketpb.BuilderID{
   606  								Project: "other-allowed-project",
   607  								Bucket:  "bucket",
   608  								Builder: "builder",
   609  							},
   610  							Build: &buildbucketpb.Build{
   611  								Id: 111112,
   612  								Builder: &buildbucketpb.BuilderID{
   613  									Project: "other-allowed-project",
   614  									Bucket:  "bucket",
   615  									Builder: "builder",
   616  								},
   617  								Status: buildbucketpb.Status_SUCCESS,
   618  							},
   619  						},
   620  					},
   621  				},
   622  				{
   623  					Console: &projectconfigpb.Console{
   624  						Realm: "project-with-external-ref:@root",
   625  						Id:    "con3",
   626  						Builders: []*projectconfigpb.Builder{
   627  							{
   628  								Id: &buildbucketpb.BuilderID{
   629  									Project: "project-with-external-ref",
   630  									Bucket:  "bucket",
   631  									Builder: "builder",
   632  								},
   633  							},
   634  						},
   635  					},
   636  					BuilderSnapshots: []*milopb.BuilderSnapshot{
   637  						{
   638  							Builder: &buildbucketpb.BuilderID{
   639  								Project: "project-with-external-ref",
   640  								Bucket:  "bucket",
   641  								Builder: "builder",
   642  							},
   643  							Build: &buildbucketpb.Build{
   644  								Id: 111114,
   645  								Builder: &buildbucketpb.BuilderID{
   646  									Project: "project-with-external-ref",
   647  									Bucket:  "bucket",
   648  									Builder: "builder",
   649  								},
   650  								Status: buildbucketpb.Status_FAILURE,
   651  							},
   652  						},
   653  					},
   654  				},
   655  			})
   656  			So(res.NextPageToken, ShouldBeEmpty)
   657  		})
   658  	})
   659  }
   660  
   661  func TestValidateQueryConsoleSnapshotsQuery(t *testing.T) {
   662  	t.Parallel()
   663  	Convey(`TestValidateQueryConsoleSnapshotsRequest`, t, func() {
   664  		Convey(`negative page size`, func() {
   665  			err := validateQueryConsoleSnapshotsRequest(&milopb.QueryConsoleSnapshotsRequest{
   666  				Predicate: &milopb.ConsolePredicate{
   667  					Project: "project",
   668  				},
   669  				PageSize: -1,
   670  			})
   671  			So(err, ShouldNotBeNil)
   672  			So(err, ShouldErrLike, "page_size can not be negative")
   673  		})
   674  
   675  		Convey(`missing project`, func() {
   676  			err := validateQueryConsoleSnapshotsRequest(&milopb.QueryConsoleSnapshotsRequest{
   677  				PageSize: 10,
   678  			})
   679  			So(err, ShouldErrLike, "predicate.project is required")
   680  		})
   681  
   682  		Convey(`valid`, func() {
   683  			err := validateQueryConsoleSnapshotsRequest(&milopb.QueryConsoleSnapshotsRequest{
   684  				Predicate: &milopb.ConsolePredicate{
   685  					Project: "project",
   686  					Builder: &buildbucketpb.BuilderID{
   687  						Project: "project",
   688  						Bucket:  "bucket",
   689  						Builder: "builder",
   690  					},
   691  				},
   692  				PageSize: 10,
   693  			})
   694  			So(err, ShouldBeNil)
   695  		})
   696  
   697  		Convey(`valid with only project`, func() {
   698  			err := validateQueryConsoleSnapshotsRequest(&milopb.QueryConsoleSnapshotsRequest{
   699  				Predicate: &milopb.ConsolePredicate{
   700  					Project: "project",
   701  				},
   702  			})
   703  			So(err, ShouldBeNil)
   704  		})
   705  	})
   706  }