More unit testing and addins of mocks from awsutil

This commit is contained in:
Chris Marchesi committed 2021-10-05 16:36:53 -07:00
1 parent f7c5709046
commit a7401ef2d5
5 files changed
+495 -33

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
package plugin
import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/iam/iamiface"
"github.com/hashicorp/go-secure-stdlib/awsutil"
)
type testMockIAMState struct {
DeleteAccessKeyCalled bool
}
func (s *testMockIAMState) Reset() {
s.DeleteAccessKeyCalled = false
}
type testMockIAM struct {
iamiface.IAMAPI
State *testMockIAMState
}
func newTestMockIAM(state *testMockIAMState, opts ...awsutil.MockIAMOption) awsutil.IAMAPIFunc {
return func(sess *session.Session) (iamiface.IAMAPI, error) {
m := &testMockIAM{
State: state,
}
f := awsutil.NewMockIAM(opts...)
var err error
m.IAMAPI, err = f(sess)
return m, err
}
}
func (m *testMockIAM) DeleteAccessKey(input *iam.DeleteAccessKeyInput) (*iam.DeleteAccessKeyOutput, error) {
m.State.DeleteAccessKeyCalled = true
return m.IAMAPI.DeleteAccessKey(input)
}
+24 -16
View File
@@ -19,6 +19,9 @@ type awsCatalogPersistedState struct {
AccessKeyId string
SecretAccessKey string
CredsLastRotatedTime time.Time
// testOpts are options that should be used for testing only
testOpts []awsutil.Option
}
func awsCatalogPersistedStateFromProto(in *pb.HostCatalogPersisted) (*awsCatalogPersistedState, error) {
@@ -63,15 +66,15 @@ func (s *awsCatalogPersistedState) ToProto() (*pb.HostCatalogPersisted, error) {
}
func (s *awsCatalogPersistedState) ValidateCreds() error {
c, err := awsutil.NewCredentialsConfig(
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
awsutil.WithAccessKey(s.AccessKeyId),
awsutil.WithSecretKey(s.SecretAccessKey),
)
}, s.testOpts...)...)
if err != nil {
return fmt.Errorf("error loading credentials: %w", err)
}
if _, err := c.GetCallerIdentity(); err != nil {
if _, err := c.GetCallerIdentity(s.testOpts...); err != nil {
return fmt.Errorf("error validating credentials: %w", err)
}
@@ -79,15 +82,17 @@ func (s *awsCatalogPersistedState) ValidateCreds() error {
}
func (s *awsCatalogPersistedState) RotateCreds() error {
c, err := awsutil.NewCredentialsConfig(
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
awsutil.WithAccessKey(s.AccessKeyId),
awsutil.WithSecretKey(s.SecretAccessKey),
)
}, s.testOpts...)...)
if err != nil {
return fmt.Errorf("error loading credentials: %w", err)
}
if err := c.RotateKeys(awsutil.WithValidityCheckTimeout(rotationWaitTimeout)); err != nil {
if err := c.RotateKeys(append([]awsutil.Option{
awsutil.WithValidityCheckTimeout(rotationWaitTimeout),
}, s.testOpts...)...); err != nil {
return fmt.Errorf("error rotating credentials: %w", err)
}
@@ -134,22 +139,25 @@ func (s *awsCatalogPersistedState) ReplaceCreds(accessKeyId, secretAccessKey str
// ID, secret access key, and rotation time fields are zeroed out in
// the state just to ensure that they cannot be re-used after.
func (s *awsCatalogPersistedState) DeleteCreds() error {
c, err := awsutil.NewCredentialsConfig(
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
awsutil.WithAccessKey(s.AccessKeyId),
awsutil.WithSecretKey(s.SecretAccessKey),
)
}, s.testOpts...)...)
if err != nil {
return fmt.Errorf("error loading credentials: %w", err)
}
if err := c.DeleteAccessKey(s.AccessKeyId); err != nil {
if err := c.DeleteAccessKey(s.AccessKeyId, s.testOpts...); err != nil {
// Determine if the deletion error was due to a missing
// resource. If it was, just pass it.
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == iam.ErrCodeNoSuchEntityException {
s.AccessKeyId = ""
s.SecretAccessKey = ""
s.CredsLastRotatedTime = time.Time{}
return nil
var awsErr awserr.Error
if errors.As(err, &awsErr) {
if awsErr.Code() == iam.ErrCodeNoSuchEntityException {
s.AccessKeyId = ""
s.SecretAccessKey = ""
s.CredsLastRotatedTime = time.Time{}
return nil
}
}
// Otherwise treat it as an actual error.
@@ -165,10 +173,10 @@ func (s *awsCatalogPersistedState) DeleteCreds() error {
// GetSession returns a configured AWS session for the credentials in
// the state.
func (s *awsCatalogPersistedState) GetSession() (*session.Session, error) {
c, err := awsutil.NewCredentialsConfig(
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
awsutil.WithAccessKey(s.AccessKeyId),
awsutil.WithSecretKey(s.SecretAccessKey),
)
}, s.testOpts...)...)
if err != nil {
return nil, err
}
+411
View File
@@ -1,13 +1,24 @@
package plugin
import (
"errors"
"fmt"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/stretchr/testify/require"
)
const testOptionErr = "test option error"
const testGetCallerIdentityErr = "test error for GetCallerIdentity"
const testGetUserErr = "test error for GetUser"
const testDeleteAccessKeyErr = "test error for DeleteAccessKey"
func TestAwsCatalogPersistedStateFromProto(t *testing.T) {
cases := []struct {
name string
@@ -108,3 +119,403 @@ func TestAwsCatalogPersistedStateFromProto(t *testing.T) {
})
}
}
func TestAwsCatalogPersistedStateToProto(t *testing.T) {
cases := []struct {
name string
in *awsCatalogPersistedState
expected *pb.HostCatalogPersisted
expectedErr string // NOTE: Here for scaffolding but hard to test deep structpb err right now
}{
{
name: "good with non-zero timestamp",
in: &awsCatalogPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredsLastRotatedTime: func() time.Time {
t, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00")
if err != nil {
panic(err)
}
return t
}(),
},
expected: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
{
name: "good with zero timestamp",
in: &awsCatalogPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredsLastRotatedTime: time.Time{},
},
expected: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
}),
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := tc.in.ToProto()
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
}
func TestAwsCatalogPersistedStateValidateCreds(t *testing.T) {
cases := []struct {
name string
in *awsCatalogPersistedState
expectedErr string
}{
{
name: "could not load credentials",
in: &awsCatalogPersistedState{
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
},
expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr),
},
{
name: "validation error",
in: &awsCatalogPersistedState{
testOpts: []awsutil.Option{awsutil.WithSTSAPIFunc(awsutil.NewMockSTS(awsutil.WithGetCallerIdentityError(errors.New(testGetCallerIdentityErr))))},
},
expectedErr: fmt.Sprintf("error validating credentials: %s", testGetCallerIdentityErr),
},
{
name: "good",
in: &awsCatalogPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
testOpts: []awsutil.Option{awsutil.WithSTSAPIFunc(awsutil.NewMockSTS())},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
err := tc.in.ValidateCreds()
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
})
}
}
func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) {
cases := []struct {
name string
in *awsCatalogPersistedState
expectedAccessKeyId string
expectedSecretAccessKey string
expectedNonZeroRotatedTime bool
expectedErr string
}{
{
name: "could not load credentials",
in: &awsCatalogPersistedState{
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
},
expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr),
},
{
name: "rotation error",
in: &awsCatalogPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserError(errors.New(testGetUserErr)),
),
),
},
},
expectedErr: fmt.Sprintf("error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr),
},
{
name: "good",
in: &awsCatalogPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
testOpts: []awsutil.Option{
awsutil.WithSTSAPIFunc(
awsutil.NewMockSTS(),
),
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserOutput(
&iam.GetUserOutput{
User: &iam.User{
Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"),
UserId: aws.String("AIDAJQABLZS4A3QDU576Q"),
UserName: aws.String("JohnDoe"),
},
},
),
awsutil.WithCreateAccessKeyOutput(
&iam.CreateAccessKeyOutput{
AccessKey: &iam.AccessKey{
AccessKeyId: aws.String("one"),
SecretAccessKey: aws.String("two"),
UserName: aws.String("JohnDoe"),
},
},
),
),
),
},
},
expectedAccessKeyId: "one",
expectedSecretAccessKey: "two",
expectedNonZeroRotatedTime: true,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
err := tc.in.RotateCreds()
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expectedAccessKeyId, tc.in.AccessKeyId)
require.Equal(tc.expectedSecretAccessKey, tc.in.SecretAccessKey)
require.Equal(tc.expectedNonZeroRotatedTime, !tc.in.CredsLastRotatedTime.IsZero())
})
}
}
func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) {
// NOTE: Not safe to run this test in parallel
state := new(testMockIAMState)
cases := []struct {
name string
in *awsCatalogPersistedState
accessKeyId string
secretAccessKey string
expectedAccessKeyId string
expectedSecretAccessKey string
expectedDeleteCalled bool
expectedErr string
}{
{
name: "missing access key id",
in: &awsCatalogPersistedState{},
expectedErr: "access key id cannot be empty",
},
{
name: "missing access key id",
in: &awsCatalogPersistedState{},
accessKeyId: "one",
expectedErr: "secret access key cannot be empty",
},
{
name: "identical access key id",
in: &awsCatalogPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
},
accessKeyId: "foobar",
secretAccessKey: "bazqux",
expectedErr: "attempting to replace access key with the same one",
},
{
name: "deletion error",
in: &awsCatalogPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredsLastRotatedTime: time.Now(),
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithDeleteAccessKeyError(errors.New(testDeleteAccessKeyErr)),
),
),
},
},
accessKeyId: "one",
secretAccessKey: "two",
expectedErr: fmt.Sprintf("error deleting old access key: error deleting old access key: %s", testDeleteAccessKeyErr),
},
{
name: "good with delete of old rotated",
in: &awsCatalogPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredsLastRotatedTime: time.Now(),
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
newTestMockIAM(state),
),
},
},
accessKeyId: "one",
secretAccessKey: "two",
expectedAccessKeyId: "one",
expectedSecretAccessKey: "two",
expectedDeleteCalled: true,
},
{
name: "good without delete of old rotated",
in: &awsCatalogPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
newTestMockIAM(state),
),
},
},
accessKeyId: "one",
secretAccessKey: "two",
expectedAccessKeyId: "one",
expectedSecretAccessKey: "two",
expectedDeleteCalled: false,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
state.Reset()
err := tc.in.ReplaceCreds(tc.accessKeyId, tc.secretAccessKey)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expectedAccessKeyId, tc.in.AccessKeyId)
require.Equal(tc.expectedSecretAccessKey, tc.in.SecretAccessKey)
require.Zero(tc.in.CredsLastRotatedTime)
require.Equal(tc.expectedDeleteCalled, state.DeleteAccessKeyCalled)
})
}
}
func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) {
cases := []struct {
name string
in *awsCatalogPersistedState
expectedErr string
}{
{
name: "could not load credentials",
in: &awsCatalogPersistedState{
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
},
expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr),
},
{
name: "deletion error",
in: &awsCatalogPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithDeleteAccessKeyError(errors.New(testDeleteAccessKeyErr)),
),
),
},
},
expectedErr: fmt.Sprintf("error deleting old access key: error deleting old access key: %s", testDeleteAccessKeyErr),
},
{
name: "deletion error, but OK because key was just gone",
in: &awsCatalogPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithDeleteAccessKeyError(
awserr.New(iam.ErrCodeNoSuchEntityException, "", nil),
),
),
),
},
},
},
{
name: "good",
in: &awsCatalogPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
testOpts: []awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(),
),
},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
err := tc.in.DeleteCreds()
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Empty(tc.in.AccessKeyId)
require.Empty(tc.in.SecretAccessKey)
require.Zero(tc.in.CredsLastRotatedTime)
})
}
}
func TestAwsCatalogPersistedStateGetSessionErr(t *testing.T) {
require := require.New(t)
state := &awsCatalogPersistedState{
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
}
_, err := state.GetSession()
require.EqualError(err, fmt.Sprintf("error reading options in NewCredentialsConfig: %s", testOptionErr))
}
func TestAwsCatalogPersistedStateEC2ClientErr(t *testing.T) {
require := require.New(t)
state := &awsCatalogPersistedState{
testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))},
}
_, err := state.EC2Client("someregion")
require.EqualError(err, fmt.Sprintf("error getting AWS session: error reading options in NewCredentialsConfig: %s", testOptionErr))
}