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 }