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:      &timestamppb.Timestamp{Nanos: 11111111},
    62  		StartTime:       &timestamppb.Timestamp{Nanos: 22222222},
    63  		EndTime:         &timestamppb.Timestamp{Nanos: 33333333},
    64  		UpdateTime:      &timestamppb.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  }