Skip to content

Commit 8382a2a

Browse files
committed
CCM-11602: param validation, max range, and OAS 400 examples
1 parent 2f97221 commit 8382a2a

File tree

11 files changed

+123
-22
lines changed

11 files changed

+123
-22
lines changed

infrastructure/terraform/components/api/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ No requirements.
1212
| <a name="input_aws_account_id"></a> [aws\_account\_id](#input\_aws\_account\_id) | The AWS Account ID (numeric) | `string` | n/a | yes |
1313
| <a name="input_ca_pem_filename"></a> [ca\_pem\_filename](#input\_ca\_pem\_filename) | Filename for the CA truststore file within the s3 bucket | `string` | `null` | no |
1414
| <a name="input_component"></a> [component](#input\_component) | The variable encapsulating the name of this component | `string` | `"supapi"` | no |
15+
| <a name="input_default_get_limit"></a> [default\_get\_limit](#input\_default\_get\_limit) | Default limit to apply to GET requests that support pagination | `number` | `2500` | no |
1516
| <a name="input_default_tags"></a> [default\_tags](#input\_default\_tags) | A map of default tags to apply to all taggable resources within the component | `map(string)` | `{}` | no |
1617
| <a name="input_enable_backups"></a> [enable\_backups](#input\_enable\_backups) | Enable backups | `bool` | `false` | no |
1718
| <a name="input_environment"></a> [environment](#input\_environment) | The name of the tfscaffold environment | `string` | n/a | yes |
1819
| <a name="input_force_lambda_code_deploy"></a> [force\_lambda\_code\_deploy](#input\_force\_lambda\_code\_deploy) | If the lambda package in s3 has the same commit id tag as the terraform build branch, the lambda will not update automatically. Set to True if making changes to Lambda code from on the same commit for example during development | `bool` | `false` | no |
1920
| <a name="input_group"></a> [group](#input\_group) | The group variables are being inherited from (often synonmous with account short-name) | `string` | n/a | yes |
2021
| <a name="input_kms_deletion_window"></a> [kms\_deletion\_window](#input\_kms\_deletion\_window) | When a kms key is deleted, how long should it wait in the pending deletion state? | `string` | `"30"` | no |
22+
| <a name="input_letter_table_ttl_hours"></a> [letter\_table\_ttl\_hours](#input\_letter\_table\_ttl\_hours) | Number of hours to set as TTL on letters table | `number` | `24` | no |
2123
| <a name="input_log_level"></a> [log\_level](#input\_log\_level) | The log level to be used in lambda functions within the component. Any log with a lower severity than the configured value will not be logged: https://docs.python.org/3/library/logging.html#levels | `string` | `"INFO"` | no |
2224
| <a name="input_log_retention_in_days"></a> [log\_retention\_in\_days](#input\_log\_retention\_in\_days) | The retention period in days for the Cloudwatch Logs events to be retained, default of 0 is indefinite | `number` | `0` | no |
2325
| <a name="input_manually_configure_mtls_truststore"></a> [manually\_configure\_mtls\_truststore](#input\_manually\_configure\_mtls\_truststore) | Manually manage the truststore used for API Gateway mTLS (e.g. for prod environment) | `bool` | `false` | no |

infrastructure/terraform/components/api/module_lambda_get_letters.tf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ module "get_letters" {
3737

3838
lambda_env_vars = {
3939
LETTERS_TABLE_NAME = aws_dynamodb_table.letters.name,
40-
LETTER_TTL_HOURS = 24
40+
LETTER_TTL_HOURS = var.letter_table_ttl_hours,
41+
DEFAULT_LIMIT = var.default_get_limit,
4142
}
4243
}
4344

infrastructure/terraform/components/api/variables.tf

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,20 @@ variable "enable_backups" {
9999
default = false
100100
}
101101

102-
103102
variable "ca_pem_filename" {
104103
type = string
105104
description = "Filename for the CA truststore file within the s3 bucket"
106105
default = null
107106
}
107+
108+
variable "letter_table_ttl_hours" {
109+
type = number
110+
description = "Number of hours to set as TTL on letters table"
111+
default = 24
112+
}
113+
114+
variable "default_get_limit" {
115+
type = number
116+
description = "Default limit to apply to GET requests that support pagination"
117+
default = 2500
118+
}

internal/datastore/src/types.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@ The schemas are generated from Zod definitions and provide a visual representati
1010
erDiagram
1111
Letter {
1212
string id
13-
string supplierId "ref: Supplier"
13+
string status "enum: PENDING, ACCEPTED, REJECTED, PRINTED, ENCLOSED, CANCELLED, DISPATCHED, FAILED, RETURNED, DESTROYED, FORWARDED, DELIVERED"
1414
string specificationId
1515
string groupId
16+
number reasonCode
17+
string reasonText
18+
string supplierId
1619
string url "url"
17-
string status "enum: PENDING, ACCEPTED, DISPATCHED, FAILED, REJECTED, DELIVERED, CANCELLED"
1820
string createdAt
1921
string updatedAt
2022
string supplierStatus
23+
string supplierStatusSk
2124
number ttl "min: -9007199254740991, max: 9007199254740991"
2225
}
23-
Supplier {
24-
}
25-
Letter }o--|| Supplier : "supplierId"
2626
```
2727

2828
## MI schema

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

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ describe('API Lambda handler', () => {
1010

1111
beforeEach(() => {
1212
jest.resetAllMocks();
13+
process.env.DEFAULT_LIMIT = '100';
1314
});
1415

1516
it('returns 200 OK with basic paginated resources', async () => {
@@ -90,11 +91,11 @@ describe('API Lambda handler', () => {
9091

9192
expect(result).toEqual({
9293
statusCode: 400,
93-
body: "Bad Request: limit parameter is not a number",
94+
body: "Invalid Request: limit parameter must be a positive number not greater than 2500",
9495
});
9596
});
9697

97-
it("returns 400 if the limit parameter is not positive", async () => {
98+
it("returns 400 if the limit parameter is negative", async () => {
9899
const event = makeApiGwEvent({
99100
path: "/letters",
100101
queryStringParameters: { limit: "-1" },
@@ -105,7 +106,37 @@ describe('API Lambda handler', () => {
105106

106107
expect(result).toEqual({
107108
statusCode: 400,
108-
body: "Bad Request: limit parameter is not positive",
109+
body: "Invalid Request: limit parameter must be a positive number not greater than 2500",
110+
});
111+
});
112+
113+
it("returns 400 if the limit parameter is out of range", async () => {
114+
const event = makeApiGwEvent({
115+
path: "/letters",
116+
queryStringParameters: { limit: "2501" },
117+
});
118+
const context = mockDeep<Context>();
119+
const callback = jest.fn();
120+
const result = await getLetters(event, context, callback);
121+
122+
expect(result).toEqual({
123+
statusCode: 400,
124+
body: "Invalid Request: limit parameter must be a positive number not greater than 2500",
125+
});
126+
});
127+
128+
it("returns 400 if unknown parameters are present", async () => {
129+
const event = makeApiGwEvent({
130+
path: "/letters",
131+
queryStringParameters: { max: "2000" },
132+
});
133+
const context = mockDeep<Context>();
134+
const callback = jest.fn();
135+
const result = await getLetters(event, context, callback);
136+
137+
expect(result).toEqual({
138+
statusCode: 400,
139+
body: "Invalid Request: Only 'limit' query parameter is allowed",
109140
});
110141
});
111142

@@ -117,7 +148,7 @@ describe('API Lambda handler', () => {
117148

118149
expect(result).toEqual({
119150
statusCode: 400,
120-
body: 'Bad Request: Missing supplier ID',
151+
body: 'Invalid Request: Missing supplier ID',
121152
});
122153
});
123154

@@ -129,7 +160,7 @@ describe('API Lambda handler', () => {
129160

130161
expect(result).toEqual({
131162
statusCode: 400,
132-
body: 'Bad Request: Missing supplier ID',
163+
body: 'Invalid Request: Missing supplier ID',
133164
});
134165
});
135166
});

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

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import pino from 'pino';
66

77
const letterRepo = createLetterRepository();
88
const log = pino();
9+
const DEFAULT_LIMIT = process.env.DEFAULT_LIMIT || '2500';
910

1011
export const getLetters: APIGatewayProxyHandler = async (event) => {
1112
if (event.path === "/letters") {
@@ -17,19 +18,32 @@ export const getLetters: APIGatewayProxyHandler = async (event) => {
1718
});
1819
return {
1920
statusCode: 400,
20-
body: "Bad Request: Missing supplier ID",
21+
body: "Invalid Request: Missing supplier ID",
2122
};
2223
}
2324

2425
// The endpoint should only return pending letters for now
2526
const status = "PENDING";
2627

27-
let limit = event.queryStringParameters?.limit;
28+
if (
29+
event.queryStringParameters &&
30+
Object.keys(event.queryStringParameters).some(
31+
(key) => key !== "limit"
32+
)
33+
) {
34+
log.info({
35+
description: "Unexpected query parameter(s) present",
36+
queryStringParameters: event.queryStringParameters,
37+
});
2838

29-
if (!limit) {
30-
limit = "10";
39+
return {
40+
statusCode: 400,
41+
body: "Invalid Request: Only 'limit' query parameter is supported",
42+
};
3143
}
3244

45+
let limit = event.queryStringParameters?.limit || DEFAULT_LIMIT;
46+
3347
let limitNumber = Number(limit);
3448

3549
if (isNaN(limitNumber)) {
@@ -39,18 +53,18 @@ export const getLetters: APIGatewayProxyHandler = async (event) => {
3953
});
4054
return {
4155
statusCode: 400,
42-
body: "Bad Request: limit parameter is not a number",
56+
body: "Invalid Request: limit parameter must be a positive number not greater than 2500",
4357
};
4458
}
4559

46-
if (limitNumber < 0) {
60+
if (limitNumber < 0 || limitNumber > 2500) {
4761
log.info({
48-
description: "limit parameter is not positive",
62+
description: "Limit value is invalid",
4963
limit,
5064
});
5165
return {
5266
statusCode: 400,
53-
body: "Bad Request: limit parameter is not positive",
67+
body: "Invalid Request: limit parameter must be a positive number not greater than 2500",
5468
};
5569
}
5670

specification/api/components/endpoints/listLetters.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ description: The key use of this endpoint is to query letters which are ready to
1111
responses:
1212
'200':
1313
$ref: "../responses/getLetters200.yml"
14+
'400':
15+
$ref: "../responses/errors/listLetters/listLettersBadRequest.yml"
1416
'404':
1517
$ref: "../responses/errors/resourceNotFound.yml"
1618
'429':
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"errors": [
3+
{
4+
"code": "NOTIFY_INVALID_REQUEST",
5+
"detail": "Invalid Request: limit parameter must be a positive number not greater than 2500",
6+
"id": "rrt-1931948104716186917-c-geu2-10664-3111479-3.0",
7+
"links": {
8+
"about": "https://digital.nhs.uk/developer/api-catalogue/nhs-notify-supplier"
9+
},
10+
"status": "400",
11+
"title": "Invalid request"
12+
}
13+
]
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"errors": [
3+
{
4+
"code": "NOTIFY_INVALID_REQUEST",
5+
"detail": "Invalid Request: Only 'limit' query parameter is supported",
6+
"id": "rrt-1931948104716186917-c-geu2-10664-3111479-3.0",
7+
"links": {
8+
"about": "https://digital.nhs.uk/developer/api-catalogue/nhs-notify-supplier"
9+
},
10+
"status": "400",
11+
"title": "Invalid request"
12+
}
13+
]
14+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: limit
22
in: query
33
description: |-
4-
The maximum number of items to return in a single request
4+
The maximum number of items to return in a single request, max 2500
55
schema:
66
type: number
7-
example: 100
7+
example: 2500

0 commit comments

Comments
 (0)