Skip to content

Commit 4a10a01

Browse files
Validate request body
1 parent fa437fb commit 4a10a01

File tree

5 files changed

+110
-34
lines changed

5 files changed

+110
-34
lines changed

lambdas/api-handler/src/contracts/errors.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ export enum ApiErrorDetail {
3232
InvalidRequestMissingSupplierId = 'The supplier ID is missing from the request',
3333
InvalidRequestMissingBody = 'The request is missing the body',
3434
InvalidRequestMissingLetterIdPathParameter = 'The request is missing the letter id path parameter',
35-
InvalidRequestLetterIdsMismatch = 'The letter ID in the request body does not match the letter ID path parameter'
35+
InvalidRequestLetterIdsMismatch = 'The letter ID in the request body does not match the letter ID path parameter',
36+
InvalidRequestBody = 'The request body is invalid'
3637
}
3738

3839
export function buildApiError(params: {
Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,41 @@
1-
export interface LetterApiAttributes {
2-
reasonCode: number;
3-
reasonText: string;
4-
requestedProductionStatus: 'ACTIVE' | 'HOLD' | 'CANCEL';
5-
status: LetterApiStatus;
6-
}
7-
8-
export interface LetterApiResource {
9-
id: string;
10-
type: 'Letter';
11-
attributes: LetterApiAttributes;
12-
}
13-
14-
export interface LetterApiDocument {
15-
data: LetterApiResource;
16-
}
17-
18-
export type LetterApiStatus = 'PENDING' | 'ACCEPTED' | 'REJECTED' | 'PRINTED' | 'ENCLOSED' | 'CANCELLED' | 'DISPATCHED' | 'FAILED' | 'RETURNED' | 'DESTROYED' | 'FORWARDED' | 'DELIVERED';
1+
import { z } from "zod";
2+
3+
export const LetterApiStatusSchema = z.enum([
4+
"PENDING",
5+
"ACCEPTED",
6+
"REJECTED",
7+
"PRINTED",
8+
"ENCLOSED",
9+
"CANCELLED",
10+
"DISPATCHED",
11+
"FAILED",
12+
"RETURNED",
13+
"DESTROYED",
14+
"FORWARDED",
15+
"DELIVERED",
16+
]);
17+
18+
export type LetterApiStatus = z.infer<typeof LetterApiStatusSchema>;
19+
20+
export const LetterApiAttributesSchema = z.object({
21+
reasonCode: z.number(),
22+
reasonText: z.string(),
23+
requestedProductionStatus: z.enum(["ACTIVE", "HOLD", "CANCEL"]),
24+
status: LetterApiStatusSchema,
25+
});
26+
27+
export type LetterApiAttributes = z.infer<typeof LetterApiAttributesSchema>;
28+
29+
export const LetterApiResourceSchema = z.object({
30+
id: z.string(),
31+
type: z.literal("Letter"),
32+
attributes: LetterApiAttributesSchema,
33+
});
34+
35+
export type LetterApiResource = z.infer<typeof LetterApiResourceSchema>;
36+
37+
export const LetterApiDocumentSchema = z.object({
38+
data: LetterApiResourceSchema,
39+
});
40+
41+
export type LetterApiDocument = z.infer<typeof LetterApiDocumentSchema>;
Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
import { ApiErrorDetail } from "../contracts/errors";
22

3-
export class NotFoundError extends Error {
3+
class ApiError extends Error {
44
detail: ApiErrorDetail;
5-
constructor(detail: ApiErrorDetail) {
6-
super(detail);
5+
constructor(detail: ApiErrorDetail, message?: string, cause?: Error) {
6+
super(message ?? detail, { cause });
77
this.detail = detail;
88
}
99
}
1010

11-
export class ValidationError extends Error {
12-
detail: ApiErrorDetail;
13-
constructor(detail: ApiErrorDetail) {
14-
super(detail);
15-
this.detail = detail;
16-
}
17-
}
11+
export class NotFoundError extends ApiError {}
12+
13+
export class ValidationError extends ApiError {}

lambdas/api-handler/src/handlers/__tests__/patch-letters.test.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { patchLetters } from '../../index';
2-
import type { APIGatewayProxyResult, Context } from 'aws-lambda';
2+
import { APIGatewayProxyResult, Context } from 'aws-lambda';
33
import { mockDeep } from 'jest-mock-extended';
44
import { makeApiGwEvent } from './utils/test-utils';
55
import * as letterService from '../../services/letter-operations';
6-
import { NotFoundError, ValidationError } from '../../errors';
76
import { LetterApiDocument, LetterApiStatus } from '../../contracts/letter-api';
8-
import { ErrorResponse, mapErrorToResponse } from '../../mappers/error-mapper';
7+
import { mapErrorToResponse } from '../../mappers/error-mapper';
98

109
jest.mock('../../services/letter-operations');
1110
jest.mock('../../mappers/error-mapper');
@@ -116,4 +115,52 @@ describe('patchLetters API Handler', () => {
116115

117116
expect(result).toEqual(expectedErrorResponse);
118117
});
118+
119+
it('returns error when request body does not have correct shape', async () => {
120+
const event = makeApiGwEvent({
121+
path: '/letters/id1',
122+
body: '{test: "test"}',
123+
pathParameters: {id: "id1"},
124+
headers: {'nhsd-supplier-id': 'supplier1'}});
125+
const context = mockDeep<Context>();
126+
const callback = jest.fn();
127+
128+
const result = await patchLetters(event, context, callback);
129+
130+
expect(result).toEqual(expectedErrorResponse);
131+
});
132+
133+
it('returns error when request body is not json', async () => {
134+
const event = makeApiGwEvent({
135+
path: '/letters/id1',
136+
body: '{#invalidJSON',
137+
pathParameters: {id: "id1"},
138+
headers: {'nhsd-supplier-id': 'supplier1'}});
139+
const context = mockDeep<Context>();
140+
const callback = jest.fn();
141+
142+
const result = await patchLetters(event, context, callback);
143+
144+
expect(result).toEqual(expectedErrorResponse);
145+
});
146+
147+
it('returns error if unexpected error is thrown', async () => {
148+
const event = makeApiGwEvent({
149+
path: '/letters/id1',
150+
body: '{#invalidJSON',
151+
pathParameters: {id: "id1"},
152+
headers: {'nhsd-supplier-id': 'supplier1'}});
153+
const context = mockDeep<Context>();
154+
const callback = jest.fn();
155+
156+
const spy = jest.spyOn(JSON, "parse").mockImplementation(() => {
157+
throw "Unexpected error";
158+
});
159+
160+
const result = await patchLetters(event, context, callback);
161+
162+
expect(result).toEqual(expectedErrorResponse);
163+
164+
spy.mockRestore();
165+
});
119166
});

lambdas/api-handler/src/handlers/patch-letters.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { APIGatewayProxyHandler } from 'aws-lambda';
22
import { createLetterRepository } from '../infrastructure/letter-repo-factory';
33
import { patchLetterStatus } from '../services/letter-operations';
4-
import { LetterApiDocument } from '../contracts/letter-api';
4+
import { LetterApiDocument, LetterApiDocumentSchema } from '../contracts/letter-api';
55
import * as errors from '../contracts/errors';
66
import { ValidationError } from '../errors';
77
import { mapErrorToResponse } from '../mappers/error-mapper';
@@ -14,7 +14,16 @@ export const patchLetters: APIGatewayProxyHandler = async (event) => {
1414
const letterId = assertNotEmpty( event.pathParameters?.id, errors.ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter);
1515
const body = assertNotEmpty(event.body, errors.ApiErrorDetail.InvalidRequestMissingBody);
1616

17-
const patchLetterRequest: LetterApiDocument = JSON.parse(body);
17+
let patchLetterRequest: LetterApiDocument;
18+
19+
try {
20+
patchLetterRequest = LetterApiDocumentSchema.parse(JSON.parse(body));
21+
} catch (error) {
22+
if (error instanceof Error) {
23+
throw new ValidationError(errors.ApiErrorDetail.InvalidRequestBody, error.message, error);
24+
}
25+
else throw error;
26+
}
1827

1928
const result = await patchLetterStatus(patchLetterRequest.data, letterId!, supplierId!, letterRepo);
2029

0 commit comments

Comments
 (0)