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 }