github.com/googleapis/api-linter@v1.65.2/rules/aip0203/field_behavior_required_test.go (about)

     1  // Copyright 2023 Google LLC
     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  //	https://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  package aip0203
    15  
    16  import (
    17  	"testing"
    18  
    19  	"github.com/googleapis/api-linter/rules/internal/testutils"
    20  )
    21  
    22  const ()
    23  
    24  func TestFieldBehaviorRequired_SingleFile_SingleMessage(t *testing.T) {
    25  	testCases := []struct {
    26  		name     string
    27  		Fields   string
    28  		problems testutils.Problems
    29  	}{
    30  		{
    31  			"ValidImmutable",
    32  			"int32 page_count = 1 [(google.api.field_behavior) = IMMUTABLE];",
    33  			nil,
    34  		},
    35  		{
    36  			"ValidRequired",
    37  			"int32 page_count = 1 [(google.api.field_behavior) = REQUIRED];",
    38  			nil,
    39  		},
    40  		{
    41  			"ValidOptional",
    42  			"int32 page_count = 1 [(google.api.field_behavior) = OPTIONAL];",
    43  			nil,
    44  		},
    45  		// Maps should not be recursed to MapEntries, as they have no field
    46  		// behavior.
    47  		{
    48  			"ValidMap",
    49  			"map<string, string> page_count = 1 [(google.api.field_behavior) = OPTIONAL];",
    50  			nil,
    51  		},
    52  		// OneOfs are not required to have an annotation, as they
    53  		// are implicitly optional.
    54  		{
    55  			"ValidOneOfNoAnnotation",
    56  			`oneof candy_bar {
    57  				bool snickers = 1;
    58  				bool chocolate = 3;
    59  			}`,
    60  			nil,
    61  		},
    62  		{
    63  			"ValidOutputOnly",
    64  			"int32 page_count = 1 [(google.api.field_behavior) = OUTPUT_ONLY];",
    65  			nil,
    66  		},
    67  		{
    68  			"ValidOptionalImmutable",
    69  			`int32 page_count = 1 [
    70  				(google.api.field_behavior) = OUTPUT_ONLY,
    71  				(google.api.field_behavior) = OPTIONAL
    72  			];`,
    73  			nil,
    74  		},
    75  		{
    76  			"ValidRecursiveMessage",
    77  			`message Foo { Foo foo = 1 [(google.api.field_behavior) = OPTIONAL]; }
    78  			 Foo foo = 1 [(google.api.field_behavior) = OPTIONAL];
    79  			`,
    80  			nil,
    81  		},
    82  		{
    83  			"InvalidEmpty",
    84  			"int32 page_count = 1;",
    85  			testutils.Problems{{Message: "annotation must be set"}},
    86  		},
    87  	}
    88  	for _, tc := range testCases {
    89  		t.Run(tc.name, func(t *testing.T) {
    90  			f := testutils.ParseProto3Tmpl(t, `
    91  				package apilinter.test.field_behavior_required;
    92  
    93  				import "google/api/field_behavior.proto";
    94  				import "google/api/resource.proto";
    95  
    96  				service Library {
    97  					rpc UpdateBook(UpdateBookRequest) returns (UpdateBookResponse) {
    98  					}
    99  				}
   100  
   101  				message UpdateBookRequest {
   102  					{{.Fields}}
   103  
   104  					Book book = 2 [(google.api.field_behavior) = REQUIRED];
   105  				}
   106  
   107  				message UpdateBookResponse {
   108  					// verifies that no error was raised on lack
   109  					// of field behavior in the response.
   110  					string name = 1;
   111  				}
   112  
   113  				message Book {
   114  					option (google.api.resource) = {
   115  						type: "library.googleapis.com/Book"
   116  						pattern: "books/{book}"
   117  					};
   118  
   119  					string name = 1;
   120  
   121  					string etag = 2;
   122  				}
   123  			`, tc)
   124  			field := f.GetMessageTypes()[0].GetFields()[0]
   125  
   126  			if diff := tc.problems.SetDescriptor(field).Diff(fieldBehaviorRequired.Lint(f)); diff != "" {
   127  				t.Errorf(diff)
   128  			}
   129  		})
   130  	}
   131  }
   132  
   133  func TestFieldBehaviorRequired_Resource_SingleFile(t *testing.T) {
   134  	testCases := []struct {
   135  		name          string
   136  		FieldBehavior string
   137  		problems      testutils.Problems
   138  	}{
   139  		{
   140  			name:          "valid with field behavior",
   141  			FieldBehavior: "[(google.api.field_behavior) = OUTPUT_ONLY]",
   142  			problems:      nil,
   143  		},
   144  		{
   145  			name:          "valid without field behavior",
   146  			FieldBehavior: "",
   147  			problems:      nil,
   148  		},
   149  	}
   150  	for _, tc := range testCases {
   151  		t.Run(tc.name, func(t *testing.T) {
   152  			f := testutils.ParseProto3Tmpl(t, `
   153  				import "google/api/field_behavior.proto";
   154  				import "google/api/resource.proto";
   155  
   156  				service Library {
   157  					rpc GetBook(GetBookRequest) returns (Book) {
   158  					}
   159  				}
   160  
   161  				message GetBookRequest {
   162  					string name = 1 [(google.api.field_behavior) = REQUIRED];
   163  				}
   164  
   165  				message Book {
   166  					option (google.api.resource) = {
   167  						type: "library.googleapis.com/Book"
   168  						pattern: "books/{book}"
   169  					};
   170  
   171  					string name = 1;
   172  
   173  					string title = 2 {{.FieldBehavior}};
   174  				}
   175  			`, tc)
   176  
   177  			field := f.GetMessageTypes()[1].GetFields()[1]
   178  
   179  			if diff := tc.problems.SetDescriptor(field).Diff(fieldBehaviorRequired.Lint(f)); diff != "" {
   180  				t.Errorf(diff)
   181  			}
   182  		})
   183  	}
   184  
   185  }
   186  
   187  func TestFieldBehaviorRequired_NestedMessages_SingleFile(t *testing.T) {
   188  	testCases := []struct {
   189  		name     string
   190  		Fields   string
   191  		problems testutils.Problems
   192  	}{
   193  		{
   194  			"ValidAnnotatedAndChildAnnotated",
   195  			"Annotated annotated = 1 [(google.api.field_behavior) = REQUIRED];",
   196  			nil,
   197  		},
   198  		{
   199  			"InvalidChildNotAnnotated",
   200  			"NonAnnotated non_annotated = 1 [(google.api.field_behavior) = REQUIRED];",
   201  			testutils.Problems{{Message: "must be set"}},
   202  		},
   203  		// Children of OneOfs should still be validated.
   204  		{
   205  			"InvalidOneOfChildNotAnnotated",
   206  			`oneof candy_bar {
   207  				NonAnnotated non_annotated = 1;
   208  			}`,
   209  			testutils.Problems{{Message: "must be set"}},
   210  		},
   211  	}
   212  	for _, tc := range testCases {
   213  		t.Run(tc.name, func(t *testing.T) {
   214  			f := testutils.ParseProto3Tmpl(t, `
   215  				import "google/api/field_behavior.proto";
   216  
   217  				service Library {
   218  					rpc UpdateBook(UpdateBookRequest) returns (UpdateBookResponse) {
   219  					}
   220  				}
   221  
   222  				message NonAnnotated {
   223  					string nested = 1;
   224  				}
   225  
   226  				message Annotated {
   227  					string nested = 1 [(google.api.field_behavior) = REQUIRED];
   228  				}
   229  
   230  				message UpdateBookRequest {
   231  					{{.Fields}}
   232  				}
   233  
   234  				message UpdateBookResponse {
   235  					// verifies that no error was raised on lack
   236  					// of field behavior in the response.
   237  					string name = 1;
   238  				}
   239  			`, tc)
   240  
   241  			it := f.GetServices()[0].GetMethods()[0].GetInputType()
   242  			nestedField := it.GetFields()[0].GetMessageType().GetFields()[0]
   243  
   244  			if diff := tc.problems.SetDescriptor(nestedField).Diff(fieldBehaviorRequired.Lint(f)); diff != "" {
   245  				t.Errorf(diff)
   246  			}
   247  		})
   248  	}
   249  }
   250  
   251  func TestFieldBehaviorRequired_NestedMessages_MultipleFile(t *testing.T) {
   252  	testCases := []struct {
   253  		name             string
   254  		MessageType      string
   255  		MessageFieldName string
   256  		problems         testutils.Problems
   257  	}{
   258  		{
   259  			"ValidAnnotatedAndChildAnnotated",
   260  			"Annotated",
   261  			"annotated",
   262  			nil,
   263  		},
   264  		{
   265  			"ValidAnnotatedAndChildInOtherPackageUnannotated",
   266  			"unannotated.NonAnnotated",
   267  			"non_annotated",
   268  			nil,
   269  		},
   270  		{
   271  			"InvalidChildNotAnnotated",
   272  			"NonAnnotated",
   273  			"non_annotated",
   274  			testutils.Problems{{Message: "must be set"}},
   275  		},
   276  	}
   277  	for _, tc := range testCases {
   278  		t.Run(tc.name, func(t *testing.T) {
   279  			f1 := `
   280  				package apilinter.test.field_behavior_required;
   281  
   282  				import "google/api/field_behavior.proto";
   283  				import "resource.proto";
   284  				import "unannotated.proto";
   285  
   286  				service Library {
   287  					rpc UpdateBook(UpdateBookRequest) returns (UpdateBookResponse) {
   288  					}
   289  				}
   290  
   291  				message UpdateBookRequest {
   292  					{{.MessageType}} {{.MessageFieldName}} = 1 [(google.api.field_behavior) = REQUIRED];
   293  				}
   294  
   295  				message UpdateBookResponse {
   296  					// verifies that no error was raised on lack
   297  					// of field behavior in the response.
   298  					string name = 1;
   299  				}
   300  			`
   301  
   302  			f2 := `
   303  				package apilinter.test.field_behavior_required;
   304  
   305  				import "google/api/field_behavior.proto";
   306  
   307  				message NonAnnotated {
   308  					string nested = 1;
   309  				}
   310  
   311  				message Annotated {
   312  					string nested = 1 [(google.api.field_behavior) = REQUIRED];
   313  				}
   314  			`
   315  
   316  			f3 := `
   317  				package apilinter.test.unannotated;
   318  
   319  				message NonAnnotated {
   320  					string nested = 1;
   321  				}
   322  			`
   323  
   324  			srcs := map[string]string{
   325  				"service.proto":     f1,
   326  				"resource.proto":    f2,
   327  				"unannotated.proto": f3,
   328  			}
   329  
   330  			ds := testutils.ParseProto3Tmpls(t, srcs, tc)
   331  			f := ds["service.proto"]
   332  			it := f.GetServices()[0].GetMethods()[0].GetInputType()
   333  			fd := it.GetFields()[0].GetMessageType().GetFields()[0]
   334  
   335  			if diff := tc.problems.SetDescriptor(fd).Diff(fieldBehaviorRequired.Lint(f)); diff != "" {
   336  				t.Errorf(diff)
   337  			}
   338  
   339  			if tc.problems != nil {
   340  				want := "resource.proto"
   341  				if got := fd.GetFile().GetName(); got != want {
   342  					t.Fatalf("got file name %q for location of field but wanted %q", got, want)
   343  				}
   344  			}
   345  		})
   346  	}
   347  }