Skip to content

Commit

Permalink
CMDCT-4252: Adding validation for update report endpoint (#95)
Browse files Browse the repository at this point in the history
  • Loading branch information
angelaco11 authored Jan 24, 2025
1 parent 35a9fea commit 6dcdcfb
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 23 deletions.
31 changes: 31 additions & 0 deletions services/app-api/handlers/reports/buildReport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ jest.mock("../../storage/reports", () => ({
putReport: () => putMock(),
}));

const validateReportPayloadMock = jest.fn();
jest.mock("../../utils/reportValidation", () => ({
validateReportPayload: () => validateReportPayloadMock(),
}));

describe("Test create report handler", () => {
beforeEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -35,3 +40,29 @@ describe("Test create report handler", () => {
expect(putMock).toHaveBeenCalled();
});
});

describe("Test validation error", () => {
beforeEach(() => {
jest.clearAllMocks();
});

test("Test that a validation failure throws invalid request error", async () => {
// Manually throw validation error
validateReportPayloadMock.mockImplementation(() => {
throw new Error("you be havin some validatin errors");
});

const state = "PA";
const user = {
fullName: "James Holden",
email: "[email protected]",
} as User;
const reportOptions = {
name: "report1",
} as ReportOptions;

expect(async () => {
await buildReport(ReportType.QMS, state, reportOptions, user);
}).rejects.toThrow("Invalid request");
});
});
16 changes: 15 additions & 1 deletion services/app-api/handlers/reports/buildReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
} from "../../types/reports";
import { User } from "../../types/types";
import { CMIT_LIST } from "../../forms/cmit";
import { validateReportPayload } from "../../utils/reportValidation";
import { logger } from "../../libs/debug-lib";

const reportTemplates = {
[ReportType.QMS]: qmsReportTemplate,
Expand Down Expand Up @@ -64,8 +66,20 @@ export const buildReport = async (
report.pages = report.pages.concat(measurePages);
}

/**
* Report should always be valid in this function, but we're going
* to send it through the report validator for a sanity check
*/
let validatedReport: Report | undefined;
try {
validatedReport = await validateReportPayload(report);
} catch (err) {
logger.error(err);
throw new Error("Invalid request");
}

// Save
await putReport(report);
await putReport(validatedReport);
return report;
};

Expand Down
91 changes: 87 additions & 4 deletions services/app-api/handlers/reports/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,23 @@ import { StatusCodes } from "../../libs/response-lib";
import { proxyEvent } from "../../testing/proxyEvent";
import { APIGatewayProxyEvent, UserRoles } from "../../types/types";
import { canWriteState } from "../../utils/authorization";
import {
incorrectTypeReport,
invalidFormPageReport,
invalidMeasureLookupReport,
invalidMeasureTemplatesReport,
invalidParentPageReport,
invalidRadioCheckedChildrenReport,
missingStateReport,
validReport,
} from "../../utils/tests/mockReport";
import { updateReport } from "./update";

jest.mock("../../utils/authentication", () => ({
authenticatedUser: jest.fn().mockResolvedValue({
role: UserRoles.STATE_USER,
state: "PA",
fullName: "Anthony Soprano",
}),
}));

Expand All @@ -19,12 +30,15 @@ jest.mock("../../storage/reports", () => ({
putReport: () => jest.fn(),
}));

const reportObj = { type: "QMS", state: "PA", id: "QMSPA123" };
const report = JSON.stringify(reportObj);
const report = JSON.stringify(validReport);

