UNPKG

serverless-offline-sns

Version:

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

1,048 lines (983 loc) 33.3 kB
import ServerlessOfflineSns from "../../src/index.js"; import { expect } from "chai"; import * as handler from "../mock/handler.js"; import * as multiDotHandler from "../mock/multi.dot.handler.js"; import * as state from "../mock/mock.state.js"; import { SQSClient, SendMessageCommand, GetQueueUrlCommand } from "@aws-sdk/client-sqs"; import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda"; import { SNSClient, SubscribeCommand, PublishBatchCommand } from "@aws-sdk/client-sns"; import { mockClient } from 'aws-sdk-client-mock'; import http from "http"; import type { AddressInfo } from "net"; let plugin; const lambdaMock = mockClient(LambdaClient); let functionRegistry: Map<string, (...args: unknown[]) => unknown> = new Map(); function buildFunctionRegistry(sl: any, handlerFn: (...args: unknown[]) => unknown) { const registry = new Map<string, (...args: unknown[]) => unknown>(); const service: string = sl.service.service ?? ""; const stage: string = sl.service.provider.stage ?? "dev"; for (const fnKey of Object.keys(sl.service.functions)) { registry.set(`${service}-${stage}-${fnKey}`, handlerFn); } return registry; } function createPlugin(sl: any, handlerFn: (...args: unknown[]) => unknown = handler.pongHandler) { functionRegistry = buildFunctionRegistry(sl, handlerFn); return new ServerlessOfflineSns(sl, {}); } describe("test", () => { let accountId; beforeEach(() => { accountId = Math.floor(Math.random() * (100000000 - 1)); handler.resetPongs(); state.resetEvent(); state.resetResult(); functionRegistry = new Map(); lambdaMock.reset(); lambdaMock.on(InvokeCommand).callsFake(async (input: { FunctionName?: string; Payload?: Uint8Array }) => { const event = input.Payload ? JSON.parse(new TextDecoder().decode(input.Payload)) : {}; const handlerFn = functionRegistry.get(input.FunctionName ?? ""); let handlerResult: unknown = null; if (handlerFn) { handlerResult = await new Promise((resolve) => { const maybePromise = handlerFn(event, {}, (_err: unknown, result: unknown) => resolve(result)); if (maybePromise instanceof Promise) maybePromise.then(resolve).catch(() => resolve(null)); }); } return { Payload: new TextEncoder().encode(JSON.stringify(handlerResult)), StatusCode: 200 }; }); }); afterEach(async () => { await plugin.stop(); }); it("should start on offline start", async () => { plugin = new ServerlessOfflineSns(createServerless(accountId), { }); 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), { }); 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 = createPlugin(createServerless(accountId)); 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 = createPlugin(createServerless(accountId)); 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 = createPlugin(createServerless(accountId)); 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 = createPlugin(createServerless(accountId)); 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 = createPlugin(createServerless(accountId)); 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("$metadata"); expect(snsResponse.$metadata).to.have.property("requestId"); }); it("should send a message to a E.164 phone number", async () => { plugin = createPlugin(createServerless(accountId)); const snsAdapter = await plugin.start(); const snsResponse = await snsAdapter.publishToPhoneNumber( `+10000000000`, "{}" ); await new Promise((res) => setTimeout(res, 100)); expect(snsResponse).to.have.property("$metadata"); expect(snsResponse.$metadata).to.have.property("requestId"); }); it("should error", async () => { plugin = createPlugin(createServerlessBad(accountId)); const snsAdapter = await plugin.start(); const err = await plugin.subscribe( plugin.serverless, "badPong", createServerlessBad(accountId).service.functions.badPong, plugin.location ); 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 = createPlugin( createServerless(accountId, "pongHandler", "0.0.0.0") ); const snsAdapter = await plugin.start(); const response = await snsAdapter.listSubscriptions(); response.Subscriptions .filter((sub) => sub.Protocol === "http") .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 = createPlugin( createServerless(accountId, "pongHandler", "0.0.0.0", "anotherHost") ); const snsAdapter = await plugin.start(); const response = await snsAdapter.listSubscriptions(); response.Subscriptions .filter((sub) => sub.Protocol === "http") .forEach((sub) => { expect(sub.Endpoint.startsWith("http://anotherHost:4002")).to.be.true; }); }); it("should unsubscribe", async () => { plugin = createPlugin(createServerless(accountId)); 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 retry HTTP delivery on failure and eventually succeed", async () => { let requestCount = 0; const testServer = http.createServer((req, res) => { requestCount++; let body = ""; req.on("data", (chunk) => (body += chunk)); req.on("end", () => { if (requestCount <= 2) { res.writeHead(503); res.end("fail"); } else { res.writeHead(200); res.end("ok"); } }); }); await new Promise<void>((resolve) => testServer.listen(0, "127.0.0.1", resolve)); const port = (testServer.address() as AddressInfo).port; const sl = createServerless(accountId); const snsConfig = sl.service.custom["serverless-offline-sns"] as Record<string, unknown>; snsConfig.retry = 2; snsConfig["retry-interval"] = 0; plugin = createPlugin(sl); const snsAdapter = await plugin.start(); const snsClient = new SNSClient({ endpoint: "http://127.0.0.1:4002", region: "us-east-1", credentials: { accessKeyId: "local", secretAccessKey: "local" }, }); await snsClient.send(new SubscribeCommand({ TopicArn: `arn:aws:sns:us-east-1:${accountId}:test-topic`, Protocol: "http", Endpoint: `http://127.0.0.1:${port}/`, })); await snsAdapter.publish(`arn:aws:sns:us-east-1:${accountId}:test-topic`, "retry-test"); await new Promise((res) => setTimeout(res, 500)); await new Promise<void>((resolve) => testServer.close(() => resolve())); expect(requestCount).to.equal(3); }); it("should read env variable", async () => { plugin = createPlugin( createServerless(accountId, "envHandler"), handler.envHandler ); 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 = createPlugin( createServerless(accountId, "envHandler"), handler.envHandler ); 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 = createPlugin( createServerless(accountId, "pseudoHandler"), handler.pseudoHandler ); 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 send event to handlers with more than one dot in the filename", async () => { plugin = createPlugin(createServerlessMultiDot(accountId), multiDotHandler.itsGotDots); 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 commonjs/default handlers', async () => { plugin = createPlugin( createServerless(accountId, "defaultExportHandler"), handler.default.defaultExportHandler ); 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 support async handlers with no callback', async () => { plugin = createPlugin( createServerless(accountId, "asyncHandler"), handler.asyncHandler ); const snsAdapter = await plugin.start(); await snsAdapter.publish( `arn:aws:sns:us-east-1:${accountId}:test-topic-async`, "{}" ); await new Promise((res) => setTimeout(res, 100)); 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 = createPlugin( 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 = createPlugin( 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: "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 = createPlugin( 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 = createPlugin( 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 = createPlugin(serverless); 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 = createPlugin(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(5); expect(topicArns).to.include( `arn:aws:sns:us-east-1:${accountId}:test-topic` ); }); it("should subscribe", async () => { const sqsMock = mockClient(SQSClient); plugin = createPlugin( createServerless(accountId, "envHandler") ); const snsAdapter = await plugin.start(); await plugin.subscribeAll(); await snsAdapter.publish( `arn:aws:sns:us-east-1:${accountId}:topic-pinging`, "{}" ); await new Promise((res) => setTimeout(res, 100)); const sqsSendArgs = sqsMock.send.args; expect(sqsMock.send.calledOnce).to.be.true; expect(sqsSendArgs[0][0].input).to.be.deep.equals({ QueueUrl: "http://127.0.0.1:4002/queue/pong6", MessageBody: "{}", MessageAttributes: {}, }); sqsMock.restore(); }); it("should route CloudFormation Protocol:sqs subscription through subscribeQueue without requiring a Lambda function", async () => { const sqsMock = mockClient(SQSClient); plugin = createPlugin(createServerlessWithStandaloneSqsSubscription(accountId)); const snsAdapter = await plugin.start(); await plugin.subscribeAll(); await snsAdapter.publish( `arn:aws:sns:us-east-1:${accountId}:topic-standalone`, "sqs-only-message" ); await new Promise((res) => setTimeout(res, 100)); const sqsSendArgs = sqsMock.send.args; expect(sqsMock.send.calledOnce).to.be.true; const sentBody = JSON.parse((sqsSendArgs[0][0].input as { MessageBody: string }).MessageBody); expect(sentBody).to.have.property("Message", "sqs-only-message"); expect(sentBody).to.have.property("Type", "Notification"); sqsMock.restore(); }); it("should not send event when MessageBody filter policy does not match", async () => { plugin = createPlugin(createServerlessWithMessageBodyFilterPolicies(accountId)); const snsAdapter = await plugin.start(); await snsAdapter.publish( `arn:aws:sns:us-east-1:${accountId}:test-topic-body-policies`, JSON.stringify({ eventType: "order.deleted" }) ); await new Promise((res) => setTimeout(res, 100)); expect(state.getPongs()).to.eq(0); }); it("should send event when MessageBody filter policy matches", async () => { plugin = createPlugin(createServerlessWithMessageBodyFilterPolicies(accountId)); const snsAdapter = await plugin.start(); await snsAdapter.publish( `arn:aws:sns:us-east-1:${accountId}:test-topic-body-policies`, JSON.stringify({ eventType: "order.created" }) ); await new Promise((res) => setTimeout(res, 100)); const event = state.getEvent(); expect(event.Records[0].Sns.Message).to.include("order.created"); }); it("should not send event when MessageBody filter policy key is missing from body", async () => { plugin = createPlugin(createServerlessWithMessageBodyFilterPolicies(accountId)); const snsAdapter = await plugin.start(); await snsAdapter.publish( `arn:aws:sns:us-east-1:${accountId}:test-topic-body-policies`, JSON.stringify({ otherKey: "order.created" }) ); await new Promise((res) => setTimeout(res, 100)); expect(state.getPongs()).to.eq(0); }); it("should not send event when message body is not valid JSON and MessageBody filter policy is set", async () => { plugin = createPlugin(createServerlessWithMessageBodyFilterPolicies(accountId)); const snsAdapter = await plugin.start(); await snsAdapter.publish( `arn:aws:sns:us-east-1:${accountId}:test-topic-body-policies`, "not-json" ); await new Promise((res) => setTimeout(res, 100)); expect(state.getPongs()).to.eq(0); }); it("should deliver SNS envelope as MessageBody to SQS in non-raw mode", async () => { const sqsMock = mockClient(SQSClient); sqsMock.on(GetQueueUrlCommand).resolves({ QueueUrl: "http://127.0.0.1:4002/queue/pong6" }); plugin = createPlugin( createServerlessWithNonRawSqsSubscription(accountId) ); const snsAdapter = await plugin.start(); await plugin.subscribeAll(); const message = "hello-non-raw"; await snsAdapter.publish( `arn:aws:sns:us-east-1:${accountId}:topic-non-raw`, message ); await new Promise((res) => setTimeout(res, 100)); const sqsSendArgs = sqsMock.send.args; expect(sqsMock.send.calledOnce).to.be.true; const sentBody = JSON.parse((sqsSendArgs[0][0].input as { MessageBody: string }).MessageBody); expect(sentBody).to.have.property("Message", message); expect(sentBody).to.have.property("Type", "Notification"); expect(sentBody).to.have.property("TopicArn").that.includes("topic-non-raw"); sqsMock.restore(); }); it("should confirm subscription via SDK without fetching SubscribeURL", async () => { let confirmedToken = ""; const snsServer = http.createServer((req, res) => { // Handle all SNS API calls (createTopic, subscribe, confirmSubscription, etc.) let body = ""; req.on("data", (chunk) => (body += chunk)); req.on("end", () => { const action = new URLSearchParams(body).get("Action") ?? ""; const topicName = new URLSearchParams(body).get("Name") ?? "topic"; if (action === "ConfirmSubscription") { confirmedToken = new URLSearchParams(body).get("Token") ?? ""; } const xml = action === "CreateTopic" ? `<CreateTopicResponse><CreateTopicResult><TopicArn>arn:aws:sns:us-east-1:${accountId}:${topicName}</TopicArn></CreateTopicResult></CreateTopicResponse>` : action === "ConfirmSubscription" ? `<ConfirmSubscriptionResponse><ConfirmSubscriptionResult><SubscriptionArn>arn:aws:sns:us-east-1:${accountId}:${topicName}:uuid</SubscriptionArn></ConfirmSubscriptionResult></ConfirmSubscriptionResponse>` : `<${action}Response></${action}Response>`; res.writeHead(200, { "Content-Type": "text/xml" }); res.end(xml); }); }); await new Promise<void>((resolve) => snsServer.listen(0, "127.0.0.1", resolve)); const snsPort = (snsServer.address() as AddressInfo).port; const sl = createServerless(accountId); (sl.service.custom["serverless-offline-sns"] as Record<string, unknown>)["sns-endpoint"] = `http://127.0.0.1:${snsPort}`; plugin = createPlugin(sl); await plugin.start(); const response = await fetch("http://127.0.0.1:4002/queue-one", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ SubscribeURL: "http://evil.com/confirm?token=abc123", Token: "abc123", TopicArn: `arn:aws:sns:us-east-1:${accountId}:topic-one`, }), }); await new Promise<void>((resolve) => snsServer.close(() => resolve())); expect(response.status).to.equal(200); expect(confirmedToken).to.equal("abc123"); }); it("should handle empty resource definition", async () => { const serverless = createServerless(accountId); serverless.service.resources = undefined; plugin = createPlugin(serverless); await plugin.start(); }); it("should deliver all entries via PublishBatch to subscribers", async () => { const sqsMock = mockClient(SQSClient); plugin = createPlugin(createServerless(accountId, "envHandler")); const snsAdapter = await plugin.start(); await plugin.subscribeAll(); const snsClient = new SNSClient({ credentials: { accessKeyId: "AKID", secretAccessKey: "SECRET" }, endpoint: "http://127.0.0.1:4002", region: "us-east-1", }); await snsClient.send( new PublishBatchCommand({ TopicArn: `arn:aws:sns:us-east-1:${accountId}:topic-pinging`, PublishBatchRequestEntries: [ { Id: "1", Message: "hello" }, { Id: "2", Message: "world" }, ], }) ); await new Promise((res) => setTimeout(res, 100)); expect(sqsMock.send.callCount).to.equal(2); sqsMock.restore(); }); it("should handle messageGroupId", async () => { const sqsMock = mockClient(SQSClient); plugin = createPlugin( createServerless(accountId, "envHandler") ); const snsAdapter = await plugin.start(); await plugin.subscribeAll(); await snsAdapter.publish( `arn:aws:sns:us-east-1:${accountId}:topic-pinging`, "{}", "", {}, "", "messageGroupId-here" ); await new Promise((res) => setTimeout(res, 100)); const sqsSendArgs = sqsMock.send.args; expect(sqsMock.send.calledOnce).to.be.true; expect(sqsSendArgs[0][0].input).to.be.deep.equals({ QueueUrl: "http://127.0.0.1:4002/queue/pong6", MessageBody: "{}", MessageAttributes: {}, MessageGroupId: "messageGroupId-here", }); sqsMock.restore(); }); }); const createServerless = ( accountId: number, handlerName: string = "pongHandler", host: string | null = null, subscribeEndpoint: string | null = null ) => { return { config: {}, service: { custom: { "serverless-offline-sns": { debug: true, port: 4002, accountId, 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`, }, }, ], }, pong6: { handler: "test/mock/handler." + handlerName, events: [ { sqs: { arn: { "Fn::GetAtt": ["pong6", "Arn"], }, }, }, ], }, }, resources: { Resources: { pong6QueueSubscription: { Type: "AWS::SNS::Subscription", Properties: { Protocol: "sqs", Endpoint: { "Fn::GetAtt": ["pong6", "Arn"], }, RawMessageDelivery: "true", TopicArn: { Ref: "pinging", }, }, }, pong6: { Type: "AWS::SQS::Queue", Properties: { QueueName: "pong6", }, }, pinging: { Type: "AWS::SNS::Topic", Properties: { TopicName: "topic-pinging", }, }, }, }, }, cli: { log: (data) => { if (process.env.DEBUG) { console.log(data); } }, }, }; }; const createServerlessMultiDot = ( accountId: number, handlerName: string = "pongHandler", host = null ) => { return { config: {}, 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`, }, ], }, }, resources: {}, }, cli: { log: (data) => { if (process.env.DEBUG) { console.log(data); } }, }, }; }; const createServerlessBad = (accountId: number) => { return { config: {}, 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`, }, }, ], }, }, resources: {}, }, cli: { log: (data) => { if (process.env.DEBUG) { console.log(data); } }, }, }; }; const createServerlessWithStandaloneSqsSubscription = (accountId: number) => { return { config: {}, service: { custom: { "serverless-offline-sns": { debug: true, port: 4002, accountId }, }, provider: { region: "us-east-1", environment: {} }, // No function is directly triggered by SNS — the Lambda is wired to SQS separately functions: {}, resources: { Resources: { standaloneQueueSubscription: { Type: "AWS::SNS::Subscription", Properties: { Protocol: "sqs", Endpoint: { "Fn::GetAtt": ["standaloneQueue", "Arn"] }, TopicArn: { Ref: "standaloneTopic" }, }, }, standaloneQueue: { Type: "AWS::SQS::Queue", Properties: { QueueName: "standalone-queue" }, }, standaloneTopic: { Type: "AWS::SNS::Topic", Properties: { TopicName: "topic-standalone" }, }, }, }, }, cli: { log: (data) => { if (process.env.DEBUG) console.log(data); } }, }; }; const createServerlessWithMessageBodyFilterPolicies = (accountId: number) => { return { config: {}, service: { custom: { "serverless-offline-sns": { debug: true, port: 4002, accountId }, }, provider: { region: "us-east-1", environment: {} }, functions: { pong: { name: "body-policy-fn", handler: "test/mock/handler.pongHandler", events: [ { sns: { topicName: "test-topic-body-policies", filterPolicyScope: "MessageBody", filterPolicy: { eventType: ["order.created", "order.updated"] }, }, }, ], }, }, resources: {}, }, cli: { log: (data) => { if (process.env.DEBUG) console.log(data); } }, }; }; const createServerlessWithNonRawSqsSubscription = (accountId: number) => { return { config: {}, service: { custom: { "serverless-offline-sns": { debug: true, port: 4002, accountId, }, }, provider: { region: "us-east-1", environment: {} }, functions: { pong6: { handler: "test/mock/handler.pongHandler", events: [{ sqs: { arn: { "Fn::GetAtt": ["pong6", "Arn"] } } }], }, }, resources: { Resources: { pong6QueueSubscription: { Type: "AWS::SNS::Subscription", Properties: { Protocol: "sqs", Endpoint: { "Fn::GetAtt": ["pong6", "Arn"] }, TopicArn: { Ref: "nonRawTopic" }, }, }, pong6: { Type: "AWS::SQS::Queue", Properties: { QueueName: "pong6" }, }, nonRawTopic: { Type: "AWS::SNS::Topic", Properties: { TopicName: "topic-non-raw" }, }, }, }, }, cli: { log: (data) => { if (process.env.DEBUG) console.log(data); } }, }; }; const createServerlessWithFilterPolicies = ( accountId: number, handlerName: string = "pongHandler", host = null, subscribeEndpoint = null ) => { return { config: {}, service: { custom: { "serverless-offline-sns": { debug: true, port: 4002, accountId, 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"], }, }, }, ], }, }, resources: {}, }, cli: { log: (data) => { if (process.env.DEBUG) { console.log(data); } }, }, }; };