UNPKG

serverless-offline-sns

Version:

Serverless plugin to run a local SNS server and call lambdas with events notifications.

543 lines (505 loc) 21.3 kB
const ServerlessOfflineSns = require("../../src/index"); import {expect} from "chai"; import handler = require("../mock/handler"); import state = require("../mock/mock.state"); import * as multiDotHandler from "../mock/multi.dot.handler"; let plugin; describe("test", () => { let accountId; beforeEach(() => { accountId = Math.floor(Math.random() * (100000000 - 1)); handler.resetPongs(); state.resetEvent(); state.resetResult(); }); afterEach(() => { plugin.stop(); }); it("should start on offline start", async () => { plugin = new ServerlessOfflineSns(createServerless(accountId), {skipCacheInvalidation: true}); await plugin.hooks["before:offline:start:init"](); await plugin.hooks["after:offline:start:end"](); }); it("should start on command start", async () => { plugin = new ServerlessOfflineSns(createServerless(accountId), {skipCacheInvalidation: true}); plugin.hooks["offline-sns:start:init"](); await new Promise(res => setTimeout(res, 100)); await plugin.hooks["offline-sns:start:end"](); }); it("should send event to topic ARN", async () => { plugin = new ServerlessOfflineSns(createServerless(accountId), {skipCacheInvalidation: true}); const snsAdapter = await plugin.start(); await snsAdapter.publish(`arn:aws:sns:us-east-1:${accountId}:test-topic`, "{}"); await new Promise(res => setTimeout(res, 100)); expect(state.getPongs()).to.eq(2); }); it("should send event to target ARN", async () => { plugin = new ServerlessOfflineSns(createServerless(accountId), {skipCacheInvalidation: true}); const snsAdapter = await plugin.start(); await snsAdapter.publishToTargetArn(`arn:aws:sns:us-east-1:${accountId}:test-topic`, "{}"); await new Promise(res => setTimeout(res, 100)); expect(state.getPongs()).to.eq(2); }); it("should send event with pseudo parameters", async () => { plugin = new ServerlessOfflineSns(createServerless(accountId), {skipCacheInvalidation: true}); const snsAdapter = await plugin.start(); await snsAdapter.publish("arn:aws:sns:us-east-1:#{AWS::AccountId}:test-topic", "{}"); await new Promise(res => setTimeout(res, 100)); expect(state.getPongs()).to.eq(2); }); it("should send event with MessageAttributes and subject", async () => { plugin = new ServerlessOfflineSns(createServerless(accountId), {skipCacheInvalidation: true}); const snsAdapter = await plugin.start(); await snsAdapter.publish( `arn:aws:sns:us-east-1:${accountId}:test-topic`, "message with attributes", "raw", { with: { DataType: "String", StringValue: "attributes" }, }, "subject" ); await new Promise(res => setTimeout(res, 100)); const event = state.getEvent(); expect(event.Records[0].Sns).to.have.property("Message", "message with attributes"); expect(event.Records[0].Sns).to.have.property("Subject", "subject"); expect(event.Records[0].Sns).to.have.deep.property( "MessageAttributes", { with: { Type: "String", Value: "attributes", }, }, ); }); it("should return a valid response to publish", async () => { plugin = new ServerlessOfflineSns(createServerless(accountId), {skipCacheInvalidation: true}); const snsAdapter = await plugin.start(); const snsResponse = await snsAdapter.publish( `arn:aws:sns:us-east-1:${accountId}:test-topic`, "'a simple message'", ); await new Promise(res => setTimeout(res, 100)); expect(snsResponse).to.have.property("ResponseMetadata"); expect(snsResponse.ResponseMetadata).to.have.property("RequestId"); expect(snsResponse).to.have.property("MessageId"); }); it("should send a message to a E.164 phone number", async () => { plugin = new ServerlessOfflineSns(createServerless(accountId), {skipCacheInvalidation: true}); const snsAdapter = await plugin.start(); const snsResponse = await snsAdapter.publishToPhoneNumber(`+10000000000`, "{}"); await new Promise(res => setTimeout(res, 100)); expect(snsResponse).to.have.property("ResponseMetadata"); expect(snsResponse.ResponseMetadata).to.have.property("RequestId"); expect(snsResponse).to.have.property("MessageId"); }); it("should error", async () => { plugin = new ServerlessOfflineSns(createServerlessBad(accountId), {}); const snsAdapter = await plugin.start(); const err = await plugin.subscribe("badPong", createServerlessBad(accountId).service.functions.badPong ); expect(err.indexOf("Please ensure the sns configuration is correct")).to.be.greaterThan(-1); await snsAdapter.publish(`arn:aws:sns:us-east-1:${accountId}:test-topic`, "{}"); await new Promise(res => setTimeout(res, 100)); expect(state.getPongs()).to.eq(0); }); it("should use the custom host for subscription urls", async () => { plugin = new ServerlessOfflineSns(createServerless(accountId, "pongHandler", "0.0.0.0"), {skipCacheInvalidation: true}); const snsAdapter = await plugin.start(); const response = await snsAdapter.listSubscriptions(); response.Subscriptions.forEach(sub => { expect(sub.Endpoint.startsWith("http://0.0.0.0:4002")).to.be.true; }); }); it("should use the custom subscribe endpoint for subscription urls", async () => { plugin = new ServerlessOfflineSns(createServerless(accountId, "pongHandler", "0.0.0.0", "anotherHost"), {skipCacheInvalidation: true}); const snsAdapter = await plugin.start(); const response = await snsAdapter.listSubscriptions(); response.Subscriptions.forEach(sub => { expect(sub.Endpoint.startsWith("http://anotherHost:4002")).to.be.true; }); }); it("should unsubscribe", async () => { plugin = new ServerlessOfflineSns(createServerless(accountId), {skipCacheInvalidation: true}); const snsAdapter = await plugin.start(); await plugin.unsubscribeAll(); await snsAdapter.publish(`arn:aws:sns:us-east-1:${accountId}:test-topic`, "{}"); await new Promise(res => setTimeout(res, 100)); expect(state.getPongs()).to.eq(0); }); it("should read env variable", async () => { plugin = new ServerlessOfflineSns(createServerless(accountId, "envHandler"), {skipCacheInvalidation: true}); const snsAdapter = await plugin.start(); await snsAdapter.publish(`arn:aws:sns:us-east-1:${accountId}:test-topic`, "{}"); expect(await state.getResult()).to.eq("MY_VAL"); }); it("should read env variable for function", async () => { plugin = new ServerlessOfflineSns(createServerless(accountId, "envHandler"), {skipCacheInvalidation: true}); const snsAdapter = await plugin.start(); await snsAdapter.publish(`arn:aws:sns:us-east-1:${accountId}:test-topic-2`, "{}"); expect(await state.getResult()).to.eq("TEST"); }); it("should convert pseudo param on load", async () => { plugin = new ServerlessOfflineSns(createServerless(accountId, "pseudoHandler"), {skipCacheInvalidation: true}); const snsAdapter = await plugin.start(); await snsAdapter.publish(`arn:aws:sns:us-east-1:${accountId}:test-topic-3`, "{}"); expect(await state.getResult()).to.eq(`arn:aws:sns:us-east-1:${accountId}:test-topic-3`); }); it("should completely reload the module every time if cache invalidation is enabled", async () => { plugin = new ServerlessOfflineSns(createServerlessCacheInvalidation(accountId), {skipCacheInvalidation: [ /mock\.state/, ]}); const snsAdapter = await plugin.start(); await snsAdapter.publish(`arn:aws:sns:us-east-1:${accountId}:test-topic`, "{}"); await new Promise(res => setTimeout(res, 100)); expect(state.getPongs()).to.eq(1, "wrong number of pongs (first check)"); await snsAdapter.publish(`arn:aws:sns:us-east-1:${accountId}:test-topic`, "{}"); await new Promise(res => setTimeout(res, 100)); expect(state.getPongs(), "wrong number of pongs (second check)").to.eq(1); }); it("should send event to handlers with more than one dot in the filename", async () => { plugin = new ServerlessOfflineSns(createServerlessMultiDot(accountId), {skipCacheInvalidation: true}); const snsAdapter = await plugin.start(); await snsAdapter.publish(`arn:aws:sns:us-east-1:${accountId}:multi-dot-topic`, "{}"); await new Promise(res => setTimeout(res, 100)); expect(state.getPongs()).to.eq(1); }); it("should support async handlers with no callback", async () => { plugin = new ServerlessOfflineSns(createServerless(accountId, "asyncHandler"), {skipCacheInvalidation: true}); const snsAdapter = await plugin.start(); await snsAdapter.publish(`arn:aws:sns:us-east-1:${accountId}:test-topic-async`, "{}"); expect(await state.getResult()).to.eq(`arn:aws:sns:us-east-1:${accountId}:test-topic-async`); expect(await snsAdapter.Deferred).to.eq("{}"); }); it("should not send event when filter policies exist and fail", async () => { plugin = new ServerlessOfflineSns(createServerlessWithFilterPolicies(accountId), {}); const snsAdapter = await plugin.start(); await snsAdapter.publish( `arn:aws:sns:us-east-1:${accountId}:test-topic-policies`, "message with filter params", "raw", { foo: { DataType: "String", StringValue: "no" }, }, ); await new Promise(res => setTimeout(res, 100)); expect(state.getPongs()).to.eq(0); }); it("should send event when filter policies exist and pass", async () => { plugin = new ServerlessOfflineSns(createServerlessWithFilterPolicies(accountId), {skipCacheInvalidation: true}); const snsAdapter = await plugin.start(); await snsAdapter.publish( `arn:aws:sns:us-east-1:${accountId}:test-topic-policies`, "message with filter params", "raw", { foo: { DataType: "String", StringValue: "bar" }, }, ); await new Promise(res => setTimeout(res, 100)); const event = state.getEvent(); expect(event.Records[0].Sns.Message).to.not.be.empty; }); it("should not send event when multiple filter policies exist and the message only contains one", async () => { plugin = new ServerlessOfflineSns(createServerlessWithFilterPolicies(accountId), {}); const snsAdapter = await plugin.start(); await snsAdapter.publish( `arn:aws:sns:us-east-1:${accountId}:test-topic-policies-multiple`, "message with filter params", "raw", { foo: { DataType: "String", StringValue: "bar" }, }, ); await new Promise(res => setTimeout(res, 100)); expect(state.getPongs()).to.eq(0); }); it("should not send event when multiple filter policies exist and the message only satisfies one", async () => { plugin = new ServerlessOfflineSns(createServerlessWithFilterPolicies(accountId), {}); const snsAdapter = await plugin.start(); await snsAdapter.publish( `arn:aws:sns:us-east-1:${accountId}:test-topic-policies-multiple`, "message with filter params", "raw", { foo: { DataType: "String", StringValue: "bar" }, second: { DataType: "String", StringValue: "bar" }, }, ); await new Promise(res => setTimeout(res, 100)); expect(state.getPongs()).to.eq(0); }); it("should not wrap the event when the sub's raw message delivery is true", async () => { const serverless = createServerless(accountId); serverless.service.functions.pong4.events[0].sns["rawMessageDelivery"] = "true"; plugin = new ServerlessOfflineSns(serverless, {skipCacheInvalidation: true}); const snsAdapter = await plugin.start(); await snsAdapter.publish( `arn:aws:sns:us-east-1:${accountId}:test-topic-3`, '{"message":"hello"}', ); await new Promise(res => setTimeout(res, 100)); expect(state.getEvent()).to.eql({ message: "hello" }); }); it("should list topics", async () => { plugin = new ServerlessOfflineSns(createServerless(accountId), {}); const snsAdapter = await plugin.start(); const { Topics } = await snsAdapter.listTopics(); await new Promise(res => setTimeout(res, 100)); const topicArns = Topics.map(topic => topic.TopicArn); expect(Topics.length).to.eq(4); expect(topicArns).to.include(`arn:aws:sns:us-east-1:${accountId}:test-topic`); }); }); const createServerless = (accountId: number, handlerName: string = "pongHandler", host: string = null, subscribeEndpoint = null) => { return { config: { // Since this is not really being used, it should be removed skipCacheInvalidation: true, }, service: { custom: { "serverless-offline-sns": { "debug": true, "port": 4002, "accountId": accountId, "host": host, "sns-subscribe-endpoint": subscribeEndpoint, }, }, provider: { region: "us-east-1", environment: { MY_VAR: "MY_VAL", }, }, functions: { pong: { name: "queue-one", handler: "test/mock/handler." + handlerName, events: [{ sns: `arn:aws:sns:us-east-1:${accountId}:test-topic`, }], }, pong2: { name: "queue-two", handler: "test/mock/handler." + handlerName, events: [{ sns: { arn: `arn:aws:sns:us-east-1:${accountId}:test-topic`, }, }], }, pong3: { name: "this-is-auto-created-when-using-serverless", handler: "test/mock/handler." + handlerName, environment: { MY_VAR: "TEST", }, events: [{ sns: { arn: `arn:aws:sns:us-east-1:${accountId}:test-topic-2`, }, }], }, pong4: { handler: "test/mock/handler." + handlerName, events: [{ sns: { arn: `arn:aws:sns:us-east-1:#{AWS::AccountId}:test-topic-3`, }, }], }, pong5: { handler: "test/mock/handler." + handlerName, events: [{ sns: { arn: `arn:aws:sns:us-east-1:#{AWS::AccountId}:test-topic-async`, }, }], }, }, }, cli: { log: (data) => { if (process.env.DEBUG) { console.log(data); } }, }, }; }; const createServerlessCacheInvalidation = (accountId: number, handlerName: string = "pongHandler", host: string = null) => { return { config: { skipCacheInvalidation: [ /mock\.state/, ], }, service: { custom: { "serverless-offline-sns": { debug: true, port: 4002, accountId, host, invalidateCache: true, }, }, provider: { region: "us-east-1", environment: { MY_VAR: "MY_VAL", }, }, functions: { pong: { handler: "test/mock/handler." + handlerName, events: [{ sns: `arn:aws:sns:us-west-2:${accountId}:test-topic`, }], }, }, }, cli: { log: (data) => { if (process.env.DEBUG) { console.log(data); } }, }, }; }; const createServerlessMultiDot = (accountId: number, handlerName: string = "pongHandler", host: string = null) => { return { config: { skipCacheInvalidation: true, }, service: { custom: { "serverless-offline-sns": { debug: true, port: 4002, accountId, host, }, }, provider: { region: "us-east-1", environment: { MY_VAR: "MY_VAL", }, }, functions: { multiDot: { handler: "test/mock/multi.dot.handler.itsGotDots", events: [{ sns: `arn:aws:sns:us-west-2:${accountId}:multi-dot-topic`, }], }, }, }, cli: { log: (data) => { if (process.env.DEBUG) { console.log(data); } }, }, }; }; const createServerlessBad = (accountId: number) => { return { config: { skipCacheInvalidation: true, }, service: { custom: { "serverless-offline-sns": { debug: true, port: 4002, accountId, }, }, provider: { region: "us-east-1", }, functions: { badPong: { handler: "test/mock/handler.pongHandler", events: [{ sns: { topicArn: `arn:aws:sns:us-east-1:${accountId}:test-topic`, }, }], }, }, }, cli: { log: (data) => { if (process.env.DEBUG) { console.log(data); } }, }, }; }; const createServerlessWithFilterPolicies = (accountId: number, handlerName: string = "pongHandler", host: string = null, subscribeEndpoint = null) => { return { config: { skipCacheInvalidation: true, }, service: { custom: { "serverless-offline-sns": { "debug": true, "port": 4002, "accountId": accountId, "host": host, "sns-subscribe-endpoint": subscribeEndpoint, }, }, provider: { region: "us-east-1", environment: { MY_VAR: "MY_VAL", }, }, functions: { pong: { name: "some-name", handler: "test/mock/handler." + handlerName, events: [{ sns: { topicName: "test-topic-policies", displayName: "test-topic-policies", filterPolicy: { foo: ["bar", "blah"], }, }, }], }, pong2: { name: "some-name2", handler: "test/mock/handler." + handlerName, events: [{ sns: { topicName: "test-topic-policies-multiple", displayName: "test-topic-policies-multiple", filterPolicy: { foo: ["bar", "blah"], second: ["policy"], }, }, }], }, }, }, cli: { log: (data) => { if (process.env.DEBUG) { console.log(data); } }, }, }; };