// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package host import ( "context" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary-plugin-aws/internal/credential" cred "github.com/hashicorp/boundary-plugin-aws/internal/credential" "github.com/hashicorp/boundary/sdk/pbs/plugin" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/structpb" ) func TestGetCatalogAttributes(t *testing.T) { cases := []struct { name string in *structpb.Struct expected *CatalogAttributes expectedErrContains string }{ { name: "missing region", in: &structpb.Struct{ Fields: make(map[string]*structpb.Value), }, expectedErrContains: "missing required value \"region\"", }, { name: "unknown fields", in: &structpb.Struct{ Fields: map[string]*structpb.Value{ "region": structpb.NewStringValue("us-west-2"), "foo": structpb.NewBoolValue(true), "bar": structpb.NewBoolValue(true), }, }, expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field", }, { name: "with dual stack", in: &structpb.Struct{ Fields: map[string]*structpb.Value{ "region": structpb.NewStringValue("us-west-2"), "dual_stack": structpb.NewBoolValue(true), }, }, expected: &CatalogAttributes{ CredentialAttributes: &credential.CredentialAttributes{ Region: "us-west-2", DisableCredentialRotation: false, }, DualStack: true, }, }, { name: "withInstanceAddressesOnly", in: &structpb.Struct{ Fields: map[string]*structpb.Value{ "region": structpb.NewStringValue("us-west-2"), "instance_addresses_only": structpb.NewBoolValue(true), }, }, expected: &CatalogAttributes{ CredentialAttributes: &credential.CredentialAttributes{ Region: "us-west-2", DisableCredentialRotation: false, }, InstanceAddressesOnly: true, }, }, { name: "default", in: &structpb.Struct{ Fields: map[string]*structpb.Value{ "region": structpb.NewStringValue("us-west-2"), }, }, expected: &CatalogAttributes{ CredentialAttributes: &credential.CredentialAttributes{ Region: "us-west-2", DisableCredentialRotation: false, }, }, }, { name: "credential attributes", in: &structpb.Struct{ Fields: map[string]*structpb.Value{ "region": structpb.NewStringValue("us-west-2"), "disable_credential_rotation": structpb.NewBoolValue(true), "role_arn": structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"), "role_external_id": structpb.NewStringValue("1234567890"), "role_session_name": structpb.NewStringValue("test-session"), "role_tags": structpb.NewStructValue(&structpb.Struct{ Fields: map[string]*structpb.Value{ "foo": structpb.NewStringValue("bar"), }, }), }, }, expected: &CatalogAttributes{ CredentialAttributes: &cred.CredentialAttributes{ Region: "us-west-2", DisableCredentialRotation: true, RoleArn: "arn:aws:iam::123456789012:role/S3Access", RoleExternalId: "1234567890", RoleSessionName: "test-session", RoleTags: map[string]string{ "foo": "bar", }, }, }, }, { name: "target_region without target_role_arn", in: &structpb.Struct{ Fields: map[string]*structpb.Value{ "region": structpb.NewStringValue("us-west-2"), "target_region": structpb.NewStringValue("eu-west-1"), }, }, expectedErrContains: "attributes.target_role_arn: missing required value \"target_role_arn\"", }, { name: "target role field without target_role_arn", in: &structpb.Struct{ Fields: map[string]*structpb.Value{ "region": structpb.NewStringValue("us-west-2"), "target_role_external_id": structpb.NewStringValue("target-ext"), }, }, expectedErrContains: "attributes.target_role_arn: missing required value \"target_role_arn\"", }, { name: "other target role fields without target_role_arn", in: &structpb.Struct{ Fields: map[string]*structpb.Value{ "region": structpb.NewStringValue("us-west-2"), "target_role_session_name": structpb.NewStringValue("target-session"), "target_role_tags": structpb.NewStructValue(&structpb.Struct{ Fields: map[string]*structpb.Value{ "env": structpb.NewStringValue("target"), }, }), }, }, expectedErrContains: "attributes.target_role_arn: missing required value \"target_role_arn\"", }, { name: "empty target_role_arn with target_region", in: &structpb.Struct{ Fields: map[string]*structpb.Value{ "region": structpb.NewStringValue("us-west-2"), "target_role_arn": structpb.NewStringValue(""), "target_region": structpb.NewStringValue("eu-west-1"), }, }, expectedErrContains: "attributes.target_role_arn: value \"target_role_arn\" cannot be empty", }, { name: "target_role_arn without target_region uses region; principal sts fields are not copied", in: &structpb.Struct{ Fields: map[string]*structpb.Value{ "region": structpb.NewStringValue("us-west-2"), "disable_credential_rotation": structpb.NewBoolValue(true), "role_arn": structpb.NewStringValue("arn:aws:iam::111111111111:role/Principal"), "role_external_id": structpb.NewStringValue("principal-ext"), "role_session_name": structpb.NewStringValue("principal-sess"), "role_tags": structpb.NewStructValue(&structpb.Struct{ Fields: map[string]*structpb.Value{ "env": structpb.NewStringValue("principal"), }, }), "target_role_arn": structpb.NewStringValue("arn:aws:iam::222222222222:role/Target"), }, }, expected: &CatalogAttributes{ CredentialAttributes: &cred.CredentialAttributes{ Region: "us-west-2", DisableCredentialRotation: true, RoleArn: "arn:aws:iam::111111111111:role/Principal", RoleExternalId: "principal-ext", RoleSessionName: "principal-sess", RoleTags: map[string]string{ "env": "principal", }, }, TargetAccount: &cred.CredentialAttributes{ Region: "us-west-2", DisableCredentialRotation: true, RoleArn: "arn:aws:iam::222222222222:role/Target", }, }, }, { name: "target attributes are independent of principal", in: &structpb.Struct{ Fields: map[string]*structpb.Value{ "region": structpb.NewStringValue("us-west-2"), "disable_credential_rotation": structpb.NewBoolValue(true), "role_arn": structpb.NewStringValue("arn:aws:iam::111111111111:role/Principal"), "role_external_id": structpb.NewStringValue("principal-ext"), "role_session_name": structpb.NewStringValue("principal-sess"), "role_tags": structpb.NewStructValue(&structpb.Struct{ Fields: map[string]*structpb.Value{ "env": structpb.NewStringValue("principal"), }, }), "target_role_arn": structpb.NewStringValue("arn:aws:iam::222222222222:role/Target"), "target_region": structpb.NewStringValue("eu-west-1"), "target_role_external_id": structpb.NewStringValue("target-ext"), "target_role_session_name": structpb.NewStringValue("target-sess"), "target_role_tags": structpb.NewStructValue(&structpb.Struct{ Fields: map[string]*structpb.Value{ "env": structpb.NewStringValue("target"), }, }), }, }, expected: &CatalogAttributes{ CredentialAttributes: &cred.CredentialAttributes{ Region: "us-west-2", DisableCredentialRotation: true, RoleArn: "arn:aws:iam::111111111111:role/Principal", RoleExternalId: "principal-ext", RoleSessionName: "principal-sess", RoleTags: map[string]string{ "env": "principal", }, }, TargetAccount: &cred.CredentialAttributes{ Region: "eu-west-1", DisableCredentialRotation: true, RoleArn: "arn:aws:iam::222222222222:role/Target", RoleExternalId: "target-ext", RoleSessionName: "target-sess", RoleTags: map[string]string{ "env": "target", }, }, }, }, { name: "catalog dual_stack and instance_addresses_only with target", in: &structpb.Struct{ Fields: map[string]*structpb.Value{ "region": structpb.NewStringValue("us-west-2"), "dual_stack": structpb.NewBoolValue(true), "instance_addresses_only": structpb.NewBoolValue(true), "target_role_arn": structpb.NewStringValue("arn:aws:iam::222222222222:role/Target"), }, }, expected: &CatalogAttributes{ CredentialAttributes: &credential.CredentialAttributes{ Region: "us-west-2", DisableCredentialRotation: false, }, TargetAccount: &cred.CredentialAttributes{ Region: "us-west-2", DisableCredentialRotation: false, RoleArn: "arn:aws:iam::222222222222:role/Target", }, DualStack: true, InstanceAddressesOnly: true, }, }, { name: "unknown field with target_role_arn", in: &structpb.Struct{ Fields: map[string]*structpb.Value{ "region": structpb.NewStringValue("us-west-2"), "target_role_arn": structpb.NewStringValue("arn:aws:iam::222222222222:role/Target"), "foo": structpb.NewBoolValue(true), }, }, expectedErrContains: "attributes.foo: unrecognized field", }, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { require := require.New(t) actual, err := getCatalogAttributes(tc.in) if tc.expectedErrContains != "" { require.Error(err) require.Contains(err.Error(), tc.expectedErrContains) require.Equal(status.Code(err), codes.InvalidArgument) return } require.NoError(err) require.Equal(tc.expected, actual) }) } } func TestGetSetAttributes(t *testing.T) { cases := []struct { name string in map[string]any normalized map[string]any expected *SetAttributes expectedErrContains string }{ { name: "missing", in: map[string]any{}, expected: &SetAttributes{}, }, { name: "non-string-slice value", in: map[string]any{ ConstDescribeInstancesFilters: "zip=foo,bar", }, expected: &SetAttributes{ Filters: []string{"zip=foo,bar"}, }, }, { name: "bad filter element value", in: map[string]any{ ConstDescribeInstancesFilters: []any{1}, }, expectedErrContains: "expected type 'string', got unconvertible type 'float64'", }, { name: "unknown fields", in: map[string]any{ "foo": true, "bar": true, }, expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field", }, { name: "good", in: map[string]any{ ConstDescribeInstancesFilters: []any{ "foo=bar", "zip=zap", }, }, expected: &SetAttributes{ Filters: []string{"foo=bar", "zip=zap"}, }, }, { name: "good with filter transform", in: map[string]any{ ConstDescribeInstancesFilters: "foo=bar", }, normalized: map[string]any{ ConstDescribeInstancesFilters: []any{"foo=bar"}, }, expected: &SetAttributes{ Filters: []string{"foo=bar"}, }, }, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { require := require.New(t) input, err := structpb.NewStruct(tc.in) require.NoError(err) p := new(HostPlugin) normalizedOut, err := p.NormalizeSetData(context.Background(), &plugin.NormalizeSetDataRequest{Attributes: input}) require.NoError(err) if tc.normalized != nil { normalized, err := structpb.NewStruct(tc.normalized) require.NoError(err) require.Empty(cmp.Diff(normalized, normalizedOut.Attributes, protocmp.Transform())) } input = normalizedOut.Attributes actual, err := getSetAttributes(input) if tc.expectedErrContains != "" { require.Error(err) require.Contains(err.Error(), tc.expectedErrContains) require.Equal(status.Code(err), codes.InvalidArgument) return } require.NoError(err) require.Equal(tc.expected, actual) }) } }