go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/model/mask_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 model 16 17 import ( 18 "encoding/json" 19 "testing" 20 21 "google.golang.org/protobuf/encoding/protojson" 22 "google.golang.org/protobuf/proto" 23 "google.golang.org/protobuf/types/known/durationpb" 24 "google.golang.org/protobuf/types/known/fieldmaskpb" 25 "google.golang.org/protobuf/types/known/structpb" 26 "google.golang.org/protobuf/types/known/timestamppb" 27 28 pb "go.chromium.org/luci/buildbucket/proto" 29 "go.chromium.org/luci/common/proto/structmask" 30 31 . "github.com/smartystreets/goconvey/convey" 32 . "go.chromium.org/luci/common/testing/assertions" 33 ) 34 35 func TestBuildMask(t *testing.T) { 36 t.Parallel() 37 38 type inner struct { 39 FieldOne string `json:"field_one,omitempty"` 40 FieldTwo string `json:"field_two,omitempty"` 41 } 42 43 type testStruct struct { 44 Str string `json:"str,omitempty"` 45 Int int `json:"int,omitempty"` 46 Map map[string]string `json:"map,omitempty"` 47 List []inner `json:"list,omitempty"` 48 } 49 50 build := pb.Build{ 51 Id: 12345, 52 Builder: &pb.BuilderID{ 53 Project: "project", 54 Bucket: "bucket", 55 Builder: "builder", 56 }, 57 Number: 7777, 58 Canary: true, 59 CreatedBy: "created_by", 60 CanceledBy: "canceled_by", 61 CreateTime: ×tamppb.Timestamp{Nanos: 11111111}, 62 StartTime: ×tamppb.Timestamp{Nanos: 22222222}, 63 EndTime: ×tamppb.Timestamp{Nanos: 33333333}, 64 UpdateTime: ×tamppb.Timestamp{Nanos: 44444444}, 65 Status: pb.Status_SUCCESS, 66 SummaryMarkdown: "summary_markdown", 67 Critical: pb.Trinary_YES, 68 StatusDetails: &pb.StatusDetails{Timeout: &pb.StatusDetails_Timeout{}}, 69 Input: &pb.Build_Input{ 70 Properties: asStructPb(testStruct{ 71 Str: "input", 72 Int: 123, 73 Map: map[string]string{ 74 "ik1": "iv1", 75 "ik2": "iv2", 76 }, 77 List: []inner{ 78 {"11", "11"}, 79 {"21", "22"}, 80 }, 81 }), 82 GerritChanges: []*pb.GerritChange{ 83 {Host: "h1"}, 84 {Host: "h2"}, 85 }, 86 GitilesCommit: &pb.GitilesCommit{Host: "ihost"}, 87 Experimental: true, 88 }, 89 Output: &pb.Build_Output{ 90 Properties: asStructPb(testStruct{ 91 Str: "output", 92 Int: 123, 93 Map: map[string]string{ 94 "ok1": "ov1", 95 "ok2": "ov2", 96 }, 97 List: []inner{ 98 {"11", "11"}, 99 {"21", "22"}, 100 }, 101 }), 102 GitilesCommit: &pb.GitilesCommit{Host: "ohost"}, 103 }, 104 Steps: []*pb.Step{ 105 {Name: "s1", Status: pb.Status_SUCCESS, SummaryMarkdown: "md1"}, 106 {Name: "s2", Status: pb.Status_SUCCESS, SummaryMarkdown: "md2"}, 107 {Name: "s3", Status: pb.Status_FAILURE, SummaryMarkdown: "md3"}, 108 }, 109 Infra: &pb.BuildInfra{ 110 Buildbucket: &pb.BuildInfra_Buildbucket{ 111 Hostname: "bb-host", 112 RequestedProperties: asStructPb(testStruct{ 113 Str: "requested", 114 Int: 123, 115 Map: map[string]string{ 116 "rk1": "rv1", 117 "rk2": "rv2", 118 }, 119 List: []inner{ 120 {"11", "11"}, 121 {"21", "22"}, 122 }, 123 }), 124 }, 125 Logdog: &pb.BuildInfra_LogDog{Hostname: "logdog-host"}, 126 }, 127 Tags: []*pb.StringPair{ 128 {Key: "k1", Value: "v1"}, 129 {Key: "k2", Value: "v2"}, 130 }, 131 Exe: &pb.Executable{}, 132 SchedulingTimeout: &durationpb.Duration{Seconds: 111}, 133 ExecutionTimeout: &durationpb.Duration{Seconds: 222}, 134 GracePeriod: &durationpb.Duration{Seconds: 333}, 135 } 136 137 afterDefaultMask := &pb.Build{ 138 Builder: build.Builder, 139 Canary: build.Canary, 140 CreateTime: build.CreateTime, 141 CreatedBy: build.CreatedBy, 142 Critical: build.Critical, 143 EndTime: build.EndTime, 144 Id: build.Id, 145 Input: &pb.Build_Input{ 146 Experimental: build.Input.Experimental, 147 GerritChanges: build.Input.GerritChanges, 148 GitilesCommit: build.Input.GitilesCommit, 149 }, 150 Number: build.Number, 151 StartTime: build.StartTime, 152 Status: build.Status, 153 StatusDetails: build.StatusDetails, 154 UpdateTime: build.UpdateTime, 155 } 156 157 apply := func(m *BuildMask) (*pb.Build, error) { 158 b := proto.Clone(&build).(*pb.Build) 159 err := m.Trim(b) 160 return b, err 161 } 162 163 Convey("Default", t, func() { 164 Convey("No masks at all", func() { 165 m, err := NewBuildMask("", nil, nil) 166 So(err, ShouldBeNil) 167 b, err := apply(m) 168 So(err, ShouldBeNil) 169 So(b, ShouldResembleProto, afterDefaultMask) 170 }) 171 172 Convey("Legacy", func() { 173 m, err := NewBuildMask("", &fieldmaskpb.FieldMask{}, nil) 174 So(err, ShouldBeNil) 175 b, err := apply(m) 176 So(err, ShouldBeNil) 177 So(b, ShouldResembleProto, afterDefaultMask) 178 }) 179 180 Convey("BuildMask", func() { 181 m, err := NewBuildMask("", nil, &pb.BuildMask{}) 182 So(err, ShouldBeNil) 183 b, err := apply(m) 184 So(err, ShouldBeNil) 185 So(b, ShouldResembleProto, afterDefaultMask) 186 }) 187 }) 188 189 Convey("Legacy", t, func() { 190 m, err := NewBuildMask("", &fieldmaskpb.FieldMask{ 191 Paths: []string{ 192 "builder", 193 "input.properties", 194 "steps.*.name", // note: extended syntax 195 }, 196 }, nil) 197 So(err, ShouldBeNil) 198 199 b, err := apply(m) 200 So(err, ShouldBeNil) 201 So(b, ShouldResembleProto, &pb.Build{ 202 Builder: build.Builder, 203 Input: &pb.Build_Input{ 204 Properties: build.Input.Properties, 205 }, 206 Steps: []*pb.Step{ 207 {Name: "s1"}, 208 {Name: "s2"}, 209 {Name: "s3"}, 210 }, 211 }) 212 }) 213 214 Convey("Simple build mask", t, func() { 215 m, err := NewBuildMask("", nil, &pb.BuildMask{ 216 Fields: &fieldmaskpb.FieldMask{ 217 Paths: []string{ 218 "builder", 219 "steps", 220 "input.properties", // will be returned unfiltered 221 }, 222 }, 223 }) 224 So(err, ShouldBeNil) 225 226 b, err := apply(m) 227 So(err, ShouldBeNil) 228 So(b, ShouldResembleProto, &pb.Build{ 229 Builder: build.Builder, 230 Steps: []*pb.Step{ 231 {Name: "s1", Status: pb.Status_SUCCESS, SummaryMarkdown: "md1"}, 232 {Name: "s2", Status: pb.Status_SUCCESS, SummaryMarkdown: "md2"}, 233 {Name: "s3", Status: pb.Status_FAILURE, SummaryMarkdown: "md3"}, 234 }, 235 Input: &pb.Build_Input{ 236 Properties: build.Input.Properties, 237 }, 238 }) 239 }) 240 241 Convey("Struct filters", t, func() { 242 m, err := NewBuildMask("", nil, &pb.BuildMask{ 243 Fields: &fieldmaskpb.FieldMask{ 244 Paths: []string{ 245 "builder", 246 "input.gerrit_changes", 247 "output.properties", // will be filtered, since we have a mask below 248 }, 249 }, 250 InputProperties: []*structmask.StructMask{ 251 {Path: []string{"str"}}, 252 {Path: []string{"map", "ik1"}}, 253 {Path: []string{"list", "*", "field_two"}}, 254 {Path: []string{"unknown"}}, 255 }, 256 OutputProperties: []*structmask.StructMask{ 257 {Path: []string{"str"}}, 258 }, 259 RequestedProperties: []*structmask.StructMask{ 260 {Path: []string{"unknown"}}, 261 }, 262 }) 263 So(err, ShouldBeNil) 264 265 b, err := apply(m) 266 So(err, ShouldBeNil) 267 So(b, ShouldResembleProto, &pb.Build{ 268 Builder: build.Builder, 269 Input: &pb.Build_Input{ 270 GerritChanges: build.Input.GerritChanges, 271 Properties: asStructPb(testStruct{ 272 Str: "input", 273 Map: map[string]string{"ik1": "iv1"}, 274 List: []inner{ 275 {"", "11"}, 276 {"", "22"}, 277 }, 278 }), 279 }, 280 Output: &pb.Build_Output{ 281 Properties: asStructPb(testStruct{ 282 Str: "output", 283 }), 284 }, 285 Infra: &pb.BuildInfra{ 286 Buildbucket: &pb.BuildInfra_Buildbucket{ 287 RequestedProperties: &structpb.Struct{}, // all was filtered out 288 }, 289 }, 290 }) 291 }) 292 293 Convey("Struct filters and default mask", t, func() { 294 m, err := NewBuildMask("", nil, &pb.BuildMask{ 295 OutputProperties: []*structmask.StructMask{ 296 {Path: []string{"str"}}, 297 }, 298 }) 299 So(err, ShouldBeNil) 300 301 b, err := apply(m) 302 So(err, ShouldBeNil) 303 304 expected := proto.Clone(afterDefaultMask).(*pb.Build) 305 expected.Output = &pb.Build_Output{ 306 Properties: asStructPb(testStruct{Str: "output"}), 307 } 308 So(b, ShouldResembleProto, expected) 309 }) 310 311 Convey("Step status with steps", t, func() { 312 m, err := NewBuildMask("", nil, &pb.BuildMask{ 313 Fields: &fieldmaskpb.FieldMask{ 314 Paths: []string{ 315 "steps", 316 }, 317 }, 318 StepStatus: []pb.Status{ 319 pb.Status_FAILURE, 320 }, 321 }) 322 So(err, ShouldBeNil) 323 324 b, err := apply(m) 325 So(err, ShouldBeNil) 326 327 expected := &pb.Build{ 328 Steps: []*pb.Step{ 329 { 330 Name: "s3", 331 Status: pb.Status_FAILURE, 332 SummaryMarkdown: "md3", 333 }, 334 }, 335 } 336 So(b, ShouldResembleProto, expected) 337 }) 338 339 Convey("Step status no steps", t, func() { 340 m, err := NewBuildMask("", nil, &pb.BuildMask{ 341 StepStatus: []pb.Status{ 342 pb.Status_FAILURE, 343 }, 344 }) 345 So(err, ShouldBeNil) 346 347 b, err := apply(m) 348 So(err, ShouldBeNil) 349 350 expected := proto.Clone(afterDefaultMask).(*pb.Build) 351 expected.Steps = nil 352 So(b, ShouldResembleProto, expected) 353 }) 354 355 Convey("Step status all fields", t, func() { 356 m, err := NewBuildMask("", nil, &pb.BuildMask{ 357 AllFields: true, 358 StepStatus: []pb.Status{ 359 pb.Status_FAILURE, 360 }, 361 }) 362 So(err, ShouldBeNil) 363 364 b, err := apply(m) 365 So(err, ShouldBeNil) 366 367 expected := proto.Clone(&build).(*pb.Build) 368 expected.Steps = []*pb.Step{ 369 { 370 Name: "s3", 371 Status: pb.Status_FAILURE, 372 SummaryMarkdown: "md3", 373 }, 374 } 375 So(b, ShouldResembleProto, expected) 376 }) 377 378 Convey("Unknown mask paths", t, func() { 379 _, err := NewBuildMask("", nil, &pb.BuildMask{ 380 Fields: &fieldmaskpb.FieldMask{ 381 Paths: []string{"builderzzz"}, 382 }, 383 }) 384 So(err, ShouldErrLike, `field "builderzzz" does not exist in message Build`) 385 }) 386 387 Convey("Bad struct mask", t, func() { 388 _, err := NewBuildMask("", nil, &pb.BuildMask{ 389 InputProperties: []*structmask.StructMask{ 390 {Path: []string{"'unbalanced"}}, 391 }, 392 }) 393 So(err, ShouldErrLike, `bad "input_properties" struct mask: bad element "'unbalanced" in the mask`) 394 }) 395 396 Convey("Unsupported extended syntax", t, func() { 397 _, err := NewBuildMask("", nil, &pb.BuildMask{ 398 Fields: &fieldmaskpb.FieldMask{ 399 Paths: []string{"steps.*.name"}, 400 }, 401 }) 402 So(err, ShouldErrLike, "no longer supported") 403 }) 404 405 Convey("Legacy and new at the same time are not allowed", t, func() { 406 _, err := NewBuildMask("", &fieldmaskpb.FieldMask{}, &pb.BuildMask{}) 407 So(err, ShouldErrLike, "can't be used together") 408 }) 409 410 Convey("all_fields", t, func() { 411 Convey("fail", func() { 412 _, err := NewBuildMask("", nil, &pb.BuildMask{ 413 AllFields: true, 414 Fields: &fieldmaskpb.FieldMask{ 415 Paths: []string{"status"}, 416 }, 417 }) 418 So(err, ShouldErrLike, "mask.AllFields is mutually exclusive with other mask fields") 419 }) 420 Convey("pass", func() { 421 m, err := NewBuildMask("", nil, &pb.BuildMask{AllFields: true}) 422 So(err, ShouldBeNil) 423 b, err := apply(m) 424 So(err, ShouldBeNil) 425 So(b, ShouldResembleProto, &build) 426 }) 427 }) 428 429 Convey("sanity check mask for list-only permission", t, func() { 430 expectedFields := []string{ 431 "id", 432 "status", 433 "status_details", 434 "can_outlive_parent", 435 "ancestor_ids", 436 } 437 So(BuildFieldsWithVisibility(pb.BuildFieldVisibility_BUILDS_LIST_PERMISSION), ShouldResemble, expectedFields) 438 }) 439 440 Convey("sanity check mask for get-limited permission", t, func() { 441 expectedFields := []string{ 442 "id", 443 "builder", 444 "number", 445 "create_time", 446 "start_time", 447 "end_time", 448 "update_time", 449 "cancel_time", 450 "status", 451 "critical", 452 "status_details", 453 "input.gitiles_commit", 454 "input.gerrit_changes", 455 "infra.resultdb", 456 "can_outlive_parent", 457 "ancestor_ids", 458 } 459 So(BuildFieldsWithVisibility(pb.BuildFieldVisibility_BUILDS_GET_LIMITED_PERMISSION), ShouldResemble, expectedFields) 460 }) 461 } 462 463 func asStructPb(v any) *structpb.Struct { 464 blob, err := json.Marshal(v) 465 if err != nil { 466 panic(err) 467 } 468 s := &structpb.Struct{} 469 if err := (protojson.UnmarshalOptions{}).Unmarshal(blob, s); err != nil { 470 panic(err) 471 } 472 return s 473 }