Skip to content

Commit 6ae8678

Browse files
Add basic error handling
1 parent 766c990 commit 6ae8678

File tree

11 files changed

+270
-125
lines changed

11 files changed

+270
-125
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { randomUUID } from 'crypto';
2+
3+
export interface ApiError {
4+
id: string;
5+
code: ApiErrorCode;
6+
links: {about: 'https://digital.nhs.uk/developer/api-catalogue/nhs-notify-supplier'};
7+
status: ApiErrorStatus;
8+
title: ApiErrorTitle;
9+
detail: ApiErrorDetail | string;
10+
}
11+
12+
export enum ApiErrorCode {
13+
InternalServerError = 'NOTIFY_INTERNAL_SERVER_ERROR',
14+
InvalidRequest = 'NOTIFY_INVALID_REQUEST',
15+
NotFound = 'NOTIFY_LETTER_NOT_FOUND'
16+
}
17+
18+
export enum ApiErrorTitle {
19+
InternalServerError = 'Internal server error',
20+
InvalidRequest = 'Invalid request',
21+
NotFound = 'Not found'
22+
}
23+
24+
export enum ApiErrorStatus {
25+
InternalServerError = "500",
26+
InvalidRequest = "400",
27+
NotFound = "404"
28+
}
29+
30+
export enum ApiErrorDetail {
31+
NotFoundLetterId = 'The provided letter ID does not exist',
32+
InvalidRequestMissingSupplierId = 'The supplier ID is missing from the request',
33+
InvalidRequestMissingBody = 'The request is missing the body',
34+
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'
36+
}
37+
38+
export function buildApiError(params: {
39+
code: ApiErrorCode;
40+
status: ApiErrorStatus;
41+
title: ApiErrorTitle;
42+
detail: ApiErrorDetail | string;
43+
}): ApiError {
44+
return {
45+
id: randomUUID(),
46+
code: params.code,
47+
links: { about: 'https://digital.nhs.uk/developer/api-catalogue/nhs-notify-supplier' },
48+
status: params.status,
49+
title: params.title,
50+
detail: params.detail,
51+
};
52+
}
Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1-
export class NotFoundError extends Error {}
2-
export class ValidationError extends Error {}
3-
export class ConflictError extends Error {}
1+
import { ApiErrorDetail } from "../contracts/errors";
2+
3+
export class NotFoundError extends Error {
4+
detail: ApiErrorDetail;
5+
constructor(detail: ApiErrorDetail) {
6+
super(detail);
7+
this.detail = detail;
8+
}
9+
}
10+
11+
export class ValidationError extends Error {
12+
detail: ApiErrorDetail;
13+
constructor(detail: ApiErrorDetail) {
14+
super(detail);
15+
this.detail = detail;
16+
}
17+
}
Lines changed: 30 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
11
import { patchLetters } from '../../index';
2-
import type { Context } from 'aws-lambda';
2+
import type { 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';
66
import { NotFoundError, ValidationError } from '../../errors';
77
import { LetterApiDocument, LetterApiStatus } from '../../contracts/letter-api';
8+
import { ErrorResponse, mapErrorToResponse } from '../../mappers/error-mapper';
89

910
jest.mock('../../services/letter-operations');
11+
jest.mock('../../mappers/error-mapper');
12+
13+
const mockedMapErrorToResponse = jest.mocked(mapErrorToResponse);
14+
const expectedErrorResponse: APIGatewayProxyResult = {
15+
statusCode: 400,
16+
body: "Error"
17+
};
18+
mockedMapErrorToResponse.mockReturnValue(expectedErrorResponse);
19+
20+
const mockedPatchLetterStatus = jest.mocked(letterService.patchLetterStatus);
21+
22+
const letterApiDocument = makeLetterApiDocument("id1", "REJECTED");
23+
const requestBody = JSON.stringify(letterApiDocument, null, 2);
1024

1125
function makeLetterApiDocument(id: string, status: LetterApiStatus) : LetterApiDocument {
1226
return {
@@ -23,13 +37,13 @@ function makeLetterApiDocument(id: string, status: LetterApiStatus) : LetterApiD
2337
};
2438
}
2539

26-
const letterApiDocument = makeLetterApiDocument("id1", "REJECTED");
27-
28-
const requestBody = JSON.stringify(letterApiDocument, null, 2);
40+
beforeEach(() => {
41+
jest.clearAllMocks();
42+
});
2943

3044
describe('patchLetters API Handler', () => {
31-
it('returns 200 OK with updated resource', async () => {
3245

46+
it('returns 200 OK with updated resource', async () => {
3347
const event = makeApiGwEvent({
3448
path: '/letters/id1',
3549
body: requestBody,
@@ -38,7 +52,6 @@ describe('patchLetters API Handler', () => {
3852
const context = mockDeep<Context>();
3953
const callback = jest.fn();
4054

41-
const mockedPatchLetterStatus = letterService.patchLetterStatus as jest.Mock;
4255
mockedPatchLetterStatus.mockResolvedValue(letterApiDocument);
4356

4457
const result = await patchLetters(event, context, callback);
@@ -49,22 +62,20 @@ describe('patchLetters API Handler', () => {
4962
});
5063
});
5164

52-
it('returns 400 Bad Request as there is no body', async () => {
65+
it('returns error response when there is no body', async () => {
5366
const event = makeApiGwEvent({
5467
path: '/letters/id1',
5568
pathParameters: {id: "id1"},
5669
headers: {'app-supplier-id': 'supplier1'}});
5770
const context = mockDeep<Context>();
5871
const callback = jest.fn();
72+
5973
const result = await patchLetters(event, context, callback);
6074

61-
expect(result).toEqual({
62-
statusCode: 400,
63-
body: 'Bad Request: Missing request body',
64-
});
75+
expect(result).toEqual(expectedErrorResponse);
6576
});
6677

67-
it('returns 404 Not Found as path parameter is not found', async () => {
78+
it('returns error response when path parameter letterId is not found', async () => {
6879
const event = makeApiGwEvent({
6980
path: '/letters/',
7081
body: requestBody,
@@ -73,15 +84,11 @@ describe('patchLetters API Handler', () => {
7384
const callback = jest.fn();
7485
const result = await patchLetters(event, context, callback);
7586

76-
expect(result).toEqual({
77-
statusCode: 404,
78-
body: 'Not Found: The requested resource does not exist',
79-
});
87+
expect(result).toEqual(expectedErrorResponse);
8088
});
8189

82-
it('returns 400 Bad Request when ValidationError is thrown by service', async () => {
83-
const mockedPatchLetterStatus = letterService.patchLetterStatus as jest.Mock;
84-
mockedPatchLetterStatus.mockRejectedValue(new ValidationError('Validation failed'));
90+
it('returns error response when error is thrown by service', async () => {
91+
mockedPatchLetterStatus.mockRejectedValue(new Error('Service error'));
8592

8693
const event = makeApiGwEvent({
8794
path: '/letters/id1',
@@ -94,61 +101,19 @@ describe('patchLetters API Handler', () => {
94101

95102
const result = await patchLetters(event, context, callback);
96103

97-
expect(result).toEqual({
98-
statusCode: 400,
99-
body: 'Validation failed'
100-
});
104+
expect(result).toEqual(expectedErrorResponse);
101105
});
102106

103-
it('returns 404 Not Found when NotFoundError is thrown by service', async () => {
104-
const mockedPatchLetterStatus = letterService.patchLetterStatus as jest.Mock;
105-
mockedPatchLetterStatus.mockRejectedValue(new NotFoundError('Letter not found'));
106-
107-
const event = makeApiGwEvent({
108-
path: '/letters/id1',
109-
body: requestBody,
110-
pathParameters: {id: "id1"},
111-
headers: {'app-supplier-id': 'supplier1'}
112-
});
113-
const context = mockDeep<Context>();
114-
const callback = jest.fn();
115-
116-
const result = await patchLetters(event, context, callback);
117-
118-
expect(result).toEqual({
119-
statusCode: 404,
120-
body: 'Letter not found'
121-
});
122-
});
123-
124-
it('throws unexpected errors from service', async () => {
125-
const mockedPatchLetterStatus = letterService.patchLetterStatus as jest.Mock;
126-
mockedPatchLetterStatus.mockRejectedValue(new Error('Unexpected error'));
127-
128-
const event = makeApiGwEvent({
129-
path: '/letters/id1',
130-
body: requestBody,
131-
pathParameters: {id: "id1"},
132-
headers: {'app-supplier-id': 'supplier1'}
133-
});
134-
const context = mockDeep<Context>();
135-
const callback = jest.fn();
136-
137-
await expect(patchLetters(event, context, callback)).rejects.toThrow('Unexpected error');
138-
});
139-
140-
it('returns 400 Bad Request: Missing supplier ID if header app-supplier-id ', async () => {
107+
it('returns error when app-supplier-id is missing', async () => {
141108
const event = makeApiGwEvent({
142109
path: '/letters/id1',
143110
body: requestBody,
144111
pathParameters: {id: "id1"}});
145112
const context = mockDeep<Context>();
146113
const callback = jest.fn();
114+
147115
const result = await patchLetters(event, context, callback);
148116

149-
expect(result).toEqual({
150-
statusCode: 400,
151-
body: 'Bad Request: Missing supplier ID',
152-
});
117+
expect(result).toEqual(expectedErrorResponse);
153118
});
154119
});

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

Lines changed: 17 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,61 +2,35 @@ import { APIGatewayProxyHandler } from 'aws-lambda';
22
import { createLetterRepository } from '../infrastructure/letter-repo-factory';
33
import { patchLetterStatus } from '../services/letter-operations';
44
import { LetterApiDocument } from '../contracts/letter-api';
5-
import { NotFoundError, ValidationError } from '../errors';
5+
import * as errors from '../contracts/errors';
6+
import { ValidationError } from '../errors';
7+
import { mapErrorToResponse } from '../mappers/error-mapper';
68

79
const letterRepo = createLetterRepository();
810
export const patchLetters: APIGatewayProxyHandler = async (event) => {
911

10-
const supplierId = event.headers['app-supplier-id'];
11-
12-
if (!supplierId) {
13-
return {
14-
statusCode: 400,
15-
body: "Bad Request: Missing supplier ID"
16-
};
17-
}
18-
19-
const letterId = event.pathParameters?.id;
20-
21-
if (!letterId) {
22-
return {
23-
statusCode: 404,
24-
body: "Not Found: The requested resource does not exist"
25-
};
26-
}
27-
28-
if (!event.body)
29-
{
30-
return {
31-
statusCode: 400,
32-
body: "Bad Request: Missing request body"
33-
}
34-
}
35-
36-
const patchLetterRequest: LetterApiDocument = JSON.parse(event.body);
37-
3812
try {
13+
const supplierId = assertNotEmpty(event.headers['app-supplier-id'], errors.ApiErrorDetail.InvalidRequestMissingSupplierId);
14+
const letterId = assertNotEmpty( event.pathParameters?.id, errors.ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter);
15+
const body = assertNotEmpty(event.body, errors.ApiErrorDetail.InvalidRequestMissingBody);
3916

40-
// TODO CCM-11188: Is it worth retrieving the letter first to check if the status is different?
17+
const patchLetterRequest: LetterApiDocument = JSON.parse(body);
4118

42-
const result = await patchLetterStatus(patchLetterRequest.data, letterId, supplierId, letterRepo);
19+
const result = await patchLetterStatus(patchLetterRequest.data, letterId!, supplierId!, letterRepo);
4320

4421
return {
4522
statusCode: 200,
4623
body: JSON.stringify(result, null, 2)
4724
};
25+
4826
} catch (error) {
49-
if (error instanceof ValidationError) {
50-
return {
51-
statusCode: 400,
52-
body: error.message
53-
};
54-
} else if (error instanceof NotFoundError) {
55-
return {
56-
statusCode: 404,
57-
body: error.message
58-
};
59-
}
60-
throw error;
27+
return mapErrorToResponse(error);
6128
}
6229
};
30+
31+
function assertNotEmpty(value: string | null | undefined, detail: errors.ApiErrorDetail): string {
32+
if (!value || value.trim() === '') {
33+
throw new ValidationError(detail);
34+
}
35+
return value;
36+
}

0 commit comments

Comments
 (0)