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 }