diff --git a/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.js b/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.js index 7733d7fe7..4d0695b42 100644 --- a/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.js +++ b/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.js @@ -2,6 +2,20 @@ const { log } = require('../../../../../../../../utils/serverless-utils/log'); +async function getAllUsagePlans(provider) { + const items = []; + let position; + + do { + const params = position ? { position, limit: 500 } : { limit: 500 }; + const response = await provider.request('APIGateway', 'getUsagePlans', params); + items.push(...(response.items || [])); + position = response.position; + } while (position); + + return items; +} + module.exports = { async disassociateUsagePlan() { const apiKeys = @@ -11,18 +25,18 @@ module.exports = { if (apiKeys && apiKeys.length) { log.info('Removing usage plan association'); const stackName = `${this.provider.naming.getStackName()}`; - const data = await Promise.all([ + const [stackResource, usagePlans] = await Promise.all([ this.provider.request('CloudFormation', 'describeStackResource', { StackName: stackName, LogicalResourceId: this.provider.naming.getRestApiLogicalId(), }), - this.provider.request('APIGateway', 'getUsagePlans', {}), + getAllUsagePlans(this.provider), ]); - const restApiId = data[0].StackResourceDetail.PhysicalResourceId; + const restApiId = stackResource.StackResourceDetail.PhysicalResourceId; return Promise.all( - data[1].items.flatMap((item) => - item.apiStages + usagePlans.flatMap((item) => + (item.apiStages || []) .filter((apiStage) => apiStage.apiId === restApiId) .map((apiStage) => this.provider.request('APIGateway', 'updateUsagePlan', { diff --git a/test/unit/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.test.js b/test/unit/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.test.js index 0ab0897fb..f18ab71f7 100644 --- a/test/unit/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.test.js +++ b/test/unit/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.test.js @@ -33,7 +33,7 @@ describe('#disassociateUsagePlan()', () => { providerRequestStub .withArgs('CloudFormation', 'describeStackResource') .resolves({ StackResourceDetail: { PhysicalResourceId: 'resource-id' } }); - providerRequestStub.withArgs('APIGateway', 'getUsagePlans').resolves({ + providerRequestStub.withArgs('APIGateway', 'getUsagePlans', { limit: 500 }).resolves({ items: [ { apiStages: [ @@ -79,9 +79,9 @@ describe('#disassociateUsagePlan()', () => { }) ).to.be.equal(true); - expect(providerRequestStub.calledWithExactly('APIGateway', 'getUsagePlans', {})).to.be.equal( - true - ); + expect( + providerRequestStub.calledWithExactly('APIGateway', 'getUsagePlans', { limit: 500 }) + ).to.be.equal(true); expect( providerRequestStub.calledWithExactly('APIGateway', 'updateUsagePlan', { @@ -99,7 +99,7 @@ describe('#disassociateUsagePlan()', () => { }); it('should remove all matching associations from a usage plan', async () => { - providerRequestStub.withArgs('APIGateway', 'getUsagePlans').resolves({ + providerRequestStub.withArgs('APIGateway', 'getUsagePlans', { limit: 500 }).resolves({ items: [ { apiStages: [ @@ -156,7 +156,7 @@ describe('#disassociateUsagePlan()', () => { }); it('should not update usage plans without matching API stages', async () => { - providerRequestStub.withArgs('APIGateway', 'getUsagePlans').resolves({ + providerRequestStub.withArgs('APIGateway', 'getUsagePlans', { limit: 500 }).resolves({ items: [ { apiStages: [{ apiId: 'another-resource-id', stage: 'dev' }], @@ -172,6 +172,73 @@ describe('#disassociateUsagePlan()', () => { }); }); + it('should remove matching usage plan associations across paginated usage plans', async () => { + providerRequestStub.withArgs('APIGateway', 'getUsagePlans', { limit: 500 }).resolves({ + items: [ + { + apiStages: [{ apiId: 'another-resource-id', stage: 'dev' }], + id: 'first-page-plan-id', + }, + ], + position: 'next-page', + }); + providerRequestStub + .withArgs('APIGateway', 'getUsagePlans', { position: 'next-page', limit: 500 }) + .resolves({ + items: [ + { + apiStages: [{ apiId: 'resource-id', stage: 'prod' }], + id: 'second-page-plan-id', + }, + ], + }); + disassociateUsagePlan.serverless.service.provider.apiGateway = { apiKeys: ['apiKey1'] }; + + await disassociateUsagePlan.disassociateUsagePlan(); + + expect( + providerRequestStub.calledWithExactly('APIGateway', 'getUsagePlans', { limit: 500 }) + ).to.equal(true); + expect( + providerRequestStub.calledWithExactly('APIGateway', 'getUsagePlans', { + position: 'next-page', + limit: 500, + }) + ).to.equal(true); + expect( + providerRequestStub.calledWithExactly('APIGateway', 'updateUsagePlan', { + usagePlanId: 'second-page-plan-id', + patchOperations: [ + { + op: 'remove', + path: '/apiStages', + value: 'resource-id:prod', + }, + ], + }) + ).to.equal(true); + }); + + it('should not update usage plans without apiStages', async () => { + providerRequestStub.withArgs('APIGateway', 'getUsagePlans', { limit: 500 }).resolves({ + items: [{ id: 'usage-plan-without-stages' }], + }); + disassociateUsagePlan.serverless.service.provider.apiGateway = { apiKeys: ['apiKey1'] }; + + await disassociateUsagePlan.disassociateUsagePlan(); + + expect(providerRequestStub.calledWith('APIGateway', 'updateUsagePlan')).to.equal(false); + }); + + it('should resolve when getUsagePlans returns no items', async () => { + providerRequestStub.withArgs('APIGateway', 'getUsagePlans', { limit: 500 }).resolves({}); + disassociateUsagePlan.serverless.service.provider.apiGateway = { apiKeys: ['apiKey1'] }; + + await disassociateUsagePlan.disassociateUsagePlan(); + + expect(providerRequestStub.calledWith('APIGateway', 'updateUsagePlan')).to.equal(false); + }); + it('should resolve if no api keys are given', async () => { disassociateUsagePlan.serverless.service.provider.apiGateway = { apiKeys: [] };