diff --git a/internal/sidekick/api/xref_aip_test.go b/internal/sidekick/api/xref_aip_test.go new file mode 100644 index 0000000000..83ca677ca1 --- /dev/null +++ b/internal/sidekick/api/xref_aip_test.go @@ -0,0 +1,978 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +type aipTestFixture struct { + resource *Resource + resourceWithoutSingular *Resource + resourceNameField *Field + resourceOtherNameField *Field + resourceNameNoSingularField *Field + resourceOtherNameNoSingularField *Field + nonExistentResourceField *Field + wildcardResourceField *Field + model *API +} + +func newAIPTestFixture() *aipTestFixture { + resource := &Resource{ + Type: "google.cloud.secretmanager.v1/Secret", + Singular: "secret", + } + resourceWithoutSingular := &Resource{ + Type: "google.cloud.secretmanager.v1/SecretWithoutSingular", + } + resourceNameField := &Field{ + Name: "name", + ResourceReference: &ResourceReference{ + Type: resource.Type, + }, + } + resourceOtherNameField := &Field{ + Name: "other_name", + ResourceReference: &ResourceReference{ + Type: resource.Type, + }, + } + resourceNameNoSingularField := &Field{ + Name: "name", + ResourceReference: &ResourceReference{ + Type: resourceWithoutSingular.Type, + }, + } + resourceOtherNameNoSingularField := &Field{ + Name: "other_name", + ResourceReference: &ResourceReference{ + Type: resourceWithoutSingular.Type, + }, + } + nonExistentResourceField := &Field{ + Name: "name", + ResourceReference: &ResourceReference{ + Type: "nonexistent.googleapis.com/NonExistent", + }, + } + + wildcardResourceField := &Field{ + Name: "name", + ResourceReference: &ResourceReference{ + Type: "*", + }, + } + + model := &API{ + ResourceDefinitions: []*Resource{resource, resourceWithoutSingular}, + resourceByType: map[string]*Resource{ + resource.Type: resource, + resourceWithoutSingular.Type: resourceWithoutSingular, + }, + } + + return &aipTestFixture{ + resource: resource, + resourceWithoutSingular: resourceWithoutSingular, + resourceNameField: resourceNameField, + resourceOtherNameField: resourceOtherNameField, + resourceNameNoSingularField: resourceNameNoSingularField, + resourceOtherNameNoSingularField: resourceOtherNameNoSingularField, + nonExistentResourceField: nonExistentResourceField, + wildcardResourceField: wildcardResourceField, + model: model, + } +} + +func TestIsAIPStandard(t *testing.T) { + f := newAIPTestFixture() + + // Setup for a valid Get operation + validGetMethod := &Method{ + Name: "GetSecret", + InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, + OutputType: &Message{Resource: f.resource}, + Model: f.model, + } + + validDeleteMethod := &Method{ + Name: "DeleteSecret", + InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{f.resourceNameField}}, + ReturnsEmpty: true, + Model: f.model, + } + + validUndeleteMethod := &Method{ + Name: "UndeleteSecret", + InputType: &Message{Name: "UndeleteSecretRequest", Fields: []*Field{f.resourceNameField}}, + OutputType: &Message{ + Resource: f.resource, + }, + Model: f.model, + } + + // Setup for an invalid Get operation (e.g., wrong name) + invalidGetMethod := &Method{ + Name: "ListSecrets", // Not a Get method + InputType: &Message{Name: "ListSecretsRequest"}, + OutputType: &Message{Resource: f.resource}, + Model: f.model, + } + + testCases := []struct { + name string + method *Method + want bool + }{ + { + name: "standard get method returns true", + method: validGetMethod, + want: true, + }, + { + name: "standard delete method returns true", + method: validDeleteMethod, + want: true, + }, + { + name: "standard undelete method returns true", + method: validUndeleteMethod, + want: true, + }, + { + name: "non-standard method returns false", + method: invalidGetMethod, + want: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + enrichMethodSamples(tc.method) + if got := tc.method.IsAIPStandard; got != tc.want { + t.Errorf("IsAIPStandard() = %v, want %v", got, tc.want) + } + }) + } +} + +func TestAIPStandardGetInfo(t *testing.T) { + f := newAIPTestFixture() + + // Helper to create an output message since Get needs it + output := &Message{ + Resource: f.resource, + } + + testCases := []struct { + name string + method *Method + want *SampleInfo + }{ + { + name: "valid get operation with wildcard resource reference", + method: &Method{ + Name: "GetSecret", + InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.wildcardResourceField}}, + OutputType: output, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: f.wildcardResourceField, + IsRequestResourceName: true, + }, + }, + { + name: "valid get operation", + method: &Method{ + Name: "GetSecret", + InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, + OutputType: output, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: f.resourceNameField, + IsRequestResourceName: true, + }, + }, + { + name: "valid get operation with missing singular name on resource", + method: &Method{ + Name: "GetSecret", + InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameNoSingularField}}, + OutputType: &Message{ + Resource: f.resourceWithoutSingular, + }, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: f.resourceNameNoSingularField, + IsRequestResourceName: true, + }, + }, + { + name: "method name is incorrect", + method: &Method{ + Name: "Get", + InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, + OutputType: output, + Model: f.model, + }, + want: nil, + }, + { + name: "request type name is incorrect", + method: &Method{ + Name: "GetSecret", + InputType: &Message{Name: "GetRequest", Fields: []*Field{f.resourceNameField}}, + OutputType: output, + Model: f.model, + }, + want: nil, + }, + { + name: "returns empty", + method: &Method{ + Name: "GetSecret", + InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, + OutputType: output, + ReturnsEmpty: true, + Model: f.model, + }, + want: nil, + }, + { + name: "output is not a resource", + method: &Method{ + Name: "GetSecret", + InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, + OutputType: &Message{ + Resource: nil, + }, + Model: f.model, + }, + want: nil, + }, + { + name: "request does not contain resource name field", + method: &Method{ + Name: "GetSecret", + InputType: &Message{Name: "GetSecretRequest"}, + OutputType: output, + Model: f.model, + }, + want: nil, + }, + { + name: "pagination method is not a standard get operation", + method: &Method{ + Name: "GetSecret", + InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, + OutputType: output, + Pagination: &Field{}, + Model: f.model, + }, + want: nil, + }, + { + name: "client streaming method is not a standard get operation", + method: &Method{ + Name: "GetSecret", + InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, + OutputType: output, + ClientSideStreaming: true, + Model: f.model, + }, + want: nil, + }, + { + name: "server streaming method is not a standard get operation", + method: &Method{ + Name: "GetSecret", + InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, + OutputType: output, + ServerSideStreaming: true, + Model: f.model, + }, + want: nil, + }, + { + name: "LRO method is not a standard get operation", + method: &Method{ + Name: "GetSecret", + InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, + OutputType: output, + OperationInfo: &OperationInfo{}, + Model: f.model, + }, + want: nil, + }, + { + name: "Discovery LRO method is not a standard get operation", + method: &Method{ + Name: "GetSecret", + InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, + OutputType: output, + DiscoveryLro: &DiscoveryLro{}, + Model: f.model, + }, + want: nil, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + enrichMethodSamples(tc.method) + got := tc.method.SampleInfo + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestAIPStandardDeleteInfo(t *testing.T) { + f := newAIPTestFixture() + + testCases := []struct { + name string + method *Method + want *SampleInfo + }{ + { + name: "valid simple delete with wildcard resource reference", + method: &Method{ + Name: "DeleteSecret", + InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{f.wildcardResourceField}}, + ReturnsEmpty: true, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: f.wildcardResourceField, + IsRequestResourceName: true, + }, + }, + { + name: "valid simple delete", + method: &Method{ + Name: "DeleteSecret", + InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{f.resourceNameField}}, + ReturnsEmpty: true, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: f.resourceNameField, + IsRequestResourceName: true, + }, + }, + { + name: "valid simple delete with missing singular name on resource", + method: &Method{ + Name: "DeleteSecret", + InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{f.resourceNameNoSingularField}}, + ReturnsEmpty: true, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: f.resourceNameNoSingularField, + IsRequestResourceName: true, + }, + }, + { + name: "valid lro delete", + method: &Method{ + Name: "DeleteSecret", + InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{f.resourceNameField}}, + OperationInfo: &OperationInfo{}, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: f.resourceNameField, + IsRequestResourceName: true, + }, + }, + { + name: "valid delete with other name matching singular", + method: &Method{ + Name: "DeleteSecret", + InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{f.resourceOtherNameField}}, + ReturnsEmpty: true, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: f.resourceOtherNameField, + IsRequestResourceName: true, + }, + }, + { + name: "incorrect method name", + method: &Method{ + Name: "RemoveSecret", + InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{f.resourceNameField}}, + Model: f.model, + }, + want: nil, + }, + { + name: "incorrect request name", + method: &Method{ + Name: "DeleteSecret", + InputType: &Message{Name: "RemoveSecretRequest", Fields: []*Field{f.resourceNameField}}, + Model: f.model, + }, + want: nil, + }, + { + name: "resource not found in ResourceByType map", + method: &Method{ + Name: "DeleteSecret", + InputType: &Message{ + Name: "DeleteSecretRequest", + Fields: []*Field{ + f.nonExistentResourceField, + }, + }, + Model: f.model, // model's ResourceByType does not contain the nonexistent resource + }, + want: nil, + }, + { + name: "invalid delete with no matching field", + method: &Method{ + Name: "DeleteSecret", + InputType: &Message{ + Name: "DeleteSecretRequest", + Fields: []*Field{ + f.nonExistentResourceField, + f.resourceOtherNameNoSingularField, + }, + }, + Model: f.model, + }, + want: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + enrichMethodSamples(tc.method) + got := tc.method.SampleInfo + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestAIPStandardUndeleteInfo(t *testing.T) { + f := newAIPTestFixture() + + testCases := []struct { + name string + method *Method + want *SampleInfo + }{ + { + name: "valid simple undelete with wildcard resource reference", + method: &Method{ + Name: "UndeleteSecret", + InputType: &Message{Name: "UndeleteSecretRequest", Fields: []*Field{f.wildcardResourceField}}, + OutputType: &Message{ + Resource: f.resource, + }, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: f.wildcardResourceField, + IsRequestResourceName: true, + }, + }, + { + name: "valid simple undelete", + method: &Method{ + Name: "UndeleteSecret", + InputType: &Message{Name: "UndeleteSecretRequest", Fields: []*Field{f.resourceNameField}}, + OutputType: &Message{ + Resource: f.resource, + }, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: f.resourceNameField, + IsRequestResourceName: true, + }, + }, + { + name: "valid simple undelete with missing singular name on resource", + method: &Method{ + Name: "UndeleteSecret", + InputType: &Message{Name: "UndeleteSecretRequest", Fields: []*Field{f.resourceNameNoSingularField}}, + OutputType: &Message{ + Resource: f.resourceWithoutSingular, + }, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: f.resourceNameNoSingularField, + IsRequestResourceName: true, + }, + }, + { + name: "valid lro undelete", + method: &Method{ + Name: "UndeleteSecret", + InputType: &Message{Name: "UndeleteSecretRequest", Fields: []*Field{f.resourceNameField}}, + OperationInfo: &OperationInfo{}, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: f.resourceNameField, + IsRequestResourceName: true, + }, + }, + { + name: "valid undelete with other name matching singular", + method: &Method{ + Name: "UndeleteSecret", + InputType: &Message{Name: "UndeleteSecretRequest", Fields: []*Field{f.resourceOtherNameField}}, + OutputType: &Message{ + Resource: f.resource, + }, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: f.resourceOtherNameField, + IsRequestResourceName: true, + }, + }, + { + name: "incorrect method name", + method: &Method{ + Name: "RestoreSecret", + InputType: &Message{Name: "UndeleteSecretRequest", Fields: []*Field{f.resourceNameField}}, + Model: f.model, + }, + want: nil, + }, + { + name: "incorrect request name", + method: &Method{ + Name: "UndeleteSecret", + InputType: &Message{Name: "RestoreSecretRequest", Fields: []*Field{f.resourceNameField}}, + Model: f.model, + }, + want: nil, + }, + { + name: "resource not found in ResourceByType map", + method: &Method{ + Name: "UndeleteSecret", + InputType: &Message{ + Name: "UndeleteSecretRequest", + Fields: []*Field{ + f.nonExistentResourceField, + }, + }, + Model: f.model, // model's ResourceByType does not contain the nonexistent resource + }, + want: nil, + }, + { + name: "invalid undelete with no matching field", + method: &Method{ + Name: "UndeleteSecret", + InputType: &Message{ + Name: "UndeleteSecretRequest", + Fields: []*Field{ + f.nonExistentResourceField, + f.resourceOtherNameNoSingularField, + }, + }, + Model: f.model, + }, + want: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + enrichMethodSamples(tc.method) + got := tc.method.SampleInfo + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestAIPStandardCreateInfo(t *testing.T) { + f := newAIPTestFixture() + + // Setup for Create + secretMessage := &Message{ID: "secret_message_id"} + f.resource.Self = secretMessage + + parentField := &Field{ + Name: "parent", + ResourceReference: &ResourceReference{ChildType: f.resource.Type}, + Typez: TypezString, + } + resourceField := &Field{ + Name: "secret", + Typez: TypezMessage, + TypezID: secretMessage.ID, + } + idField := &Field{ + Name: "secret_id", + Typez: TypezString, + } + + testCases := []struct { + name string + method *Method + want *SampleInfo + }{ + { + name: "valid create operation", + method: &Method{ + Name: "CreateSecret", + InputType: &Message{ + Name: "CreateSecretRequest", + Fields: []*Field{ + parentField, + idField, + resourceField, + }, + }, + OutputType: &Message{Resource: f.resource}, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: parentField, + IsRequestResourceName: true, + ResourceIDField: idField, + MessageField: resourceField, + }, + }, + { + name: "valid create operation without id", + method: &Method{ + Name: "CreateSecret", + InputType: &Message{ + Name: "CreateSecretRequest", + Fields: []*Field{ + parentField, + resourceField, + }, + }, + OutputType: &Message{Resource: f.resource}, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: parentField, + IsRequestResourceName: true, + MessageField: resourceField, + }, + }, + { + name: "invalid create operation (wrong name)", + method: &Method{ + Name: "MakeSecret", + InputType: &Message{ + Name: "CreateSecretRequest", + Fields: []*Field{ + parentField, + resourceField, + }, + }, + OutputType: &Message{Resource: f.resource}, + Model: f.model, + }, + want: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + enrichMethodSamples(tc.method) + got := tc.method.SampleInfo + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestAIPStandardUpdateInfo(t *testing.T) { + f := newAIPTestFixture() + + // Setup for Update + secretMessage := &Message{ + ID: "secret_message_id", + Fields: []*Field{ + f.resourceNameField, + }, + } + f.resource.Self = secretMessage + + resourceField := &Field{ + Name: "secret", + Typez: TypezMessage, + TypezID: secretMessage.ID, + MessageType: secretMessage, + } + updateMaskField := &Field{ + Name: "update_mask", + TypezID: ".google.protobuf.FieldMask", + } + + testCases := []struct { + name string + method *Method + want *SampleInfo + }{ + { + name: "valid update operation", + method: &Method{ + Name: "UpdateSecret", + InputType: &Message{ + Name: "UpdateSecretRequest", + Fields: []*Field{ + resourceField, + updateMaskField, + }, + }, + OutputType: &Message{Resource: f.resource}, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: f.resourceNameField, + MessageField: resourceField, + IsMessageResourceName: true, + UpdateMaskField: updateMaskField, + }, + }, + { + name: "valid update operation without mask", + method: &Method{ + Name: "UpdateSecret", + InputType: &Message{ + Name: "UpdateSecretRequest", + Fields: []*Field{ + resourceField, + }, + }, + OutputType: &Message{Resource: f.resource}, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: f.resourceNameField, + MessageField: resourceField, + IsMessageResourceName: true, + }, + }, + { + name: "invalid update operation (wrong name)", + method: &Method{ + Name: "ModifySecret", + InputType: &Message{ + Name: "UpdateSecretRequest", + Fields: []*Field{ + resourceField, + }, + }, + OutputType: &Message{Resource: f.resource}, + Model: f.model, + }, + want: nil, + }, + { + name: "invalid update operation (wrong request name)", + method: &Method{ + Name: "UpdateSecret", + InputType: &Message{ + Name: "ModifySecretRequest", + Fields: []*Field{ + resourceField, + }, + }, + OutputType: &Message{Resource: f.resource}, + Model: f.model, + }, + want: nil, + }, + { + name: "invalid update operation (missing resource)", + method: &Method{ + Name: "UpdateSecret", + InputType: &Message{ + Name: "UpdateSecretRequest", + Fields: []*Field{ + updateMaskField, + }, + }, + OutputType: &Message{Resource: f.resource}, + Model: f.model, + }, + want: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + enrichMethodSamples(tc.method) + got := tc.method.SampleInfo + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestAIPStandardListInfo(t *testing.T) { + f := newAIPTestFixture() + + // Create a resource type for the list items + resourceType := "google.cloud.secretmanager.v1/Secret" + // Ensure the parent field points to this resource type in child_type? + // No, parent field child_type matches the listed resource type. + + secretResource := &Resource{Type: resourceType, Plural: "Secrets"} + secretMessage := &Message{Resource: secretResource} + parentField := &Field{ + Name: "parent", + ResourceReference: &ResourceReference{ChildType: resourceType}, + } + otherParentField := &Field{ + Name: "database", // Not named "parent" + ResourceReference: &ResourceReference{ChildType: resourceType}, + } + plainParentField := &Field{Name: "parent"} // No child_type + + pageableItem := &Field{Name: "secrets", MessageType: secretMessage} + paginationInfo := &PaginationInfo{PageableItem: pageableItem} + listOutput := &Message{ + Name: "ListSecretsResponse", + Pagination: paginationInfo, + } + + testCases := []struct { + name string + method *Method + want *SampleInfo + }{ + { + name: "valid list operation with parent field match by child_type", + method: &Method{ + Name: "ListSecrets", + InputType: &Message{Name: "ListSecretsRequest", Fields: []*Field{parentField}}, + OutputType: listOutput, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: parentField, + IsRequestResourceName: true, + }, + }, + { + name: "valid list operation with other field match by child_type", + method: &Method{ + Name: "ListSecrets", + InputType: &Message{Name: "ListSecretsRequest", Fields: []*Field{otherParentField}}, + OutputType: listOutput, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: otherParentField, + IsRequestResourceName: true, + }, + }, + { + name: "valid list operation with parent field name match (fallback)", + method: &Method{ + Name: "ListSecrets", + InputType: &Message{Name: "ListSecretsRequest", Fields: []*Field{plainParentField}}, + OutputType: listOutput, + Model: f.model, + }, + want: &SampleInfo{ + ResourceNameField: plainParentField, + IsRequestResourceName: true, + }, + }, + { + name: "list operation missing parent field", + method: &Method{ + Name: "ListSecrets", + InputType: &Message{Name: "ListSecretsRequest", Fields: []*Field{}}, + OutputType: listOutput, + Model: f.model, + }, + want: nil, + }, + { + name: "method name does not start with List", + method: &Method{ + Name: "EnumerateSecrets", + InputType: &Message{Name: "ListSecretsRequest", Fields: []*Field{parentField}}, + OutputType: listOutput, + Model: f.model, + }, + want: nil, + }, + { + name: "input type name mismatch", + method: &Method{ + Name: "ListSecrets", + InputType: &Message{Name: "EnumerateSecretsRequest", Fields: []*Field{parentField}}, + OutputType: listOutput, + Model: f.model, + }, + want: nil, + }, + { + name: "output type name mismatch", + method: &Method{ + Name: "ListSecrets", + InputType: &Message{Name: "ListSecretsRequest", Fields: []*Field{parentField}}, + OutputType: &Message{Name: "EnumerateSecretsResponse", Pagination: paginationInfo}, + Model: f.model, + }, + want: nil, + }, + { + name: "not a list operation (no pagination)", + method: &Method{ + Name: "ListSecrets", + InputType: &Message{Name: "ListSecretsRequest", Fields: []*Field{parentField}}, + OutputType: &Message{Name: "ListSecretsResponse"}, // No pagination + Model: f.model, + }, + want: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + enrichMethodSamples(tc.method) + got := tc.method.SampleInfo + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/sidekick/api/xref_lookup_test.go b/internal/sidekick/api/xref_lookup_test.go new file mode 100644 index 0000000000..d3937e9125 --- /dev/null +++ b/internal/sidekick/api/xref_lookup_test.go @@ -0,0 +1,541 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func TestFindBestResourceFieldByType(t *testing.T) { + f := newAIPTestFixture() + targetType := f.resource.Type + + for _, tc := range []struct { + name string + fields []*Field + want *Field + }{ + { + name: "name field with wildcard", + fields: []*Field{f.wildcardResourceField}, + want: f.wildcardResourceField, + }, + { + name: "name field with exact match", + fields: []*Field{f.resourceNameField}, + want: f.resourceNameField, + }, + { + name: "other field with exact match", + fields: []*Field{f.resourceOtherNameField}, + want: f.resourceOtherNameField, + }, + { + name: "name field with exact match wins over other field with exact match", + fields: []*Field{f.resourceNameField, f.resourceOtherNameField}, + want: f.resourceNameField, + }, + { + name: "name field with wildcard wins over other field with exact match", + fields: []*Field{f.wildcardResourceField, f.resourceOtherNameField}, + want: f.wildcardResourceField, + }, + { + name: "no match", + fields: []*Field{f.nonExistentResourceField}, + want: nil, + }, + } { + t.Run(tc.name, func(t *testing.T) { + msg := &Message{Fields: tc.fields} + got := findBestResourceFieldByType(msg, f.model, targetType) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestFindBestResourceFieldBySingular(t *testing.T) { + f := newAIPTestFixture() + targetSingular := f.resource.Singular + + for _, tc := range []struct { + name string + fields []*Field + want *Field + }{ + { + name: "name field with wildcard", + fields: []*Field{f.wildcardResourceField}, + want: f.wildcardResourceField, + }, + { + name: "name field with exact match", + fields: []*Field{f.resourceNameField}, + want: f.resourceNameField, + }, + { + name: "name field with empty singular match", + fields: []*Field{f.resourceNameNoSingularField}, + want: f.resourceNameNoSingularField, + }, + { + name: "other field with exact match", + fields: []*Field{f.resourceOtherNameField}, + want: f.resourceOtherNameField, + }, + { + name: "name field with exact match wins over other field with exact match", + fields: []*Field{f.resourceNameField, f.resourceOtherNameField}, + want: f.resourceNameField, + }, + { + name: "name field with wildcard wins over other field with exact match", + fields: []*Field{f.wildcardResourceField, f.resourceOtherNameField}, + want: f.wildcardResourceField, + }, + { + name: "no match", + fields: []*Field{f.nonExistentResourceField}, + want: nil, + }, + } { + t.Run(tc.name, func(t *testing.T) { + msg := &Message{Fields: tc.fields} + got := findBestResourceFieldBySingular(msg, f.model, targetSingular) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestFindBestParentFieldByType(t *testing.T) { + childType := "google.cloud.secretmanager.v1/SecretVersion" + + parentField := &Field{ + Name: "parent", + ResourceReference: &ResourceReference{ChildType: childType}, + } + + parentFieldByName := &Field{ + Name: "parent", + // No matching child type, just name + } + + parentFieldByChildType := &Field{ + Name: "database", + ResourceReference: &ResourceReference{ChildType: childType}, + } + + wrongField := &Field{Name: "wrong"} + + for _, tc := range []struct { + name string + fields []*Field + want *Field + }{ + { + name: "exact match (name + child type)", + fields: []*Field{parentField}, + want: parentField, + }, + { + name: "name match only (fallback)", + fields: []*Field{parentFieldByName}, + want: parentFieldByName, + }, + { + name: "child type match only", + fields: []*Field{parentFieldByChildType}, + want: parentFieldByChildType, + }, + { + name: "name match prefers exact name", + fields: []*Field{parentFieldByName, parentFieldByChildType}, + want: parentFieldByName, + }, + { + name: "no match", + fields: []*Field{wrongField}, + want: nil, + }, + } { + t.Run(tc.name, func(t *testing.T) { + msg := &Message{Fields: tc.fields} + got := findBestParentFieldByType(msg, childType) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestFieldTypePredicates(t *testing.T) { + type TestCase struct { + field *Field + isString bool + isBytes bool + isBool bool + isInt bool + isUInt bool + isFloat bool + isEnum bool + isObject bool + } + testCases := []TestCase{ + {field: &Field{Typez: TypezString}, isString: true}, + {field: &Field{Typez: TypezBytes}, isBytes: true}, + {field: &Field{Typez: TypezBool}, isBool: true}, + {field: &Field{Typez: TypezInt32}, isInt: true}, + {field: &Field{Typez: TypezInt64}, isInt: true}, + {field: &Field{Typez: TypezSint32}, isInt: true}, + {field: &Field{Typez: TypezSint64}, isInt: true}, + {field: &Field{Typez: TypezSfixed32}, isInt: true}, + {field: &Field{Typez: TypezSfixed64}, isInt: true}, + {field: &Field{Typez: TypezUint32}, isUInt: true}, + {field: &Field{Typez: TypezUint64}, isUInt: true}, + {field: &Field{Typez: TypezFixed32}, isUInt: true}, + {field: &Field{Typez: TypezFixed64}, isUInt: true}, + {field: &Field{Typez: TypezFloat}, isFloat: true}, + {field: &Field{Typez: TypezDouble}, isFloat: true}, + {field: &Field{Typez: TypezEnum}, isEnum: true}, + {field: &Field{Typez: TypezMessage}, isObject: true}, + } + for _, tc := range testCases { + if tc.field.IsString() != tc.isString { + t.Errorf("IsString() for %v should be %v", tc.field.Typez, tc.isString) + } + if tc.field.IsBytes() != tc.isBytes { + t.Errorf("IsBytes() for %v should be %v", tc.field.Typez, tc.isBytes) + } + if tc.field.IsBool() != tc.isBool { + t.Errorf("IsBool() for %v should be %v", tc.field.Typez, tc.isBool) + } + if tc.field.IsLikeInt() != tc.isInt { + t.Errorf("IsLikeInt() for %v should be %v", tc.field.Typez, tc.isInt) + } + if tc.field.IsLikeUInt() != tc.isUInt { + t.Errorf("IsLikeUInt() for %v should be %v", tc.field.Typez, tc.isUInt) + } + if tc.field.IsLikeFloat() != tc.isFloat { + t.Errorf("IsLikeFloat() for %v should be %v", tc.field.Typez, tc.isFloat) + } + if tc.field.IsEnum() != tc.isEnum { + t.Errorf("IsEnum() for %v should be %v", tc.field.Typez, tc.isEnum) + } + if tc.field.IsObject() != tc.isObject { + t.Errorf("IsObject() for %v should be %v", tc.field.Typez, tc.isObject) + } + } +} + +func TestFlatPath(t *testing.T) { + for _, test := range []struct { + Input *PathTemplate + Want string + }{ + { + Input: (&PathTemplate{}), + Want: "", + }, + { + Input: (&PathTemplate{}). + WithLiteral("projects"). + WithVariableNamed("project"). + WithLiteral("zones"). + WithVariableNamed("zone"), + Want: "projects/{project}/zones/{zone}", + }, + { + Input: (&PathTemplate{}). + WithLiteral("projects"). + WithVariableNamed("project"). + WithLiteral("global"). + WithLiteral("location"), + Want: "projects/{project}/global/location", + }, + { + Input: (&PathTemplate{}). + WithLiteral("projects"). + WithVariable(NewPathVariable("a", "b", "c").WithMatchRecursive()), + Want: "projects/{a.b.c}", + }, + } { + got := test.Input.FlatPath() + if got != test.Want { + t.Errorf("mismatch want=%q, got=%q", test.Want, got) + } + } +} + +func TestField_IsResourceReference(t *testing.T) { + for _, test := range []struct { + name string + field *Field + want bool + }{ + { + name: "nil ResourceReference", + field: &Field{}, + want: false, + }, + { + name: "non-nil ResourceReference", + field: &Field{ResourceReference: &ResourceReference{}}, + want: true, + }, + } { + t.Run(test.name, func(t *testing.T) { + got := test.field.IsResourceReference() + if got != test.want { + t.Errorf("IsResourceReference() got = %v, want %v", got, test.want) + } + }) + } +} + +func TestFindBodyField(t *testing.T) { + messageType := &Message{ID: "message_id"} + bodyField := &Field{Name: "body_field"} + typeMatchField := &Field{Typez: TypezMessage, TypezID: messageType.ID, Name: "type_match"} + otherField := &Field{Name: "other"} + + testCases := []struct { + name string + message *Message + pathInfo *PathInfo + targetType string + singular string + want *Field + }{ + { + name: "match by path info", + message: &Message{Fields: []*Field{bodyField, otherField}}, + pathInfo: &PathInfo{BodyFieldPath: "body_field"}, + want: bodyField, + }, + { + name: "match by type and name", + message: &Message{Fields: []*Field{typeMatchField, otherField}}, + targetType: messageType.ID, + singular: "type_match", + want: typeMatchField, + }, + { + name: "match by type but wrong name", + message: &Message{Fields: []*Field{typeMatchField, otherField}}, + targetType: messageType.ID, + singular: "wrong_name", + want: nil, + }, + { + name: "match by name but wrong type", + message: &Message{Fields: []*Field{typeMatchField, otherField}}, + targetType: "different_message_id", + singular: "type_match", + want: nil, + }, + { + name: "path info overrides type match", + message: &Message{Fields: []*Field{typeMatchField, bodyField}}, + pathInfo: &PathInfo{BodyFieldPath: "body_field"}, + targetType: messageType.ID, + singular: "type_match", + want: bodyField, + }, + { + name: "no match", + message: &Message{Fields: []*Field{otherField}}, + want: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := findBodyField(tc.message, tc.pathInfo, tc.targetType, tc.singular) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestFindResourceIDField(t *testing.T) { + idField := &Field{Name: "book_id", Typez: TypezString} + otherField := &Field{Name: "other"} + + testCases := []struct { + name string + message *Message + singular string + want *Field + }{ + { + name: "found id field", + message: &Message{Fields: []*Field{idField, otherField}}, + singular: "book", + want: idField, + }, + { + name: "not found", + message: &Message{Fields: []*Field{otherField}}, + singular: "book", + want: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := findResourceIDField(tc.message, tc.singular) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestFindQuickstartMethod(t *testing.T) { + fooMethod := &Method{Name: "FooMethod"} + listPolicies := &Method{Name: "ListAccessPolicies", IsAIPStandardList: true, OutputType: &Message{Resource: &Resource{Singular: "accesspolicy"}}} + getPolicy := &Method{Name: "GetAccessPolicy", IsAIPStandardGet: true} + createPolicy := &Method{Name: "CreateAccessPolicy", IsAIPStandardCreate: true} + deletePolicy := &Method{Name: "DeleteAccessPolicy", IsAIPStandardDelete: true} + updatePolicy := &Method{Name: "UpdateAccessPolicy", IsAIPStandardUpdate: true} + listOther := &Method{Name: "ListOtherThings", IsAIPStandardList: true, OutputType: &Message{Resource: &Resource{Singular: "otherthing"}}} + + testCases := []struct { + name string + service *Service + want *Method + }{ + { + name: "empty service", + service: &Service{Name: "AccessPolicyService", Methods: []*Method{}}, + want: nil, + }, + { + name: "fallback to simple method when no standard methods exist", + service: &Service{Name: "AccessPolicyService", Methods: []*Method{fooMethod}}, + want: fooMethod, + }, + { + name: "prefer non-deprecated simple method", + service: &Service{Name: "AccessPolicyService", Methods: []*Method{{Name: "DeprecatedList", Deprecated: true}, fooMethod}}, + want: fooMethod, + }, + { + name: "only get method", + service: &Service{Name: "AccessPolicyService", Methods: []*Method{getPolicy}}, + want: getPolicy, + }, + { + name: "prioritizes list over get", + service: &Service{Name: "AccessPolicyService", Methods: []*Method{getPolicy, listOther}}, + want: listOther, + }, + { + name: "prioritizes create over delete", + service: &Service{Name: "AccessPolicyService", Methods: []*Method{deletePolicy, createPolicy}}, + want: createPolicy, + }, + { + name: "prioritizes delete over update", + service: &Service{Name: "AccessPolicyService", Methods: []*Method{updatePolicy, deletePolicy}}, + want: deletePolicy, + }, + { + name: "tie-breaking on name matching (ListAccessPolicies vs ListOtherThings for AccessPolicyService)", + service: &Service{Name: "AccessPolicyService", Methods: []*Method{listOther, listPolicies}}, + want: listPolicies, + }, + { + name: "tie-breaking fallback to resource singular/plural", + service: &Service{Name: "AccessPolicyService", Methods: []*Method{listOther, {Name: "ListPolicies", IsAIPStandardList: true, OutputType: &Message{Resource: &Resource{Singular: "accesspolicy"}}}}}, + want: &Method{Name: "ListPolicies", IsAIPStandardList: true, OutputType: &Message{Resource: &Resource{Singular: "accesspolicy"}}}, // matches singular 'accesspolicy' + }, + { + name: "fallback to server streaming method when no simple methods exist", + service: &Service{Name: "EchoService", Methods: []*Method{{Name: "Expand", ServerSideStreaming: true}}}, + want: &Method{Name: "Expand", ServerSideStreaming: true}, + }, + { + name: "fallback to bidi streaming method when no unary or server streaming methods exist", + service: &Service{Name: "EchoService", Methods: []*Method{{Name: "Chat", ClientSideStreaming: true, ServerSideStreaming: true}}}, + want: &Method{Name: "Chat", ClientSideStreaming: true, ServerSideStreaming: true}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := findQuickstartMethod(tc.service) + if diff := cmp.Diff(tc.want, got, cmpopts.IgnoreFields(Method{}, "Service", "Model")); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestFindQuickstartService(t *testing.T) { + fooMethod := &Method{Name: "FooMethod"} + serviceA := &Service{Name: "ServiceA", Methods: []*Method{fooMethod}} + serviceB := &Service{Name: "SecretManagerService", Methods: []*Method{fooMethod}} + deprecatedService := &Service{Name: "SecretManagerService", Deprecated: true, Methods: []*Method{fooMethod}} + + testCases := []struct { + name string + api *API + want *Service + }{ + { + name: "no services", + api: &API{Name: "secretmanager", Services: nil}, + want: nil, + }, + { + name: "one service", + api: &API{Name: "secretmanager", Services: []*Service{serviceA}}, + want: serviceA, + }, + { + name: "match service name to api name", + api: &API{Name: "secretmanager", Services: []*Service{serviceA, serviceB}}, + want: serviceB, + }, + { + name: "no match defaults to first", + api: &API{Name: "otherapi", Services: []*Service{serviceA, serviceB}}, + want: serviceA, + }, + { + name: "prefer non-deprecated service", + api: &API{Name: "secretmanager", Services: []*Service{deprecatedService, serviceA}}, + want: serviceA, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := findQuickstartService(tc.api) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/sidekick/api/xref_method_test.go b/internal/sidekick/api/xref_method_test.go new file mode 100644 index 0000000000..b425696423 --- /dev/null +++ b/internal/sidekick/api/xref_method_test.go @@ -0,0 +1,228 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "testing" +) + +func TestIsSimpleMethod(t *testing.T) { + somePagination := &Field{} + someOperationInfo := &OperationInfo{} + someDiscoverLro := &DiscoveryLro{} + testCases := []struct { + name string + method *Method + isSimple bool + }{ + { + name: "simple method", + method: &Method{}, + isSimple: true, + }, + { + name: "pagination method", + method: &Method{Pagination: somePagination}, + isSimple: false, + }, + { + name: "client streaming method", + method: &Method{ClientSideStreaming: true}, + isSimple: false, + }, + { + name: "server streaming method", + method: &Method{ServerSideStreaming: true}, + isSimple: false, + }, + { + name: "LRO method", + method: &Method{OperationInfo: someOperationInfo}, + isSimple: false, + }, + { + name: "Discovery LRO method", + method: &Method{DiscoveryLro: someDiscoverLro}, + isSimple: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + enrichMethodSamples(tc.method) + if got := tc.method.IsSimple; got != tc.isSimple { + t.Errorf("IsSimple() = %v, want %v", got, tc.isSimple) + } + }) + } +} + +func TestIsLRO(t *testing.T) { + for _, test := range []struct { + name string + method *Method + want bool + }{ + { + name: "simple method is not LRO", + method: &Method{}, + want: false, + }, + { + name: "LRO method is LRO", + method: &Method{OperationInfo: &OperationInfo{}}, + want: true, + }, + { + name: "LRO method is discovery LRO", + method: &Method{DiscoveryLro: &DiscoveryLro{}}, + want: true, + }, + } { + t.Run(test.name, func(t *testing.T) { + enrichMethodSamples(test.method) + if got := test.method.IsLRO; got != test.want { + t.Errorf("IsLRO() = %v, want %v", got, test.want) + } + }) + } +} + +func TestLongRunningHelpers(t *testing.T) { + emptyMsg := &Message{ID: ".google.protobuf.Empty"} + responseMsg := &Message{ID: "some.response.Message"} + model := &API{ + messageByID: map[string]*Message{ + emptyMsg.ID: emptyMsg, + responseMsg.ID: responseMsg, + }, + } + + testCases := []struct { + name string + method *Method + wantResponse *Message + wantEmpty bool + }{ + { + name: "LRO with empty response", + method: &Method{ + OperationInfo: &OperationInfo{ResponseTypeID: emptyMsg.ID}, + Model: model, + }, + wantResponse: emptyMsg, + wantEmpty: true, + }, + { + name: "LRO with non-empty response", + method: &Method{ + OperationInfo: &OperationInfo{ResponseTypeID: responseMsg.ID}, + Model: model, + }, + wantResponse: responseMsg, + wantEmpty: false, + }, + { + name: "non-LRO method", + method: &Method{ + Model: model, + }, + wantResponse: nil, + wantEmpty: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + enrichMethodSamples(tc.method) + if got := tc.method.LongRunningResponseType; got != tc.wantResponse { + t.Errorf("LongRunningResponseType() = %v, want %v", got, tc.wantResponse) + } + enrichMethodSamples(tc.method) + if got := tc.method.LongRunningReturnsEmpty; got != tc.wantEmpty { + t.Errorf("LongRunningReturnsEmpty() = %v, want %v", got, tc.wantEmpty) + } + }) + } +} + +func TestIsList(t *testing.T) { + testCases := []struct { + name string + method *Method + want bool + }{ + { + name: "list method", + method: &Method{OutputType: &Message{Pagination: &PaginationInfo{}}}, + want: true, + }, + { + name: "simple method", + method: &Method{}, + want: false, + }, + { + name: "no output type", + method: &Method{OutputType: nil}, + want: false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + enrichMethodSamples(tc.method) + if got := tc.method.IsList; got != tc.want { + t.Errorf("IsList() = %v, want %v", got, tc.want) + } + }) + } +} + +func TestIsStreaming(t *testing.T) { + testCases := []struct { + name string + method *Method + want bool + }{ + { + name: "unary method", + method: &Method{}, + want: false, + }, + { + name: "client streaming", + method: &Method{ClientSideStreaming: true}, + want: true, + }, + { + name: "server streaming", + method: &Method{ServerSideStreaming: true}, + want: true, + }, + { + name: "bidi streaming", + method: &Method{ClientSideStreaming: true, ServerSideStreaming: true}, + want: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + enrichMethodSamples(tc.method) + if got := tc.method.IsStreaming; got != tc.want { + t.Errorf("IsStreaming() = %v, want %v", got, tc.want) + } + }) + } +} diff --git a/internal/sidekick/api/xref_samples_test.go b/internal/sidekick/api/xref_samples_test.go new file mode 100644 index 0000000000..cccfee83f2 --- /dev/null +++ b/internal/sidekick/api/xref_samples_test.go @@ -0,0 +1,424 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func TestEnrichSamplesEnumValues(t *testing.T) { + v_good1 := &EnumValue{Name: "GOOD_1", Number: 1} + v_good2 := &EnumValue{Name: "GOOD_2", Number: 2} + v_good3 := &EnumValue{Name: "GOOD_3", Number: 3} + v_good4 := &EnumValue{Name: "GOOD_4", Number: 4} + v_bad_deprecated := &EnumValue{Name: "BAD_DEPRECATED", Number: 5, Deprecated: true} + v_bad_default := &EnumValue{Name: "BAD_DEFAULT", Number: 0} + + testCases := []struct { + name string + values []*EnumValue + wantExamples []*SampleValue + }{ + { + name: "more than 3 good values", + values: []*EnumValue{v_good1, v_good2, v_good3, v_good4}, + wantExamples: []*SampleValue{ + {EnumValue: v_good1, Index: 0}, + {EnumValue: v_good2, Index: 1}, + {EnumValue: v_good3, Index: 2}, + }, + }, + { + name: "less than 3 good values", + values: []*EnumValue{v_good1, v_good2, v_bad_deprecated}, + wantExamples: []*SampleValue{ + {EnumValue: v_good1, Index: 0}, + {EnumValue: v_good2, Index: 1}, + }, + }, + { + name: "no good values", + values: []*EnumValue{v_bad_default, v_bad_deprecated}, + wantExamples: []*SampleValue{ + {EnumValue: v_bad_default, Index: 0}, + {EnumValue: v_bad_deprecated, Index: 1}, + }, + }, + { + name: "no values", + values: []*EnumValue{}, + wantExamples: []*SampleValue{}, + }, + { + name: "mixed good and bad values", + values: []*EnumValue{v_bad_default, v_good1, v_bad_deprecated, v_good2}, + wantExamples: []*SampleValue{ + {EnumValue: v_good1, Index: 0}, + {EnumValue: v_good2, Index: 1}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + enum := &Enum{ + Name: "TestEnum", + ID: ".test.v1.TestEnum", + Package: "test.v1", + Values: tc.values, + } + model := NewTestAPI([]*Message{}, []*Enum{enum}, []*Service{}) + if err := CrossReference(model); err != nil { + t.Fatal(err) + } + + got := enum.ValuesForExamples + if diff := cmp.Diff(tc.wantExamples, got, cmpopts.IgnoreFields(EnumValue{}, "Parent")); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestEnrichSamplesOneOfExampleField(t *testing.T) { + deprecated := &Field{ + Name: "deprecated_field", + ID: ".test.Message.deprecated_field", + Typez: TypezString, + IsOneOf: true, + Deprecated: true, + } + mapMessage := &Message{ + Name: "$map", + ID: "$map", + IsMap: true, + Fields: []*Field{ + {Name: "key", ID: "$map.key", Typez: TypezString}, + {Name: "value", ID: "$map.value", Typez: TypezString}, + }, + } + mapField := &Field{ + Name: "map_field", + ID: ".test.Message.map_field", + Typez: TypezMessage, + TypezID: "$map", + IsOneOf: true, + Map: true, + } + repeated := &Field{ + Name: "repeated_field", + ID: ".test.Message.repeated_field", + Typez: TypezString, + Repeated: true, + IsOneOf: true, + } + scalar := &Field{ + Name: "scalar_field", + ID: ".test.Message.scalar_field", + Typez: TypezInt32, + IsOneOf: true, + } + messageField := &Field{ + Name: "message_field", + ID: ".test.Message.message_field", + Typez: TypezMessage, + TypezID: ".test.OneMessage", + IsOneOf: true, + } + anotherMessageField := &Field{ + Name: "another_message_field", + ID: ".test.Message.another_message_field", + Typez: TypezMessage, + TypezID: ".test.AnotherMessage", + IsOneOf: true, + } + + testCases := []struct { + name string + fields []*Field + want *Field + }{ + { + name: "all types", + fields: []*Field{deprecated, mapField, repeated, scalar, messageField}, + want: scalar, + }, + { + name: "no primitives", + fields: []*Field{deprecated, mapField, repeated, messageField}, + want: messageField, + }, + { + name: "only scalars and messages", + fields: []*Field{messageField, scalar, anotherMessageField}, + want: scalar, + }, + { + name: "no scalars", + fields: []*Field{deprecated, mapField, repeated}, + want: repeated, + }, + { + name: "only map and deprecated", + fields: []*Field{deprecated, mapField}, + want: mapField, + }, + { + name: "only deprecated", + fields: []*Field{deprecated}, + want: deprecated, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + group := &OneOf{ + Name: "test_oneof", + ID: ".test.Message.test_oneof", + Fields: tc.fields, + } + message := &Message{ + Name: "Message", + ID: ".test.Message", + Package: "test", + Fields: tc.fields, + OneOfs: []*OneOf{group}, + } + oneMessage := &Message{ + Name: "OneMessage", + ID: ".test.OneMessage", + Package: "test", + } + anotherMessage := &Message{ + Name: "AnotherMessage", + ID: ".test.AnotherMessage", + Package: "test", + } + model := NewTestAPI([]*Message{message, oneMessage, anotherMessage, mapMessage}, []*Enum{}, []*Service{}) + if err := CrossReference(model); err != nil { + t.Fatal(err) + } + + got := group.ExampleField + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestEnrichSamplesWithResourceNamePattern(t *testing.T) { + t.Run("resource type resolution", func(t *testing.T) { + res := &Resource{ + Type: "test.googleapis.com/Resource", + Patterns: []ResourcePattern{ + { + *(&PathSegment{}).WithLiteral("resources"), + *(&PathSegment{}).WithVariable(NewPathVariable("resource").WithMatch()), + }, + }, + } + field := &Field{ + Name: "notName", + ID: ".test.ResourceMessage.name", + Typez: TypezString, + ResourceReference: &ResourceReference{ + Type: "test.googleapis.com/Resource", + }, + } + message := &Message{ + Name: "ResourceMessage", + ID: ".test.ResourceMessage", + Fields: []*Field{field}, + Resource: res, + } + model := NewTestAPI([]*Message{message}, []*Enum{}, []*Service{}) + + if err := CrossReference(model); err != nil { + t.Fatal(err) + } + + got := field.ResourceNamePattern + want := &ResourceNamePattern{ + Segments: []ResourceNameSegment{ + {Literal: "resources", Variable: "resource"}, + }, + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("child type resolution", func(t *testing.T) { + res := &Resource{ + Type: "test.googleapis.com/Child", + Patterns: []ResourcePattern{ + { + *(&PathSegment{}).WithLiteral("parents"), + *(&PathSegment{}).WithVariable(NewPathVariable("parent").WithMatch()), + *(&PathSegment{}).WithLiteral("children"), + *(&PathSegment{}).WithVariable(NewPathVariable("child").WithMatch()), + }, + }, + } + field := &Field{ + Name: "parent", + ID: ".test.Message.parent", + Typez: TypezString, + ResourceReference: &ResourceReference{ + ChildType: "test.googleapis.com/Child", + }, + } + message := &Message{ + Name: "Message", + ID: ".test.Message", + Fields: []*Field{field}, + } + model := NewTestAPI([]*Message{message}, []*Enum{}, []*Service{}) + model.AddResource(res) + + if err := CrossReference(model); err != nil { + t.Fatal(err) + } + + got := field.ResourceNamePattern + want := &ResourceNamePattern{ + Segments: []ResourceNameSegment{ + {Literal: "parents", Variable: "parent"}, + }, + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("name field resolution", func(t *testing.T) { + res := &Resource{ + Type: "test.googleapis.com/Resource", + Patterns: []ResourcePattern{ + { + *(&PathSegment{}).WithLiteral("resources"), + *(&PathSegment{}).WithVariable(NewPathVariable("resource").WithMatch()), + }, + }, + } + field := &Field{ + Name: "name", + ID: ".test.ResourceMessage.name", + Typez: TypezString, + } + message := &Message{ + Name: "ResourceMessage", + ID: ".test.ResourceMessage", + Fields: []*Field{field}, + Resource: res, + } + field.MessageType = message + model := NewTestAPI([]*Message{message}, []*Enum{}, []*Service{}) + + if err := CrossReference(model); err != nil { + t.Fatal(err) + } + + got := field.ResourceNamePattern + want := &ResourceNamePattern{ + Segments: []ResourceNameSegment{ + {Literal: "resources", Variable: "resource"}, + }, + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) +} + +func TestToResourceNamePattern(t *testing.T) { + for _, test := range []struct { + name string + pattern ResourcePattern + skipLast bool + want *ResourceNamePattern + }{ + { + name: "simple", + pattern: ResourcePattern{ + *(&PathSegment{}).WithLiteral("projects"), + *(&PathSegment{}).WithVariable(NewPathVariable("project").WithMatch()), + }, + skipLast: false, + want: &ResourceNamePattern{ + Segments: []ResourceNameSegment{ + {Literal: "projects", Variable: "project"}, + }, + }, + }, + { + name: "multi-segment", + pattern: ResourcePattern{ + *(&PathSegment{}).WithLiteral("projects"), + *(&PathSegment{}).WithVariable(NewPathVariable("project").WithMatch()), + *(&PathSegment{}).WithLiteral("secrets"), + *(&PathSegment{}).WithVariable(NewPathVariable("secret").WithMatch()), + }, + skipLast: false, + want: &ResourceNamePattern{ + Segments: []ResourceNameSegment{ + {Literal: "projects", Variable: "project"}, + {Literal: "secrets", Variable: "secret"}, + }, + }, + }, + { + name: "multi-segment-skip-last", + pattern: ResourcePattern{ + *(&PathSegment{}).WithLiteral("projects"), + *(&PathSegment{}).WithVariable(NewPathVariable("project").WithMatch()), + *(&PathSegment{}).WithLiteral("secrets"), + *(&PathSegment{}).WithVariable(NewPathVariable("secret").WithMatch()), + }, + skipLast: true, + want: &ResourceNamePattern{ + Segments: []ResourceNameSegment{ + {Literal: "projects", Variable: "project"}, + }, + }, + }, + { + name: "with trailing literal", + pattern: ResourcePattern{ + *(&PathSegment{}).WithLiteral("projects"), + *(&PathSegment{}).WithVariable(NewPathVariable("project").WithMatch()), + *(&PathSegment{}).WithLiteral("config"), + }, + skipLast: false, + want: &ResourceNamePattern{ + Segments: []ResourceNameSegment{ + {Literal: "projects", Variable: "project"}, + {Literal: "config", Variable: ""}, + }, + }, + }, + } { + t.Run(test.name, func(t *testing.T) { + got := toResourceNamePattern(test.pattern, test.skipLast) + if diff := cmp.Diff(test.want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/sidekick/api/xref_test.go b/internal/sidekick/api/xref_test.go index dc0c302db0..1482ac60a1 100644 --- a/internal/sidekick/api/xref_test.go +++ b/internal/sidekick/api/xref_test.go @@ -17,9 +17,6 @@ package api import ( "fmt" "testing" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" ) func TestCrossReferenceOneOfs(t *testing.T) { @@ -217,2090 +214,3 @@ func TestCrossReferenceService(t *testing.T) { t.Errorf("mismatched model, got=%v, want=%v", mixin.Model, model) } } - -func TestEnrichSamplesEnumValues(t *testing.T) { - v_good1 := &EnumValue{Name: "GOOD_1", Number: 1} - v_good2 := &EnumValue{Name: "GOOD_2", Number: 2} - v_good3 := &EnumValue{Name: "GOOD_3", Number: 3} - v_good4 := &EnumValue{Name: "GOOD_4", Number: 4} - v_bad_deprecated := &EnumValue{Name: "BAD_DEPRECATED", Number: 5, Deprecated: true} - v_bad_default := &EnumValue{Name: "BAD_DEFAULT", Number: 0} - - testCases := []struct { - name string - values []*EnumValue - wantExamples []*SampleValue - }{ - { - name: "more than 3 good values", - values: []*EnumValue{v_good1, v_good2, v_good3, v_good4}, - wantExamples: []*SampleValue{ - {EnumValue: v_good1, Index: 0}, - {EnumValue: v_good2, Index: 1}, - {EnumValue: v_good3, Index: 2}, - }, - }, - { - name: "less than 3 good values", - values: []*EnumValue{v_good1, v_good2, v_bad_deprecated}, - wantExamples: []*SampleValue{ - {EnumValue: v_good1, Index: 0}, - {EnumValue: v_good2, Index: 1}, - }, - }, - { - name: "no good values", - values: []*EnumValue{v_bad_default, v_bad_deprecated}, - wantExamples: []*SampleValue{ - {EnumValue: v_bad_default, Index: 0}, - {EnumValue: v_bad_deprecated, Index: 1}, - }, - }, - { - name: "no values", - values: []*EnumValue{}, - wantExamples: []*SampleValue{}, - }, - { - name: "mixed good and bad values", - values: []*EnumValue{v_bad_default, v_good1, v_bad_deprecated, v_good2}, - wantExamples: []*SampleValue{ - {EnumValue: v_good1, Index: 0}, - {EnumValue: v_good2, Index: 1}, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - enum := &Enum{ - Name: "TestEnum", - ID: ".test.v1.TestEnum", - Package: "test.v1", - Values: tc.values, - } - model := NewTestAPI([]*Message{}, []*Enum{enum}, []*Service{}) - if err := CrossReference(model); err != nil { - t.Fatal(err) - } - - got := enum.ValuesForExamples - if diff := cmp.Diff(tc.wantExamples, got, cmpopts.IgnoreFields(EnumValue{}, "Parent")); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestEnrichSamplesOneOfExampleField(t *testing.T) { - deprecated := &Field{ - Name: "deprecated_field", - ID: ".test.Message.deprecated_field", - Typez: TypezString, - IsOneOf: true, - Deprecated: true, - } - mapMessage := &Message{ - Name: "$map", - ID: "$map", - IsMap: true, - Fields: []*Field{ - {Name: "key", ID: "$map.key", Typez: TypezString}, - {Name: "value", ID: "$map.value", Typez: TypezString}, - }, - } - mapField := &Field{ - Name: "map_field", - ID: ".test.Message.map_field", - Typez: TypezMessage, - TypezID: "$map", - IsOneOf: true, - Map: true, - } - repeated := &Field{ - Name: "repeated_field", - ID: ".test.Message.repeated_field", - Typez: TypezString, - Repeated: true, - IsOneOf: true, - } - scalar := &Field{ - Name: "scalar_field", - ID: ".test.Message.scalar_field", - Typez: TypezInt32, - IsOneOf: true, - } - messageField := &Field{ - Name: "message_field", - ID: ".test.Message.message_field", - Typez: TypezMessage, - TypezID: ".test.OneMessage", - IsOneOf: true, - } - anotherMessageField := &Field{ - Name: "another_message_field", - ID: ".test.Message.another_message_field", - Typez: TypezMessage, - TypezID: ".test.AnotherMessage", - IsOneOf: true, - } - - testCases := []struct { - name string - fields []*Field - want *Field - }{ - { - name: "all types", - fields: []*Field{deprecated, mapField, repeated, scalar, messageField}, - want: scalar, - }, - { - name: "no primitives", - fields: []*Field{deprecated, mapField, repeated, messageField}, - want: messageField, - }, - { - name: "only scalars and messages", - fields: []*Field{messageField, scalar, anotherMessageField}, - want: scalar, - }, - { - name: "no scalars", - fields: []*Field{deprecated, mapField, repeated}, - want: repeated, - }, - { - name: "only map and deprecated", - fields: []*Field{deprecated, mapField}, - want: mapField, - }, - { - name: "only deprecated", - fields: []*Field{deprecated}, - want: deprecated, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - group := &OneOf{ - Name: "test_oneof", - ID: ".test.Message.test_oneof", - Fields: tc.fields, - } - message := &Message{ - Name: "Message", - ID: ".test.Message", - Package: "test", - Fields: tc.fields, - OneOfs: []*OneOf{group}, - } - oneMessage := &Message{ - Name: "OneMessage", - ID: ".test.OneMessage", - Package: "test", - } - anotherMessage := &Message{ - Name: "AnotherMessage", - ID: ".test.AnotherMessage", - Package: "test", - } - model := NewTestAPI([]*Message{message, oneMessage, anotherMessage, mapMessage}, []*Enum{}, []*Service{}) - if err := CrossReference(model); err != nil { - t.Fatal(err) - } - - got := group.ExampleField - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestEnrichSamplesWithResourceNamePattern(t *testing.T) { - t.Run("resource type resolution", func(t *testing.T) { - res := &Resource{ - Type: "test.googleapis.com/Resource", - Patterns: []ResourcePattern{ - { - *(&PathSegment{}).WithLiteral("resources"), - *(&PathSegment{}).WithVariable(NewPathVariable("resource").WithMatch()), - }, - }, - } - field := &Field{ - Name: "notName", - ID: ".test.ResourceMessage.name", - Typez: TypezString, - ResourceReference: &ResourceReference{ - Type: "test.googleapis.com/Resource", - }, - } - message := &Message{ - Name: "ResourceMessage", - ID: ".test.ResourceMessage", - Fields: []*Field{field}, - Resource: res, - } - model := NewTestAPI([]*Message{message}, []*Enum{}, []*Service{}) - - if err := CrossReference(model); err != nil { - t.Fatal(err) - } - - got := field.ResourceNamePattern - want := &ResourceNamePattern{ - Segments: []ResourceNameSegment{ - {Literal: "resources", Variable: "resource"}, - }, - } - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - - t.Run("child type resolution", func(t *testing.T) { - res := &Resource{ - Type: "test.googleapis.com/Child", - Patterns: []ResourcePattern{ - { - *(&PathSegment{}).WithLiteral("parents"), - *(&PathSegment{}).WithVariable(NewPathVariable("parent").WithMatch()), - *(&PathSegment{}).WithLiteral("children"), - *(&PathSegment{}).WithVariable(NewPathVariable("child").WithMatch()), - }, - }, - } - field := &Field{ - Name: "parent", - ID: ".test.Message.parent", - Typez: TypezString, - ResourceReference: &ResourceReference{ - ChildType: "test.googleapis.com/Child", - }, - } - message := &Message{ - Name: "Message", - ID: ".test.Message", - Fields: []*Field{field}, - } - model := NewTestAPI([]*Message{message}, []*Enum{}, []*Service{}) - model.AddResource(res) - - if err := CrossReference(model); err != nil { - t.Fatal(err) - } - - got := field.ResourceNamePattern - want := &ResourceNamePattern{ - Segments: []ResourceNameSegment{ - {Literal: "parents", Variable: "parent"}, - }, - } - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - - t.Run("name field resolution", func(t *testing.T) { - res := &Resource{ - Type: "test.googleapis.com/Resource", - Patterns: []ResourcePattern{ - { - *(&PathSegment{}).WithLiteral("resources"), - *(&PathSegment{}).WithVariable(NewPathVariable("resource").WithMatch()), - }, - }, - } - field := &Field{ - Name: "name", - ID: ".test.ResourceMessage.name", - Typez: TypezString, - } - message := &Message{ - Name: "ResourceMessage", - ID: ".test.ResourceMessage", - Fields: []*Field{field}, - Resource: res, - } - field.MessageType = message - model := NewTestAPI([]*Message{message}, []*Enum{}, []*Service{}) - - if err := CrossReference(model); err != nil { - t.Fatal(err) - } - - got := field.ResourceNamePattern - want := &ResourceNamePattern{ - Segments: []ResourceNameSegment{ - {Literal: "resources", Variable: "resource"}, - }, - } - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) -} - -func TestToResourceNamePattern(t *testing.T) { - for _, test := range []struct { - name string - pattern ResourcePattern - skipLast bool - want *ResourceNamePattern - }{ - { - name: "simple", - pattern: ResourcePattern{ - *(&PathSegment{}).WithLiteral("projects"), - *(&PathSegment{}).WithVariable(NewPathVariable("project").WithMatch()), - }, - skipLast: false, - want: &ResourceNamePattern{ - Segments: []ResourceNameSegment{ - {Literal: "projects", Variable: "project"}, - }, - }, - }, - { - name: "multi-segment", - pattern: ResourcePattern{ - *(&PathSegment{}).WithLiteral("projects"), - *(&PathSegment{}).WithVariable(NewPathVariable("project").WithMatch()), - *(&PathSegment{}).WithLiteral("secrets"), - *(&PathSegment{}).WithVariable(NewPathVariable("secret").WithMatch()), - }, - skipLast: false, - want: &ResourceNamePattern{ - Segments: []ResourceNameSegment{ - {Literal: "projects", Variable: "project"}, - {Literal: "secrets", Variable: "secret"}, - }, - }, - }, - { - name: "multi-segment-skip-last", - pattern: ResourcePattern{ - *(&PathSegment{}).WithLiteral("projects"), - *(&PathSegment{}).WithVariable(NewPathVariable("project").WithMatch()), - *(&PathSegment{}).WithLiteral("secrets"), - *(&PathSegment{}).WithVariable(NewPathVariable("secret").WithMatch()), - }, - skipLast: true, - want: &ResourceNamePattern{ - Segments: []ResourceNameSegment{ - {Literal: "projects", Variable: "project"}, - }, - }, - }, - { - name: "with trailing literal", - pattern: ResourcePattern{ - *(&PathSegment{}).WithLiteral("projects"), - *(&PathSegment{}).WithVariable(NewPathVariable("project").WithMatch()), - *(&PathSegment{}).WithLiteral("config"), - }, - skipLast: false, - want: &ResourceNamePattern{ - Segments: []ResourceNameSegment{ - {Literal: "projects", Variable: "project"}, - {Literal: "config", Variable: ""}, - }, - }, - }, - } { - t.Run(test.name, func(t *testing.T) { - got := toResourceNamePattern(test.pattern, test.skipLast) - if diff := cmp.Diff(test.want, got); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestIsSimpleMethod(t *testing.T) { - somePagination := &Field{} - someOperationInfo := &OperationInfo{} - someDiscoverLro := &DiscoveryLro{} - testCases := []struct { - name string - method *Method - isSimple bool - }{ - { - name: "simple method", - method: &Method{}, - isSimple: true, - }, - { - name: "pagination method", - method: &Method{Pagination: somePagination}, - isSimple: false, - }, - { - name: "client streaming method", - method: &Method{ClientSideStreaming: true}, - isSimple: false, - }, - { - name: "server streaming method", - method: &Method{ServerSideStreaming: true}, - isSimple: false, - }, - { - name: "LRO method", - method: &Method{OperationInfo: someOperationInfo}, - isSimple: false, - }, - { - name: "Discovery LRO method", - method: &Method{DiscoveryLro: someDiscoverLro}, - isSimple: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - enrichMethodSamples(tc.method) - if got := tc.method.IsSimple; got != tc.isSimple { - t.Errorf("IsSimple() = %v, want %v", got, tc.isSimple) - } - }) - } -} - -func TestIsLRO(t *testing.T) { - for _, test := range []struct { - name string - method *Method - want bool - }{ - { - name: "simple method is not LRO", - method: &Method{}, - want: false, - }, - { - name: "LRO method is LRO", - method: &Method{OperationInfo: &OperationInfo{}}, - want: true, - }, - { - name: "LRO method is discovery LRO", - method: &Method{DiscoveryLro: &DiscoveryLro{}}, - want: true, - }, - } { - t.Run(test.name, func(t *testing.T) { - enrichMethodSamples(test.method) - if got := test.method.IsLRO; got != test.want { - t.Errorf("IsLRO() = %v, want %v", got, test.want) - } - }) - } -} - -func TestLongRunningHelpers(t *testing.T) { - emptyMsg := &Message{ID: ".google.protobuf.Empty"} - responseMsg := &Message{ID: "some.response.Message"} - model := &API{ - messageByID: map[string]*Message{ - emptyMsg.ID: emptyMsg, - responseMsg.ID: responseMsg, - }, - } - - testCases := []struct { - name string - method *Method - wantResponse *Message - wantEmpty bool - }{ - { - name: "LRO with empty response", - method: &Method{ - OperationInfo: &OperationInfo{ResponseTypeID: emptyMsg.ID}, - Model: model, - }, - wantResponse: emptyMsg, - wantEmpty: true, - }, - { - name: "LRO with non-empty response", - method: &Method{ - OperationInfo: &OperationInfo{ResponseTypeID: responseMsg.ID}, - Model: model, - }, - wantResponse: responseMsg, - wantEmpty: false, - }, - { - name: "non-LRO method", - method: &Method{ - Model: model, - }, - wantResponse: nil, - wantEmpty: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - enrichMethodSamples(tc.method) - if got := tc.method.LongRunningResponseType; got != tc.wantResponse { - t.Errorf("LongRunningResponseType() = %v, want %v", got, tc.wantResponse) - } - enrichMethodSamples(tc.method) - if got := tc.method.LongRunningReturnsEmpty; got != tc.wantEmpty { - t.Errorf("LongRunningReturnsEmpty() = %v, want %v", got, tc.wantEmpty) - } - }) - } -} - -func TestIsList(t *testing.T) { - testCases := []struct { - name string - method *Method - want bool - }{ - { - name: "list method", - method: &Method{OutputType: &Message{Pagination: &PaginationInfo{}}}, - want: true, - }, - { - name: "simple method", - method: &Method{}, - want: false, - }, - { - name: "no output type", - method: &Method{OutputType: nil}, - want: false, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - enrichMethodSamples(tc.method) - if got := tc.method.IsList; got != tc.want { - t.Errorf("IsList() = %v, want %v", got, tc.want) - } - }) - } -} - -func TestIsStreaming(t *testing.T) { - testCases := []struct { - name string - method *Method - want bool - }{ - { - name: "unary method", - method: &Method{}, - want: false, - }, - { - name: "client streaming", - method: &Method{ClientSideStreaming: true}, - want: true, - }, - { - name: "server streaming", - method: &Method{ServerSideStreaming: true}, - want: true, - }, - { - name: "bidi streaming", - method: &Method{ClientSideStreaming: true, ServerSideStreaming: true}, - want: true, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - enrichMethodSamples(tc.method) - if got := tc.method.IsStreaming; got != tc.want { - t.Errorf("IsStreaming() = %v, want %v", got, tc.want) - } - }) - } -} - -type aipTestFixture struct { - resource *Resource - resourceWithoutSingular *Resource - resourceNameField *Field - resourceOtherNameField *Field - resourceNameNoSingularField *Field - resourceOtherNameNoSingularField *Field - nonExistentResourceField *Field - wildcardResourceField *Field - model *API -} - -func newAIPTestFixture() *aipTestFixture { - resource := &Resource{ - Type: "google.cloud.secretmanager.v1/Secret", - Singular: "secret", - } - resourceWithoutSingular := &Resource{ - Type: "google.cloud.secretmanager.v1/SecretWithoutSingular", - } - resourceNameField := &Field{ - Name: "name", - ResourceReference: &ResourceReference{ - Type: resource.Type, - }, - } - resourceOtherNameField := &Field{ - Name: "other_name", - ResourceReference: &ResourceReference{ - Type: resource.Type, - }, - } - resourceNameNoSingularField := &Field{ - Name: "name", - ResourceReference: &ResourceReference{ - Type: resourceWithoutSingular.Type, - }, - } - resourceOtherNameNoSingularField := &Field{ - Name: "other_name", - ResourceReference: &ResourceReference{ - Type: resourceWithoutSingular.Type, - }, - } - nonExistentResourceField := &Field{ - Name: "name", - ResourceReference: &ResourceReference{ - Type: "nonexistent.googleapis.com/NonExistent", - }, - } - - wildcardResourceField := &Field{ - Name: "name", - ResourceReference: &ResourceReference{ - Type: "*", - }, - } - - model := &API{ - ResourceDefinitions: []*Resource{resource, resourceWithoutSingular}, - resourceByType: map[string]*Resource{ - resource.Type: resource, - resourceWithoutSingular.Type: resourceWithoutSingular, - }, - } - - return &aipTestFixture{ - resource: resource, - resourceWithoutSingular: resourceWithoutSingular, - resourceNameField: resourceNameField, - resourceOtherNameField: resourceOtherNameField, - resourceNameNoSingularField: resourceNameNoSingularField, - resourceOtherNameNoSingularField: resourceOtherNameNoSingularField, - nonExistentResourceField: nonExistentResourceField, - wildcardResourceField: wildcardResourceField, - model: model, - } -} - -func TestIsAIPStandard(t *testing.T) { - f := newAIPTestFixture() - - // Setup for a valid Get operation - validGetMethod := &Method{ - Name: "GetSecret", - InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, - OutputType: &Message{Resource: f.resource}, - Model: f.model, - } - - validDeleteMethod := &Method{ - Name: "DeleteSecret", - InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{f.resourceNameField}}, - ReturnsEmpty: true, - Model: f.model, - } - - validUndeleteMethod := &Method{ - Name: "UndeleteSecret", - InputType: &Message{Name: "UndeleteSecretRequest", Fields: []*Field{f.resourceNameField}}, - OutputType: &Message{ - Resource: f.resource, - }, - Model: f.model, - } - - // Setup for an invalid Get operation (e.g., wrong name) - invalidGetMethod := &Method{ - Name: "ListSecrets", // Not a Get method - InputType: &Message{Name: "ListSecretsRequest"}, - OutputType: &Message{Resource: f.resource}, - Model: f.model, - } - - testCases := []struct { - name string - method *Method - want bool - }{ - { - name: "standard get method returns true", - method: validGetMethod, - want: true, - }, - { - name: "standard delete method returns true", - method: validDeleteMethod, - want: true, - }, - { - name: "standard undelete method returns true", - method: validUndeleteMethod, - want: true, - }, - { - name: "non-standard method returns false", - method: invalidGetMethod, - want: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - enrichMethodSamples(tc.method) - if got := tc.method.IsAIPStandard; got != tc.want { - t.Errorf("IsAIPStandard() = %v, want %v", got, tc.want) - } - }) - } -} - -func TestAIPStandardGetInfo(t *testing.T) { - f := newAIPTestFixture() - - // Helper to create an output message since Get needs it - output := &Message{ - Resource: f.resource, - } - - testCases := []struct { - name string - method *Method - want *SampleInfo - }{ - { - name: "valid get operation with wildcard resource reference", - method: &Method{ - Name: "GetSecret", - InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.wildcardResourceField}}, - OutputType: output, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: f.wildcardResourceField, - IsRequestResourceName: true, - }, - }, - { - name: "valid get operation", - method: &Method{ - Name: "GetSecret", - InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, - OutputType: output, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: f.resourceNameField, - IsRequestResourceName: true, - }, - }, - { - name: "valid get operation with missing singular name on resource", - method: &Method{ - Name: "GetSecret", - InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameNoSingularField}}, - OutputType: &Message{ - Resource: f.resourceWithoutSingular, - }, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: f.resourceNameNoSingularField, - IsRequestResourceName: true, - }, - }, - { - name: "method name is incorrect", - method: &Method{ - Name: "Get", - InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, - OutputType: output, - Model: f.model, - }, - want: nil, - }, - { - name: "request type name is incorrect", - method: &Method{ - Name: "GetSecret", - InputType: &Message{Name: "GetRequest", Fields: []*Field{f.resourceNameField}}, - OutputType: output, - Model: f.model, - }, - want: nil, - }, - { - name: "returns empty", - method: &Method{ - Name: "GetSecret", - InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, - OutputType: output, - ReturnsEmpty: true, - Model: f.model, - }, - want: nil, - }, - { - name: "output is not a resource", - method: &Method{ - Name: "GetSecret", - InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, - OutputType: &Message{ - Resource: nil, - }, - Model: f.model, - }, - want: nil, - }, - { - name: "request does not contain resource name field", - method: &Method{ - Name: "GetSecret", - InputType: &Message{Name: "GetSecretRequest"}, - OutputType: output, - Model: f.model, - }, - want: nil, - }, - { - name: "pagination method is not a standard get operation", - method: &Method{ - Name: "GetSecret", - InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, - OutputType: output, - Pagination: &Field{}, - Model: f.model, - }, - want: nil, - }, - { - name: "client streaming method is not a standard get operation", - method: &Method{ - Name: "GetSecret", - InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, - OutputType: output, - ClientSideStreaming: true, - Model: f.model, - }, - want: nil, - }, - { - name: "server streaming method is not a standard get operation", - method: &Method{ - Name: "GetSecret", - InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, - OutputType: output, - ServerSideStreaming: true, - Model: f.model, - }, - want: nil, - }, - { - name: "LRO method is not a standard get operation", - method: &Method{ - Name: "GetSecret", - InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, - OutputType: output, - OperationInfo: &OperationInfo{}, - Model: f.model, - }, - want: nil, - }, - { - name: "Discovery LRO method is not a standard get operation", - method: &Method{ - Name: "GetSecret", - InputType: &Message{Name: "GetSecretRequest", Fields: []*Field{f.resourceNameField}}, - OutputType: output, - DiscoveryLro: &DiscoveryLro{}, - Model: f.model, - }, - want: nil, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - enrichMethodSamples(tc.method) - got := tc.method.SampleInfo - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestAIPStandardDeleteInfo(t *testing.T) { - f := newAIPTestFixture() - - testCases := []struct { - name string - method *Method - want *SampleInfo - }{ - { - name: "valid simple delete with wildcard resource reference", - method: &Method{ - Name: "DeleteSecret", - InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{f.wildcardResourceField}}, - ReturnsEmpty: true, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: f.wildcardResourceField, - IsRequestResourceName: true, - }, - }, - { - name: "valid simple delete", - method: &Method{ - Name: "DeleteSecret", - InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{f.resourceNameField}}, - ReturnsEmpty: true, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: f.resourceNameField, - IsRequestResourceName: true, - }, - }, - { - name: "valid simple delete with missing singular name on resource", - method: &Method{ - Name: "DeleteSecret", - InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{f.resourceNameNoSingularField}}, - ReturnsEmpty: true, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: f.resourceNameNoSingularField, - IsRequestResourceName: true, - }, - }, - { - name: "valid lro delete", - method: &Method{ - Name: "DeleteSecret", - InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{f.resourceNameField}}, - OperationInfo: &OperationInfo{}, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: f.resourceNameField, - IsRequestResourceName: true, - }, - }, - { - name: "valid delete with other name matching singular", - method: &Method{ - Name: "DeleteSecret", - InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{f.resourceOtherNameField}}, - ReturnsEmpty: true, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: f.resourceOtherNameField, - IsRequestResourceName: true, - }, - }, - { - name: "incorrect method name", - method: &Method{ - Name: "RemoveSecret", - InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{f.resourceNameField}}, - Model: f.model, - }, - want: nil, - }, - { - name: "incorrect request name", - method: &Method{ - Name: "DeleteSecret", - InputType: &Message{Name: "RemoveSecretRequest", Fields: []*Field{f.resourceNameField}}, - Model: f.model, - }, - want: nil, - }, - { - name: "resource not found in ResourceByType map", - method: &Method{ - Name: "DeleteSecret", - InputType: &Message{ - Name: "DeleteSecretRequest", - Fields: []*Field{ - f.nonExistentResourceField, - }, - }, - Model: f.model, // model's ResourceByType does not contain the nonexistent resource - }, - want: nil, - }, - { - name: "invalid delete with no matching field", - method: &Method{ - Name: "DeleteSecret", - InputType: &Message{ - Name: "DeleteSecretRequest", - Fields: []*Field{ - f.nonExistentResourceField, - f.resourceOtherNameNoSingularField, - }, - }, - Model: f.model, - }, - want: nil, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - enrichMethodSamples(tc.method) - got := tc.method.SampleInfo - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestAIPStandardUndeleteInfo(t *testing.T) { - f := newAIPTestFixture() - - testCases := []struct { - name string - method *Method - want *SampleInfo - }{ - { - name: "valid simple undelete with wildcard resource reference", - method: &Method{ - Name: "UndeleteSecret", - InputType: &Message{Name: "UndeleteSecretRequest", Fields: []*Field{f.wildcardResourceField}}, - OutputType: &Message{ - Resource: f.resource, - }, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: f.wildcardResourceField, - IsRequestResourceName: true, - }, - }, - { - name: "valid simple undelete", - method: &Method{ - Name: "UndeleteSecret", - InputType: &Message{Name: "UndeleteSecretRequest", Fields: []*Field{f.resourceNameField}}, - OutputType: &Message{ - Resource: f.resource, - }, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: f.resourceNameField, - IsRequestResourceName: true, - }, - }, - { - name: "valid simple undelete with missing singular name on resource", - method: &Method{ - Name: "UndeleteSecret", - InputType: &Message{Name: "UndeleteSecretRequest", Fields: []*Field{f.resourceNameNoSingularField}}, - OutputType: &Message{ - Resource: f.resourceWithoutSingular, - }, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: f.resourceNameNoSingularField, - IsRequestResourceName: true, - }, - }, - { - name: "valid lro undelete", - method: &Method{ - Name: "UndeleteSecret", - InputType: &Message{Name: "UndeleteSecretRequest", Fields: []*Field{f.resourceNameField}}, - OperationInfo: &OperationInfo{}, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: f.resourceNameField, - IsRequestResourceName: true, - }, - }, - { - name: "valid undelete with other name matching singular", - method: &Method{ - Name: "UndeleteSecret", - InputType: &Message{Name: "UndeleteSecretRequest", Fields: []*Field{f.resourceOtherNameField}}, - OutputType: &Message{ - Resource: f.resource, - }, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: f.resourceOtherNameField, - IsRequestResourceName: true, - }, - }, - { - name: "incorrect method name", - method: &Method{ - Name: "RestoreSecret", - InputType: &Message{Name: "UndeleteSecretRequest", Fields: []*Field{f.resourceNameField}}, - Model: f.model, - }, - want: nil, - }, - { - name: "incorrect request name", - method: &Method{ - Name: "UndeleteSecret", - InputType: &Message{Name: "RestoreSecretRequest", Fields: []*Field{f.resourceNameField}}, - Model: f.model, - }, - want: nil, - }, - { - name: "resource not found in ResourceByType map", - method: &Method{ - Name: "UndeleteSecret", - InputType: &Message{ - Name: "UndeleteSecretRequest", - Fields: []*Field{ - f.nonExistentResourceField, - }, - }, - Model: f.model, // model's ResourceByType does not contain the nonexistent resource - }, - want: nil, - }, - { - name: "invalid undelete with no matching field", - method: &Method{ - Name: "UndeleteSecret", - InputType: &Message{ - Name: "UndeleteSecretRequest", - Fields: []*Field{ - f.nonExistentResourceField, - f.resourceOtherNameNoSingularField, - }, - }, - Model: f.model, - }, - want: nil, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - enrichMethodSamples(tc.method) - got := tc.method.SampleInfo - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestAIPStandardCreateInfo(t *testing.T) { - f := newAIPTestFixture() - - // Setup for Create - secretMessage := &Message{ID: "secret_message_id"} - f.resource.Self = secretMessage - - parentField := &Field{ - Name: "parent", - ResourceReference: &ResourceReference{ChildType: f.resource.Type}, - Typez: TypezString, - } - resourceField := &Field{ - Name: "secret", - Typez: TypezMessage, - TypezID: secretMessage.ID, - } - idField := &Field{ - Name: "secret_id", - Typez: TypezString, - } - - testCases := []struct { - name string - method *Method - want *SampleInfo - }{ - { - name: "valid create operation", - method: &Method{ - Name: "CreateSecret", - InputType: &Message{ - Name: "CreateSecretRequest", - Fields: []*Field{ - parentField, - idField, - resourceField, - }, - }, - OutputType: &Message{Resource: f.resource}, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: parentField, - IsRequestResourceName: true, - ResourceIDField: idField, - MessageField: resourceField, - }, - }, - { - name: "valid create operation without id", - method: &Method{ - Name: "CreateSecret", - InputType: &Message{ - Name: "CreateSecretRequest", - Fields: []*Field{ - parentField, - resourceField, - }, - }, - OutputType: &Message{Resource: f.resource}, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: parentField, - IsRequestResourceName: true, - MessageField: resourceField, - }, - }, - { - name: "invalid create operation (wrong name)", - method: &Method{ - Name: "MakeSecret", - InputType: &Message{ - Name: "CreateSecretRequest", - Fields: []*Field{ - parentField, - resourceField, - }, - }, - OutputType: &Message{Resource: f.resource}, - Model: f.model, - }, - want: nil, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - enrichMethodSamples(tc.method) - got := tc.method.SampleInfo - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestAIPStandardUpdateInfo(t *testing.T) { - f := newAIPTestFixture() - - // Setup for Update - secretMessage := &Message{ - ID: "secret_message_id", - Fields: []*Field{ - f.resourceNameField, - }, - } - f.resource.Self = secretMessage - - resourceField := &Field{ - Name: "secret", - Typez: TypezMessage, - TypezID: secretMessage.ID, - MessageType: secretMessage, - } - updateMaskField := &Field{ - Name: "update_mask", - TypezID: ".google.protobuf.FieldMask", - } - - testCases := []struct { - name string - method *Method - want *SampleInfo - }{ - { - name: "valid update operation", - method: &Method{ - Name: "UpdateSecret", - InputType: &Message{ - Name: "UpdateSecretRequest", - Fields: []*Field{ - resourceField, - updateMaskField, - }, - }, - OutputType: &Message{Resource: f.resource}, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: f.resourceNameField, - MessageField: resourceField, - IsMessageResourceName: true, - UpdateMaskField: updateMaskField, - }, - }, - { - name: "valid update operation without mask", - method: &Method{ - Name: "UpdateSecret", - InputType: &Message{ - Name: "UpdateSecretRequest", - Fields: []*Field{ - resourceField, - }, - }, - OutputType: &Message{Resource: f.resource}, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: f.resourceNameField, - MessageField: resourceField, - IsMessageResourceName: true, - }, - }, - { - name: "invalid update operation (wrong name)", - method: &Method{ - Name: "ModifySecret", - InputType: &Message{ - Name: "UpdateSecretRequest", - Fields: []*Field{ - resourceField, - }, - }, - OutputType: &Message{Resource: f.resource}, - Model: f.model, - }, - want: nil, - }, - { - name: "invalid update operation (wrong request name)", - method: &Method{ - Name: "UpdateSecret", - InputType: &Message{ - Name: "ModifySecretRequest", - Fields: []*Field{ - resourceField, - }, - }, - OutputType: &Message{Resource: f.resource}, - Model: f.model, - }, - want: nil, - }, - { - name: "invalid update operation (missing resource)", - method: &Method{ - Name: "UpdateSecret", - InputType: &Message{ - Name: "UpdateSecretRequest", - Fields: []*Field{ - updateMaskField, - }, - }, - OutputType: &Message{Resource: f.resource}, - Model: f.model, - }, - want: nil, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - enrichMethodSamples(tc.method) - got := tc.method.SampleInfo - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestAIPStandardListInfo(t *testing.T) { - f := newAIPTestFixture() - - // Create a resource type for the list items - resourceType := "google.cloud.secretmanager.v1/Secret" - // Ensure the parent field points to this resource type in child_type? - // No, parent field child_type matches the listed resource type. - - secretResource := &Resource{Type: resourceType, Plural: "Secrets"} - secretMessage := &Message{Resource: secretResource} - parentField := &Field{ - Name: "parent", - ResourceReference: &ResourceReference{ChildType: resourceType}, - } - otherParentField := &Field{ - Name: "database", // Not named "parent" - ResourceReference: &ResourceReference{ChildType: resourceType}, - } - plainParentField := &Field{Name: "parent"} // No child_type - - pageableItem := &Field{Name: "secrets", MessageType: secretMessage} - paginationInfo := &PaginationInfo{PageableItem: pageableItem} - listOutput := &Message{ - Name: "ListSecretsResponse", - Pagination: paginationInfo, - } - - testCases := []struct { - name string - method *Method - want *SampleInfo - }{ - { - name: "valid list operation with parent field match by child_type", - method: &Method{ - Name: "ListSecrets", - InputType: &Message{Name: "ListSecretsRequest", Fields: []*Field{parentField}}, - OutputType: listOutput, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: parentField, - IsRequestResourceName: true, - }, - }, - { - name: "valid list operation with other field match by child_type", - method: &Method{ - Name: "ListSecrets", - InputType: &Message{Name: "ListSecretsRequest", Fields: []*Field{otherParentField}}, - OutputType: listOutput, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: otherParentField, - IsRequestResourceName: true, - }, - }, - { - name: "valid list operation with parent field name match (fallback)", - method: &Method{ - Name: "ListSecrets", - InputType: &Message{Name: "ListSecretsRequest", Fields: []*Field{plainParentField}}, - OutputType: listOutput, - Model: f.model, - }, - want: &SampleInfo{ - ResourceNameField: plainParentField, - IsRequestResourceName: true, - }, - }, - { - name: "list operation missing parent field", - method: &Method{ - Name: "ListSecrets", - InputType: &Message{Name: "ListSecretsRequest", Fields: []*Field{}}, - OutputType: listOutput, - Model: f.model, - }, - want: nil, - }, - { - name: "method name does not start with List", - method: &Method{ - Name: "EnumerateSecrets", - InputType: &Message{Name: "ListSecretsRequest", Fields: []*Field{parentField}}, - OutputType: listOutput, - Model: f.model, - }, - want: nil, - }, - { - name: "input type name mismatch", - method: &Method{ - Name: "ListSecrets", - InputType: &Message{Name: "EnumerateSecretsRequest", Fields: []*Field{parentField}}, - OutputType: listOutput, - Model: f.model, - }, - want: nil, - }, - { - name: "output type name mismatch", - method: &Method{ - Name: "ListSecrets", - InputType: &Message{Name: "ListSecretsRequest", Fields: []*Field{parentField}}, - OutputType: &Message{Name: "EnumerateSecretsResponse", Pagination: paginationInfo}, - Model: f.model, - }, - want: nil, - }, - { - name: "not a list operation (no pagination)", - method: &Method{ - Name: "ListSecrets", - InputType: &Message{Name: "ListSecretsRequest", Fields: []*Field{parentField}}, - OutputType: &Message{Name: "ListSecretsResponse"}, // No pagination - Model: f.model, - }, - want: nil, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - enrichMethodSamples(tc.method) - got := tc.method.SampleInfo - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestFindBestResourceFieldByType(t *testing.T) { - f := newAIPTestFixture() - targetType := f.resource.Type - - for _, tc := range []struct { - name string - fields []*Field - want *Field - }{ - { - name: "name field with wildcard", - fields: []*Field{f.wildcardResourceField}, - want: f.wildcardResourceField, - }, - { - name: "name field with exact match", - fields: []*Field{f.resourceNameField}, - want: f.resourceNameField, - }, - { - name: "other field with exact match", - fields: []*Field{f.resourceOtherNameField}, - want: f.resourceOtherNameField, - }, - { - name: "name field with exact match wins over other field with exact match", - fields: []*Field{f.resourceNameField, f.resourceOtherNameField}, - want: f.resourceNameField, - }, - { - name: "name field with wildcard wins over other field with exact match", - fields: []*Field{f.wildcardResourceField, f.resourceOtherNameField}, - want: f.wildcardResourceField, - }, - { - name: "no match", - fields: []*Field{f.nonExistentResourceField}, - want: nil, - }, - } { - t.Run(tc.name, func(t *testing.T) { - msg := &Message{Fields: tc.fields} - got := findBestResourceFieldByType(msg, f.model, targetType) - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestFindBestResourceFieldBySingular(t *testing.T) { - f := newAIPTestFixture() - targetSingular := f.resource.Singular - - for _, tc := range []struct { - name string - fields []*Field - want *Field - }{ - { - name: "name field with wildcard", - fields: []*Field{f.wildcardResourceField}, - want: f.wildcardResourceField, - }, - { - name: "name field with exact match", - fields: []*Field{f.resourceNameField}, - want: f.resourceNameField, - }, - { - name: "name field with empty singular match", - fields: []*Field{f.resourceNameNoSingularField}, - want: f.resourceNameNoSingularField, - }, - { - name: "other field with exact match", - fields: []*Field{f.resourceOtherNameField}, - want: f.resourceOtherNameField, - }, - { - name: "name field with exact match wins over other field with exact match", - fields: []*Field{f.resourceNameField, f.resourceOtherNameField}, - want: f.resourceNameField, - }, - { - name: "name field with wildcard wins over other field with exact match", - fields: []*Field{f.wildcardResourceField, f.resourceOtherNameField}, - want: f.wildcardResourceField, - }, - { - name: "no match", - fields: []*Field{f.nonExistentResourceField}, - want: nil, - }, - } { - t.Run(tc.name, func(t *testing.T) { - msg := &Message{Fields: tc.fields} - got := findBestResourceFieldBySingular(msg, f.model, targetSingular) - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestFindBestParentFieldByType(t *testing.T) { - childType := "google.cloud.secretmanager.v1/SecretVersion" - - parentField := &Field{ - Name: "parent", - ResourceReference: &ResourceReference{ChildType: childType}, - } - - parentFieldByName := &Field{ - Name: "parent", - // No matching child type, just name - } - - parentFieldByChildType := &Field{ - Name: "database", - ResourceReference: &ResourceReference{ChildType: childType}, - } - - wrongField := &Field{Name: "wrong"} - - for _, tc := range []struct { - name string - fields []*Field - want *Field - }{ - { - name: "exact match (name + child type)", - fields: []*Field{parentField}, - want: parentField, - }, - { - name: "name match only (fallback)", - fields: []*Field{parentFieldByName}, - want: parentFieldByName, - }, - { - name: "child type match only", - fields: []*Field{parentFieldByChildType}, - want: parentFieldByChildType, - }, - { - name: "name match prefers exact name", - fields: []*Field{parentFieldByName, parentFieldByChildType}, - want: parentFieldByName, - }, - { - name: "no match", - fields: []*Field{wrongField}, - want: nil, - }, - } { - t.Run(tc.name, func(t *testing.T) { - msg := &Message{Fields: tc.fields} - got := findBestParentFieldByType(msg, childType) - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestFieldTypePredicates(t *testing.T) { - type TestCase struct { - field *Field - isString bool - isBytes bool - isBool bool - isInt bool - isUInt bool - isFloat bool - isEnum bool - isObject bool - } - testCases := []TestCase{ - {field: &Field{Typez: TypezString}, isString: true}, - {field: &Field{Typez: TypezBytes}, isBytes: true}, - {field: &Field{Typez: TypezBool}, isBool: true}, - {field: &Field{Typez: TypezInt32}, isInt: true}, - {field: &Field{Typez: TypezInt64}, isInt: true}, - {field: &Field{Typez: TypezSint32}, isInt: true}, - {field: &Field{Typez: TypezSint64}, isInt: true}, - {field: &Field{Typez: TypezSfixed32}, isInt: true}, - {field: &Field{Typez: TypezSfixed64}, isInt: true}, - {field: &Field{Typez: TypezUint32}, isUInt: true}, - {field: &Field{Typez: TypezUint64}, isUInt: true}, - {field: &Field{Typez: TypezFixed32}, isUInt: true}, - {field: &Field{Typez: TypezFixed64}, isUInt: true}, - {field: &Field{Typez: TypezFloat}, isFloat: true}, - {field: &Field{Typez: TypezDouble}, isFloat: true}, - {field: &Field{Typez: TypezEnum}, isEnum: true}, - {field: &Field{Typez: TypezMessage}, isObject: true}, - } - for _, tc := range testCases { - if tc.field.IsString() != tc.isString { - t.Errorf("IsString() for %v should be %v", tc.field.Typez, tc.isString) - } - if tc.field.IsBytes() != tc.isBytes { - t.Errorf("IsBytes() for %v should be %v", tc.field.Typez, tc.isBytes) - } - if tc.field.IsBool() != tc.isBool { - t.Errorf("IsBool() for %v should be %v", tc.field.Typez, tc.isBool) - } - if tc.field.IsLikeInt() != tc.isInt { - t.Errorf("IsLikeInt() for %v should be %v", tc.field.Typez, tc.isInt) - } - if tc.field.IsLikeUInt() != tc.isUInt { - t.Errorf("IsLikeUInt() for %v should be %v", tc.field.Typez, tc.isUInt) - } - if tc.field.IsLikeFloat() != tc.isFloat { - t.Errorf("IsLikeFloat() for %v should be %v", tc.field.Typez, tc.isFloat) - } - if tc.field.IsEnum() != tc.isEnum { - t.Errorf("IsEnum() for %v should be %v", tc.field.Typez, tc.isEnum) - } - if tc.field.IsObject() != tc.isObject { - t.Errorf("IsObject() for %v should be %v", tc.field.Typez, tc.isObject) - } - } -} - -func TestFlatPath(t *testing.T) { - for _, test := range []struct { - Input *PathTemplate - Want string - }{ - { - Input: (&PathTemplate{}), - Want: "", - }, - { - Input: (&PathTemplate{}). - WithLiteral("projects"). - WithVariableNamed("project"). - WithLiteral("zones"). - WithVariableNamed("zone"), - Want: "projects/{project}/zones/{zone}", - }, - { - Input: (&PathTemplate{}). - WithLiteral("projects"). - WithVariableNamed("project"). - WithLiteral("global"). - WithLiteral("location"), - Want: "projects/{project}/global/location", - }, - { - Input: (&PathTemplate{}). - WithLiteral("projects"). - WithVariable(NewPathVariable("a", "b", "c").WithMatchRecursive()), - Want: "projects/{a.b.c}", - }, - } { - got := test.Input.FlatPath() - if got != test.Want { - t.Errorf("mismatch want=%q, got=%q", test.Want, got) - } - } -} - -func TestField_IsResourceReference(t *testing.T) { - for _, test := range []struct { - name string - field *Field - want bool - }{ - { - name: "nil ResourceReference", - field: &Field{}, - want: false, - }, - { - name: "non-nil ResourceReference", - field: &Field{ResourceReference: &ResourceReference{}}, - want: true, - }, - } { - t.Run(test.name, func(t *testing.T) { - got := test.field.IsResourceReference() - if got != test.want { - t.Errorf("IsResourceReference() got = %v, want %v", got, test.want) - } - }) - } -} - -func TestFindBodyField(t *testing.T) { - messageType := &Message{ID: "message_id"} - bodyField := &Field{Name: "body_field"} - typeMatchField := &Field{Typez: TypezMessage, TypezID: messageType.ID, Name: "type_match"} - otherField := &Field{Name: "other"} - - testCases := []struct { - name string - message *Message - pathInfo *PathInfo - targetType string - singular string - want *Field - }{ - { - name: "match by path info", - message: &Message{Fields: []*Field{bodyField, otherField}}, - pathInfo: &PathInfo{BodyFieldPath: "body_field"}, - want: bodyField, - }, - { - name: "match by type and name", - message: &Message{Fields: []*Field{typeMatchField, otherField}}, - targetType: messageType.ID, - singular: "type_match", - want: typeMatchField, - }, - { - name: "match by type but wrong name", - message: &Message{Fields: []*Field{typeMatchField, otherField}}, - targetType: messageType.ID, - singular: "wrong_name", - want: nil, - }, - { - name: "match by name but wrong type", - message: &Message{Fields: []*Field{typeMatchField, otherField}}, - targetType: "different_message_id", - singular: "type_match", - want: nil, - }, - { - name: "path info overrides type match", - message: &Message{Fields: []*Field{typeMatchField, bodyField}}, - pathInfo: &PathInfo{BodyFieldPath: "body_field"}, - targetType: messageType.ID, - singular: "type_match", - want: bodyField, - }, - { - name: "no match", - message: &Message{Fields: []*Field{otherField}}, - want: nil, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got := findBodyField(tc.message, tc.pathInfo, tc.targetType, tc.singular) - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestFindResourceIDField(t *testing.T) { - idField := &Field{Name: "book_id", Typez: TypezString} - otherField := &Field{Name: "other"} - - testCases := []struct { - name string - message *Message - singular string - want *Field - }{ - { - name: "found id field", - message: &Message{Fields: []*Field{idField, otherField}}, - singular: "book", - want: idField, - }, - { - name: "not found", - message: &Message{Fields: []*Field{otherField}}, - singular: "book", - want: nil, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got := findResourceIDField(tc.message, tc.singular) - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestFindQuickstartMethod(t *testing.T) { - fooMethod := &Method{Name: "FooMethod"} - listPolicies := &Method{Name: "ListAccessPolicies", IsAIPStandardList: true, OutputType: &Message{Resource: &Resource{Singular: "accesspolicy"}}} - getPolicy := &Method{Name: "GetAccessPolicy", IsAIPStandardGet: true} - createPolicy := &Method{Name: "CreateAccessPolicy", IsAIPStandardCreate: true} - deletePolicy := &Method{Name: "DeleteAccessPolicy", IsAIPStandardDelete: true} - updatePolicy := &Method{Name: "UpdateAccessPolicy", IsAIPStandardUpdate: true} - listOther := &Method{Name: "ListOtherThings", IsAIPStandardList: true, OutputType: &Message{Resource: &Resource{Singular: "otherthing"}}} - - testCases := []struct { - name string - service *Service - want *Method - }{ - { - name: "empty service", - service: &Service{Name: "AccessPolicyService", Methods: []*Method{}}, - want: nil, - }, - { - name: "fallback to simple method when no standard methods exist", - service: &Service{Name: "AccessPolicyService", Methods: []*Method{fooMethod}}, - want: fooMethod, - }, - { - name: "prefer non-deprecated simple method", - service: &Service{Name: "AccessPolicyService", Methods: []*Method{{Name: "DeprecatedList", Deprecated: true}, fooMethod}}, - want: fooMethod, - }, - { - name: "only get method", - service: &Service{Name: "AccessPolicyService", Methods: []*Method{getPolicy}}, - want: getPolicy, - }, - { - name: "prioritizes list over get", - service: &Service{Name: "AccessPolicyService", Methods: []*Method{getPolicy, listOther}}, - want: listOther, - }, - { - name: "prioritizes create over delete", - service: &Service{Name: "AccessPolicyService", Methods: []*Method{deletePolicy, createPolicy}}, - want: createPolicy, - }, - { - name: "prioritizes delete over update", - service: &Service{Name: "AccessPolicyService", Methods: []*Method{updatePolicy, deletePolicy}}, - want: deletePolicy, - }, - { - name: "tie-breaking on name matching (ListAccessPolicies vs ListOtherThings for AccessPolicyService)", - service: &Service{Name: "AccessPolicyService", Methods: []*Method{listOther, listPolicies}}, - want: listPolicies, - }, - { - name: "tie-breaking fallback to resource singular/plural", - service: &Service{Name: "AccessPolicyService", Methods: []*Method{listOther, {Name: "ListPolicies", IsAIPStandardList: true, OutputType: &Message{Resource: &Resource{Singular: "accesspolicy"}}}}}, - want: &Method{Name: "ListPolicies", IsAIPStandardList: true, OutputType: &Message{Resource: &Resource{Singular: "accesspolicy"}}}, // matches singular 'accesspolicy' - }, - { - name: "fallback to server streaming method when no simple methods exist", - service: &Service{Name: "EchoService", Methods: []*Method{{Name: "Expand", ServerSideStreaming: true}}}, - want: &Method{Name: "Expand", ServerSideStreaming: true}, - }, - { - name: "fallback to bidi streaming method when no unary or server streaming methods exist", - service: &Service{Name: "EchoService", Methods: []*Method{{Name: "Chat", ClientSideStreaming: true, ServerSideStreaming: true}}}, - want: &Method{Name: "Chat", ClientSideStreaming: true, ServerSideStreaming: true}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got := findQuickstartMethod(tc.service) - if diff := cmp.Diff(tc.want, got, cmpopts.IgnoreFields(Method{}, "Service", "Model")); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestFindQuickstartService(t *testing.T) { - fooMethod := &Method{Name: "FooMethod"} - serviceA := &Service{Name: "ServiceA", Methods: []*Method{fooMethod}} - serviceB := &Service{Name: "SecretManagerService", Methods: []*Method{fooMethod}} - deprecatedService := &Service{Name: "SecretManagerService", Deprecated: true, Methods: []*Method{fooMethod}} - - testCases := []struct { - name string - api *API - want *Service - }{ - { - name: "no services", - api: &API{Name: "secretmanager", Services: nil}, - want: nil, - }, - { - name: "one service", - api: &API{Name: "secretmanager", Services: []*Service{serviceA}}, - want: serviceA, - }, - { - name: "match service name to api name", - api: &API{Name: "secretmanager", Services: []*Service{serviceA, serviceB}}, - want: serviceB, - }, - { - name: "no match defaults to first", - api: &API{Name: "otherapi", Services: []*Service{serviceA, serviceB}}, - want: serviceA, - }, - { - name: "prefer non-deprecated service", - api: &API{Name: "secretmanager", Services: []*Service{deprecatedService, serviceA}}, - want: serviceA, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got := findQuickstartService(tc.api) - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) - } -}