-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: send a Slack notification when flow status is updated on produc…
…tion (#3535)
- Loading branch information
1 parent
7eb7c9d
commit 602c4c9
Showing
8 changed files
with
280 additions
and
39 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,46 @@ | ||
import SlackNotify from "slack-notify"; | ||
import { z } from "zod"; | ||
import { ServerError } from "../../errors/index.js"; | ||
import { ValidatedRequestHandler } from "../../shared/middleware/validate.js"; | ||
|
||
interface SendSlackNotificationResponse { | ||
message: string; | ||
} | ||
|
||
export const slackNotificationSchema = z.object({ | ||
body: z.object({ | ||
message: z.string(), | ||
}), | ||
}); | ||
|
||
export type SendSlackNotificationController = ValidatedRequestHandler< | ||
typeof slackNotificationSchema, | ||
SendSlackNotificationResponse | ||
>; | ||
|
||
export const sendSlackNotificationController: SendSlackNotificationController = | ||
async (_req, res, next) => { | ||
const { message } = res.locals.parsedReq.body; | ||
|
||
const isProduction = process.env.APP_ENVIRONMENT === "production"; | ||
if (!isProduction) { | ||
return res.status(200).send({ | ||
message: `Staging environment, skipping Slack notification. Message "${message}"`, | ||
}); | ||
} | ||
|
||
try { | ||
const slack = SlackNotify(process.env.SLACK_WEBHOOK_URL!); | ||
await slack.send(message); | ||
|
||
return res.status(200).send({ | ||
message: `Sent Slack notification. Message "${message}"`, | ||
}); | ||
} catch (error) { | ||
return next( | ||
new ServerError({ | ||
message: `Failed to send Slack notification. Error ${error}`, | ||
}), | ||
); | ||
} | ||
}; |
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,37 @@ | ||
openapi: 3.1.0 | ||
info: | ||
title: Plan✕ API | ||
version: 0.1.0 | ||
components: | ||
schemas: | ||
SendSlackNotificationDirect: | ||
type: object | ||
properties: | ||
message: string | ||
responses: | ||
SendSlackNotificationDirect: | ||
content: | ||
application/json: | ||
schema: | ||
type: object | ||
properties: | ||
message: string | ||
paths: | ||
/send-slack-notification: | ||
post: | ||
summary: Send a Slack notification | ||
description: Allows authenticated users to trigger a Slack notification when they update settings in the Planx Editor | ||
tags: ["misc"] | ||
security: | ||
- bearerAuth: [] | ||
requestBody: | ||
required: true | ||
content: | ||
application/json: | ||
schema: | ||
$ref: "#/components/schemas/SendSlackNotificationDirect" | ||
responses: | ||
"200": | ||
$ref: "#/components/responses/SendSlackNotificationDirect" | ||
"500": | ||
$ref: "#/components/responses/ErrorMessage" |
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,97 @@ | ||
import SlackNotify from "slack-notify"; | ||
import supertest from "supertest"; | ||
|
||
import app from "../../server.js"; | ||
import { authHeader } from "../../tests/mockJWT.js"; | ||
|
||
const validAuth = authHeader({ role: "teamEditor" }); | ||
const validBody = { | ||
message: ":emoji: *council-a/find-out-if* is now online", | ||
}; | ||
const invalidBody = { | ||
wrong: "message", | ||
}; | ||
|
||
const mockSend = jest.fn(); | ||
jest.mock<typeof SlackNotify>("slack-notify", () => | ||
jest.fn().mockImplementation(() => { | ||
return { send: mockSend }; | ||
}), | ||
); | ||
|
||
afterEach(jest.clearAllMocks); | ||
|
||
it("returns an error if authorization headers are not set", async () => { | ||
await supertest(app) | ||
.post("/send-slack-notifcation") | ||
.send(validBody) | ||
.expect(404); | ||
}); | ||
|
||
it("returns an error if the user does not have the correct role", async () => { | ||
await supertest(app) | ||
.post("/send-slack-notification") | ||
.send(validBody) | ||
.set(authHeader({ role: "teamViewer" })) | ||
.expect(403); | ||
}); | ||
|
||
it("returns an error if a message isn't provided in the request body", async () => { | ||
await supertest(app) | ||
.post("/send-slack-notification") | ||
.send(invalidBody) | ||
.set(validAuth) | ||
.expect(400) | ||
.then((res) => { | ||
expect(res.body).toHaveProperty("issues"); | ||
expect(res.body).toHaveProperty("name", "ZodError"); | ||
}); | ||
}); | ||
|
||
it("skips the staging environment", async () => { | ||
process.env.APP_ENVIRONMENT = "staging"; | ||
await supertest(app) | ||
.post("/send-slack-notification") | ||
.send(validBody) | ||
.set(validAuth) | ||
.expect(200) | ||
.then((res) => { | ||
expect(res.body).toHaveProperty("message"); | ||
expect(res.body.message).toMatch( | ||
/Staging environment, skipping Slack notification. Message/, | ||
); | ||
}); | ||
}); | ||
|
||
it("successfully sends a Slack message", async () => { | ||
process.env.APP_ENVIRONMENT = "production"; | ||
await supertest(app) | ||
.post("/send-slack-notification") | ||
.send(validBody) | ||
.set(validAuth) | ||
.expect(200) | ||
.then((res) => { | ||
expect(SlackNotify).toHaveBeenCalledWith(process.env.SLACK_WEBHOOK_URL); | ||
expect(mockSend).toHaveBeenCalledTimes(1); | ||
expect(res.body.message).toEqual( | ||
'Sent Slack notification. Message ":emoji: *council-a/find-out-if* is now online"', | ||
); | ||
}); | ||
}); | ||
|
||
it("returns an error when Slack fails", async () => { | ||
process.env.APP_ENVIRONMENT = "production"; | ||
mockSend.mockRejectedValue("Fail!"); | ||
|
||
await supertest(app) | ||
.post("/send-slack-notification") | ||
.send(validBody) | ||
.set(validAuth) | ||
.expect(500) | ||
.then((res) => { | ||
expect(mockSend).toHaveBeenCalledTimes(1); | ||
expect(res.body.error).toMatch( | ||
/Failed to send Slack notification. Error/, | ||
); | ||
}); | ||
}); |
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,18 @@ | ||
import { Router } from "express"; | ||
import { validate } from "../../shared/middleware/validate.js"; | ||
import { useTeamEditorAuth } from "../auth/middleware.js"; | ||
import { | ||
sendSlackNotificationController, | ||
slackNotificationSchema, | ||
} from "./controller.js"; | ||
|
||
const router = Router(); | ||
|
||
router.post( | ||
"/send-slack-notification", | ||
useTeamEditorAuth, | ||
validate(slackNotificationSchema), | ||
sendSlackNotificationController, | ||
); | ||
|
||
export default router; |
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
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
Oops, something went wrong.