diff --git a/.eslintrc b/.eslintrc index 71b46ba..122285f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,7 @@ { "extends": "@aptoma/eslint-config", "parserOptions": { - "ecmaVersion": "2017" + "ecmaVersion": 2023 }, "env": { "node": true, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7638967 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: Node.js CI + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20] + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + registry-url: 'https://registry.npmjs.org' + cache: 'npm' + cache-dependency-path: ./package.json + - run: npm install + - run: npm test diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6d4bba3..0000000 --- a/.travis.yml +++ /dev/null @@ -1,4 +0,0 @@ -language: node_js -node_js: - - 10 - diff --git a/handlers.js b/handlers.js index c29365d..476e10c 100644 --- a/handlers.js +++ b/handlers.js @@ -1,8 +1,6 @@ 'use strict'; -const Promise = require('bluebird'); const Hoek = require('@hapi/hoek'); -const request = require('request-prom'); exports.hook = (sns, {handlers, skipPayloadValidation, topic}) => { handlers = Hoek.applyToDefaults({ @@ -41,9 +39,8 @@ async function confirmSubscription(sns, topicOpts, req, h, payload) { } try { - await request.get(payload.SubscribeURL); + await fetch(payload.SubscribeURL); req.log(['hookido', 'info'], `SNS subscription confirmed for ${payload.TopicArn}`); - } catch (err) { req.log(['hookido', 'error'], `Unable to confirm SNS subscription for ${payload.TopicArn}, err: ${err.message}`); throw err; diff --git a/index.js b/index.js index 4d0ebc1..1d9f54b 100644 --- a/index.js +++ b/index.js @@ -37,23 +37,22 @@ function register(server, opts) { snsInstances.push(sns); const subscribe = Hoek.reach(config, 'topic.subscribe'); - function requestSubscription() { - return sns - .subscribe(config.topic.arn, subscribe.protocol, subscribe.endpoint) - .then(() => server.log(['hookido', 'subscribe'], `Subscription request sent for ${config.topic.arn}`)); - } - if (subscribe) { - - server.ext('onPostStart', () => { - return sns - .findSubscriptionArn(config.topic.arn, subscribe.protocol, subscribe.endpoint) - .then(() => server.log(['hookido', 'subscribe'], `Subscription already exists for ${config.topic.arn}`)) - .catch({code: 'NOT_FOUND'}, requestSubscription) - .catch({code: 'PENDING'}, requestSubscription) - .catch((err) => server.log(['hookido', 'subscribe', 'error'], err)); + server.ext('onPostStart', async () => { + try { + await sns.findSubscriptionArn(config.topic.arn, subscribe.protocol, subscribe.endpoint); + server.log(['hookido', 'subscribe'], `Subscription already exists for ${config.topic.arn}`); + } catch (err) { + if (err.code === 'NOT_FOUND' || err.code === 'PENDING') { + await sns + .subscribe(config.topic.arn, subscribe.protocol, subscribe.endpoint) + .then(() => server.log(['hookido', 'subscribe'], `Subscription request sent for ${config.topic.arn}`)) + .catch((err) => server.log(['hookido', 'subscribe', 'error'], err)); + } else { + server.log(['hookido', 'subscribe', 'error'], err); + } + } }); - } const topicAttributes = Hoek.reach(config, 'topic.attributes'); diff --git a/lib/sns.js b/lib/sns.js index a39a2f8..e5d04fa 100644 --- a/lib/sns.js +++ b/lib/sns.js @@ -1,51 +1,52 @@ 'use strict'; -const Promise = require('bluebird'); -const AWS = require('aws-sdk'); +const { + SNSClient, + SubscribeCommand, + SetTopicAttributesCommand, + ListSubscriptionsByTopicCommand, + SetSubscriptionAttributesCommand +} = require('@aws-sdk/client-sns'); const MessageValidator = require('sns-validator'); const validator = new MessageValidator(); class SNS { constructor(awsConfig) { - const sns = new AWS.SNS(awsConfig || {}); - this.sns = { - subscribe: Promise.promisify(sns.subscribe, {context: sns}), - setTopicAttributes: Promise.promisify(sns.setTopicAttributes, {context: sns}), - listSubscriptionsByTopic: Promise.promisify(sns.listSubscriptionsByTopic, {context: sns}), - setSubscriptionAttributes: Promise.promisify(sns.setSubscriptionAttributes, {context: sns}) - }; + this.snsClient = new SNSClient(awsConfig || {}); } - setTopicAttributes(TopicArn, attributes) { - // this needs to be done sequentially otherwise only one attribute will be set, have no idea why. - return Promise.mapSeries(Object.keys(attributes), (key) => { - return this.sns.setTopicAttributes({ + async setTopicAttributes(TopicArn, attributes) { + // Process attributes sequentially + for (const key of Object.keys(attributes)) { + const command = new SetTopicAttributesCommand({ TopicArn, AttributeName: key, AttributeValue: attributes[key] }); - }); + await this.snsClient.send(command); + } } subscribe(TopicArn, Protocol, Endpoint) { - const params = { + const command = new SubscribeCommand({ TopicArn, Protocol, Endpoint - }; + }); - return this.sns.subscribe(params); + return this.snsClient.send(command); } - setSubscriptionAttributes(SubscriptionArn, attributes) { - return Promise.mapSeries(Object.keys(attributes), (key) => { - return this.sns.setSubscriptionAttributes({ + async setSubscriptionAttributes(SubscriptionArn, attributes) { + for (const key of Object.keys(attributes)) { + const command = new SetSubscriptionAttributesCommand({ SubscriptionArn, AttributeName: key, AttributeValue: attributes[key] }); - }); + await this.snsClient.send(command); + } } /** @@ -63,8 +64,10 @@ class SNS { params.NextToken = NextToken; } - return this.sns - .listSubscriptionsByTopic(params) + const command = new ListSubscriptionsByTopicCommand(params); + + return this.snsClient + .send(command) .then((data) => { const arn = data.Subscriptions.find((sub) => { return sub.Protocol.toLowerCase() === Protocol.toLowerCase() && diff --git a/package.json b/package.json index 4145d0b..74dbd1c 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "release:major": "npm run test && release-it -n -i major" }, "engines": { - "node": ">=10.x.x" + "node": ">=20.x.x" }, "repository": { "type": "git", @@ -31,21 +31,20 @@ }, "homepage": "https://github.com/martinj/hookido", "dependencies": { - "@hapi/hoek": "^9.2.1", - "aws-sdk": "^2.1094.0", - "bluebird": "^3.7.2", - "joi": "^17.6.0", - "request-prom": "^4.0.1", - "sns-validator": "^0.3.4" + "@aws-sdk/client-sns": "^3.731.1", + "@hapi/hoek": "^11.0.7", + "joi": "^17.13.3", + "sns-validator": "^0.3.5" }, "devDependencies": { "@aptoma/eslint-config": "^7.0.1", - "@hapi/hapi": "^20.2.1", - "chai": "^4.3.6", - "eslint": "^8.11.0", - "mocha": "^9.2.2", - "nock": "^13.2.4", + "@hapi/hapi": "^21.3.12", + "aws-sdk-client-mock": "^4.1.0", + "chai": "^4.5.0", + "eslint": "^8.57.1", + "mocha": "^11.0.1", "nyc": "^15.1.0", - "release-it": "^2.7.3" + "release-it": "^2.7.3", + "sinon": "^19.0.2" } } diff --git a/test/index.test.js b/test/index.test.js index 202858f..3d0f8ec 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,15 +1,35 @@ 'use strict'; -const Promise = require('bluebird'); const Hapi = require('@hapi/hapi'); +const {mockClient} = require('aws-sdk-client-mock'); +const { + SNSClient, + SubscribeCommand, + SetTopicAttributesCommand, + ListSubscriptionsByTopicCommand, + SetSubscriptionAttributesCommand +} = require('@aws-sdk/client-sns'); const plugin = require('../'); -const expect = require('chai').expect; -const nock = require('nock'); +const {expect} = require('chai'); +const sinon = require('sinon'); describe('Hookido Hapi Plugin', () => { + let snsMock; + let fetchStub; + + beforeEach(() => { + snsMock = mockClient(SNSClient); + // Mock global fetch + fetchStub = sinon.stub(global, 'fetch'); + fetchStub.resolves(new Response()); + }); - it('merges route options with route', async () => { + afterEach(() => { + snsMock.reset(); + fetchStub.restore(); + }); + it('merges route options with route', async () => { const server = new Hapi.Server(); await server.register({ @@ -26,11 +46,9 @@ describe('Hookido Hapi Plugin', () => { const table = server.table(); expect(table[0].path).to.equal('/foobar'); - }); it('skips subscribe request if subscription exist', async () => { - const server = new Hapi.Server(); await server.register({ @@ -49,18 +67,19 @@ describe('Hookido Hapi Plugin', () => { } }); - server.plugins.hookido.snsInstances[0].findSubscriptionArn = () => { - return Promise.resolve('foo'); - }; + snsMock.on(ListSubscriptionsByTopicCommand).resolves({ + Subscriptions: [{ + SubscriptionArn: 'foo', + Protocol: 'HTTP', + Endpoint: 'http://foo.com' + }] + }); await server.start(); await server.stop(); - }); - it('sends subscribe request onPostStart if subscribe option is set and subscription does not exist', async () => { - const server = new Hapi.Server(); await server.register({ @@ -79,28 +98,25 @@ describe('Hookido Hapi Plugin', () => { } }); - server.plugins.hookido.snsInstances[0].findSubscriptionArn = () => { - const err = new Error(); - err.code = 'NOT_FOUND'; - return Promise.reject(err); - }; - - server.plugins.hookido.snsInstances[0].subscribe = (arn, protocol, endpoint) => { - - expect(arn).to.equal('foo'); - expect(protocol).to.equal('HTTP'); - expect(endpoint).to.equal('http://foo.com'); - return Promise.resolve(); - - }; + snsMock + .on(ListSubscriptionsByTopicCommand) + .rejects({code: 'NOT_FOUND'}) + .on(SubscribeCommand) + .resolves({}); await server.start(); await server.stop(); + const subscribeCalls = snsMock.commandCalls(SubscribeCommand); + expect(subscribeCalls).length(1); + expect(subscribeCalls[0].args[0].input).to.deep.equal({ + TopicArn: 'foo', + Protocol: 'HTTP', + Endpoint: 'http://foo.com' + }); }); it('sends new subscribe request onPostStart if subscribe option is set and subscription is pending', async () => { - const server = new Hapi.Server(); await server.register({ @@ -119,28 +135,31 @@ describe('Hookido Hapi Plugin', () => { } }); - server.plugins.hookido.snsInstances[0].findSubscriptionArn = () => { - const err = new Error(); - err.code = 'PENDING'; - return Promise.reject(err); - }; - - server.plugins.hookido.snsInstances[0].subscribe = (arn, protocol, endpoint) => { - - expect(arn).to.equal('foo'); - expect(protocol).to.equal('HTTP'); - expect(endpoint).to.equal('http://foo.com'); - return Promise.resolve(); - - }; + snsMock + .on(ListSubscriptionsByTopicCommand) + .resolves({ + Subscriptions: [{ + SubscriptionArn: 'PendingConfirmation', + Protocol: 'HTTP', + Endpoint: 'http://foo.com' + }] + }) + .on(SubscribeCommand) + .resolves({}); await server.start(); await server.stop(); + const subscribeCalls = snsMock.commandCalls(SubscribeCommand); + expect(subscribeCalls).length(1); + expect(subscribeCalls[0].args[0].input).to.deep.equal({ + TopicArn: 'foo', + Protocol: 'HTTP', + Endpoint: 'http://foo.com' + }); }); it('sends setTopicAttributes request onPostStart if topicAttributes option is set', async () => { - const server = new Hapi.Server(); await server.register({ @@ -158,21 +177,21 @@ describe('Hookido Hapi Plugin', () => { } }); - server.plugins.hookido.snsInstances[0].setTopicAttributes = (topic, attributes) => { - - expect(topic).to.equal('foo'); - expect(attributes).to.deep.equal({foo: 'bar'}); - return Promise.resolve(); - - }; + snsMock.on(SetTopicAttributesCommand).resolves({}); await server.start(); await server.stop(); + const attributeCalls = snsMock.commandCalls(SetTopicAttributesCommand); + expect(attributeCalls).length(1); + expect(attributeCalls[0].args[0].input).to.deep.equal({ + TopicArn: 'foo', + AttributeName: 'foo', + AttributeValue: 'bar' + }); }); it('dispatches SNS message of type Notification to configured handler', async () => { - const server = new Hapi.Server(); const payload = {Type: 'Notification'}; @@ -192,11 +211,9 @@ describe('Hookido Hapi Plugin', () => { const res = await server.inject({method: 'POST', url: '/hookido', payload}); expect(res.statusCode).to.equal(200); expect(res.result).to.deep.equal({called: true}); - }); it('dispatches SNS message of type SubscriptionConfirmation to configured handler', async () => { - const server = new Hapi.Server(); const payload = {Type: 'SubscriptionConfirmation'}; @@ -218,19 +235,10 @@ describe('Hookido Hapi Plugin', () => { expect(res.statusCode).to.equal(200); expect(res.result).to.deep.equal({called: true}); - }); describe('ConfirmSubscription', () => { - - afterEach(() => nock.cleanAll()); - it('handles SubscriptionConfirmation if no handler is registered for that type', async () => { - - const confirmRequest = nock('http://localtest.com') - .get('/foo') - .reply(200); - const server = new Hapi.Server(); const payload = { @@ -253,12 +261,11 @@ describe('Hookido Hapi Plugin', () => { const res = await server.inject({method: 'POST', url: '/hookido', payload}); expect(res.statusCode).to.equal(200); - expect(confirmRequest.isDone()).to.be.true; - + expect(fetchStub.calledOnce).to.be.true; + expect(fetchStub.firstCall.args[0]).to.equal('http://localtest.com/foo'); }); it('fails if configured topic arn doesn\'t match TopicArn in SubscriptionConfirmation payload', async () => { - const server = new Hapi.Server(); const payload = { @@ -281,35 +288,9 @@ describe('Hookido Hapi Plugin', () => { const res = await server.inject({method: 'POST', url: '/hookido', payload}); expect(res.statusCode).to.equal(500); - }); it('sets subscription attributes if supplied in options', async () => { - - const mock1 = nock('http://localtest.com') - .get('/foo') - .reply(200); - - const mock2 = nock('https://sns.eu-west-1.amazonaws.com:443') - .post('/', 'Action=ListSubscriptionsByTopic&TopicArn=mytopic&Version=2010-03-31') - .reply(200, ` - - - - - arn:aws:sns:us-east-1:123456789012:My-Topic - http - arn:aws:sns:eu-west-1:111111111111:mytopic - 123456789012 - http://foo.com/bar - - - - - `) - .post('/', 'Action=SetSubscriptionAttributes&AttributeName=foo&AttributeValue=bar&SubscriptionArn=arn%3Aaws%3Asns%3Aeu-west-1%3A111111111111%3Amytopic&Version=2010-03-31') - .reply(200); - const server = new Hapi.Server(); const payload = { @@ -318,7 +299,20 @@ describe('Hookido Hapi Plugin', () => { TopicArn: 'mytopic' }; - server.register({ + snsMock + .on(ListSubscriptionsByTopicCommand) + .resolves({ + Subscriptions: [{ + TopicArn: 'mytopic', + Protocol: 'http', + SubscriptionArn: 'arn:aws:sns:eu-west-1:111111111111:mytopic', + Endpoint: 'http://foo.com/bar' + }] + }) + .on(SetSubscriptionAttributesCommand) + .resolves({}); + + await server.register({ plugin, options: { skipPayloadValidation: true, @@ -345,15 +339,20 @@ describe('Hookido Hapi Plugin', () => { await server.inject({method: 'POST', url: '/hookido', payload}); - expect(mock1.isDone()).to.be.true; - expect(mock2.isDone()).to.be.true; + expect(fetchStub.calledOnce).to.be.true; + expect(fetchStub.firstCall.args[0]).to.equal('http://localtest.com/foo'); + const attributeCalls = snsMock.commandCalls(SetSubscriptionAttributesCommand); + expect(attributeCalls).length(1); + expect(attributeCalls[0].args[0].input).to.deep.equal({ + SubscriptionArn: 'arn:aws:sns:eu-west-1:111111111111:mytopic', + AttributeName: 'foo', + AttributeValue: 'bar' + }); }); - }); it('supports multiple configurations', async () => { - const server = new Hapi.Server(); await server.register({ @@ -380,11 +379,9 @@ describe('Hookido Hapi Plugin', () => { expect(table[0].path).to.equal('/foobar'); expect(table[1].path).to.equal('/foobar2'); expect(server.plugins.hookido.snsInstances).to.have.a.lengthOf(2); - }); it('supports to load multiple times', async () => { - const server = new Hapi.Server(); await server.register({ @@ -416,7 +413,5 @@ describe('Hookido Hapi Plugin', () => { expect(table[0].path).to.equal('/foobar'); expect(table[1].path).to.equal('/foobar2'); expect(server.plugins.hookido.snsInstances).to.have.a.lengthOf(2); - }); - }); diff --git a/test/lib/sns.test.js b/test/lib/sns.test.js index 8c042eb..8e4427d 100644 --- a/test/lib/sns.test.js +++ b/test/lib/sns.test.js @@ -1,298 +1,221 @@ 'use strict'; -const nock = require('nock'); -const expect = require('chai').expect; +const {mockClient} = require('aws-sdk-client-mock'); +const { + SNSClient, + SubscribeCommand, + SetTopicAttributesCommand, + ListSubscriptionsByTopicCommand, + SetSubscriptionAttributesCommand +} = require('@aws-sdk/client-sns'); const SNS = require('../../lib/sns'); +const {expect} = require('chai'); describe('SNS', () => { let sns; + let snsMock; beforeEach(() => { - sns = new SNS({ - region: 'eu-west-1', - accessKeyId: 'a', - secretAccessKey: 'a' - }); + snsMock = mockClient(SNSClient); + sns = new SNS({region: 'eu-west-1'}); }); - describe('#validatePayload', () => { - - it('rejects on invalid JSON', (done) => { - - sns - .validatePayload('ada') - .catch((err) => { - expect(err.message).to.equal('Invalid SNS payload: Unexpected token a in JSON at position 0'); - return sns.validatePayload(null); - }) - .catch((err) => { - - expect(err.message).to.equal('Invalid SNS payload: Not valid JSON'); - done(); - - }); + afterEach(() => { + snsMock.reset(); + }); + describe('#validatePayload', () => { + it('rejects on invalid JSON', async () => { + try { + await sns.validatePayload('ada'); + throw new Error('Expected error'); + } catch (err) { + expect(err.message).to.match(/Invalid SNS payload: Unexpected token/); + } }); - it('rejects on invalid payload', (done) => { - - sns - .validatePayload('{"foo":"bar"}') - .catch((err) => { - - expect(err.message).to.equal('Message missing required keys.'); - done(); - - }); - + it('rejects on null payload', async () => { + try { + await sns.validatePayload(null); + throw new Error('Expected error'); + } catch (err) { + expect(err.message).to.equal('Invalid SNS payload: Not valid JSON'); + } }); - it('accepts object or array', () => { - - return sns - .validatePayload([], true) - .then((data) => expect(data).to.deep.equal([])) - .then(() => sns.validatePayload({}, true)) - .then((data) => expect(data).to.deep.equal({})); - + it('rejects on invalid payload', async () => { + try { + await sns.validatePayload('{"foo":"bar"}'); + throw new Error('Expected error'); + } catch (err) { + expect(err.message).to.equal('Message missing required keys.'); + } }); - it('skipValidation skips SNS message validation only parses', () => { - - return sns - .validatePayload('{"foo":"bar"}', true) - .then((data) => { - expect(data).to.deep.equal({foo: 'bar'}); - }); + it('accepts array', async () => { + const data = await sns.validatePayload([], true); + expect(data).to.deep.equal([]); + }); + it('accepts object', async () => { + const data = await sns.validatePayload({}, true); + expect(data).to.deep.equal({}); }); + it('skipValidation skips SNS message validation only parses', async () => { + const data = await sns.validatePayload('{"foo":"bar"}', true); + expect(data).to.deep.equal({foo: 'bar'}); + }); }); describe('#subscribe', () => { - - afterEach(() => nock.cleanAll()); - - it('sends subscribe request', () => { - - const req = nock('https://sns.eu-west-1.amazonaws.com:443') - .post('/', 'Action=Subscribe&Endpoint=http%3A%2F%2Ffoobar.com&Protocol=HTTP&TopicArn=arn%3Aaws%3Asns%3Aeu-west-1%3A111111111111%3Amytopic&Version=2010-03-31') - .reply(200); - - return sns - .subscribe('arn:aws:sns:eu-west-1:111111111111:mytopic', 'HTTP', 'http://foobar.com') - .then(() => { - - expect(req.isDone()).to.be.true; - - }); + it('sends subscribe request', async () => { + snsMock.on(SubscribeCommand).resolves({ + SubscriptionArn: 'arn:aws:sns:eu-west-1:111111111111:mytopic:subscription-id' + }); + + await sns.subscribe( + 'arn:aws:sns:eu-west-1:111111111111:mytopic', + 'HTTP', + 'http://foobar.com' + ); + + expect(snsMock.calls()).length(1); + const [subscribeCall] = snsMock.calls(); + expect(subscribeCall.args[0].input).to.deep.equal({ + TopicArn: 'arn:aws:sns:eu-west-1:111111111111:mytopic', + Protocol: 'HTTP', + Endpoint: 'http://foobar.com' + }); }); - }); describe('#setTopicAttributes', () => { - - afterEach(() => nock.cleanAll()); - - it('sends setTopicAttributes request', () => { - - const firstReq = nock('https://sns.eu-west-1.amazonaws.com:443') - .post('/', 'Action=SetTopicAttributes&AttributeName=HTTPSuccessFeedbackRoleArn&AttributeValue=arn%3Aaws%3Aiam%3A%3Axxxx%3Arole%2FmyRole&TopicArn=arn%3Aaws%3Asns%3Aeu-west-1%3A111111111111%3Amytopic&Version=2010-03-31') - .reply(200); - - const secondReq = nock('https://sns.eu-west-1.amazonaws.com:443') - .post('/', 'Action=SetTopicAttributes&AttributeName=HTTPSuccessFeedbackSampleRate&AttributeValue=100&TopicArn=arn%3Aaws%3Asns%3Aeu-west-1%3A111111111111%3Amytopic&Version=2010-03-31') - .reply(200); - - return sns - .setTopicAttributes('arn:aws:sns:eu-west-1:111111111111:mytopic', { - HTTPSuccessFeedbackRoleArn: 'arn:aws:iam::xxxx:role/myRole', - HTTPSuccessFeedbackSampleRate: '100' - }) - .then(() => { - - expect(firstReq.isDone()).to.be.true; - expect(secondReq.isDone()).to.be.true; - - }); - - }); - - }); - - describe('#setSubscriptionAttributes', () => { - - afterEach(() => nock.cleanAll()); - - it('sends setSubscriptionAttributes request', () => { - - const firstReq = nock('https://sns.eu-west-1.amazonaws.com:443') - .post('/', 'Action=SetSubscriptionAttributes&AttributeName=foo&AttributeValue=bar&SubscriptionArn=arn&Version=2010-03-31') - .reply(200); - - const secondReq = nock('https://sns.eu-west-1.amazonaws.com:443') - .post('/', 'Action=SetSubscriptionAttributes&AttributeName=bar&AttributeValue=foo&SubscriptionArn=arn&Version=2010-03-31') - .reply(200); - - return sns - .setSubscriptionAttributes('arn', { - foo: 'bar', - bar: 'foo' - }) - .then(() => { - - expect(firstReq.isDone()).to.be.true; - expect(secondReq.isDone()).to.be.true; - - }); - + it('sets topic attributes sequentially', async () => { + snsMock.on(SetTopicAttributesCommand).resolves({}); + + await sns.setTopicAttributes( + 'arn:aws:sns:eu-west-1:111111111111:mytopic', + { + DisplayName: 'My Topic', + Policy: '{"Version":"2012-10-17"}' + } + ); + + expect(snsMock.calls()).length(2); + const calls = snsMock.calls(); + + expect(calls[0].args[0].input).to.deep.equal({ + TopicArn: 'arn:aws:sns:eu-west-1:111111111111:mytopic', + AttributeName: 'DisplayName', + AttributeValue: 'My Topic' + }); + + expect(calls[1].args[0].input).to.deep.equal({ + TopicArn: 'arn:aws:sns:eu-west-1:111111111111:mytopic', + AttributeName: 'Policy', + AttributeValue: '{"Version":"2012-10-17"}' + }); }); - }); describe('#findSubscriptionArn', () => { - - afterEach(() => nock.cleanAll()); - - it('rejects with error containing code NOT_FOUND if not found', (done) => { - - nock('https://sns.eu-west-1.amazonaws.com:443') - .post('/', 'Action=ListSubscriptionsByTopic&TopicArn=arn%3Aaws%3Asns%3Aeu-west-1%3A111111111111%3Amytopic&Version=2010-03-31') - .reply(200, ` - - - - - arn:aws:sns:us-east-1:123456789012:My-Topic - email - arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca - 123456789012 - example@amazon.com - - - - - `); - - sns - .findSubscriptionArn('arn:aws:sns:eu-west-1:111111111111:mytopic', 'HTTP', 'http://foo.com/bar') - .catch((err) => { - - expect(err.code).to.equal('NOT_FOUND'); - done(); - - }); - + it('finds subscription arn', async () => { + snsMock.on(ListSubscriptionsByTopicCommand).resolves({ + Subscriptions: [{ + SubscriptionArn: 'arn:aws:sns:eu-west-1:111111111111:mytopic:subscription-id', + Protocol: 'HTTP', + Endpoint: 'http://foobar.com' + }] + }); + + const arn = await sns.findSubscriptionArn( + 'arn:aws:sns:eu-west-1:111111111111:mytopic', + 'HTTP', + 'http://foobar.com' + ); + + expect(arn).to.equal('arn:aws:sns:eu-west-1:111111111111:mytopic:subscription-id'); + expect(snsMock.calls()).length(1); + expect(snsMock.calls()[0].args[0].input).to.deep.equal({ + TopicArn: 'arn:aws:sns:eu-west-1:111111111111:mytopic' + }); }); - it('rejects with error containing code PENDING if subscription exists but is pending confirmation', (done) => { - - nock('https://sns.eu-west-1.amazonaws.com:443') - .post('/', 'Action=ListSubscriptionsByTopic&TopicArn=arn%3Aaws%3Asns%3Aeu-west-1%3A111111111111%3Amytopic&Version=2010-03-31') - .reply(200, ` - - - - - arn:aws:sns:us-east-1:123456789012:My-Topic - http - PendingConfirmation - 123456789012 - http://foo.com/bar - - - - - `); - - sns - .findSubscriptionArn('arn:aws:sns:eu-west-1:111111111111:mytopic', 'HTTP', 'http://foo.com/bar') - .catch((err) => { - - expect(err.code).to.equal('PENDING'); - done(); - - }); + it('handles pending confirmation', async () => { + snsMock.on(ListSubscriptionsByTopicCommand).resolves({ + Subscriptions: [{ + SubscriptionArn: 'PendingConfirmation', + Protocol: 'HTTP', + Endpoint: 'http://foobar.com' + }] + }); + + try { + await sns.findSubscriptionArn( + 'arn:aws:sns:eu-west-1:111111111111:mytopic', + 'HTTP', + 'http://foobar.com' + ); + throw new Error('Should have thrown'); + } catch (err) { + expect(err.code).to.equal('PENDING'); + expect(err.message).to.equal('Subscription is pending confirmation'); + } }); - - it('handles NextToken for paged results', () => { - - nock('https://sns.eu-west-1.amazonaws.com:443') - .post('/', 'Action=ListSubscriptionsByTopic&TopicArn=arn%3Aaws%3Asns%3Aeu-west-1%3A111111111111%3Amytopic&Version=2010-03-31') - .reply(200, ` - - - - - arn:aws:sns:us-east-1:123456789012:My-Topic - email - arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca - 123456789012 - fo@foo.com - - - foo - - - `) - .post('/', 'Action=ListSubscriptionsByTopic&NextToken=foo&TopicArn=arn%3Aaws%3Asns%3Aeu-west-1%3A111111111111%3Amytopic&Version=2010-03-31') - .reply(200, ` - - - - - arn:aws:sns:us-east-1:123456789012:My-Topic - http - arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca - 123456789012 - http://foo.com/bar - - - foo - - - `); - - return sns - .findSubscriptionArn('arn:aws:sns:eu-west-1:111111111111:mytopic', 'HTTP', 'http://foo.com/bar') - .then((arn) => { - - expect(arn).to.equal('arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca'); - + it('handles pagination', async () => { + snsMock + .on(ListSubscriptionsByTopicCommand) + .resolvesOnce({ + NextToken: 'next-token', + Subscriptions: [] + }) + .resolvesOnce({ + Subscriptions: [{ + SubscriptionArn: 'arn:aws:sns:eu-west-1:111111111111:mytopic:subscription-id', + Protocol: 'HTTP', + Endpoint: 'http://foobar.com' + }] }); - }); - - it('resolves with the subscription arn if found', () => { - - nock('https://sns.eu-west-1.amazonaws.com:443') - .post('/', 'Action=ListSubscriptionsByTopic&TopicArn=arn%3Aaws%3Asns%3Aeu-west-1%3A111111111111%3Amytopic&Version=2010-03-31') - .reply(200, ` - - - - - arn:aws:sns:us-east-1:123456789012:My-Topic - http - arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca - 123456789012 - http://foo.com/bar - - - - - `); - - return sns - .findSubscriptionArn('arn:aws:sns:eu-west-1:111111111111:mytopic', 'HTTP', 'http://foo.com/bar') - .then((arn) => { - - expect(arn).to.equal('arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca'); - - }); + const arn = await sns.findSubscriptionArn( + 'arn:aws:sns:eu-west-1:111111111111:mytopic', + 'HTTP', + 'http://foobar.com' + ); + expect(arn).to.equal('arn:aws:sns:eu-west-1:111111111111:mytopic:subscription-id'); + expect(snsMock.calls()).length(2); }); + }); + describe('#setSubscriptionAttributes', () => { + it('sets subscription attributes sequentially', async () => { + snsMock.on(SetSubscriptionAttributesCommand).resolves({}); + + await sns.setSubscriptionAttributes( + 'arn:aws:sns:eu-west-1:111111111111:mytopic:subscription-id', + { + RawMessageDelivery: 'true', + FilterPolicy: '{"event":["order_placed"]}' + } + ); + + expect(snsMock.calls()).length(2); + const calls = snsMock.calls(); + + expect(calls[0].args[0].input).to.deep.equal({ + SubscriptionArn: 'arn:aws:sns:eu-west-1:111111111111:mytopic:subscription-id', + AttributeName: 'RawMessageDelivery', + AttributeValue: 'true' + }); + + expect(calls[1].args[0].input).to.deep.equal({ + SubscriptionArn: 'arn:aws:sns:eu-west-1:111111111111:mytopic:subscription-id', + AttributeName: 'FilterPolicy', + AttributeValue: '{"event":["order_placed"]}' + }); + }); }); });