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

Production deploy #2498

Merged
merged 8 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
23 changes: 23 additions & 0 deletions api.planx.uk/client/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CoreDomainClient } from "@opensystemslab/planx-core";
import { getClient } from ".";
import { userContext } from "../modules/auth/middleware";
import { getJWT } from "../tests/mockJWT";

test("getClient() throws an error if a store is not set", () => {
expect(() => getClient()).toThrow();
});

test("getClient() returns a client if store is set", () => {
const getStoreMock = jest.spyOn(userContext, "getStore");
getStoreMock.mockReturnValue({
user: {
sub: "123",
jwt: getJWT({ role: "teamEditor" }),
},
});

const client = getClient();

expect(client).toBeDefined();
expect(client).toBeInstanceOf(CoreDomainClient);
});
24 changes: 23 additions & 1 deletion api.planx.uk/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { ComponentType } from "@opensystemslab/planx-core/types";
import { dataMerged, getFormattedEnvironment, isLiveEnv } from "./helpers";
import {
dataMerged,
getFlowData,
getFormattedEnvironment,
isLiveEnv,
} from "./helpers";
import { queryMock } from "./tests/graphqlQueryMock";
import { userContext } from "./modules/auth/middleware";
import { getJWT } from "./tests/mockJWT";
Expand Down Expand Up @@ -167,6 +172,7 @@ describe("dataMerged() function", () => {
},
});
});