const testEvent: APIGatewayProxyEvent = {
...proxyEvent,
pathParameters: { reportType: "QMS", state: "PA", id: "QMSPA123" },
pathParameters: {
reportType: "QMS",
state: "NJ",
id: "2rRaoAFm8yLB2N2wSkTJ0iRTDu0",
},
headers: { "cognito-identity-id": "test" },
body: report,
};
Expand Down Expand Up @@ -68,7 +82,7 @@ describe("Test update report handler", () => {
const badState = {
...proxyEvent,
pathParameters: { reportType: "QMS", state: "PA", id: "QMSPA123" },
body: JSON.stringify({ ...reportObj, state: "OR" }),
body: JSON.stringify({ ...validReport, state: "OR" }),
} as APIGatewayProxyEvent;
const badId = {
...proxyEvent,
Expand All @@ -90,3 +104,72 @@ describe("Test update report handler", () => {
expect(res.statusCode).toBe(StatusCodes.Ok);
});
});

describe("Test update report validation failures", () => {
beforeEach(() => {
jest.clearAllMocks();
});

test("throws an error when validating a report with missing state", async () => {
const missingStateEvent = {
...testEvent,
body: JSON.stringify(missingStateReport),
};

const res = await updateReport(missingStateEvent);
expect(res.statusCode).toBe(StatusCodes.BadRequest);
});
test("throws an error when validating a report with incorrect report type", async () => {
const incorrectReportTypeEvent = {
...testEvent,
body: JSON.stringify(incorrectTypeReport),
};

const res = await updateReport(incorrectReportTypeEvent);
expect(res.statusCode).toBe(StatusCodes.BadRequest);
});
test("throws an error when validating invalid measure templates", async () => {
const invalidMeasureTemplatesEvent = {
...testEvent,
body: JSON.stringify(invalidMeasureTemplatesReport),
};

const res = await updateReport(invalidMeasureTemplatesEvent);
expect(res.statusCode).toBe(StatusCodes.BadRequest);
});
test("throws an error when validating invalid measure lookup object", async () => {
const invalidMeasureLookupEvent = {
...testEvent,
body: JSON.stringify(invalidMeasureLookupReport),
};

const res = await updateReport(invalidMeasureLookupEvent);
expect(res.statusCode).toBe(StatusCodes.BadRequest);
});
test("throws an error when validating invalid form page object", async () => {
const invalidFormPageEvent = {
...testEvent,
body: JSON.stringify(invalidFormPageReport),
};

const res = await updateReport(invalidFormPageEvent);
expect(res.statusCode).toBe(StatusCodes.BadRequest);
});
test("throws an error when validating invalid parent page object", async () => {
const invalidParentPageEvent = {
...testEvent,
body: JSON.stringify(invalidParentPageReport),
};

const res = await updateReport(invalidParentPageEvent);
expect(res.statusCode).toBe(StatusCodes.BadRequest);
});
test("throws an error when validating invalid radio element checked children object", async () => {
const invalidRadioCheckedChildrenEvent = {
...testEvent,
body: JSON.stringify(invalidRadioCheckedChildrenReport),
};
const res = await updateReport(invalidRadioCheckedChildrenEvent);
expect(res.statusCode).toBe(StatusCodes.BadRequest);
});
});
13 changes: 11 additions & 2 deletions services/app-api/handlers/reports/update.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { logger } from "../../libs/debug-lib";
import { handler } from "../../libs/handler-lib";
import { parseReportParameters } from "../../libs/param-lib";
import { badRequest, forbidden, ok } from "../../libs/response-lib";
import { putReport } from "../../storage/reports";
import { Report, ReportStatus } from "../../types/reports";
import { canWriteState } from "../../utils/authorization";
import { error } from "../../utils/constants";
import { validateReportPayload } from "../../utils/reportValidation";

export const updateReport = handler(parseReportParameters, async (request) => {
const { reportType, state, id } = request.parameters;
Expand All @@ -31,8 +33,15 @@ export const updateReport = handler(parseReportParameters, async (request) => {
report.lastEdited = Date.now();
report.lastEditedBy = user.fullName;

// Validation required.
await putReport(report);
let validatedPayload: Report | undefined;
try {
validatedPayload = await validateReportPayload(request.body);
} catch (err) {
logger.error(err);
return badRequest("Invalid request");
}

await putReport(validatedPayload);

return ok();
});
1 change: 1 addition & 0 deletions services/app-api/types/reports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ export const isResultRowButton = (

export type RadioTemplate = {
type: ElementType.Radio;
formKey?: string;
label: string;
helperText?: string;
value: ChoiceTemplate[];
Expand Down
10 changes: 5 additions & 5 deletions services/app-api/utils/reportValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
string,
} from "yup";
import {
Report,
ReportStatus,
ReportType,
MeasureTemplateName,
Expand Down Expand Up @@ -91,12 +92,13 @@ const pageElementSchema = lazy((value: PageElement): Schema<any> => {
case ElementType.StatusTable:
return statusTableTemplateSchema;
default:
return mixed().notRequired(); // Fallback, although it should never be hit
throw new Error("Page Element type is not valid");
}
});

const radioTemplateSchema = object().shape({
type: string().required(ElementType.Radio),
formKey: string().notRequired(), // TODO: may be able to remove in future
label: string().required(),
helperText: string().notRequired(),
value: array().of(
Expand Down Expand Up @@ -237,9 +239,7 @@ const reportValidateSchema = object().shape({
measureTemplates: measureTemplatesSchema,
});

export const validateUpdateReportPayload = async (
payload: object | undefined
) => {
export const validateReportPayload = async (payload: object | undefined) => {
if (!payload) {
throw new Error(error.MISSING_DATA);
}
Expand All @@ -248,5 +248,5 @@ export const validateUpdateReportPayload = async (
stripUnknown: true,
});

return validatedPayload;
return validatedPayload as Report;
};
18 changes: 18 additions & 0 deletions services/app-api/utils/tests/mockReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,21 @@ export const invalidRadioCheckedChildrenReport = {
},
} as Record<MeasureTemplateName, MeasurePageTemplate>,
};

export const invalidPageElementType = {
...validReport,
pages: [
{
id: "general-info",
title: "General Info",
type: PageType.Standard,
sidebar: true,
elements: [
{
type: "badElementType", // Doesn't use ElementType enum
text: "State of Program Information",
},
],
},
],
};
28 changes: 17 additions & 11 deletions services/app-api/utils/tests/reportValidation.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { validateUpdateReportPayload } from "../reportValidation";
import { validateReportPayload } from "../reportValidation";
import {
validReport,
missingStateReport,
Expand All @@ -9,54 +9,60 @@ import {
invalidFormPageReport,
invalidParentPageReport,
invalidRadioCheckedChildrenReport,
invalidPageElementType,
} from "./mockReport";

describe("Test validateUpdateReportPayload function with valid report", () => {
describe("Test validateReportPayload function with valid report", () => {
it("successfully validates a valid report object", async () => {
const validatedData = await validateUpdateReportPayload(validReport);
const validatedData = await validateReportPayload(validReport);
expect(validatedData).toEqual(validReport);
});
});

describe("Test invalid reports", () => {
it("throws an error when validating a report with missing state", () => {
expect(async () => {
await validateUpdateReportPayload(missingStateReport);
await validateReportPayload(missingStateReport);
}).rejects.toThrow();
});
it("throws an error when validating a report with incorrect status", () => {
expect(async () => {
await validateUpdateReportPayload(incorrectStatusReport);
await validateReportPayload(incorrectStatusReport);
}).rejects.toThrow();
});
it("throws an error when validating a report with incorrect report type", () => {
expect(async () => {
await validateUpdateReportPayload(incorrectTypeReport);
await validateReportPayload(incorrectTypeReport);
}).rejects.toThrow();
});
it("throws an error when validating invalid measure templates", () => {
expect(async () => {
await validateUpdateReportPayload(invalidMeasureTemplatesReport);
await validateReportPayload(invalidMeasureTemplatesReport);
}).rejects.toThrow();
});
it("throws an error when validating invalid measure lookup object", () => {
expect(async () => {
await validateUpdateReportPayload(invalidMeasureLookupReport);
await validateReportPayload(invalidMeasureLookupReport);
}).rejects.toThrow();
});
it("throws an error when validating invalid form page object", () => {
expect(async () => {
await validateUpdateReportPayload(invalidFormPageReport);
await validateReportPayload(invalidFormPageReport);
}).rejects.toThrow();
});
it("throws an error when validating invalid parent page object", () => {
expect(async () => {
await validateUpdateReportPayload(invalidParentPageReport);
await validateReportPayload(invalidParentPageReport);
}).rejects.toThrow();
});
it("throws an error when validating invalid radio element checked children object", () => {
expect(async () => {
await validateUpdateReportPayload(invalidRadioCheckedChildrenReport);
await validateReportPayload(invalidRadioCheckedChildrenReport);
}).rejects.toThrow();
});
it("throws an error when validating invalid page element type", () => {
expect(async () => {
await validateReportPayload(invalidPageElementType);
}).rejects.toThrow();
});
});

0 comments on commit 6dcdcfb

Please sign in to comment.