Skip to content

Commit

Permalink
chore(api): Add validation middleware to send integration endpoints (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr authored Oct 25, 2024
1 parent e275baa commit 4fdddaf
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 153 deletions.
20 changes: 12 additions & 8 deletions api.planx.uk/modules/send/bops/bops.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe(`sending an application to BOPS (v2)`, () => {
bopsApplications: [],
},
variables: {
session_id: "123",
session_id: "37e3649b-4854-4249-a5c3-7937c1b952b9",
search_string: "%/api/v2/planning_applications",
},
});
Expand Down Expand Up @@ -93,7 +93,7 @@ describe(`sending an application to BOPS (v2)`, () => {
await supertest(app)
.post("/bops/southwark")
.set({ Authorization: process.env.HASURA_PLANX_API_KEY! })
.send({ payload: { sessionId: "123" } })
.send({ payload: { sessionId: "37e3649b-4854-4249-a5c3-7937c1b952b9" } })
.expect(200)
.then((res) => {
expect(res.body).toEqual({
Expand All @@ -108,7 +108,7 @@ describe(`sending an application to BOPS (v2)`, () => {
it("requires auth", async () => {
await supertest(app)
.post("/bops/southwark")
.send({ payload: { sessionId: "123" } })
.send({ payload: { sessionId: "37e3649b-4854-4249-a5c3-7937c1b952b9" } })
.expect(401);
});

Expand All @@ -119,15 +119,16 @@ describe(`sending an application to BOPS (v2)`, () => {
.send({ payload: null })
.expect(400)
.then((res) => {
expect(res.body.error).toMatch(/Missing application/);
expect(res.body).toHaveProperty("issues");
expect(res.body).toHaveProperty("name", "ZodError");
});
});

it("throws an error if team is unsupported", async () => {
await supertest(app)
.post("/bops/unsupported-team")
.set({ Authorization: process.env.HASURA_PLANX_API_KEY! })
.send({ payload: { sessionId: "123" } })
.send({ payload: { sessionId: "37e3649b-4854-4249-a5c3-7937c1b952b9" } })
.expect(500)
.then((res) => {
expect(res.body.error).toMatch(
Expand All @@ -137,6 +138,9 @@ describe(`sending an application to BOPS (v2)`, () => {
});

it("does not re-send an application which has already been submitted", async () => {
const previouslySubmittedApplicationID =
"07e4dd0d-ec3c-48f5-adc7-aee90116f6c7";

queryMock.mockQuery({
name: "FindApplication",
data: {
Expand All @@ -145,19 +149,19 @@ describe(`sending an application to BOPS (v2)`, () => {
],
},
variables: {
session_id: "previously_submitted_app",
session_id: previouslySubmittedApplicationID,
search_string: "%/api/v2/planning_applications",
},
});

await supertest(app)
.post("/bops/southwark")
.set({ Authorization: process.env.HASURA_PLANX_API_KEY! })
.send({ payload: { sessionId: "previously_submitted_app" } })
.send({ payload: { sessionId: previouslySubmittedApplicationID } })
.expect(200)
.then((res) => {
expect(res.body).toEqual({
sessionId: "previously_submitted_app",
sessionId: previouslySubmittedApplicationID,
bopsId: "bops_app_id",
message: "Skipping send, already successfully submitted",
});
Expand Down
46 changes: 14 additions & 32 deletions api.planx.uk/modules/send/bops/bops.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import type { AxiosResponse } from "axios";
import axios from "axios";
import { markSessionAsSubmitted } from "../../saveAndReturn/service/utils.js";
import type { NextFunction, Request, Response } from "express";
import { gql } from "graphql-request";
import { $api } from "../../../client/index.js";
import { ServerError } from "../../../errors/index.js";

interface SendToBOPSRequest {
payload: {
sessionId: string;
};
}
import type { SendIntegrationController } from "../types.js";

interface CreateBopsApplication {
insertBopsApplication: {
Expand All @@ -19,23 +13,16 @@ interface CreateBopsApplication {
};
}

const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => {
// `/bops/:localAuthority` is only called via Hasura's scheduled event webhook now, so body is wrapped in a "payload" key
const { payload }: SendToBOPSRequest = req.body;
if (!payload) {
return next(
new ServerError({
status: 400,
message: `Missing application payload data to send to BOPS`,
}),
);
}
const sendToBOPS: SendIntegrationController = async (_req, res, next) => {
const {
payload: { sessionId },
} = res.locals.parsedReq.body;

// confirm that this session has not already been successfully submitted before proceeding
const submittedApp = await checkBOPSAuditTable(payload?.sessionId, "v2");
const submittedApp = await checkBOPSAuditTable(sessionId, "v2");
if (submittedApp?.message === "Application created") {
return res.status(200).send({
sessionId: payload?.sessionId,
sessionId,
bopsId: submittedApp?.id,
message: `Skipping send, already successfully submitted`,
});
Expand All @@ -44,7 +31,7 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => {
// confirm this local authority (aka team) is supported by BOPS before creating the proxy
// a local or staging API instance should send to the BOPS staging endpoint
// production should send to the BOPS production endpoint
const localAuthority = req.params.localAuthority;
const localAuthority = res.locals.parsedReq.params.localAuthority;
const env =
process.env.APP_ENVIRONMENT === "production" ? "production" : "staging";

Expand All @@ -54,9 +41,7 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => {
encryptionKey: process.env.ENCRYPTION_KEY!,
env,
});
const exportData = await $api.export.digitalPlanningDataPayload(
payload?.sessionId,
);
const exportData = await $api.export.digitalPlanningDataPayload(sessionId);

const bopsResponse = await axios({
method: "POST",
Expand All @@ -70,7 +55,7 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => {
})
.then(async (res: AxiosResponse<{ id: string }>) => {
// Mark session as submitted so that reminder and expiry emails are not triggered
markSessionAsSubmitted(payload?.sessionId);
markSessionAsSubmitted(sessionId);

const applicationId = await $api.client.request<CreateBopsApplication>(
gql`
Expand Down Expand Up @@ -105,7 +90,7 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => {
request: exportData,
response: res.data,
response_headers: res.headers,
session_id: payload?.sessionId,
session_id: sessionId,
},
);

Expand All @@ -119,14 +104,14 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => {
.catch((error) => {
if (error.response) {
throw new Error(
`Sending to BOPS v2 failed (${[localAuthority, payload?.sessionId]
`Sending to BOPS v2 failed (${[localAuthority, sessionId]
.filter(Boolean)
.join(" - ")}):\n${JSON.stringify(error.response.data, null, 2)}`,
);
} else {
// re-throw other errors
throw new Error(
`Sending to BOPS v2 failed (${[localAuthority, payload?.sessionId]
`Sending to BOPS v2 failed (${[localAuthority, sessionId]
.filter(Boolean)
.join(" - ")}):\n${error}`,
);
Expand All @@ -137,10 +122,7 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => {
next(
new ServerError({
status: 500,
message: `Sending to BOPS v2 failed (${[
localAuthority,
payload?.sessionId,
]
message: `Sending to BOPS v2 failed (${[localAuthority, sessionId]
.filter(Boolean)
.join(" - ")}):\n${err}`,
cause: err,
Expand Down
35 changes: 18 additions & 17 deletions api.planx.uk/modules/send/email/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe(`sending an application by email to a planning office`, () => {
data: {
session: { data: {} },
},
variables: { id: "123" },
variables: { id: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" },
matchOnVariables: true,
});

Expand All @@ -77,16 +77,16 @@ describe(`sending an application by email to a planning office`, () => {
flow: { slug: "test-flow", name: "Test Flow" },
},
},
variables: { id: "123" },
variables: { id: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" },
});

queryMock.mockQuery({
name: "MarkSessionAsSubmitted",
matchOnVariables: false,
data: {
session: { id: "123" },
session: { id: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" },
},
variables: { sessionId: "123" },
variables: { sessionId: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" },
});

queryMock.mockQuery({
Expand All @@ -96,7 +96,7 @@ describe(`sending an application by email to a planning office`, () => {
application: { id: 1 },
},
variables: {
sessionId: "123",
sessionId: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49",
teamSlug: "southwark",
recipient: "[email protected]",
request: {
Expand All @@ -116,7 +116,7 @@ describe(`sending an application by email to a planning office`, () => {
await supertest(app)
.post("/email-submission/southwark")
.set({ Authorization: process.env.HASURA_PLANX_API_KEY! })
.send({ payload: { sessionId: "123" } })
.send({ payload: { sessionId: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" } })
.expect(200)
.then((res) => {
expect(res.body).toEqual({
Expand All @@ -130,20 +130,21 @@ describe(`sending an application by email to a planning office`, () => {
it("fails without authorization header", async () => {
await supertest(app)
.post("/email-submission/southwark")
.send({ payload: { sessionId: "123" } })
.send({ payload: { sessionId: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" } })
.expect(401);
});

it("errors if the payload body does not include a sessionId", async () => {
await supertest(app)
.post("/email-submission/southwark")
.set({ Authorization: process.env.HASURA_PLANX_API_KEY! })
.send({ payload: { somethingElse: "123" } })
.send({
payload: { somethingElse: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" },
})
.expect(400)
.then((res) => {
expect(res.body).toEqual({
error: "Missing application payload data to send to email",
});
expect(res.body).toHaveProperty("issues");
expect(res.body).toHaveProperty("name", "ZodError");
});
});

Expand All @@ -167,7 +168,7 @@ describe(`sending an application by email to a planning office`, () => {
await supertest(app)
.post("/email-submission/other-council")
.set({ Authorization: process.env.HASURA_PLANX_API_KEY! })
.send({ payload: { sessionId: "123" } })
.send({ payload: { sessionId: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" } })
.expect(400)
.then((res) => {
expect(res.body).toEqual({
Expand All @@ -184,13 +185,13 @@ describe(`sending an application by email to a planning office`, () => {
data: {
session: null,
},
variables: { id: "123" },
variables: { id: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" },
});

await supertest(app)
.post("/email-submission/other-council")
.set({ Authorization: process.env.HASURA_PLANX_API_KEY! })
.send({ payload: { sessionId: "123" } })
.send({ payload: { sessionId: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" } })
.expect(500)
.then((res) => {
expect(res.body.error).toMatch(/Cannot find session/);
Expand Down Expand Up @@ -221,7 +222,7 @@ describe(`downloading application data received by email`, () => {
data: {
session: { data: { passport: { test: "dummy data" } } },
},
variables: { id: "123" },
variables: { id: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49" },
});
});

Expand Down Expand Up @@ -274,12 +275,12 @@ describe(`downloading application data received by email`, () => {
it("calls addTemplateFilesToZip()", async () => {
await supertest(app)
.get(
"/download-application-files/123[email protected]&localAuthority=southwark",
"/download-application-files/33d373d4-fff2-4ef7-a5f2-2a36e39ccc49[email protected]&localAuthority=southwark",
)
.expect(200)
.then((_res) => {
expect(mockBuildSubmissionExportZip).toHaveBeenCalledWith({
sessionId: "123",
sessionId: "33d373d4-fff2-4ef7-a5f2-2a36e39ccc49",
includeDigitalPlanningJSON: true,
});
});
Expand Down
38 changes: 16 additions & 22 deletions api.planx.uk/modules/send/email/index.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,24 @@
import type { NextFunction, Request, Response } from "express";
import { sendEmail } from "../../../lib/notify/index.js";
import type { EmailSubmissionNotifyConfig } from "../../../types.js";
import { markSessionAsSubmitted } from "../../saveAndReturn/service/utils.js";
import type { SendIntegrationController } from "../types.js";
import {
getSessionEmailDetailsById,
getTeamEmailSettings,
insertAuditEntry,
} from "./service.js";

export async function sendToEmail(
req: Request,
res: Response,
next: NextFunction,
) {
export const sendToEmail: SendIntegrationController = async (
req,
res,
next,
) => {
req.setTimeout(120 * 1000); // Temporary bump to address submission timeouts

// `/email-submission/:localAuthority` is only called via Hasura's scheduled event webhook, so body is wrapped in a "payload" key
const { payload } = req.body;
const localAuthority = req.params.localAuthority;

if (!payload?.sessionId) {
return next({
status: 400,
message: `Missing application payload data to send to email`,
});
}
const {
payload: { sessionId },
} = res.locals.parsedReq.body;
const localAuthority = res.locals.parsedReq.params.localAuthority;

try {
// Confirm this local authority (aka team) has an email configured in teams.submission_email
Expand All @@ -37,16 +31,16 @@ export async function sendToEmail(
}

// Get the applicant email and flow slug associated with the session
const { email, flow } = await getSessionEmailDetailsById(payload.sessionId);
const { email, flow } = await getSessionEmailDetailsById(sessionId);
const flowName = flow.name;

// Prepare email template
const config: EmailSubmissionNotifyConfig = {
personalisation: {
serviceName: flowName,
sessionId: payload.sessionId,
sessionId,
applicantEmail: email,
downloadLink: `${process.env.API_URL_EXT}/download-application-files/${payload.sessionId}?email=${teamSettings.submissionEmail}&localAuthority=${localAuthority}`,
downloadLink: `${process.env.API_URL_EXT}/download-application-files/${sessionId}?email=${teamSettings.submissionEmail}&localAuthority=${localAuthority}`,
...teamSettings,
},
};
Expand All @@ -59,11 +53,11 @@ export async function sendToEmail(
);

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

// Create audit table entry, which triggers a Slack notification on `insert` if production
insertAuditEntry(
payload.sessionId,
sessionId,
localAuthority,
teamSettings.submissionEmail,
config,
Expand All @@ -83,4 +77,4 @@ export async function sendToEmail(
}`,
});
}
}
};
Loading

0 comments on commit 4fdddaf

Please sign in to comment.