-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update facia-responder to notify SNS queue on publish succeed/fail
- Loading branch information
1 parent
e550657
commit a52cb9f
Showing
5 changed files
with
305 additions
and
152 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { mandatoryParameter } from 'lib/recipes-data/src/lib/config'; | ||
|
||
export const faciaPublicationStatusTopicArn = mandatoryParameter("FACIA_PUBLISH_STATUS_TOPIC_ARN"); | ||
export const faciaPublicationStatusRoleArn = mandatoryParameter("FACIA_PUBLISH_STATUS_ROLE_ARN"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { SNS } from '@aws-sdk/client-sns'; | ||
import { TemporaryCredentials } from 'aws-sdk'; | ||
import { faciaPublicationStatusRoleArn, faciaPublicationStatusTopicArn } from './config'; | ||
|
||
// The publication status event we send over SNS. | ||
type PublicationStatusEventEnvelope = { | ||
event: PublicationStatusEvent; | ||
}; | ||
|
||
export type PublicationStatusEvent = { | ||
edition: string; | ||
issueDate: string; | ||
version: string; | ||
status: | ||
| 'Started' | ||
| 'Proofing' | ||
| 'Proofed' | ||
| 'Publishing' | ||
| 'Published' | ||
| 'Failed' | ||
| 'PostProcessing'; | ||
message: string; | ||
timestamp: number; | ||
}; | ||
|
||
export async function notifyFaciaTool( | ||
event: PublicationStatusEvent, | ||
): Promise<void> { | ||
const payload = JSON.stringify({ event } as PublicationStatusEventEnvelope); | ||
|
||
console.log(`Publishing publish event to SNS: ${payload}`); | ||
|
||
const sns = new SNS({ | ||
region: 'eu-west-1', | ||
credentials: new TemporaryCredentials({ | ||
RoleArn: faciaPublicationStatusRoleArn, | ||
RoleSessionName: 'front-assume-role-access-for-sns', | ||
}), | ||
}); | ||
|
||
const sendStatus = await sns.publish({ | ||
TopicArn: faciaPublicationStatusTopicArn, | ||
Message: payload, | ||
}); | ||
|
||
console.log(`SNS status publish response: ${JSON.stringify(sendStatus)}`); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
export const validMessageContent = { | ||
id: 'D9AEEA41-F8DB-4FC8-A0DA-275571EA7331', | ||
edition: 'feast-northern-hemisphere', | ||
version: 'v1', | ||
issueDate: '2024-01-02', | ||
fronts: { | ||
'all-recipes': [ | ||
{ | ||
id: 'd353e2de-1a65-45de-85ca-d229bc1fafad', | ||
title: 'Dish of the day', | ||
body: '', | ||
items: [ | ||
{ | ||
recipe: { | ||
id: '14129325', | ||
}, | ||
}, | ||
], | ||
}, | ||
], | ||
'meat-free': [ | ||
{ | ||
id: 'fa6ccb35-926b-4eff-b3a9-5d0ca88387ff', | ||
title: 'Dish of the day', | ||
body: '', | ||
items: [ | ||
{ | ||
recipe: { | ||
id: '14132263', | ||
}, | ||
}, | ||
], | ||
}, | ||
], | ||
}, | ||
}; | ||
|
||
export const validMessage = { | ||
Message: JSON.stringify(validMessageContent), | ||
}; | ||
|
||
export const brokenMessage = { | ||
...validMessage, | ||
Message: JSON.stringify({ | ||
...validMessageContent, | ||
issueDate: 'dfsdfsjk', | ||
}), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,131 +1,150 @@ | ||
import {deployCurationData} from "@recipes-api/lib/recipes-data"; | ||
import {ZodError} from "zod"; | ||
import {handler} from "./main"; | ||
import { deployCurationData } from '@recipes-api/lib/recipes-data'; | ||
import { ZodError } from 'zod'; | ||
import { notifyFaciaTool } from './facia-notifications'; | ||
import { | ||
brokenMessage, | ||
validMessage, | ||
validMessageContent, | ||
} from './fixtures/sns'; | ||
import { handler } from './main'; | ||
|
||
jest.mock("@recipes-api/lib/recipes-data", ()=>({ | ||
deployCurationData: jest.fn() | ||
jest.mock('@recipes-api/lib/recipes-data', () => ({ | ||
deployCurationData: jest.fn(), | ||
})); | ||
|
||
const message = { | ||
id: "D9AEEA41-F8DB-4FC8-A0DA-275571EA7331", | ||
edition: "feast-northern-hemisphere", | ||
version: "v1", | ||
issueDate: "2024-01-02", | ||
fronts: { | ||
"all-recipes": [ | ||
{ | ||
"id": "d353e2de-1a65-45de-85ca-d229bc1fafad", | ||
"title": "Dish of the day", | ||
"body": "", | ||
"items": [ | ||
{ | ||
"recipe": { | ||
"id": "14129325" | ||
} | ||
} | ||
] | ||
} | ||
], | ||
"meat-free": [ | ||
{ | ||
"id": "fa6ccb35-926b-4eff-b3a9-5d0ca88387ff", | ||
"title": "Dish of the day", | ||
"body": "", | ||
"items": [ | ||
{ | ||
"recipe": { | ||
"id": "14132263" | ||
} | ||
} | ||
] | ||
} | ||
] | ||
} | ||
}; | ||
|
||
const rawContent = { | ||
Message: JSON.stringify(message) | ||
} | ||
|
||
describe("main", ()=>{ | ||
it("should publish the content it was given", async ()=>{ | ||
const rec = { | ||
Records: [ | ||
{ | ||
eventSource: "sqs", | ||
awsRegion: "xx-north-n", | ||
messageId: "BDB66A64-F095-4F4D-9B6A-135173E262A5", | ||
body: JSON.stringify(rawContent) | ||
} | ||
] | ||
}; | ||
|
||
// @ts-ignore | ||
await handler(rec, null, null); | ||
|
||
const importCurationDataMock = deployCurationData as jest.Mock; | ||
expect(importCurationDataMock.mock.calls.length).toEqual(2); | ||
expect(importCurationDataMock.mock.calls[0][0]).toEqual(JSON.stringify(message.fronts["all-recipes"])); | ||
expect(importCurationDataMock.mock.calls[0][1]).toEqual(message.edition); | ||
expect(importCurationDataMock.mock.calls[0][2]).toEqual("all-recipes"); | ||
expect(importCurationDataMock.mock.calls[0][3]).toEqual(new Date(2024, 0, 2)); | ||
|
||
expect(importCurationDataMock.mock.calls[1][0]).toEqual(JSON.stringify(message.fronts["meat-free"])); | ||
expect(importCurationDataMock.mock.calls[1][1]).toEqual(message.edition); | ||
expect(importCurationDataMock.mock.calls[1][2]).toEqual("meat-free"); | ||
expect(importCurationDataMock.mock.calls[1][3]).toEqual(new Date(2024, 0, 2)); | ||
}); | ||
|
||
|
||
it("should not accept valid json that does not match schema", async ()=>{ | ||
const brokenContent = { | ||
...rawContent, | ||
Message: JSON.stringify({ | ||
...message, | ||
issueDate: 'dfsdfsjk', | ||
}), | ||
jest.mock('./config', () => ({ | ||
faciaPublicationStatusTopicArn: 'config-param', | ||
faciaPublicationStatusRoleArn: 'config-param', | ||
})); | ||
|
||
jest.mock('./facia-notifications', () => ({ | ||
notifyFaciaTool: jest.fn(), | ||
})); | ||
|
||
const importCurationDataMock = deployCurationData as jest.Mock; | ||
const notifyFaciaToolMock = notifyFaciaTool as jest.Mock; | ||
|
||
describe('main', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should publish the content it was given', async () => { | ||
const rec = { | ||
Records: [ | ||
{ | ||
eventSource: 'sqs', | ||
awsRegion: 'xx-north-n', | ||
messageId: 'BDB66A64-F095-4F4D-9B6A-135173E262A5', | ||
body: JSON.stringify(validMessage), | ||
}, | ||
], | ||
}; | ||
|
||
const rec = { | ||
Records: [ | ||
{ | ||
eventSource: "sqs", | ||
awsRegion: "xx-north-n", | ||
messageId: "BDB66A64-F095-4F4D-9B6A-135173E262A5", | ||
body: JSON.stringify(brokenContent) | ||
} | ||
] | ||
}; | ||
|
||
const expectedError = new ZodError([ | ||
{ | ||
"code": "custom", | ||
"fatal": true, | ||
"path": [ | ||
"issueDate" | ||
], | ||
"message": "Invalid input" | ||
} | ||
]) | ||
|
||
// @ts-ignore | ||
await expect(()=>handler(rec, null, null)).rejects.toEqual(expectedError); | ||
}); | ||
|
||
it("should not accept invalid json", async ()=>{ | ||
const rec = { | ||
Records: [ | ||
{ | ||
eventSource: "sqs", | ||
awsRegion: "xx-north-n", | ||
messageId: "BDB66A64-F095-4F4D-9B6A-135173E262A5", | ||
body: "blahblahblahthisisnotjson" | ||
} | ||
] | ||
}; | ||
|
||
const expectedError = new SyntaxError("Unexpected token b in JSON at position 0"); | ||
// @ts-ignore | ||
await expect(()=>handler(rec, null, null)).rejects.toEqual(expectedError); | ||
}) | ||
}) | ||
// @ts-ignore | ||
await handler(rec, null, null); | ||
|
||
expect(importCurationDataMock.mock.calls.length).toEqual(2); | ||
expect(importCurationDataMock.mock.calls[0][0]).toEqual( | ||
JSON.stringify(validMessageContent.fronts['all-recipes']), | ||
); | ||
expect(importCurationDataMock.mock.calls[0][1]).toEqual( | ||
validMessageContent.edition, | ||
); | ||
expect(importCurationDataMock.mock.calls[0][2]).toEqual('all-recipes'); | ||
expect(importCurationDataMock.mock.calls[0][3]).toEqual( | ||
new Date(2024, 0, 2), | ||
); | ||
|
||
expect(importCurationDataMock.mock.calls[1][0]).toEqual( | ||
JSON.stringify(validMessageContent.fronts['meat-free']), | ||
); | ||
expect(importCurationDataMock.mock.calls[1][1]).toEqual( | ||
validMessageContent.edition, | ||
); | ||
expect(importCurationDataMock.mock.calls[1][2]).toEqual('meat-free'); | ||
expect(importCurationDataMock.mock.calls[1][3]).toEqual( | ||
new Date(2024, 0, 2), | ||
); | ||
|
||
const notifyFaciaToolMock = notifyFaciaTool as jest.Mock; | ||
expect(notifyFaciaToolMock.mock.calls[0][0]).toMatchObject({ | ||
edition: 'feast-northern-hemisphere', | ||
issueDate: '2024-01-02', | ||
message: 'This issue has been published', | ||
status: 'Published', | ||
version: 'v1', | ||
}); | ||
}); | ||
|
||
it('should not accept valid json that does not match schema', async () => { | ||
const rec = { | ||
Records: [ | ||
{ | ||
eventSource: 'sqs', | ||
awsRegion: 'xx-north-n', | ||
messageId: 'BDB66A64-F095-4F4D-9B6A-135173E262A5', | ||
body: JSON.stringify(brokenMessage), | ||
}, | ||
], | ||
}; | ||
|
||
const expectedError = new ZodError([ | ||
{ | ||
code: 'custom', | ||
fatal: true, | ||
path: ['issueDate'], | ||
message: 'Invalid input', | ||
}, | ||
]); | ||
|
||
// @ts-ignore | ||
await expect(() => handler(rec, null, null)).rejects.toEqual(expectedError); | ||
}); | ||
|
||
it('should not accept invalid json', async () => { | ||
const rec = { | ||
Records: [ | ||
{ | ||
eventSource: 'sqs', | ||
awsRegion: 'xx-north-n', | ||
messageId: 'BDB66A64-F095-4F4D-9B6A-135173E262A5', | ||
body: 'blahblahblahthisisnotjson', | ||
}, | ||
], | ||
}; | ||
|
||
const expectedError = new SyntaxError( | ||
'Unexpected token b in JSON at position 0', | ||
); | ||
// @ts-ignore | ||
await expect(() => handler(rec, null, null)).rejects.toEqual(expectedError); | ||
}); | ||
|
||
it('should notify when the deploy fails', async () => { | ||
const rec = { | ||
Records: [ | ||
{ | ||
eventSource: 'sqs', | ||
awsRegion: 'xx-north-n', | ||
messageId: 'BDB66A64-F095-4F4D-9B6A-135173E262A5', | ||
body: JSON.stringify(validMessage), | ||
}, | ||
], | ||
}; | ||
|
||
const expectedError = new Error('Error deploying content'); | ||
importCurationDataMock.mockRejectedValueOnce(expectedError); | ||
|
||
// @ts-ignore | ||
await handler(rec, null, null); | ||
|
||
expect(notifyFaciaToolMock.mock.calls[0][0]).toMatchObject({ | ||
edition: 'feast-northern-hemisphere', | ||
issueDate: '2024-01-02', | ||
message: 'Failed to publish this issue. Error: Error deploying content', | ||
status: 'Failed', | ||
version: 'v1', | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.