github.com/googleapis/api-linter@v1.65.2/rules/internal/utils/extension_test.go (about) 1 // Copyright 2019 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 15 package utils 16 17 import ( 18 "testing" 19 20 "bitbucket.org/creachadair/stringset" 21 "github.com/google/go-cmp/cmp" 22 "github.com/googleapis/api-linter/rules/internal/testutils" 23 apb "google.golang.org/genproto/googleapis/api/annotations" 24 "google.golang.org/protobuf/proto" 25 ) 26 27 func TestGetFieldBehavior(t *testing.T) { 28 fd := testutils.ParseProto3String(t, ` 29 import "google/api/field_behavior.proto"; 30 31 message Book { 32 string name = 1 [ 33 (google.api.field_behavior) = IMMUTABLE, 34 (google.api.field_behavior) = OUTPUT_ONLY]; 35 36 string title = 2 [(google.api.field_behavior) = REQUIRED]; 37 38 string summary = 3; 39 } 40 `) 41 msg := fd.GetMessageTypes()[0] 42 tests := []struct { 43 fieldName string 44 fieldBehaviors stringset.Set 45 }{ 46 {"name", stringset.New("IMMUTABLE", "OUTPUT_ONLY")}, 47 {"title", stringset.New("REQUIRED")}, 48 {"summary", stringset.New()}, 49 } 50 for _, test := range tests { 51 t.Run(test.fieldName, func(t *testing.T) { 52 f := msg.FindFieldByName(test.fieldName) 53 if diff := cmp.Diff(GetFieldBehavior(f), test.fieldBehaviors); diff != "" { 54 t.Errorf(diff) 55 } 56 }) 57 } 58 } 59 60 func TestGetMethodSignatures(t *testing.T) { 61 for _, test := range []struct { 62 name string 63 want [][]string 64 Signatures string 65 }{ 66 {"Zero", [][]string{}, ""}, 67 {"One", [][]string{{"name"}}, `option (google.api.method_signature) = "name";`}, 68 { 69 "Two", 70 [][]string{{"name"}, {"name", "read_mask"}}, 71 `option (google.api.method_signature) = "name"; 72 option (google.api.method_signature) = "name,read_mask";`, 73 }, 74 } { 75 t.Run(test.name, func(t *testing.T) { 76 f := testutils.ParseProto3Tmpl(t, ` 77 import "google/api/client.proto"; 78 service Library { 79 rpc GetBook(GetBookRequest) returns (Book) { 80 {{.Signatures}} 81 } 82 } 83 message Book {} 84 message GetBookRequest {} 85 `, test) 86 method := f.GetServices()[0].GetMethods()[0] 87 if diff := cmp.Diff(GetMethodSignatures(method), test.want); diff != "" { 88 t.Errorf(diff) 89 } 90 }) 91 } 92 } 93 94 func TestGetOperationInfo(t *testing.T) { 95 fd := testutils.ParseProto3String(t, ` 96 import "google/longrunning/operations.proto"; 97 service Library { 98 rpc WriteBook(WriteBookRequest) returns (google.longrunning.Operation) { 99 option (google.longrunning.operation_info) = { 100 response_type: "WriteBookResponse" 101 metadata_type: "WriteBookMetadata" 102 }; 103 } 104 } 105 message WriteBookRequest {} 106 `) 107 lro := GetOperationInfo(fd.GetServices()[0].GetMethods()[0]) 108 if got, want := lro.ResponseType, "WriteBookResponse"; got != want { 109 t.Errorf("Response type - got %q, want %q.", got, want) 110 } 111 if got, want := lro.MetadataType, "WriteBookMetadata"; got != want { 112 t.Errorf("Metadata type - got %q, want %q.", got, want) 113 } 114 } 115 116 func TestGetOperationInfoNone(t *testing.T) { 117 fd := testutils.ParseProto3String(t, ` 118 service Library { 119 rpc GetBook(GetBookRequest) returns (Book); 120 } 121 message GetBookRequest {} 122 message Book {} 123 `) 124 lro := GetOperationInfo(fd.GetServices()[0].GetMethods()[0]) 125 if lro != nil { 126 t.Errorf("Got %v, expected nil LRO annotation.", lro) 127 } 128 } 129 130 func TestGetOperationInfoResponseType(t *testing.T) { 131 // Set up testing permutations. 132 tests := []struct { 133 testName string 134 ResponseType string 135 valid bool 136 }{ 137 {"Valid", "WriteBookResponse", true}, 138 {"Invalid", "Foo", false}, 139 } 140 for _, test := range tests { 141 t.Run(test.testName, func(t *testing.T) { 142 fd := testutils.ParseProto3Tmpl(t, ` 143 import "google/longrunning/operations.proto"; 144 service Library { 145 rpc WriteBook(WriteBookRequest) returns (google.longrunning.Operation) { 146 option (google.longrunning.operation_info) = { 147 response_type: "{{ .ResponseType }}" 148 metadata_type: "WriteBookMetadata" 149 }; 150 } 151 } 152 message WriteBookRequest {} 153 message WriteBookResponse {} 154 `, test) 155 156 typ := GetOperationResponseType(fd.GetServices()[0].GetMethods()[0]) 157 158 if validType := typ != nil; validType != test.valid { 159 t.Fatalf("Expected valid(%v) response_type message", test.valid) 160 } 161 162 if !test.valid { 163 return 164 } 165 166 if got, want := typ.GetName(), test.ResponseType; got != want { 167 t.Errorf("Response type - got %q, want %q.", got, want) 168 } 169 }) 170 } 171 } 172 173 func TestGetOperationInfoMetadataType(t *testing.T) { 174 // Set up testing permutations. 175 tests := []struct { 176 testName string 177 MetadataType string 178 valid bool 179 }{ 180 {"Valid", "WriteBookMetadata", true}, 181 {"Invalid", "Foo", false}, 182 } 183 for _, test := range tests { 184 t.Run(test.testName, func(t *testing.T) { 185 fd := testutils.ParseProto3Tmpl(t, ` 186 import "google/longrunning/operations.proto"; 187 service Library { 188 rpc WriteBook(WriteBookRequest) returns (google.longrunning.Operation) { 189 option (google.longrunning.operation_info) = { 190 response_type: "WriteBookResponse" 191 metadata_type: "{{ .MetadataType }}" 192 }; 193 } 194 } 195 message WriteBookRequest {} 196 message WriteBookMetadata {} 197 `, test) 198 199 typ := GetMetadataType(fd.GetServices()[0].GetMethods()[0]) 200 201 if validType := typ != nil; validType != test.valid { 202 t.Fatalf("Expected valid(%v) metadata_type message", test.valid) 203 } 204 205 if !test.valid { 206 return 207 } 208 209 if got, want := typ.GetName(), test.MetadataType; got != want { 210 t.Errorf("Metadata type - got %q, want %q.", got, want) 211 } 212 }) 213 } 214 } 215 216 func TestGetResource(t *testing.T) { 217 t.Run("Present", func(t *testing.T) { 218 f := testutils.ParseProto3String(t, ` 219 import "google/api/resource.proto"; 220 message Book { 221 option (google.api.resource) = { 222 type: "library.googleapis.com/Book" 223 pattern: "publishers/{publisher}/books/{book}" 224 }; 225 } 226 `) 227 resource := GetResource(f.GetMessageTypes()[0]) 228 if got, want := resource.GetType(), "library.googleapis.com/Book"; got != want { 229 t.Errorf("Got %q, expected %q.", got, want) 230 } 231 if got, want := resource.GetPattern()[0], "publishers/{publisher}/books/{book}"; got != want { 232 t.Errorf("Got %q, expected %q.", got, want) 233 } 234 }) 235 t.Run("Absent", func(t *testing.T) { 236 f := testutils.ParseProto3String(t, "message Book {}") 237 if got := GetResource(f.GetMessageTypes()[0]); got != nil { 238 t.Errorf(`Got "%v", expected nil.`, got) 239 } 240 }) 241 t.Run("Nil", func(t *testing.T) { 242 if got := GetResource(nil); got != nil { 243 t.Errorf(`Got "%v", expected nil.`, got) 244 } 245 }) 246 } 247 248 func TestGetResourceDefinition(t *testing.T) { 249 t.Run("Zero", func(t *testing.T) { 250 f := testutils.ParseProto3String(t, ` 251 import "google/api/resource.proto"; 252 `) 253 if got := GetResourceDefinitions(f); got != nil { 254 t.Errorf("Got %v, expected nil.", got) 255 } 256 }) 257 t.Run("One", func(t *testing.T) { 258 f := testutils.ParseProto3String(t, ` 259 import "google/api/resource.proto"; 260 option (google.api.resource_definition) = { 261 type: "library.googleapis.com/Book" 262 }; 263 `) 264 defs := GetResourceDefinitions(f) 265 if got, want := len(defs), 1; got != want { 266 t.Errorf("Got %d definitions, expected %d.", got, want) 267 } 268 if got, want := defs[0].GetType(), "library.googleapis.com/Book"; got != want { 269 t.Errorf("Got %s for type, expected %s.", got, want) 270 } 271 }) 272 t.Run("Two", func(t *testing.T) { 273 f := testutils.ParseProto3String(t, ` 274 import "google/api/resource.proto"; 275 option (google.api.resource_definition) = { 276 type: "library.googleapis.com/Book" 277 }; 278 option (google.api.resource_definition) = { 279 type: "library.googleapis.com/Author" 280 }; 281 `) 282 defs := GetResourceDefinitions(f) 283 if got, want := len(defs), 2; got != want { 284 t.Errorf("Got %d definitions, expected %d.", got, want) 285 } 286 if got, want := defs[0].GetType(), "library.googleapis.com/Book"; got != want { 287 t.Errorf("Got %s for type, expected %s.", got, want) 288 } 289 if got, want := defs[1].GetType(), "library.googleapis.com/Author"; got != want { 290 t.Errorf("Got %s for type, expected %s.", got, want) 291 } 292 }) 293 } 294 295 func TestGetResourceReference(t *testing.T) { 296 t.Run("Present", func(t *testing.T) { 297 f := testutils.ParseProto3String(t, ` 298 import "google/api/resource.proto"; 299 message GetBookRequest { 300 string name = 1 [(google.api.resource_reference) = { 301 type: "library.googleapis.com/Book" 302 }]; 303 } 304 `) 305 ref := GetResourceReference(f.GetMessageTypes()[0].GetFields()[0]) 306 if got, want := ref.GetType(), "library.googleapis.com/Book"; got != want { 307 t.Errorf("Got %q, expected %q.", got, want) 308 } 309 }) 310 t.Run("Absent", func(t *testing.T) { 311 f := testutils.ParseProto3String(t, "message GetBookRequest { string name = 1; }") 312 if got := GetResourceReference(f.GetMessageTypes()[0].GetFields()[0]); got != nil { 313 t.Errorf(`Got "%v", expected nil`, got) 314 } 315 }) 316 } 317 318 func TestFindResource(t *testing.T) { 319 files := testutils.ParseProtoStrings(t, map[string]string{ 320 "book.proto": ` 321 syntax = "proto3"; 322 package test; 323 324 import "google/api/resource.proto"; 325 326 message Book { 327 option (google.api.resource) = { 328 type: "library.googleapis.com/Book" 329 pattern: "publishers/{publisher}/books/{book}" 330 }; 331 332 string name = 1; 333 } 334 `, 335 "shelf.proto": ` 336 syntax = "proto3"; 337 package test; 338 339 import "book.proto"; 340 import "google/api/resource.proto"; 341 342 message Shelf { 343 option (google.api.resource) = { 344 type: "library.googleapis.com/Shelf" 345 pattern: "shelves/{shelf}" 346 }; 347 348 string name = 1; 349 350 repeated Book books = 2; 351 } 352 `, 353 }) 354 355 for _, tst := range []struct { 356 name, reference string 357 notFound bool 358 }{ 359 {"local_reference", "library.googleapis.com/Shelf", false}, 360 {"imported_reference", "library.googleapis.com/Book", false}, 361 {"unresolvable", "foo.googleapis.com/Bar", true}, 362 } { 363 t.Run(tst.name, func(t *testing.T) { 364 got := FindResource(tst.reference, files["shelf.proto"]) 365 366 if tst.notFound && got != nil { 367 t.Fatalf("Expected to not find the resource, but found %q", got.GetType()) 368 } 369 370 if !tst.notFound && got == nil { 371 t.Errorf("Got nil, expected %q", tst.reference) 372 } else if !tst.notFound && got.GetType() != tst.reference { 373 t.Errorf("Got %q, expected %q", got.GetType(), tst.reference) 374 } 375 }) 376 } 377 } 378 379 func TestFindResourceMessage(t *testing.T) { 380 files := testutils.ParseProtoStrings(t, map[string]string{ 381 "book.proto": ` 382 syntax = "proto3"; 383 package test; 384 385 import "google/api/resource.proto"; 386 387 message Book { 388 option (google.api.resource) = { 389 type: "library.googleapis.com/Book" 390 pattern: "publishers/{publisher}/books/{book}" 391 }; 392 393 string name = 1; 394 } 395 `, 396 "shelf.proto": ` 397 syntax = "proto3"; 398 package test; 399 400 import "book.proto"; 401 import "google/api/resource.proto"; 402 403 message Shelf { 404 option (google.api.resource) = { 405 type: "library.googleapis.com/Shelf" 406 pattern: "shelves/{shelf}" 407 }; 408 409 string name = 1; 410 411 repeated Book books = 2; 412 } 413 `, 414 }) 415 416 for _, tst := range []struct { 417 name, reference, wantMsg string 418 notFound bool 419 }{ 420 {"local_reference", "library.googleapis.com/Shelf", "Shelf", false}, 421 {"imported_reference", "library.googleapis.com/Book", "Book", false}, 422 {"unresolvable", "foo.googleapis.com/Bar", "", true}, 423 } { 424 t.Run(tst.name, func(t *testing.T) { 425 got := FindResourceMessage(tst.reference, files["shelf.proto"]) 426 427 if tst.notFound && got != nil { 428 t.Fatalf("Expected to not find the message, but found %q", got.GetName()) 429 } 430 431 if !tst.notFound && got == nil { 432 t.Errorf("Got nil, expected %q", tst.wantMsg) 433 } else if !tst.notFound && got.GetName() != tst.wantMsg { 434 t.Errorf("Got %q, expected %q", got.GetName(), tst.wantMsg) 435 } 436 }) 437 } 438 } 439 440 func TestSplitResourceTypeName(t *testing.T) { 441 for _, tst := range []struct { 442 name, input, service, typeName string 443 ok bool 444 }{ 445 {"Valid", "foo.googleapis.com/Foo", "foo.googleapis.com", "Foo", true}, 446 {"InvalidExtraSlashes", "foo.googleapis.com/Foo/Bar", "", "", false}, 447 {"InvalidNoService", "/Foo", "", "", false}, 448 {"InvalidNoTypeName", "foo.googleapis.com/", "", "", false}, 449 } { 450 t.Run(tst.name, func(t *testing.T) { 451 s, typ, ok := SplitResourceTypeName(tst.input) 452 if ok != tst.ok { 453 t.Fatalf("Expected %v for ok, but got %v", tst.ok, ok) 454 } 455 if diff := cmp.Diff(s, tst.service); diff != "" { 456 t.Errorf("service: got(-),want(+):\n%s", diff) 457 } 458 if diff := cmp.Diff(typ, tst.typeName); diff != "" { 459 t.Errorf("type name: got(-),want(+):\n%s", diff) 460 } 461 }) 462 } 463 } 464 465 func TestGetOutputOrLROResponseMessage(t *testing.T) { 466 for _, test := range []struct { 467 name string 468 RPCs string 469 want string 470 }{ 471 {"BookOutputType", ` 472 rpc CreateBook(CreateBookRequest) returns (Book) {}; 473 `, "Book"}, 474 {"BespokeOperationResource", ` 475 rpc CreateBook(CreateBookRequest) returns (Operation) {}; 476 `, "Operation"}, 477 {"LROBookResponse", ` 478 rpc CreateBook(CreateBookRequest) returns (google.longrunning.Operation) { 479 option (google.longrunning.operation_info) = { 480 response_type: "Book" 481 }; 482 }; 483 `, "Book"}, 484 {"LROMissingResponse", ` 485 rpc CreateBook(CreateBookRequest) returns (google.longrunning.Operation) { 486 }; 487 `, ""}, 488 } { 489 t.Run(test.name, func(t *testing.T) { 490 file := testutils.ParseProto3Tmpl(t, ` 491 import "google/api/resource.proto"; 492 import "google/longrunning/operations.proto"; 493 import "google/protobuf/field_mask.proto"; 494 service Foo { 495 {{.RPCs}} 496 } 497 498 // This is at the top to make it retrievable 499 // by the test code. 500 message Book { 501 option (google.api.resource) = { 502 type: "library.googleapis.com/Book" 503 pattern: "books/{book}" 504 singular: "book" 505 plural: "books" 506 }; 507 } 508 509 message CreateBookRequest { 510 // The parent resource where this book will be created. 511 // Format: publishers/{publisher} 512 string parent = 1; 513 514 // The book to create. 515 Book book = 2; 516 } 517 518 // bespoke operation message (not an LRO) 519 message Operation { 520 } 521 `, test) 522 method := file.GetServices()[0].GetMethods()[0] 523 resp := GetResponseType(method) 524 got := "" 525 if resp != nil { 526 got = resp.GetName() 527 } 528 if got != test.want { 529 t.Errorf( 530 "GetOutputOrLROResponseMessage got %q, want %q", 531 got, test.want, 532 ) 533 } 534 }) 535 } 536 } 537 538 func TestFindResourceChildren(t *testing.T) { 539 publisher := &apb.ResourceDescriptor{ 540 Type: "library.googleapis.com/Publisher", 541 Pattern: []string{ 542 "publishers/{publisher}", 543 }, 544 } 545 shelf := &apb.ResourceDescriptor{ 546 Type: "library.googleapis.com/Shelf", 547 Pattern: []string{ 548 "shelves/{shelf}", 549 }, 550 } 551 book := &apb.ResourceDescriptor{ 552 Type: "library.googleapis.com/Book", 553 Pattern: []string{ 554 "publishers/{publisher}/books/{book}", 555 }, 556 } 557 edition := &apb.ResourceDescriptor{ 558 Type: "library.googleapis.com/Edition", 559 Pattern: []string{ 560 "publishers/{publisher}/books/{book}/editions/{edition}", 561 }, 562 } 563 files := testutils.ParseProtoStrings(t, map[string]string{ 564 "book.proto": ` 565 syntax = "proto3"; 566 package test; 567 568 import "google/api/resource.proto"; 569 570 message Book { 571 option (google.api.resource) = { 572 type: "library.googleapis.com/Book" 573 pattern: "publishers/{publisher}/books/{book}" 574 }; 575 576 string name = 1; 577 } 578 579 message Edition { 580 option (google.api.resource) = { 581 type: "library.googleapis.com/Edition" 582 pattern: "publishers/{publisher}/books/{book}/editions/{edition}" 583 }; 584 585 string name = 1; 586 } 587 `, 588 "shelf.proto": ` 589 syntax = "proto3"; 590 package test; 591 592 import "book.proto"; 593 import "google/api/resource.proto"; 594 595 message Shelf { 596 option (google.api.resource) = { 597 type: "library.googleapis.com/Shelf" 598 pattern: "shelves/{shelf}" 599 }; 600 601 string name = 1; 602 603 repeated Book books = 2; 604 } 605 `, 606 }) 607 608 for _, tst := range []struct { 609 name string 610 parent *apb.ResourceDescriptor 611 want []*apb.ResourceDescriptor 612 }{ 613 {"has_child_same_file", book, []*apb.ResourceDescriptor{edition}}, 614 {"has_child_other_file", publisher, []*apb.ResourceDescriptor{book, edition}}, 615 {"no_children", shelf, nil}, 616 } { 617 t.Run(tst.name, func(t *testing.T) { 618 got := FindResourceChildren(tst.parent, files["shelf.proto"]) 619 if diff := cmp.Diff(tst.want, got, cmp.Comparer(proto.Equal)); diff != "" { 620 t.Errorf("got(-),want(+):\n%s", diff) 621 } 622 }) 623 } 624 } 625 626 func TestHasFieldInfo(t *testing.T) { 627 testCases := []struct { 628 name, FieldInfo string 629 want bool 630 }{ 631 { 632 name: "HasFieldInfo", 633 FieldInfo: "[(google.api.field_info).format = UUID4]", 634 want: true, 635 }, 636 { 637 name: "NoFieldInfo", 638 want: false, 639 }, 640 } 641 for _, tc := range testCases { 642 t.Run(tc.name, func(t *testing.T) { 643 file := testutils.ParseProto3Tmpl(t, ` 644 import "google/api/field_info.proto"; 645 646 message CreateBookRequest { 647 string foo = 1 {{.FieldInfo}}; 648 } 649 `, tc) 650 fd := file.FindMessage("CreateBookRequest").FindFieldByName("foo") 651 if got := HasFieldInfo(fd); got != tc.want { 652 t.Errorf("HasFieldInfo(%+v): expected %v, got %v", fd, tc.want, got) 653 } 654 }) 655 } 656 } 657 658 func TestGetFieldInfo(t *testing.T) { 659 testCases := []struct { 660 name, FieldInfo string 661 want *apb.FieldInfo 662 }{ 663 { 664 name: "HasFieldInfo", 665 FieldInfo: "[(google.api.field_info).format = UUID4]", 666 want: &apb.FieldInfo{Format: apb.FieldInfo_UUID4}, 667 }, 668 { 669 name: "NoFieldInfo", 670 }, 671 } 672 for _, tc := range testCases { 673 t.Run(tc.name, func(t *testing.T) { 674 file := testutils.ParseProto3Tmpl(t, ` 675 import "google/api/field_info.proto"; 676 677 message CreateBookRequest { 678 string foo = 1 {{.FieldInfo}}; 679 } 680 `, tc) 681 fd := file.FindMessage("CreateBookRequest").FindFieldByName("foo") 682 got := GetFieldInfo(fd) 683 if diff := cmp.Diff(got, tc.want, cmp.Comparer(proto.Equal)); diff != "" { 684 t.Errorf("GetFieldInfo(%+v): got(-),want(+):\n%s", fd, diff) 685 } 686 }) 687 } 688 } 689 690 func TestHasFormat(t *testing.T) { 691 testCases := []struct { 692 name, Format string 693 want bool 694 }{ 695 { 696 name: "HasFormat", 697 Format: "format: UUID4", 698 want: true, 699 }, 700 { 701 name: "NoFormat", 702 want: false, 703 }, 704 } 705 for _, tc := range testCases { 706 t.Run(tc.name, func(t *testing.T) { 707 file := testutils.ParseProto3Tmpl(t, ` 708 import "google/api/field_info.proto"; 709 710 message CreateBookRequest { 711 string foo = 1 [(google.api.field_info) = { 712 {{.Format}} 713 }]; 714 } 715 `, tc) 716 fd := file.FindMessage("CreateBookRequest").FindFieldByName("foo") 717 if got := HasFormat(fd); got != tc.want { 718 t.Errorf("HasFormat(%+v): expected %v, got %v", fd, tc.want, got) 719 } 720 }) 721 } 722 } 723 724 func TestGetFormat(t *testing.T) { 725 testCases := []struct { 726 name, Format string 727 want apb.FieldInfo_Format 728 }{ 729 { 730 name: "HasUUID4Format", 731 Format: "format: UUID4", 732 want: apb.FieldInfo_UUID4, 733 }, 734 { 735 name: "NoFormat", 736 want: apb.FieldInfo_FORMAT_UNSPECIFIED, 737 }, 738 } 739 for _, tc := range testCases { 740 t.Run(tc.name, func(t *testing.T) { 741 file := testutils.ParseProto3Tmpl(t, ` 742 import "google/api/field_info.proto"; 743 744 message CreateBookRequest { 745 string foo = 1 [(google.api.field_info) = { 746 {{.Format}} 747 }]; 748 } 749 `, tc) 750 fd := file.FindMessage("CreateBookRequest").FindFieldByName("foo") 751 if got := GetFormat(fd); got != tc.want { 752 t.Errorf("GetFormat(%+v): expected %v, got %v", fd, tc.want, got) 753 } 754 }) 755 } 756 }