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

     1  // Copyright 2021 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/golang/mock/gomock"
    22  	. "github.com/smartystreets/goconvey/convey"
    23  	"go.chromium.org/luci/auth/identity"
    24  	"go.chromium.org/luci/buildbucket/bbperms"
    25  	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
    26  	"go.chromium.org/luci/common/proto"
    27  	"go.chromium.org/luci/gae/impl/memory"
    28  	"go.chromium.org/luci/gae/service/datastore"
    29  	"go.chromium.org/luci/milo/internal/projectconfig"
    30  	"go.chromium.org/luci/milo/internal/testutils"
    31  	configpb "go.chromium.org/luci/milo/proto/config"
    32  	milopb "go.chromium.org/luci/milo/proto/v1"
    33  	"go.chromium.org/luci/server/auth"
    34  	"go.chromium.org/luci/server/auth/authtest"
    35  	"go.chromium.org/luci/server/caching"
    36  )
    37  
    38  func TestListBuilders(t *testing.T) {
    39  	t.Parallel()
    40  	Convey(`TestListBuilders`, t, func() {
    41  		ctx := memory.Use(context.Background())
    42  		ctx = caching.WithEmptyProcessCache(ctx)
    43  		ctx = testutils.SetUpTestGlobalCache(ctx)
    44  
    45  		ctx = auth.WithState(ctx, &authtest.FakeState{
    46  			Identity: "user",
    47  			IdentityPermissions: []authtest.RealmPermission{
    48  				{
    49  					Realm:      "this_project:fake.bucket_1",
    50  					Permission: bbperms.BuildersList,
    51  				},
    52  				{
    53  					Realm:      "this_project:fake.bucket_2",
    54  					Permission: bbperms.BuildersList,
    55  				},
    56  				{
    57  					Realm:      "other_project:fake.bucket_2",
    58  					Permission: bbperms.BuildersList,
    59  				},
    60  			},
    61  		})
    62  
    63  		datastore.GetTestable(ctx).AddIndexes(&datastore.IndexDefinition{
    64  			Kind: "BuildSummary",
    65  			SortBy: []datastore.IndexColumn{
    66  				{Property: "BuilderID"},
    67  				{Property: "Created", Descending: true},
    68  			},
    69  		})
    70  		datastore.GetTestable(ctx).Consistent(true)
    71  		mockBuildersClient := buildbucketpb.NewMockBuildersClient(gomock.NewController(t))
    72  		srv := &MiloInternalService{
    73  			GetSettings: func(c context.Context) (*configpb.Settings, error) {
    74  				return &configpb.Settings{
    75  					Buildbucket: &configpb.Settings_Buildbucket{
    76  						Host: "buildbucket_host",
    77  					},
    78  				}, nil
    79  			},
    80  			GetBuildersClient: func(c context.Context, host string, as auth.RPCAuthorityKind) (buildbucketpb.BuildersClient, error) {
    81  				return mockBuildersClient, nil
    82  			},
    83  		}
    84  
    85  		err := datastore.Put(ctx, []*projectconfig.Project{
    86  			{
    87  				ID:  "this_project",
    88  				ACL: projectconfig.ACL{Identities: []identity.Identity{"user"}},
    89  				ExternalBuilderIDs: []string{
    90  					"other_project/bucket_without.access/fake.builder 1",
    91  					"other_project/fake.bucket_2/fake.builder 1",
    92  					"other_project/fake.bucket_2/fake.builder 2",
    93  				},
    94  			},
    95  			{
    96  				ID: "other_project",
    97  			},
    98  		})
    99  		So(err, ShouldBeNil)
   100  
   101  		err = datastore.Put(ctx, []*projectconfig.Console{
   102  			{
   103  				Parent: datastore.MakeKey(ctx, "Project", "this_project"),
   104  				ID:     "console1",
   105  				Builders: []string{
   106  					"buildbucket/luci.other_project.fake.bucket_2/fake.builder 2",
   107  					"buildbucket/luci.other_project.bucket_without.access/fake.builder 1",
   108  					"buildbucket/luci.this_project.fake.bucket_2/fake.builder 1",
   109  					"buildbucket/luci.this_project.fake.bucket_1/fake.builder 1",
   110  				},
   111  			},
   112  			{
   113  				Parent: datastore.MakeKey(ctx, "Project", "this_project"),
   114  				ID:     "console2",
   115  				Builders: []string{
   116  					"buildbucket/luci.other_project.fake.bucket_2/fake.builder 1",
   117  					"buildbucket/luci.this_project.fake.bucket_2/fake.builder 1",
   118  					"buildbucket/luci.this_project.fake.bucket_1/fake.builder 2",
   119  				},
   120  			},
   121  		})
   122  		So(err, ShouldBeNil)
   123  
   124  		// Mock the first buildbucket ListBuilders response.
   125  		expectedReq := &buildbucketpb.ListBuildersRequest{
   126  			PageSize: 1000,
   127  		}
   128  		mockBuildersClient.
   129  			EXPECT().
   130  			ListBuilders(gomock.Any(), proto.MatcherEqual(expectedReq)).
   131  			MaxTimes(1).
   132  			Return(&buildbucketpb.ListBuildersResponse{
   133  				Builders: []*buildbucketpb.BuilderItem{
   134  					{
   135  						Id: &buildbucketpb.BuilderID{
   136  							Project: "other_project",
   137  							Bucket:  "fake.bucket_2",
   138  							Builder: "fake.builder 1",
   139  						},
   140  					},
   141  					{
   142  						Id: &buildbucketpb.BuilderID{
   143  							Project: "this_project",
   144  							Bucket:  "fake.bucket_1",
   145  							Builder: "fake.builder 1",
   146  						},
   147  					},
   148  					{
   149  						Id: &buildbucketpb.BuilderID{
   150  							Project: "this_project",
   151  							Bucket:  "fake.bucket_1",
   152  							Builder: "fake.builder 2",
   153  						},
   154  					},
   155  				},
   156  				NextPageToken: "page 2",
   157  			}, nil)
   158  
   159  		// Mock the second buildbucket ListBuilders response.
   160  		expectedReq = &buildbucketpb.ListBuildersRequest{
   161  			PageSize:  1000,
   162  			PageToken: "page 2",
   163  		}
   164  		mockBuildersClient.
   165  			EXPECT().
   166  			ListBuilders(gomock.Any(), proto.MatcherEqual(expectedReq)).
   167  			MaxTimes(1).
   168  			Return(&buildbucketpb.ListBuildersResponse{
   169  				Builders: []*buildbucketpb.BuilderItem{
   170  					{
   171  						Id: &buildbucketpb.BuilderID{
   172  							Project: "this_project",
   173  							Bucket:  "fake.bucket_2",
   174  							Builder: "fake.builder 1",
   175  						},
   176  					},
   177  					{
   178  						Id: &buildbucketpb.BuilderID{
   179  							Project: "this_project",
   180  							Bucket:  "bucket_without.access",
   181  							Builder: "fake.builder 1",
   182  						},
   183  					},
   184  				},
   185  			}, nil)
   186  
   187  		Convey(`list all builders E2E`, func() {
   188  			// Test the first page.
   189  			// It should return builders from the first page of the buildbucket.ListBuilders Response.
   190  			res, err := srv.ListBuilders(ctx, &milopb.ListBuildersRequest{
   191  				PageSize: 3,
   192  			})
   193  			So(err, ShouldBeNil)
   194  			So(res.Builders, ShouldResemble, []*buildbucketpb.BuilderItem{
   195  				{
   196  					Id: &buildbucketpb.BuilderID{
   197  						Project: "other_project",
   198  						Bucket:  "fake.bucket_2",
   199  						Builder: "fake.builder 1",
   200  					},
   201  				},
   202  				{
   203  					Id: &buildbucketpb.BuilderID{
   204  						Project: "this_project",
   205  						Bucket:  "fake.bucket_1",
   206  						Builder: "fake.builder 1",
   207  					},
   208  				},
   209  				{
   210  					Id: &buildbucketpb.BuilderID{
   211  						Project: "this_project",
   212  						Bucket:  "fake.bucket_1",
   213  						Builder: "fake.builder 2",
   214  					},
   215  				},
   216  			})
   217  			So(res.NextPageToken, ShouldNotBeEmpty)
   218  
   219  			// Test the second page.
   220  			// It should return builders from the second page of the buildbucket.ListBuilders Response.
   221  			// without returning anything from the external builders list defined in the consoles, since
   222  			// they should be included in the list builders response already.
   223  			res, err = srv.ListBuilders(ctx, &milopb.ListBuildersRequest{
   224  				PageSize:  3,
   225  				PageToken: res.NextPageToken,
   226  			})
   227  			So(err, ShouldBeNil)
   228  			So(res.Builders, ShouldResemble, []*buildbucketpb.BuilderItem{
   229  				{
   230  					Id: &buildbucketpb.BuilderID{
   231  						Project: "this_project",
   232  						Bucket:  "fake.bucket_2",
   233  						Builder: "fake.builder 1",
   234  					},
   235  				},
   236  			})
   237  			So(res.NextPageToken, ShouldBeEmpty)
   238  		})
   239  
   240  		Convey(`list project builders E2E`, func() {
   241  			// Test the first page.
   242  			// It should return builders from the first page of the buildbucket.ListBuilders Response.
   243  			// With builders from other_project filtered out.
   244  			res, err := srv.ListBuilders(ctx, &milopb.ListBuildersRequest{
   245  				Project:  "this_project",
   246  				PageSize: 2,
   247  			})
   248  			So(err, ShouldBeNil)
   249  			So(res.Builders, ShouldResemble, []*buildbucketpb.BuilderItem{
   250  				{
   251  					Id: &buildbucketpb.BuilderID{
   252  						Project: "this_project",
   253  						Bucket:  "fake.bucket_1",
   254  						Builder: "fake.builder 1",
   255  					},
   256  				},
   257  				{
   258  					Id: &buildbucketpb.BuilderID{
   259  						Project: "this_project",
   260  						Bucket:  "fake.bucket_1",
   261  						Builder: "fake.builder 2",
   262  					},
   263  				},
   264  			})
   265  			So(res.NextPageToken, ShouldNotBeEmpty)
   266  
   267  			// Test the second page.
   268  			// It should return builders from the second page of the buildbucket.ListBuilders Response.
   269  			// with accessable external builders filling the rest of the page.
   270  			res, err = srv.ListBuilders(ctx, &milopb.ListBuildersRequest{
   271  				Project:   "this_project",
   272  				PageSize:  2,
   273  				PageToken: res.NextPageToken,
   274  			})
   275  			So(err, ShouldBeNil)
   276  			So(res.Builders, ShouldResemble, []*buildbucketpb.BuilderItem{
   277  				{
   278  					Id: &buildbucketpb.BuilderID{
   279  						Project: "this_project",
   280  						Bucket:  "fake.bucket_2",
   281  						Builder: "fake.builder 1",
   282  					},
   283  				},
   284  				{
   285  					Id: &buildbucketpb.BuilderID{
   286  						Project: "other_project",
   287  						Bucket:  "fake.bucket_2",
   288  						Builder: "fake.builder 1",
   289  					},
   290  				},
   291  			})
   292  			So(res.NextPageToken, ShouldNotBeEmpty)
   293  
   294  			// Test the third page.
   295  			// It should return the remaining accessible external builders.
   296  			res, err = srv.ListBuilders(ctx, &milopb.ListBuildersRequest{
   297  				Project:   "this_project",
   298  				PageSize:  2,
   299  				PageToken: res.NextPageToken,
   300  			})
   301  			So(err, ShouldBeNil)
   302  			So(res.Builders, ShouldResemble, []*buildbucketpb.BuilderItem{
   303  				{
   304  					Id: &buildbucketpb.BuilderID{
   305  						Project: "other_project",
   306  						Bucket:  "fake.bucket_2",
   307  						Builder: "fake.builder 2",
   308  					},
   309  				},
   310  			})
   311  			So(res.NextPageToken, ShouldBeEmpty)
   312  		})
   313  
   314  		Convey(`list group builders E2E`, func() {
   315  			// Test the first page.
   316  			// It should return accessible internal builders first.
   317  			res, err := srv.ListBuilders(ctx, &milopb.ListBuildersRequest{
   318  				Project:  "this_project",
   319  				Group:    "console1",
   320  				PageSize: 2,
   321  			})
   322  			So(err, ShouldBeNil)
   323  			So(res.Builders, ShouldResemble, []*buildbucketpb.BuilderItem{
   324  				{
   325  					Id: &buildbucketpb.BuilderID{
   326  						Project: "this_project",
   327  						Bucket:  "fake.bucket_1",
   328  						Builder: "fake.builder 1",
   329  					},
   330  				},
   331  				{
   332  					Id: &buildbucketpb.BuilderID{
   333  						Project: "this_project",
   334  						Bucket:  "fake.bucket_2",
   335  						Builder: "fake.builder 1",
   336  					},
   337  				},
   338  			})
   339  			So(res.NextPageToken, ShouldNotBeEmpty)
   340  
   341  			// Test the second page.
   342  			// It should return the remaining accessible external builders.
   343  			res, err = srv.ListBuilders(ctx, &milopb.ListBuildersRequest{
   344  				Project:   "this_project",
   345  				Group:     "console1",
   346  				PageSize:  2,
   347  				PageToken: res.NextPageToken,
   348  			})
   349  			So(err, ShouldBeNil)
   350  			So(res.Builders, ShouldResemble, []*buildbucketpb.BuilderItem{
   351  				{
   352  					Id: &buildbucketpb.BuilderID{
   353  						Project: "other_project",
   354  						Bucket:  "fake.bucket_2",
   355  						Builder: "fake.builder 2",
   356  					},
   357  				},
   358  			})
   359  			So(res.NextPageToken, ShouldBeEmpty)
   360  		})
   361  
   362  		Convey(`reject users without access to the project`, func() {
   363  			c := auth.WithState(ctx, &authtest.FakeState{Identity: "user2"})
   364  
   365  			_, err := srv.ListBuilders(c, &milopb.ListBuildersRequest{
   366  				Project:  "this_project",
   367  				Group:    "console1",
   368  				PageSize: 2,
   369  			})
   370  			So(err, ShouldNotBeNil)
   371  		})
   372  	})
   373  }