it("handles multiple external portal nodes", async () => {
const result = await dataMerged("parent-id");
const nodeTypes = Object.values(result).map((node) =>
Expand All @@ -180,3 +186,19 @@ describe("dataMerged() function", () => {
expect(areAllPortalsFlattened).toBe(true);
});
});

describe("getFlowData() function", () => {
it("throws an error if a flow is not found", async () => {
queryMock.mockQuery({
name: "GetFlowData",
variables: {
id: "child-id",
},
data: {
flow: null,
},
});

await expect(getFlowData("child-id")).rejects.toThrow();
});
});
93 changes: 61 additions & 32 deletions api.planx.uk/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { gql } from "graphql-request";
import { capitalize } from "lodash";
import { Flow, Node } from "./types";
import { ComponentType, FlowGraph } from "@opensystemslab/planx-core/types";
import { $public, getClient } from "./client";
import { $api, $public, getClient } from "./client";

// Get a flow's data (unflattened, without external portal nodes)
const getFlowData = async (id: string): Promise<Flow> => {
Expand All @@ -23,6 +23,11 @@ const getFlowData = async (id: string): Promise<Flow> => {
return flow;
};

interface InsertFlow {
flow: {
id: string;
};
}
// Insert a new flow into the `flows` table
const insertFlow = async (
teamId: number,
Expand All @@ -32,40 +37,64 @@ const insertFlow = async (
copiedFrom?: Flow["id"],
) => {
const { client: $client } = getClient();
const data = await $client.request<{ flow: { id: string } }>(
gql`
mutation InsertFlow(
$team_id: Int!
$slug: String!
$data: jsonb = {}
$creator_id: Int
$copied_from: uuid
) {
flow: insert_flows_one(
object: {
team_id: $team_id
slug: $slug
data: $data
version: 1
creator_id: $creator_id
copied_from: $copied_from
}
try {
const {
flow: { id },
} = await $client.request<InsertFlow>(
gql`
mutation InsertFlow(
$team_id: Int!
$slug: String!
$creator_id: Int
$copied_from: uuid
) {
id
flow: insert_flows_one(
object: {
team_id: $team_id
slug: $slug
version: 1
creator_id: $creator_id
copied_from: $copied_from
}
) {
id
}
}
}
`,
{
team_id: teamId,
slug: slug,
data: flowData,
creator_id: creatorId,
copied_from: copiedFrom,
},
);
`,
{
team_id: teamId,
slug: slug,
creator_id: creatorId,
copied_from: copiedFrom,
},
);

// Populate flow data using API role now that we know user has permission to insert flow
// Access to flow.data column is restricted to limit unsafe content that could be inserted
await $api.client.request<InsertFlow>(
gql`
mutation UpdateFlowData($data: jsonb = {}, $id: uuid!) {
flow: update_flows_by_pk(
pk_columns: { id: $id }
_set: { data: $data }
) {
id
}
}
`,
{
id: id,
data: flowData,
},
);

if (data) await createAssociatedOperation(data?.flow?.id);
return data?.flow;
await createAssociatedOperation(id);
return { id };
} catch (error) {
throw Error(
`User ${creatorId} failed to insert flow to teamId ${teamId}. Please check permissions.`,
);
}
};

// Add a row to `operations` for an inserted flow, otherwise ShareDB throws a silent error when opening the flow in the UI
Expand Down
12 changes: 10 additions & 2 deletions api.planx.uk/lib/hasura/metadata/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { createScheduledEvent, RequiredScheduledEventArgs } from ".";
import Axios from "axios";
import Axios, { AxiosError } from "axios";

jest.mock("axios");
jest.mock("axios", () => ({
...jest.requireActual("axios"),
post: jest.fn(),
}));
const mockAxios = Axios as jest.Mocked<typeof Axios>;

const mockScheduledEvent: RequiredScheduledEventArgs = {
Expand All @@ -16,6 +19,11 @@ test("createScheduledEvent returns an error if request fails", async () => {
await expect(createScheduledEvent(mockScheduledEvent)).rejects.toThrow();
});

test("createScheduledEvent returns an error if Axios errors", async () => {
mockAxios.post.mockRejectedValue(new AxiosError());
await expect(createScheduledEvent(mockScheduledEvent)).rejects.toThrow();
});

test("createScheduledEvent returns response data on success", async () => {
mockAxios.post.mockResolvedValue({ data: "test data" });
await expect(createScheduledEvent(mockScheduledEvent)).resolves.toBe(
Expand Down
12 changes: 10 additions & 2 deletions api.planx.uk/lib/hasura/schema/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { runSQL } from ".";
import Axios from "axios";
import Axios, { AxiosError } from "axios";

jest.mock("axios");
jest.mock("axios", () => ({
...jest.requireActual("axios"),
post: jest.fn(),
}));
const mockAxios = Axios as jest.Mocked<typeof Axios>;

const sql = "SELECT * FROM TEST";
Expand All @@ -11,6 +14,11 @@ test("runSQL returns an error if request fails", async () => {
await expect(runSQL(sql)).rejects.toThrow();
});

test("runSQL returns an error if Axios errors", async () => {
mockAxios.post.mockRejectedValue(new AxiosError());
await expect(runSQL(sql)).rejects.toThrow();
});

test("runSQL returns response data on success", async () => {
mockAxios.post.mockResolvedValue({ data: "test data" });
await expect(runSQL(sql)).resolves.toBe("test data");
Expand Down
39 changes: 39 additions & 0 deletions api.planx.uk/lib/notify/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { sendEmail } from ".";
import { NotifyClient } from "notifications-node-client";
import { NotifyConfig } from "../../types";

jest.mock("notifications-node-client");

const TEST_EMAIL = "[email protected]";
const mockConfig: NotifyConfig = {
personalisation: {
teamName: "test",
emailReplyToId: "test",
helpEmail: "test",
helpOpeningHours: "test",
helpPhone: "test",
},
};

describe("sendEmail", () => {
it("throws an error if an invalid template is used", async () => {
await expect(
// @ts-expect-error Argument of type "invalidTemplate" is not assignable to parameter
sendEmail("invalidTemplate", "[email protected]", {}),
).rejects.toThrow();
});

it("throw an error if an error is thrown within sendEmail()", async () => {
const mockNotifyClient = NotifyClient.mock.instances[0];
mockNotifyClient.sendEmail.mockRejectedValue(new Error());
await expect(sendEmail("save", TEST_EMAIL, mockConfig)).rejects.toThrow();
});

it("throw an error if the NotifyClient errors", async () => {
const mockNotifyClient = NotifyClient.mock.instances[0];
mockNotifyClient.sendEmail.mockRejectedValue({
response: { data: { errors: ["Invalid email"] } },
});
await expect(sendEmail("save", TEST_EMAIL, mockConfig)).rejects.toThrow();
});
});
2 changes: 1 addition & 1 deletion api.planx.uk/lib/notify/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@
expiryDate?: string;
} = { message: "Success" };
if (template === "expiry")
softDeleteSession(config.personalisation.sessionId!);
await softDeleteSession(config.personalisation.sessionId!);
if (template === "save")
returnValue.expiryDate = config.personalisation.expiryDate;
return returnValue;
} catch (error: any) {

Check warning on line 73 in api.planx.uk/lib/notify/index.ts

View workflow job for this annotation

GitHub Actions / Run API Tests

Unexpected any. Specify a different type
const notifyError = error?.response?.data?.errors?.length
? JSON.stringify(error?.response?.data?.errors?.[0])
: error?.message;
Expand Down
12 changes: 12 additions & 0 deletions api.planx.uk/modules/admin/session/bops.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ describe("BOPS payload admin endpoint", () => {
.expect(403);
});

it("returns an error if the BOPS payload generation fails", async () => {
mockGenerateBOPSPayload.mockRejectedValueOnce("Error!");

await supertest(app)
.get(endpoint`123`)
.set(authHeader({ role: "platformAdmin" }))
.expect(500)
.then((res) => {
expect(res.body.error).toMatch(/Failed to get BOPS payload/);
});
});

it("returns a JSON payload", async () => {
await supertest(app)
.get(endpoint`123`)
Expand Down
16 changes: 16 additions & 0 deletions api.planx.uk/modules/admin/session/digitalPlanningData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ describe("Digital Planning Application payload admin endpoint", () => {
.expect(403);
});

it("returns an error if the Digital Planning payload generation fails", async () => {
mockGenerateDigitalPlanningApplicationPayload.mockRejectedValueOnce(
"Error!",
);

await supertest(app)
.get(endpoint`123`)
.set(authHeader({ role: "platformAdmin" }))
.expect(500)
.then((res) => {
expect(res.body.error).toMatch(
/Failed to make Digital Planning Application payload/,
);
});
});

it("returns a valid JSON payload", async () => {
await supertest(app)
.get(endpoint`123`)
Expand Down
12 changes: 12 additions & 0 deletions api.planx.uk/modules/admin/session/oneAppXML.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ describe("OneApp XML endpoint", () => {
.expect(403);
});

it("returns an error if the XML generation fails", async () => {
mockGenerateOneAppXML.mockRejectedValueOnce("Error!");

await supertest(app)
.get(endpoint`123`)
.set(authHeader({ role: "platformAdmin" }))
.expect(500)
.then((res) => {
expect(res.body.error).toMatch(/Failed to get OneApp XML/);
});
});

it("returns XML", async () => {
await supertest(app)
.get(endpoint`123`)
Expand Down
4 changes: 4 additions & 0 deletions api.planx.uk/modules/admin/session/summary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ describe("Session summary admin endpoint", () => {
.expect(403);
});

it.todo("returns an error if the service fails");

it.todo("returns an error if the session can't be found");

it("returns JSON", async () => {
await supertest(app)
.get(endpoint`abc123`)
Expand Down
2 changes: 2 additions & 0 deletions api.planx.uk/modules/admin/session/zip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ describe("zip data admin endpoint", () => {
.expect(200)
.expect("content-type", "application/zip");
});

it.todo("returns an error if the service fails");
});
4 changes: 2 additions & 2 deletions api.planx.uk/modules/file/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ paths:
/file/private/upload:
post:
tags: ["file"]
security:
- bearerAuth: []
requestBody:
content:
multipart/form-data:
Expand All @@ -63,6 +61,8 @@ paths:
/file/public/upload:
post:
tags: ["file"]
security:
- bearerAuth: []
requestBody:
content:
multipart/form-data:
Expand Down
4 changes: 2 additions & 2 deletions api.planx.uk/modules/flows/copyFlow/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export type CopyFlowController = ValidatedRequestHandler<
>;

export const copyFlowController: CopyFlowController = async (
req,
_req,
res,
next,
) => {
Expand All @@ -48,7 +48,7 @@ export const copyFlowController: CopyFlowController = async (
});
} catch (error) {
return next(
new ServerError({ message: "Failed to copy flow", cause: error }),
new ServerError({ message: `Failed to copy flow. Error: ${error}` }),
);
}
};
Loading
Loading