...

Source file src/go.einride.tech/aip/fieldbehavior/fieldbehavior_test.go

Documentation: go.einride.tech/aip/fieldbehavior

     1  package fieldbehavior
     2  
     3  import (
     4  	"testing"
     5  
     6  	examplefreightv1 "go.einride.tech/aip/proto/gen/einride/example/freight/v1"
     7  	syntaxv1 "go.einride.tech/aip/proto/gen/einride/example/syntax/v1"
     8  	"google.golang.org/genproto/googleapis/api/annotations"
     9  	"google.golang.org/genproto/googleapis/example/library/v1"
    10  	"google.golang.org/protobuf/testing/protocmp"
    11  	"google.golang.org/protobuf/types/known/fieldmaskpb"
    12  	"google.golang.org/protobuf/types/known/timestamppb"
    13  	"gotest.tools/v3/assert"
    14  	"gotest.tools/v3/assert/cmp"
    15  )
    16  
    17  func TestClearFields(t *testing.T) {
    18  	t.Parallel()
    19  	t.Run("clear fields with set field_behavior", func(t *testing.T) {
    20  		t.Parallel()
    21  		site := &examplefreightv1.Site{
    22  			Name:        "site1",           // has no field_behaviors; should not be cleared.
    23  			CreateTime:  timestamppb.Now(), // has OUTPUT_ONLY field_behavior; should be cleared.
    24  			DisplayName: "site one",        // has REQUIRED field_behavior; should not be cleared.
    25  		}
    26  		ClearFields(site, annotations.FieldBehavior_OUTPUT_ONLY)
    27  		assert.Equal(t, site.GetCreateTime(), (*timestamppb.Timestamp)(nil))
    28  		assert.Equal(t, site.GetDisplayName(), "site one")
    29  		assert.Equal(t, site.GetName(), "site1")
    30  	})
    31  	t.Run("clear field with set field_behavior on nested message", func(t *testing.T) {
    32  		t.Parallel()
    33  		input := &syntaxv1.FieldBehaviorMessage{
    34  			Field:         "field",    // has no field_behaviors; should not be cleared.
    35  			OptionalField: "optional", // has OPTIONAL field_behavior; should not be cleared.
    36  			MessageWithoutFieldBehavior: &syntaxv1.FieldBehaviorMessage{ // has no field_behaviors; should not be cleared.
    37  				Field:           "field",       // has no field_behaviors; should not be cleared.
    38  				OptionalField:   "optional",    // has OPTIONAL field_behavior; should not be cleared.
    39  				OutputOnlyField: "output_only", // has OUTPUT_ONLY field_behavior; should be cleared.
    40  			},
    41  		}
    42  
    43  		expected := &syntaxv1.FieldBehaviorMessage{
    44  			Field:         "field",
    45  			OptionalField: "optional",
    46  			MessageWithoutFieldBehavior: &syntaxv1.FieldBehaviorMessage{
    47  				Field:         "field",
    48  				OptionalField: "optional",
    49  			},
    50  		}
    51  
    52  		ClearFields(input, annotations.FieldBehavior_OUTPUT_ONLY)
    53  		assert.DeepEqual(t, input, expected, protocmp.Transform())
    54  	})
    55  
    56  	t.Run("clear field with set field_behavior on multiple levels of nested messages", func(t *testing.T) {
    57  		t.Parallel()
    58  		input := &syntaxv1.FieldBehaviorMessage{
    59  			Field:         "field",    // has no field_behaviors; should not be cleared.
    60  			OptionalField: "optional", // has OPTIONAL field_behavior; should not be cleared.
    61  			MessageWithoutFieldBehavior: &syntaxv1.FieldBehaviorMessage{ // has no field_behaviors; should not be cleared.
    62  				Field:           "field",       // has no field_behaviors; should not be cleared.
    63  				OptionalField:   "optional",    // has OPTIONAL field_behavior; should not be cleared.
    64  				OutputOnlyField: "output_only", // has OUTPUT_ONLY field_behavior; should be cleared.
    65  				MessageWithoutFieldBehavior: &syntaxv1.FieldBehaviorMessage{ // has no field_behaviors; should not be cleared.
    66  					Field:           "field",       // has no field_behaviors; should not be cleared.
    67  					OptionalField:   "optional",    // has OPTIONAL field_behavior; should not be cleared.
    68  					OutputOnlyField: "output_only", // has OUTPUT_ONLY field_behavior; should be cleared.
    69  				},
    70  			},
    71  		}
    72  
    73  		expected := &syntaxv1.FieldBehaviorMessage{
    74  			Field:         "field",
    75  			OptionalField: "optional",
    76  			MessageWithoutFieldBehavior: &syntaxv1.FieldBehaviorMessage{
    77  				Field:         "field",
    78  				OptionalField: "optional",
    79  				MessageWithoutFieldBehavior: &syntaxv1.FieldBehaviorMessage{
    80  					Field:         "field",
    81  					OptionalField: "optional",
    82  				},
    83  			},
    84  		}
    85  
    86  		ClearFields(input, annotations.FieldBehavior_OUTPUT_ONLY)
    87  		assert.DeepEqual(t, input, expected, protocmp.Transform())
    88  	})
    89  
    90  	t.Run("clear fields with set field_behavior on repeated message", func(t *testing.T) {
    91  		t.Parallel()
    92  		input := &syntaxv1.FieldBehaviorMessage{
    93  			Field:         "field",    // has no field_behaviors; should not be cleared.
    94  			OptionalField: "optional", // has OPTIONAL field_behavior; should not be cleared.
    95  			RepeatedMessage: []*syntaxv1.FieldBehaviorMessage{ // has no field_behaviors; should not be cleared.
    96  				{
    97  					Field:           "field",       // has no field_behaviors; should not be cleared.
    98  					OptionalField:   "optional",    // has OPTIONAL field_behavior; should not be cleared.
    99  					OutputOnlyField: "output_only", // has OUTPUT_ONLY field_behavior; should be cleared.
   100  				},
   101  			},
   102  			StringList: []string{ // not a message type, should not be traversed
   103  				"string",
   104  			},
   105  		}
   106  
   107  		expected := &syntaxv1.FieldBehaviorMessage{
   108  			Field:         "field",
   109  			OptionalField: "optional",
   110  			RepeatedMessage: []*syntaxv1.FieldBehaviorMessage{
   111  				{
   112  					Field:         "field",
   113  					OptionalField: "optional",
   114  				},
   115  			},
   116  			StringList: []string{
   117  				"string",
   118  			},
   119  		}
   120  
   121  		ClearFields(input, annotations.FieldBehavior_OUTPUT_ONLY)
   122  		assert.DeepEqual(t, input, expected, protocmp.Transform())
   123  	})
   124  
   125  	t.Run("clear fields with set field_behavior on multiple levels of repeated messages", func(t *testing.T) {
   126  		t.Parallel()
   127  		input := &syntaxv1.FieldBehaviorMessage{
   128  			Field:         "field",    // has no field_behaviors; should not be cleared.
   129  			OptionalField: "optional", // has OPTIONAL field_behavior; should not be cleared.
   130  			RepeatedMessage: []*syntaxv1.FieldBehaviorMessage{ // has no field_behaviors; should not be cleared.
   131  				{
   132  					Field:           "field",       // has no field_behaviors; should not be cleared.
   133  					OptionalField:   "optional",    // has OPTIONAL field_behavior; should not be cleared.
   134  					OutputOnlyField: "output_only", // has OUTPUT_ONLY field_behavior; should be cleared.
   135  					RepeatedMessage: []*syntaxv1.FieldBehaviorMessage{ // has no field_behaviors; should not be cleared.
   136  						{
   137  							Field:           "field",       // has no field_behaviors; should not be cleared.
   138  							OptionalField:   "optional",    // has OPTIONAL field_behavior; should not be cleared.
   139  							OutputOnlyField: "output_only", // has OUTPUT_ONLY field_behavior; should be cleared.
   140  						},
   141  					},
   142  				},
   143  			},
   144  			StringList: []string{ // not a message type, should not be traversed
   145  				"string",
   146  			},
   147  		}
   148  
   149  		expected := &syntaxv1.FieldBehaviorMessage{
   150  			Field:         "field",
   151  			OptionalField: "optional",
   152  			RepeatedMessage: []*syntaxv1.FieldBehaviorMessage{
   153  				{
   154  					Field:         "field",
   155  					OptionalField: "optional",
   156  					RepeatedMessage: []*syntaxv1.FieldBehaviorMessage{
   157  						{
   158  							Field:         "field",
   159  							OptionalField: "optional",
   160  						},
   161  					},
   162  				},
   163  			},
   164  			StringList: []string{
   165  				"string",
   166  			},
   167  		}
   168  
   169  		ClearFields(input, annotations.FieldBehavior_OUTPUT_ONLY)
   170  		assert.DeepEqual(t, input, expected, protocmp.Transform())
   171  	})
   172  
   173  	t.Run("clear repeated field with set field_behavior", func(t *testing.T) {
   174  		t.Parallel()
   175  		input := &syntaxv1.FieldBehaviorMessage{
   176  			Field:         "field",    // has no field_behaviors; should not be cleared.
   177  			OptionalField: "optional", // has OPTIONAL field_behavior; should not be cleared.
   178  			RepeatedMessage: []*syntaxv1.FieldBehaviorMessage{ // has no field_behaviors; should not be cleared.
   179  				{
   180  					Field:         "field",    // has no field_behaviors; should not be cleared.
   181  					OptionalField: "optional", // has OPTIONAL field_behavior; should not be cleared.
   182  				},
   183  			},
   184  			RepeatedOutputOnlyMessage: []*syntaxv1.FieldBehaviorMessage{ // has OUTPUT_ONLY field_behavior; should be cleared.
   185  				{
   186  					Field:           "field",
   187  					OptionalField:   "optional",
   188  					OutputOnlyField: "output_only",
   189  				},
   190  			},
   191  		}
   192  
   193  		expected := &syntaxv1.FieldBehaviorMessage{
   194  			Field:         "field",    // has no field_behaviors; should not be cleared.
   195  			OptionalField: "optional", // has OPTIONAL field_behavior; should not be cleared.
   196  			RepeatedMessage: []*syntaxv1.FieldBehaviorMessage{ // has no field_behaviors; should not be cleared.
   197  				{
   198  					Field:         "field",    // has no field_behaviors; should not be cleared.
   199  					OptionalField: "optional", // has OPTIONAL field_behavior; should not be cleared.
   200  				},
   201  			},
   202  		}
   203  
   204  		ClearFields(input, annotations.FieldBehavior_OUTPUT_ONLY)
   205  		assert.DeepEqual(t, input, expected, protocmp.Transform())
   206  	})
   207  
   208  	t.Run("clear fields with set field_behavior on message in map", func(t *testing.T) {
   209  		t.Parallel()
   210  		input := &syntaxv1.FieldBehaviorMessage{
   211  			OptionalField: "optional", // has OPTIONAL field_behavior; should not be cleared.
   212  			MapOptionalMessage: map[string]*syntaxv1.FieldBehaviorMessage{
   213  				"key_1": {
   214  					OptionalField:   "optional",    // has OPTIONAL field_behavior; should not be cleared.
   215  					OutputOnlyField: "output_only", // has OUTPUT_ONLY field_behavior; should be cleared.
   216  				},
   217  			},
   218  			StringMap: map[string]string{
   219  				"string_key": "string", // not a message type, should not be traversed
   220  			},
   221  		}
   222  
   223  		expected := &syntaxv1.FieldBehaviorMessage{
   224  			OptionalField: "optional",
   225  			MapOptionalMessage: map[string]*syntaxv1.FieldBehaviorMessage{
   226  				"key_1": {
   227  					OptionalField: "optional",
   228  				},
   229  			},
   230  			StringMap: map[string]string{
   231  				"string_key": "string",
   232  			},
   233  		}
   234  
   235  		ClearFields(input, annotations.FieldBehavior_OUTPUT_ONLY)
   236  		assert.DeepEqual(
   237  			t,
   238  			input,
   239  			expected,
   240  			protocmp.Transform(),
   241  		)
   242  	})
   243  
   244  	t.Run("clear map field with set field_behavior", func(t *testing.T) {
   245  		t.Parallel()
   246  		input := &syntaxv1.FieldBehaviorMessage{
   247  			OptionalField: "optional",
   248  			// has OUTPUT_ONLY field_behavior; should be cleared.
   249  			MapOutputOnlyMessage: map[string]*syntaxv1.FieldBehaviorMessage{
   250  				"key_1": {
   251  					OutputOnlyField: "output_only", // has OUTPUT_ONLY field_behavior; but should be cleared with parent message.
   252  				},
   253  			},
   254  		}
   255  
   256  		expected := &syntaxv1.FieldBehaviorMessage{
   257  			OptionalField: "optional",
   258  		}
   259  
   260  		ClearFields(input, annotations.FieldBehavior_OUTPUT_ONLY)
   261  		assert.DeepEqual(
   262  			t,
   263  			input,
   264  			expected,
   265  			protocmp.Transform(),
   266  		)
   267  	})
   268  
   269  	t.Run("clear field with set field_behavior on oneof message", func(t *testing.T) {
   270  		t.Parallel()
   271  		input := &syntaxv1.FieldBehaviorMessage{
   272  			Field:         "field",    // has no field_behaviors; should not be cleared.
   273  			OptionalField: "optional", // has OPTIONAL field_behavior; should not be cleared.
   274  			Oneof: &syntaxv1.FieldBehaviorMessage_FieldBehaviorMessage{
   275  				FieldBehaviorMessage: &syntaxv1.FieldBehaviorMessage{
   276  					Field:           "field",       // has no field_behaviors; should not be cleared.
   277  					OptionalField:   "optional",    // has OPTIONAL field_behavior; should not be cleared.
   278  					OutputOnlyField: "output_only", // has OUTPUT_ONLY field_behavior; should be cleared.
   279  				},
   280  			},
   281  		}
   282  
   283  		expected := &syntaxv1.FieldBehaviorMessage{
   284  			Field:         "field",
   285  			OptionalField: "optional",
   286  			Oneof: &syntaxv1.FieldBehaviorMessage_FieldBehaviorMessage{
   287  				FieldBehaviorMessage: &syntaxv1.FieldBehaviorMessage{
   288  					Field:         "field",
   289  					OptionalField: "optional",
   290  				},
   291  			},
   292  		}
   293  
   294  		ClearFields(input, annotations.FieldBehavior_OUTPUT_ONLY)
   295  		assert.DeepEqual(t, input, expected, protocmp.Transform())
   296  	})
   297  }
   298  
   299  func TestCopyFields(t *testing.T) {
   300  	t.Parallel()
   301  	t.Run("different types", func(t *testing.T) {
   302  		t.Parallel()
   303  		assert.Assert(t, cmp.Panics(func() {
   304  			CopyFields(&library.Book{}, &library.Shelf{}, annotations.FieldBehavior_REQUIRED)
   305  		}))
   306  	})
   307  }
   308  
   309  func TestValidateRequiredFields(t *testing.T) {
   310  	t.Parallel()
   311  	assert.NilError(t, ValidateRequiredFields(&examplefreightv1.GetShipmentRequest{Name: "testbook"}))
   312  	assert.Error(t, ValidateRequiredFields(&examplefreightv1.GetShipmentRequest{}), "missing required field: name")
   313  }
   314  
   315  func TestValidateRequiredFieldsWithMask(t *testing.T) {
   316  	t.Parallel()
   317  	t.Run("ok", func(t *testing.T) {
   318  		t.Parallel()
   319  		assert.NilError(
   320  			t,
   321  			ValidateRequiredFieldsWithMask(
   322  				&library.Book{Name: "testbook"},
   323  				nil,
   324  			),
   325  		)
   326  	})
   327  	t.Run("ok - empty mask", func(t *testing.T) {
   328  		t.Parallel()
   329  		assert.NilError(
   330  			t,
   331  			ValidateRequiredFieldsWithMask(
   332  				&library.Book{},
   333  				nil,
   334  			),
   335  		)
   336  	})
   337  	t.Run("missing field", func(t *testing.T) {
   338  		t.Parallel()
   339  		assert.Error(
   340  			t,
   341  			ValidateRequiredFieldsWithMask(
   342  				&examplefreightv1.GetShipmentRequest{},
   343  				&fieldmaskpb.FieldMask{Paths: []string{"*"}},
   344  			),
   345  			"missing required field: name",
   346  		)
   347  	})
   348  	t.Run("missing but not in mask", func(t *testing.T) {
   349  		t.Parallel()
   350  		assert.NilError(
   351  			t,
   352  			ValidateRequiredFieldsWithMask(
   353  				&library.Book{},
   354  				&fieldmaskpb.FieldMask{Paths: []string{"author"}},
   355  			),
   356  		)
   357  	})
   358  	t.Run("missing nested", func(t *testing.T) {
   359  		t.Parallel()
   360  		assert.Error(
   361  			t,
   362  			ValidateRequiredFieldsWithMask(
   363  				&examplefreightv1.UpdateShipmentRequest{
   364  					Shipment: &examplefreightv1.Shipment{},
   365  				},
   366  				&fieldmaskpb.FieldMask{Paths: []string{"shipment.origin_site"}},
   367  			),
   368  			"missing required field: shipment.origin_site",
   369  		)
   370  	})
   371  	t.Run("missing nested not in mask", func(t *testing.T) {
   372  		t.Parallel()
   373  		assert.NilError(
   374  			t,
   375  			ValidateRequiredFieldsWithMask(
   376  				&library.UpdateBookRequest{
   377  					Book: &library.Book{},
   378  				},
   379  				&fieldmaskpb.FieldMask{Paths: []string{"book.author"}},
   380  			),
   381  		)
   382  	})
   383  	t.Run("support maps", func(t *testing.T) {
   384  		t.Parallel()
   385  		assert.NilError(
   386  			t,
   387  			ValidateRequiredFieldsWithMask(
   388  				&examplefreightv1.Shipment{
   389  					Annotations: map[string]string{
   390  						"x": "y",
   391  					},
   392  				},
   393  				&fieldmaskpb.FieldMask{Paths: []string{"annotations"}},
   394  			),
   395  		)
   396  	})
   397  }
   398  
   399  func TestValidateImmutableFieldsWithMask(t *testing.T) {
   400  	t.Parallel()
   401  	t.Run("no error when immutable field not set", func(t *testing.T) {
   402  		t.Parallel()
   403  		req := &examplefreightv1.UpdateShipmentRequest{
   404  			Shipment: &examplefreightv1.Shipment{},
   405  			UpdateMask: &fieldmaskpb.FieldMask{
   406  				Paths: []string{""},
   407  			},
   408  		}
   409  		err := ValidateImmutableFieldsWithMask(req, req.GetUpdateMask())
   410  		assert.NilError(t, err)
   411  	})
   412  	t.Run("no error when immutable field not part of fieldmask", func(t *testing.T) {
   413  		t.Parallel()
   414  		req := &examplefreightv1.UpdateShipmentRequest{
   415  			Shipment: &examplefreightv1.Shipment{
   416  				ExternalReferenceId: "external-reference-id",
   417  				OriginSite:          "shippers/shipper1/sites/site1",
   418  			},
   419  			UpdateMask: &fieldmaskpb.FieldMask{
   420  				Paths: []string{"shipment.origin_site"},
   421  			},
   422  		}
   423  		err := ValidateImmutableFieldsWithMask(req, req.GetUpdateMask())
   424  		assert.NilError(t, err)
   425  	})
   426  	t.Run("errors when wildcard fieldmask used", func(t *testing.T) {
   427  		t.Parallel()
   428  		req := &examplefreightv1.UpdateShipmentRequest{
   429  			Shipment: &examplefreightv1.Shipment{},
   430  			UpdateMask: &fieldmaskpb.FieldMask{
   431  				Paths: []string{"*"},
   432  			},
   433  		}
   434  		err := ValidateImmutableFieldsWithMask(req, req.GetUpdateMask())
   435  		assert.ErrorContains(t, err, "field is immutable")
   436  	})
   437  	t.Run("errors when immutable field set in fieldmask", func(t *testing.T) {
   438  		t.Parallel()
   439  		req := &examplefreightv1.UpdateShipmentRequest{
   440  			Shipment: &examplefreightv1.Shipment{},
   441  			UpdateMask: &fieldmaskpb.FieldMask{
   442  				Paths: []string{"shipment.external_reference_id"},
   443  			},
   444  		}
   445  		err := ValidateImmutableFieldsWithMask(req, req.GetUpdateMask())
   446  		assert.ErrorContains(t, err, "field is immutable")
   447  	})
   448  	t.Run("errors when immutable field set in message", func(t *testing.T) {
   449  		t.Parallel()
   450  		req := &examplefreightv1.UpdateShipmentRequest{
   451  			Shipment: &examplefreightv1.Shipment{
   452  				ExternalReferenceId: "I am immutable!",
   453  			},
   454  			UpdateMask: &fieldmaskpb.FieldMask{
   455  				Paths: []string{},
   456  			},
   457  		}
   458  		err := ValidateImmutableFieldsWithMask(req, req.GetUpdateMask())
   459  		assert.ErrorContains(t, err, "field is immutable")
   460  	})
   461  	t.Run("errors when immutable field set in nested field", func(t *testing.T) {
   462  		t.Parallel()
   463  		req := &examplefreightv1.UpdateShipmentRequest{
   464  			Shipment: &examplefreightv1.Shipment{
   465  				LineItems: []*examplefreightv1.LineItem{
   466  					{
   467  						ExternalReferenceId: "I am immutable",
   468  					},
   469  				},
   470  			},
   471  			UpdateMask: &fieldmaskpb.FieldMask{
   472  				Paths: []string{},
   473  			},
   474  		}
   475  		err := ValidateImmutableFieldsWithMask(req, req.GetUpdateMask())
   476  		assert.ErrorContains(t, err, "field is immutable")
   477  	})
   478  }
   479  

View as plain text