Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: fix audit entries and setup Slack notifications for production S3 + Power Automate submissions #3439

Merged
merged 5 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions api.planx.uk/modules/send/email/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { NextFunction, Request, Response } from "express";
import capitalize from "lodash/capitalize";
import { markSessionAsSubmitted } from "../../saveAndReturn/service/utils";
import { sendEmail } from "../../../lib/notify";
import { EmailSubmissionNotifyConfig } from "../../../types";
import { markSessionAsSubmitted } from "../../saveAndReturn/service/utils";
import {
getSessionEmailDetailsById,
getTeamEmailSettings,
Expand Down
24 changes: 11 additions & 13 deletions api.planx.uk/modules/send/s3/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import axios from "axios";
import axios, { AxiosRequestConfig } from "axios";
import type { NextFunction, Request, Response } from "express";
import { gql } from "graphql-request";

import { $api } from "../../../client";
import { Passport } from "../../../types";
import { uploadPrivateFile } from "../../file/service/uploadFile";
Expand Down Expand Up @@ -60,8 +61,8 @@ const sendToS3 = async (req: Request, res: Response, next: NextFunction) => {
`${sessionId}.json`,
);

// Send a notification with the file URL to the Power Automate webook
const webhookRequest = {
// Send a notification with the file URL to the Power Automate webhook
const webhookRequest: AxiosRequestConfig = {
method: "POST",
url: powerAutomateWebhookURL,
adapter: "http",
Expand All @@ -76,11 +77,8 @@ const sendToS3 = async (req: Request, res: Response, next: NextFunction) => {
payload: doValidation ? "Validated ODP Schema" : "Discretionary",
},
};
let webhookResponseStatus: number | undefined;
await axios(webhookRequest)
const webhookResponse = await axios(webhookRequest)
.then(async (res) => {
webhookResponseStatus = res.status;

// Mark session as submitted so that reminder and expiry emails are not triggered
markSessionAsSubmitted(sessionId);

Expand Down Expand Up @@ -109,14 +107,13 @@ const sendToS3 = async (req: Request, res: Response, next: NextFunction) => {
session_id: sessionId,
team_slug: localAuthority,
webhook_request: webhookRequest,
webhook_response: res,
webhook_response: res.headers,
},
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved
);

return {
application: {
...applicationId.insertS3Application,
},
id: applicationId.insertS3Application?.id,
axiosResponse: res,
};
})
.catch((error) => {
Expand All @@ -125,10 +122,11 @@ const sendToS3 = async (req: Request, res: Response, next: NextFunction) => {
);
});

return res.status(200).send({
res.status(200).send({
message: `Successfully uploaded submission to S3: ${fileUrl}`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the return here was the original issue - the audit table step wasn't being properly awaited?

Can't quite follow what the exact issue was but it could be caused by mixing await and then(), which can lead to some issues. It might be worth just using async/await throughout and adding back the return here.

Not sure if this is totally necessary but it might make things a little easier to follow and reason with.

Copy link
Member Author

@jessicamcinchak jessicamcinchak Jul 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct! Everything working as expected without this return now, sorry I didn't explicitly call this out.

Agree the mix of async/await and an Axios .then() are a bit confusing to reason about, but this now exactly matches the pattern we have for auditing BOPS etc (perhaps worth revisiting in whole in a future PR for consistency!): https://github.com/theopensystemslab/planx-new/blob/main/api.planx.uk/modules/send/bops/bops.ts#L134

payload: doValidation ? "Validated ODP Schema" : "Discretionary",
webhookResponse: webhookResponseStatus,
webhookResponse: webhookResponse.axiosResponse.status,
auditEntryId: webhookResponse.id,
});
} catch (error) {
return next({
Expand Down
22 changes: 22 additions & 0 deletions api.planx.uk/modules/webhooks/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,33 @@ components:
type: string
required:
- body
S3Submission:
type: object
properties:
body:
type: object
properties:
event:
type: object
properties:
data:
type: object
properties:
new:
type: object
properties:
session_id:
type: string
team_slug:
type: string
required:
- body
SendSlackNotification:
oneOf:
- $ref: "#/components/schemas/BopsSubmission"
- $ref: "#/components/schemas/UniformSubmission"
- $ref: "#/components/schemas/EmailSubmission"
- $ref: "#/components/schemas/S3Submission"
CreatePaymentEvent:
type: object
properties:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import supertest from "supertest";
import app from "../../../../server";
import SlackNotify from "slack-notify";
import { BOPSBody, EmailBody, UniformBody } from "./types";
import { BOPSBody, EmailBody, S3Body, UniformBody } from "./types";
import { $api } from "../../../../client";
import { CoreDomainClient } from "@opensystemslab/planx-core";

Expand Down Expand Up @@ -340,4 +340,63 @@ describe("Send Slack notifications endpoint", () => {
});
});
});

describe("S3 notifications", () => {
const body: S3Body = {
event: {
data: {
new: {
session_id: "s3-pa-123",
team_slug: "test-team",
},
},
},
};

it("skips the staging environment", async () => {
process.env.APP_ENVIRONMENT = "staging";
await post(ENDPOINT)
.query({ type: "s3-submission" })
.set({ Authorization: process.env.HASURA_PLANX_API_KEY! })
.send(body)
.expect(200)
.then((response) => {
expect(response.body.message).toMatch(/skipping Slack notification/);
});
});

it("posts to Slack on success", async () => {
process.env.APP_ENVIRONMENT = "production";

await post(ENDPOINT)
.query({ type: "s3-submission" })
.set({ Authorization: process.env.HASURA_PLANX_API_KEY! })
.send(body)
.expect(200)
.then((response) => {
expect(SlackNotify).toHaveBeenCalledWith(
process.env.SLACK_WEBHOOK_URL,
);
expect(mockSend).toHaveBeenCalledTimes(1);
expect(response.body.message).toBe("Posted to Slack");
expect(response.body.data).toMatch(/s3-pa-123/);
expect(response.body.data).toMatch(/test-team/);
});
});

it("returns error when Slack fails", async () => {
process.env.APP_ENVIRONMENT = "production";
mockSend.mockRejectedValue("Fail!");

await post(ENDPOINT)
.query({ type: "s3-submission" })
.set({ Authorization: process.env.HASURA_PLANX_API_KEY! })
.send(body)
.expect(500)
.then((response) => {
expect(mockSend).toHaveBeenCalledTimes(1);
expect(response.body.error).toMatch(/Failed to send/);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
EmailEventData,
EventData,
EventType,
S3EventData,
UniformEventData,
} from "./types";
import { $api } from "../../../../client";
Expand Down Expand Up @@ -41,13 +42,19 @@ const getMessageForEventType = (data: EventData, type: EventType) => {
const { request, session_id, team_slug } = data as EmailEventData;
return `New email submission "${request.personalisation.serviceName}" *${session_id}* [${team_slug}]`;
}

if (type === "s3-submission") {
const { session_id, team_slug } = data as S3EventData;
return `New S3 + Power Automate submission *${session_id}* [${team_slug}]`;
}
};

const getSessionIdFromEvent = (data: EventData, type: EventType) =>
({
"bops-submission": (data as BOPSEventData).session_id,
"uniform-submission": (data as UniformEventData).payload?.sessionId,
"email-submission": (data as EmailEventData).session_id,
"s3-submission": (data as S3EventData).session_id,
})[type];

const getExemptionStatusesForSession = async (sessionId: string) => {
Expand Down
17 changes: 17 additions & 0 deletions api.planx.uk/modules/webhooks/service/sendNotification/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,25 @@ export const emailSubmissionSchema = z.object({
}),
});

export const s3SubmissionSchema = z.object({
body: z.object({
event: z.object({
data: z.object({
new: z.object({
session_id: z.string(),
team_slug: z.string(),
}),
}),
}),
}),
query: z.object({
type: z.literal("s3-submission"),
}),
});

export const sendSlackNotificationSchema = z.union([
bopsSubmissionSchema,
uniformSubmissionSchema,
emailSubmissionSchema,
s3SubmissionSchema,
]);
10 changes: 9 additions & 1 deletion api.planx.uk/modules/webhooks/service/sendNotification/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ValidatedRequestHandler } from "../../../../shared/middleware/validate"
import {
bopsSubmissionSchema,
emailSubmissionSchema,
s3SubmissionSchema,
sendSlackNotificationSchema,
uniformSubmissionSchema,
} from "./schema";
Expand All @@ -25,7 +26,14 @@ export type UniformEventData = UniformBody["event"]["data"]["new"];
export type EmailBody = z.infer<typeof emailSubmissionSchema>["body"];
export type EmailEventData = EmailBody["event"]["data"]["new"];

export type EventData = BOPSEventData | UniformEventData | EmailEventData;
export type S3Body = z.infer<typeof s3SubmissionSchema>["body"];
export type S3EventData = S3Body["event"]["data"]["new"];

export type EventData =
| BOPSEventData
| UniformEventData
| EmailEventData
| S3EventData;

export type SendSlackNotification = ValidatedRequestHandler<
typeof sendSlackNotificationSchema,
Expand Down
21 changes: 21 additions & 0 deletions hasura.planx.uk/metadata/tables.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,27 @@
filter: {}
check: null
comment: ""
event_triggers:
- name: setup_s3_applications_notifications
definition:
enable_manual: false
insert:
columns: '*'
retry_conf:
interval_sec: 30
num_retries: 1
timeout_sec: 60
webhook: '{{HASURA_PLANX_API_URL}}'
headers:
- name: authorization
value_from_env: HASURA_PLANX_API_KEY
request_transform:
method: POST
query_params:
type: s3-submission
template_engine: Kriti
url: '{{$base_url}}/webhooks/hasura/send-slack-notification'
version: 2
- table:
name: sessions
schema: public
Expand Down
Loading