Skip to content

Commit

Permalink
Merge pull request #714 from NHSDigital/release
Browse files Browse the repository at this point in the history
V4.25.0 Release
  • Loading branch information
adadigital authored Aug 29, 2024
2 parents 046271a + 00d61ce commit 8cd3487
Show file tree
Hide file tree
Showing 11 changed files with 161 additions and 8 deletions.
20 changes: 19 additions & 1 deletion proxies/shared/partials/Partial.Target.FaultRules.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
<FaultRule name="combined_backend_fault_handler">
[% include './partials/Partial.Component.SetResponseDefaults.xml' %]
<Step>
<Name>ExtractVariables.NhsAppAccounts.Error</Name>
<Name>ExtractVariables.ErrorMessage</Name>
<Condition>
response.content ~~ "\{\u0022message\u0022:\u0022.*\u0022\}"
</Condition>
</Step>
<Step>
<Name>ExtractVariables.ErrorDetails</Name>
<Condition>
response.content ~~ ".*\u0022errors\u0022:.*"
</Condition>
</Step>
<Step>
<Name>JavaScript.EnhanceErrorDetails</Name>
<Condition>
data.errors != null
</Condition>
</Step>
<Step>
<Name>RaiseFault.GenericErrorDetails</Name>
<Condition>
data.enhancedErrors != null
</Condition>
</Step>
<Step>
<Name>RaiseFault.404NhsAppAccounts</Name>
<Condition>
Expand Down
10 changes: 10 additions & 0 deletions proxies/shared/policies/ExtractVariables.ErrorDetails.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<ExtractVariables async="false" continueOnError="false" enabled="true" name="ExtractVariables.ErrorDetails">
<VariablePrefix>data</VariablePrefix>
<Source>error.content</Source>
<JSONPayload>
<Variable name="errors" type="string">
<JSONPath>$.errors</JSONPath>
</Variable>
</JSONPayload>
<IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
</ExtractVariables>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<ExtractVariables async="false" continueOnError="false" enabled="true" name="ExtractVariables.NhsAppAccounts.Error">
<ExtractVariables async="false" continueOnError="false" enabled="true" name="ExtractVariables.ErrorMessage">
<VariablePrefix>data</VariablePrefix>
<Source>error.content</Source>
<JSONPayload>
Expand Down
7 changes: 7 additions & 0 deletions proxies/shared/policies/JavaScript.EnhanceErrorDetails.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<Javascript async="false" continueOnError="false" enabled="true" timeLimit="200" name="JavaScript.EnhanceErrorDetails">
<DisplayName>JavaScript.EnhanceErrorDetails</DisplayName>
<Properties/>
<ResourceURL>jsc://EnhanceErrorDetails.js</ResourceURL>
</Javascript>
22 changes: 22 additions & 0 deletions proxies/shared/policies/RaiseFault.GenericErrorDetails.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<!--
Generic error response format. The errors are taken from data.enhancedErrors. Note the status code and reason phrase are not set but
derived from the target response.
-->

<RaiseFault async="false" continueOnError="false" enabled="true" name="RaiseFault.GenericErrorDetails">
<DisplayName>RaiseFault.GenericErrorDetails</DisplayName>
<Properties/>
<FaultResponse>
<Set>
<Headers/>
<Payload variablePrefix="%" variableSuffix="#">
{
"errors" : %data.enhancedErrors#
}
</Payload>
</Set>
</FaultResponse>
<IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
</RaiseFault>
29 changes: 29 additions & 0 deletions proxies/shared/resources/jsc/EnhanceErrorDetails.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const errors = context.getVariable('data.errors');
const messageId = context.getVariable('messageid');
const statusCode = context.getVariable('response.status.code');
const links = {
about : "{{ ERROR_ABOUT_LINK }}"
};

const enhancedErrors = [];

JSON.parse(errors).forEach((error, index) => {
var code = 'CM_INVALID_VALUE';
if (error.title === 'Missing value') {
code = 'CM_MISSING_VALUE';
}

enhancedErrors.push({
id: messageId + '.' + index,
code: code,
links: links,
status: statusCode,
title: error.title,
detail: error.message,
source: {
pointer: error.field
}
});
});

context.setVariable("data.enhancedErrors", JSON.stringify(enhancedErrors));
35 changes: 35 additions & 0 deletions sandbox/__test__/messages.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,41 @@ describe("/api/v1/messages", () => {
.expect("Content-Type", /json/, done);
});

it("returns a 400 for invalid email address", (done) => {
request(server)
.post("/api/v1/messages")
.send({
data: {
attributes: {
routingPlanId: "b838b13c-f98c-4def-93f0-515d4e4f4ee1",
messageReference: "b5bb84b9-a522-41e9-aa8b-ad1b6a454243",
recipient: {
nhsNumber: "1",
dateOfBirth: "1",
contactDetails: {
email: "invalidEmailAddress",
}
},
originator: {
odsCode: "X123"
},
personalisation: {},
},
},
})
.expect(400, {
message: "Invalid recipient contact details. Field 'email': Input failed format check",
errors: [
{
title: 'Invalid value',
field: '/data/attributes/recipient/contactDetails/email',
message: 'Input failed format check'
}
]
})
.expect("Content-Type", /json/, done);
});

