From ff4607d2afa1a491e2868fb15f0eca1fb515e24f Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Tue, 9 Jun 2026 00:20:21 +0100 Subject: [PATCH] Clarify AWS state test doubles --- test/unit/src/state/S3StateStorage.test.js | 40 ++-- .../get-s3-state-storage-from-config.test.js | 142 ++++++----- .../state/utils/get-state-bucket-name.test.js | 222 +++++++++--------- .../utils/get-state-bucket-region.test.js | 164 ++++++------- 4 files changed, 287 insertions(+), 281 deletions(-) diff --git a/test/unit/src/state/S3StateStorage.test.js b/test/unit/src/state/S3StateStorage.test.js index 933f213..30012cc 100644 --- a/test/unit/src/state/S3StateStorage.test.js +++ b/test/unit/src/state/S3StateStorage.test.js @@ -147,30 +147,32 @@ describe('test/unit/src/state/S3StateStorage.test.js', () => { }); }); - it('passes full client config into the S3 client constructor', () => { - const S3 = sinon.stub().returns({}); - const S3StateStorageWithStubbedClient = proxyquire - .noCallThru() - .load('../../../../src/state/S3StateStorage', { - '@aws-sdk/client-s3': { S3 }, + describe('S3 client config', () => { + it('passes full client config into the S3 client constructor', () => { + const S3 = sinon.stub().returns({}); + const S3StateStorageWithStubbedClient = proxyquire + .noCallThru() + .load('../../../../src/state/S3StateStorage', { + '@aws-sdk/client-s3': { S3 }, + }); + + const stateStorage = new S3StateStorageWithStubbedClient({ + bucketName, + stateKey, + region: 'eu-central-1', + clientConfig: { + region: 'eu-central-1', + credentials: 'creds', + retryMode: 'standard', + }, }); - const stateStorage = new S3StateStorageWithStubbedClient({ - bucketName, - stateKey, - region: 'eu-central-1', - clientConfig: { + expect(stateStorage).to.be.instanceOf(S3StateStorageWithStubbedClient); + expect(S3).to.have.been.calledOnceWithExactly({ region: 'eu-central-1', credentials: 'creds', retryMode: 'standard', - }, - }); - - expect(stateStorage).to.be.instanceOf(S3StateStorageWithStubbedClient); - expect(S3).to.have.been.calledOnceWithExactly({ - region: 'eu-central-1', - credentials: 'creds', - retryMode: 'standard', + }); }); }); diff --git a/test/unit/src/state/get-s3-state-storage-from-config.test.js b/test/unit/src/state/get-s3-state-storage-from-config.test.js index ad7379c..48b124c 100644 --- a/test/unit/src/state/get-s3-state-storage-from-config.test.js +++ b/test/unit/src/state/get-s3-state-storage-from-config.test.js @@ -6,33 +6,51 @@ const sinon = require('sinon'); const expect = chai.expect; -describe('test/unit/src/state/get-s3-state-storage-from-config.test.js', () => { - afterEach(() => { - sinon.restore(); - }); +const loadGetS3StateStorageFromConfig = ({ + getStateBucketName = sinon.stub().resolves('bucket'), + getConfiguredStateBucketName = sinon.stub().returns(null), + getStateBucketRegion = sinon.stub().resolves('us-east-1'), + getAwsClientConfig = sinon.stub().returns({ region: 'us-east-1' }), +} = {}) => { + class S3StateStorage { + constructor(config) { + this.config = config; + } + } + + const getS3StateStorageFromConfig = proxyquire + .noCallThru() + .load('../../../../src/state/get-s3-state-storage-from-config', { + '../utils/aws': { getAwsClientConfig }, + './S3StateStorage': S3StateStorage, + './utils/get-configured-state-bucket-name': getConfiguredStateBucketName, + './utils/get-state-bucket-name': getStateBucketName, + './utils/get-state-bucket-region': getStateBucketRegion, + }); + + return { + getS3StateStorageFromConfig, + getStateBucketName, + getConfiguredStateBucketName, + getStateBucketRegion, + getAwsClientConfig, + }; +}; +describe('test/unit/src/state/get-s3-state-storage-from-config.test.js', () => { it('uses us-east-1 for compose-managed buckets', async () => { - const getStateBucketName = sinon.stub().resolves('managed-bucket'); - const getConfiguredStateBucketName = sinon.stub().returns(null); - const getStateBucketRegion = sinon.stub(); const awsClientConfig = { region: 'us-east-1', credentials: 'creds', retryMode: 'standard' }; - const getAwsClientConfig = sinon.stub().returns(awsClientConfig); - - class S3StateStorage { - constructor(config) { - this.config = config; - } - } - - const getS3StateStorageFromConfig = proxyquire - .noCallThru() - .load('../../../../src/state/get-s3-state-storage-from-config', { - '../utils/aws': { getAwsClientConfig }, - './S3StateStorage': S3StateStorage, - './utils/get-configured-state-bucket-name': getConfiguredStateBucketName, - './utils/get-state-bucket-name': getStateBucketName, - './utils/get-state-bucket-region': getStateBucketRegion, - }); + const { + getS3StateStorageFromConfig, + getStateBucketName, + getConfiguredStateBucketName, + getStateBucketRegion, + getAwsClientConfig, + } = loadGetS3StateStorageFromConfig({ + getStateBucketName: sinon.stub().resolves('managed-bucket'), + getStateBucketRegion: sinon.stub(), + getAwsClientConfig: sinon.stub().returns(awsClientConfig), + }); const stateStorage = await getS3StateStorageFromConfig( { backend: 's3', prefix: 'custom', profile: 'team' }, @@ -66,22 +84,12 @@ describe('test/unit/src/state/get-s3-state-storage-from-config.test.js', () => { const getStateBucketRegion = sinon.stub().resolves('eu-central-1'); const awsClientConfig = { region: 'eu-central-1', credentials: 'creds', retryMode: 'standard' }; const getAwsClientConfig = sinon.stub().returns(awsClientConfig); - - class S3StateStorage { - constructor(config) { - this.config = config; - } - } - - const getS3StateStorageFromConfig = proxyquire - .noCallThru() - .load('../../../../src/state/get-s3-state-storage-from-config', { - '../utils/aws': { getAwsClientConfig }, - './S3StateStorage': S3StateStorage, - './utils/get-configured-state-bucket-name': getConfiguredStateBucketName, - './utils/get-state-bucket-name': getStateBucketName, - './utils/get-state-bucket-region': getStateBucketRegion, - }); + const { getS3StateStorageFromConfig } = loadGetS3StateStorageFromConfig({ + getStateBucketName, + getConfiguredStateBucketName, + getStateBucketRegion, + getAwsClientConfig, + }); const stateStorage = await getS3StateStorageFromConfig(stateConfiguration, { stage: 'dev' }); @@ -114,22 +122,12 @@ describe('test/unit/src/state/get-s3-state-storage-from-config.test.js', () => { const getStateBucketRegion = sinon.stub().resolves('eu-central-1'); const awsClientConfig = { region: 'eu-central-1', credentials: 'creds', retryMode: 'standard' }; const getAwsClientConfig = sinon.stub().returns(awsClientConfig); - - class S3StateStorage { - constructor(config) { - this.config = config; - } - } - - const getS3StateStorageFromConfig = proxyquire - .noCallThru() - .load('../../../../src/state/get-s3-state-storage-from-config', { - '../utils/aws': { getAwsClientConfig }, - './S3StateStorage': S3StateStorage, - './utils/get-configured-state-bucket-name': getConfiguredStateBucketName, - './utils/get-state-bucket-name': getStateBucketName, - './utils/get-state-bucket-region': getStateBucketRegion, - }); + const { getS3StateStorageFromConfig } = loadGetS3StateStorageFromConfig({ + getStateBucketName, + getConfiguredStateBucketName, + getStateBucketRegion, + getAwsClientConfig, + }); const stateStorage = await getS3StateStorageFromConfig(stateConfiguration, { stage: 'dev' }); @@ -152,26 +150,18 @@ describe('test/unit/src/state/get-s3-state-storage-from-config.test.js', () => { }); it('rejects invalid stage before composing S3 state key', async () => { - const getStateBucketName = sinon.stub(); - const getConfiguredStateBucketName = sinon.stub(); - const getStateBucketRegion = sinon.stub(); - const getAwsClientConfig = sinon.stub(); - - class S3StateStorage { - constructor(config) { - this.config = config; - } - } - - const getS3StateStorageFromConfig = proxyquire - .noCallThru() - .load('../../../../src/state/get-s3-state-storage-from-config', { - '../utils/aws': { getAwsClientConfig }, - './S3StateStorage': S3StateStorage, - './utils/get-configured-state-bucket-name': getConfiguredStateBucketName, - './utils/get-state-bucket-name': getStateBucketName, - './utils/get-state-bucket-region': getStateBucketRegion, - }); + const { + getS3StateStorageFromConfig, + getStateBucketName, + getConfiguredStateBucketName, + getStateBucketRegion, + getAwsClientConfig, + } = loadGetS3StateStorageFromConfig({ + getStateBucketName: sinon.stub(), + getConfiguredStateBucketName: sinon.stub(), + getStateBucketRegion: sinon.stub(), + getAwsClientConfig: sinon.stub(), + }); await expect( getS3StateStorageFromConfig({ backend: 's3' }, { stage: 'foo/../../tmp/x' }) diff --git a/test/unit/src/state/utils/get-state-bucket-name.test.js b/test/unit/src/state/utils/get-state-bucket-name.test.js index d6a7015..cb9cc70 100644 --- a/test/unit/src/state/utils/get-state-bucket-name.test.js +++ b/test/unit/src/state/utils/get-state-bucket-name.test.js @@ -19,6 +19,7 @@ const expect = chai.expect; describe('test/unit/src/state/utils/get-state-bucket-name.test.js', () => { let cfMock; let context; + before(() => { cfMock = mockClient(CloudFormationClient); const contextConfig = { @@ -33,125 +34,134 @@ describe('test/unit/src/state/utils/get-state-bucket-name.test.js', () => { cfMock.reset(); }); - it('resolves external bucket name from config', async () => { - const configuration = { - backend: 's3', - existingBucket: 'existing', - }; - expect(await getStateBucketName(configuration, context)).to.equal('existing'); - }); - - it('supports externalBucket as a compatibility alias', async () => { - const configuration = { - backend: 's3', - externalBucket: 'external', - }; - - expect(await getStateBucketName(configuration, context)).to.equal('external'); - }); - - it('resolves already existing bucket name provisioned by compose', async () => { - const configuration = { backend: 's3' }; - cfMock - .on(DescribeStackResourceCommand) - .resolves({ StackResourceDetail: { PhysicalResourceId: 'fromcf' } }); + describe('configured bucket compatibility', () => { + it('resolves existing bucket name from config', async () => { + const configuration = { + backend: 's3', + existingBucket: 'existing', + }; + expect(await getStateBucketName(configuration, context)).to.equal('existing'); + }); - expect(await getStateBucketName(configuration, context)).to.equal('fromcf'); - }); + it('supports externalBucket as a compatibility alias', async () => { + const configuration = { + backend: 's3', + externalBucket: 'external', + }; - it('resolves bucket that had to be created', async () => { - const configuration = { backend: 's3' }; - const stackDoesNotExistError = new Error('Stack "test" does not exist'); - stackDoesNotExistError.Code = 'ValidationError'; - cfMock - .on(DescribeStackResourceCommand) - .rejectsOnce(stackDoesNotExistError) - .on(CreateStackCommand) - .resolves() - .on(DescribeStacksCommand) - .resolves({ Stacks: [{ StackStatus: 'CREATE_COMPLETE' }] }); - - expect( - (await getStateBucketName(configuration, context)).startsWith('serverless-compose-state-') - ).to.be.true; + expect(await getStateBucketName(configuration, context)).to.equal('external'); + }); }); - it('handles SDK v3 ValidationError names when bucket stack has to be created', async () => { - const configuration = { backend: 's3' }; - const stackDoesNotExistError = new Error('Stack "test" does not exist'); - stackDoesNotExistError.name = 'ValidationError'; - cfMock - .on(DescribeStackResourceCommand) - .rejectsOnce(stackDoesNotExistError) - .on(CreateStackCommand) - .resolves() - .on(DescribeStacksCommand) - .resolves({ Stacks: [{ StackStatus: 'CREATE_COMPLETE' }] }); - - expect( - (await getStateBucketName(configuration, context)).startsWith('serverless-compose-state-') - ).to.be.true; - }); + describe('CloudFormation command behavior', () => { + it('resolves already existing bucket name provisioned by compose', async () => { + const configuration = { backend: 's3' }; + cfMock + .on(DescribeStackResourceCommand) + .resolves({ StackResourceDetail: { PhysicalResourceId: 'fromcf' } }); - it('handles unexpected error when resolving bucket from s3', async () => { - const configuration = { backend: 's3' }; - const unknownError = new Error('unknown error'); - cfMock.on(DescribeStackResourceCommand).rejects(unknownError); + expect(await getStateBucketName(configuration, context)).to.equal('fromcf'); + }); - await expect( - getStateBucketName(configuration, context) - ).to.be.eventually.rejected.and.have.property('code', 'CANNOT_RETRIEVE_REMOTE_STATE_S3_BUCKET'); - }); + it('resolves bucket that had to be created', async () => { + const configuration = { backend: 's3' }; + const stackDoesNotExistError = new Error('Stack "test" does not exist'); + stackDoesNotExistError.Code = 'ValidationError'; + cfMock + .on(DescribeStackResourceCommand) + .rejectsOnce(stackDoesNotExistError) + .on(CreateStackCommand) + .resolves() + .on(DescribeStacksCommand) + .resolves({ Stacks: [{ StackStatus: 'CREATE_COMPLETE' }] }); + + expect( + (await getStateBucketName(configuration, context)).startsWith('serverless-compose-state-') + ).to.be.true; + }); - it('handles unexpected error when creating bucket from s3', async () => { - const configuration = { backend: 's3' }; - const stackDoesNotExistError = new Error('Stack "test" does not exist'); - stackDoesNotExistError.Code = 'ValidationError'; - cfMock - .on(DescribeStackResourceCommand) - .rejects(stackDoesNotExistError) - .on(CreateStackCommand) - .resolves() - .on(DescribeStacksCommand) - .resolves({ Stacks: [{ StackStatus: 'CREATE_FAILED' }] }); - - await expect( - getStateBucketName(configuration, context) - ).to.be.eventually.rejected.and.have.property('code', 'CANNOT_DEPLOY_S3_REMOTE_STATE_STACK'); - }); + it('handles SDK v3 ValidationError names when bucket stack has to be created', async () => { + const configuration = { backend: 's3' }; + const stackDoesNotExistError = new Error('Stack "test" does not exist'); + stackDoesNotExistError.name = 'ValidationError'; + cfMock + .on(DescribeStackResourceCommand) + .rejectsOnce(stackDoesNotExistError) + .on(CreateStackCommand) + .resolves() + .on(DescribeStacksCommand) + .resolves({ Stacks: [{ StackStatus: 'CREATE_COMPLETE' }] }); + + expect( + (await getStateBucketName(configuration, context)).startsWith('serverless-compose-state-') + ).to.be.true; + }); - it('uses profile-aware AWS config for CloudFormation access', async () => { - const describeStackResource = sinon.stub().resolves({ - StackResourceDetail: { PhysicalResourceId: 'fromcf' }, + it('handles unexpected error when resolving bucket from s3', async () => { + const configuration = { backend: 's3' }; + const unknownError = new Error('unknown error'); + cfMock.on(DescribeStackResourceCommand).rejects(unknownError); + + await expect( + getStateBucketName(configuration, context) + ).to.be.eventually.rejected.and.have.property( + 'code', + 'CANNOT_RETRIEVE_REMOTE_STATE_S3_BUCKET' + ); }); - const CloudFormation = sinon.stub().callsFake(() => ({ - describeStackResource, - })); - const getAwsClientConfig = sinon.stub().returns({ - region: 'us-east-1', - credentials: 'creds', - retryMode: 'standard', + + it('handles unexpected error when creating bucket from s3', async () => { + const configuration = { backend: 's3' }; + const stackDoesNotExistError = new Error('Stack "test" does not exist'); + stackDoesNotExistError.Code = 'ValidationError'; + cfMock + .on(DescribeStackResourceCommand) + .rejects(stackDoesNotExistError) + .on(CreateStackCommand) + .resolves() + .on(DescribeStacksCommand) + .resolves({ Stacks: [{ StackStatus: 'CREATE_FAILED' }] }); + + await expect( + getStateBucketName(configuration, context) + ).to.be.eventually.rejected.and.have.property('code', 'CANNOT_DEPLOY_S3_REMOTE_STATE_STACK'); }); + }); - const getStateBucketNameWithStubs = proxyquire - .noCallThru() - .load('../../../../../src/state/utils/get-state-bucket-name', { - '@aws-sdk/client-cloudformation': { CloudFormation }, - '../../utils/aws': { getAwsClientConfig }, + describe('CloudFormation client config', () => { + it('uses profile-aware AWS config for CloudFormation access', async () => { + const describeStackResource = sinon.stub().resolves({ + StackResourceDetail: { PhysicalResourceId: 'fromcf' }, + }); + const CloudFormation = sinon.stub().callsFake(() => ({ + describeStackResource, + })); + const getAwsClientConfig = sinon.stub().returns({ + region: 'us-east-1', + credentials: 'creds', + retryMode: 'standard', }); - expect(await getStateBucketNameWithStubs({ backend: 's3', profile: 'team' }, context)).to.equal( - 'fromcf' - ); - expect(getAwsClientConfig).to.have.been.calledOnceWithExactly({ - profile: 'team', - region: 'us-east-1', - stage: 'dev', - }); - expect(CloudFormation).to.have.been.calledOnceWithExactly({ - region: 'us-east-1', - credentials: 'creds', - retryMode: 'standard', + const getStateBucketNameWithStubs = proxyquire + .noCallThru() + .load('../../../../../src/state/utils/get-state-bucket-name', { + '@aws-sdk/client-cloudformation': { CloudFormation }, + '../../utils/aws': { getAwsClientConfig }, + }); + + expect( + await getStateBucketNameWithStubs({ backend: 's3', profile: 'team' }, context) + ).to.equal('fromcf'); + expect(getAwsClientConfig).to.have.been.calledOnceWithExactly({ + profile: 'team', + region: 'us-east-1', + stage: 'dev', + }); + expect(CloudFormation).to.have.been.calledOnceWithExactly({ + region: 'us-east-1', + credentials: 'creds', + retryMode: 'standard', + }); }); }); }); diff --git a/test/unit/src/state/utils/get-state-bucket-region.test.js b/test/unit/src/state/utils/get-state-bucket-region.test.js index 68d5370..15f3124 100644 --- a/test/unit/src/state/utils/get-state-bucket-region.test.js +++ b/test/unit/src/state/utils/get-state-bucket-region.test.js @@ -22,101 +22,105 @@ describe('test/unit/src/state/utils/get-state-bucket-region.test.js', () => { const bucketName = 'test-bucket'; - it('correctly resolves region for `us-east-1` bucket', async () => { - s3Mock.on(GetBucketLocationCommand).resolves({ LocationConstraint: undefined }); - expect(await getStateBucketRegion(bucketName)).to.equal('us-east-1'); - }); - - it('correctly resolves region for non `us-east-1` bucket', async () => { - s3Mock.on(GetBucketLocationCommand).resolves({ LocationConstraint: 'eu-central-1' }); - expect(await getStateBucketRegion(bucketName)).to.equal('eu-central-1'); - }); + describe('S3 command behavior', () => { + it('correctly resolves region for `us-east-1` bucket', async () => { + s3Mock.on(GetBucketLocationCommand).resolves({ LocationConstraint: undefined }); + expect(await getStateBucketRegion(bucketName)).to.equal('us-east-1'); + }); - it('normalizes the legacy `EU` region alias', async () => { - s3Mock.on(GetBucketLocationCommand).resolves({ LocationConstraint: 'EU' }); - expect(await getStateBucketRegion(bucketName)).to.equal('eu-west-1'); - }); + it('correctly resolves region for non `us-east-1` bucket', async () => { + s3Mock.on(GetBucketLocationCommand).resolves({ LocationConstraint: 'eu-central-1' }); + expect(await getStateBucketRegion(bucketName)).to.equal('eu-central-1'); + }); - it('rejects when bucket cannot be found', async () => { - const bucketDoesNotExistError = new Error('No such bucket'); - bucketDoesNotExistError.Code = 'NoSuchBucket'; + it('normalizes the legacy `EU` region alias', async () => { + s3Mock.on(GetBucketLocationCommand).resolves({ LocationConstraint: 'EU' }); + expect(await getStateBucketRegion(bucketName)).to.equal('eu-west-1'); + }); - s3Mock.on(GetBucketLocationCommand).rejects(bucketDoesNotExistError); - await expect(getStateBucketRegion(bucketName)).to.be.eventually.rejected.and.have.property( - 'code', - 'CANNOT_FIND_PROVIDED_REMOTE_STATE_BUCKET' - ); - }); + it('rejects when bucket cannot be found', async () => { + const bucketDoesNotExistError = new Error('No such bucket'); + bucketDoesNotExistError.Code = 'NoSuchBucket'; - it('rejects when SDK v3 reports bucket cannot be found by error name', async () => { - const bucketDoesNotExistError = new Error('No such bucket'); - bucketDoesNotExistError.name = 'NoSuchBucket'; + s3Mock.on(GetBucketLocationCommand).rejects(bucketDoesNotExistError); + await expect(getStateBucketRegion(bucketName)).to.be.eventually.rejected.and.have.property( + 'code', + 'CANNOT_FIND_PROVIDED_REMOTE_STATE_BUCKET' + ); + }); - s3Mock.on(GetBucketLocationCommand).rejects(bucketDoesNotExistError); - await expect(getStateBucketRegion(bucketName)).to.be.eventually.rejected.and.have.property( - 'code', - 'CANNOT_FIND_PROVIDED_REMOTE_STATE_BUCKET' - ); - }); + it('rejects when SDK v3 reports bucket cannot be found by error name', async () => { + const bucketDoesNotExistError = new Error('No such bucket'); + bucketDoesNotExistError.name = 'NoSuchBucket'; - it('rejects when access to bucket is denied', async () => { - const bucketCannotBeAccessedError = new Error('No such bucket'); - bucketCannotBeAccessedError.Code = 'AccessDenied'; + s3Mock.on(GetBucketLocationCommand).rejects(bucketDoesNotExistError); + await expect(getStateBucketRegion(bucketName)).to.be.eventually.rejected.and.have.property( + 'code', + 'CANNOT_FIND_PROVIDED_REMOTE_STATE_BUCKET' + ); + }); - s3Mock.on(GetBucketLocationCommand).rejects(bucketCannotBeAccessedError); - await expect(getStateBucketRegion(bucketName)).to.be.eventually.rejected.and.have.property( - 'code', - 'CANNOT_ACCESS_PROVIDED_REMOTE_STATE_BUCKET' - ); - }); + it('rejects when access to bucket is denied', async () => { + const bucketCannotBeAccessedError = new Error('No such bucket'); + bucketCannotBeAccessedError.Code = 'AccessDenied'; - it('rejects when SDK v3 reports bucket access denial by error name', async () => { - const bucketCannotBeAccessedError = new Error('No such bucket'); - bucketCannotBeAccessedError.name = 'AccessDenied'; + s3Mock.on(GetBucketLocationCommand).rejects(bucketCannotBeAccessedError); + await expect(getStateBucketRegion(bucketName)).to.be.eventually.rejected.and.have.property( + 'code', + 'CANNOT_ACCESS_PROVIDED_REMOTE_STATE_BUCKET' + ); + }); - s3Mock.on(GetBucketLocationCommand).rejects(bucketCannotBeAccessedError); - await expect(getStateBucketRegion(bucketName)).to.be.eventually.rejected.and.have.property( - 'code', - 'CANNOT_ACCESS_PROVIDED_REMOTE_STATE_BUCKET' - ); - }); + it('rejects when SDK v3 reports bucket access denial by error name', async () => { + const bucketCannotBeAccessedError = new Error('No such bucket'); + bucketCannotBeAccessedError.name = 'AccessDenied'; - it('rejects on generic error', async () => { - s3Mock.on(GetBucketLocationCommand).rejects(new Error('failure')); - await expect(getStateBucketRegion(bucketName)).to.be.eventually.rejected.and.have.property( - 'code', - 'GENERIC_CANNOT_ACCESS_PROVIDED_REMOTE_STATE_BUCKET' - ); - }); + s3Mock.on(GetBucketLocationCommand).rejects(bucketCannotBeAccessedError); + await expect(getStateBucketRegion(bucketName)).to.be.eventually.rejected.and.have.property( + 'code', + 'CANNOT_ACCESS_PROVIDED_REMOTE_STATE_BUCKET' + ); + }); - it('uses profile-aware AWS client config for bucket lookups', async () => { - const getBucketLocation = sinon.stub().resolves({ LocationConstraint: 'eu-central-1' }); - const S3 = sinon.stub().callsFake(() => ({ getBucketLocation })); - const getAwsClientConfig = sinon.stub().returns({ - region: 'us-east-1', - credentials: 'creds', - retryMode: 'standard', + it('rejects on generic error', async () => { + s3Mock.on(GetBucketLocationCommand).rejects(new Error('failure')); + await expect(getStateBucketRegion(bucketName)).to.be.eventually.rejected.and.have.property( + 'code', + 'GENERIC_CANNOT_ACCESS_PROVIDED_REMOTE_STATE_BUCKET' + ); }); + }); - const getStateBucketRegionWithStubs = proxyquire - .noCallThru() - .load('../../../../../src/state/utils/get-state-bucket-region', { - '@aws-sdk/client-s3': { S3 }, - '../../utils/aws': { getAwsClientConfig }, + describe('S3 client config', () => { + it('uses profile-aware AWS client config for bucket lookups', async () => { + const getBucketLocation = sinon.stub().resolves({ LocationConstraint: 'eu-central-1' }); + const S3 = sinon.stub().callsFake(() => ({ getBucketLocation })); + const getAwsClientConfig = sinon.stub().returns({ + region: 'us-east-1', + credentials: 'creds', + retryMode: 'standard', }); - expect( - await getStateBucketRegionWithStubs(bucketName, { profile: 'team' }, { stage: 'prod' }) - ).to.equal('eu-central-1'); - expect(getAwsClientConfig).to.have.been.calledOnceWithExactly({ - profile: 'team', - region: 'us-east-1', - stage: 'prod', - }); - expect(S3).to.have.been.calledOnceWithExactly({ - region: 'us-east-1', - credentials: 'creds', - retryMode: 'standard', + const getStateBucketRegionWithStubs = proxyquire + .noCallThru() + .load('../../../../../src/state/utils/get-state-bucket-region', { + '@aws-sdk/client-s3': { S3 }, + '../../utils/aws': { getAwsClientConfig }, + }); + + expect( + await getStateBucketRegionWithStubs(bucketName, { profile: 'team' }, { stage: 'prod' }) + ).to.equal('eu-central-1'); + expect(getAwsClientConfig).to.have.been.calledOnceWithExactly({ + profile: 'team', + region: 'us-east-1', + stage: 'prod', + }); + expect(S3).to.have.been.calledOnceWithExactly({ + region: 'us-east-1', + credentials: 'creds', + retryMode: 'standard', + }); }); }); });