it("responds with a 201 when the request is correctly formatted", (done) => {
request(server)
.post("/api/v1/messages")
Expand Down
2 changes: 2 additions & 0 deletions sandbox/handlers/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ validSendingGroupIds[trigger500SendingGroupId] = "Jc96S9y4AKjncXndUXiS7M7yIZ3jNt
validSendingGroupIds[trigger425SendingGroupId] = "_oivtOz8fHRXhtZAyFxJmT2j_xpWzq9s";
validSendingGroupIds[sendingGroupIdWithMissingNHSTemplates] = "DjAAu455M2TMq0VKEaD_1WZfwjkspJDL";
validSendingGroupIds[globalFreeTextNhsAppSendingGroupId] = "odbGpMQvYRM7sp7jueU2lRHQiueKujVO";
const invalidEmailAddress = "invalidEmailAddress";

const duplicateTemplates = [
{
Expand Down Expand Up @@ -58,4 +59,5 @@ export {
globalFreeTextNhsAppSendingGroupId,
noDefaultOdsClientAuth,
noOdsChangeClientAuth,
invalidEmailAddress
}
23 changes: 21 additions & 2 deletions sandbox/handlers/messages.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import KSUID from "ksuid";
import { sendError, writeLog, hasValidGlobalTemplatePersonalisation } from "./utils.js";
import { sendError, writeLog, hasValidGlobalTemplatePersonalisation, sendErrorWithDetails } from "./utils.js";
import {
sendingGroupIdWithMissingNHSTemplates,
sendingGroupIdWithMissingTemplates,
Expand All @@ -9,7 +9,8 @@ import {
validSendingGroupIds,
globalFreeTextNhsAppSendingGroupId,
noDefaultOdsClientAuth,
noOdsChangeClientAuth
noOdsChangeClientAuth,
invalidEmailAddress
} from "./config.js"

// Note: the docker container uses node:12 which does not support optional chaining
Expand All @@ -23,6 +24,10 @@ function getOriginatorOdsCode(req) {
return odsCode;
}

function getEmailOverride(req) {
return req?.body?.data?.attributes?.recipient?.contactDetails?.email
}

export async function messages(req, res, next) {
if (req.headers.authorization === "banned") {
sendError(
Expand Down Expand Up @@ -119,6 +124,20 @@ export async function messages(req, res, next) {
return;
}

const emailOverride = getEmailOverride(req);
if (emailOverride === invalidEmailAddress) {
const errors = [
{
title: "Invalid value",
field: "/data/attributes/recipient/contactDetails/email",
message: "Input failed format check"
}
];
sendErrorWithDetails(res, 400, "Invalid recipient contact details. Field 'email': Input failed format check", errors);
next();
return;
}

writeLog(res, "warn", {
message: "/api/v1/messages",
req: {
Expand Down
8 changes: 8 additions & 0 deletions sandbox/handlers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ export function sendError(res, code, message) {
});
}

export function sendErrorWithDetails(res, code, message, errors) {
res.status(code);
res.json({
message,
errors
});
}

export function hasValidGlobalTemplatePersonalisation(personalisation) {
if (!personalisation) {
return false;
Expand Down
11 changes: 7 additions & 4 deletions tests/lib/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,22 @@ def generate_authentication(self, env):
else:
raise ValueError("Unknown value: ", env)

_, timestamp_of_last_token_fetch = self.tokens.get(env, (None, 0))
_, latest_token_expiry = self.tokens.get(env, (None, 0))

if env not in self.tokens or timestamp_of_last_token_fetch + 585 < int(time()):
# Generate new token if latest token will expire in 15 seconds
if env not in self.tokens or latest_token_expiry < int(time()) + 15:
pk_pem = None
with open(private_key, "r") as f:
pk_pem = f.read()

token_expiry = int(time()) + 180

claims = {
"sub": api_key,
"iss": api_key,
"jti": str(uuid.uuid4()),
"aud": url,
"exp": int(time()) + 180,
"exp": token_expiry,
}
additional_headers = {"kid": kid}

Expand All @@ -61,7 +64,7 @@ def generate_authentication(self, env):
)
details = json.loads(resp.content)

self.tokens[env] = (f"Bearer {details.get('access_token')}", int(time()))
self.tokens[env] = (f"Bearer {details.get('access_token')}", token_expiry)

bearer_token = self.tokens[env][0]
return Secret(bearer_token)

0 comments on commit 8cd3487

Please sign in to comment.