Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/proxies.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,37 @@ flowchart
MRESP --> E
```

### Get Message Responses

This flow maps get message responses requests to the app-response service and maps the response to the public API format.

Source: [proxies/shared/partials/Partial.Flows.GetMessageResponsesEndpoint.xml](https://github.com/NHSDigital/communications-manager-api/blob/release/proxies/shared/partials/Partial.Flows.GetMessageResponsesEndpoint.xml)

```mermaid
flowchart
S[Start] --> Q1{Matches get message responses endpoint?}
Q1 --> |No| E[End]
Q1 --> |Yes| EV[Extract messageId from request

<em><a href='https://github.com/NHSDigital/communications-manager-api/blob/release/proxies/shared/policies/ExtractVariables.MessageResponses.Get.Request.xml'>ExtractVariables.MessageResponses.Get.Request</a></em>]
EV --> V[Validate messageId

<em><a href='https://github.com/NHSDigital/communications-manager-api/blob/release/proxies/shared/policies/JavaScript.MessageResponses.Get.Validate.xml'>JavaScript.MessageResponses.Get.Validate</a></em>]
V --> Q2{Validation errors found?}
Q2 --> |Yes| 400[Raise 400 error

<em><a href='https://github.com/NHSDigital/communications-manager-api/blob/release/proxies/shared/policies/RaiseFault.4xxGeneric.xml'>RaiseFault.4xxGeneric</a></em>]
400 --> E
Q2 --> |No| MR[Create app-response request

<em><a href='https://github.com/NHSDigital/communications-manager-api/blob/release/proxies/shared/policies/AssignMessage.MessageResponses.Get.Request.xml'>AssignMessage.MessageResponses.Get.Request</a></em>]
MR --> SEND[Send request to app-response service]
SEND --> RESP[Convert response

<em><a href='https://github.com/NHSDigital/communications-manager-api/blob/release/proxies/shared/policies/AssignMessage.MessageResponses.Get.Response.xml'>AssignMessage.MessageResponses.Get.Response</a></em>]
RESP --> E
```

### Target Post Flow

This flow runs on all outgoing responses from the target.
Expand Down
4 changes: 4 additions & 0 deletions proxies/live/apiproxy/proxies/default.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
<VirtualHost>secure</VirtualHost>
</HTTPProxyConnection>
[% include './partials/Partial.Proxy.Routes.xml' %]
<RouteRule name="app-response-target">
<Condition>(proxy.pathsuffix MatchesPath "/v1/message-responses/{messageId}") and (request.verb = "GET")</Condition>
<TargetEndpoint>app-response-target</TargetEndpoint>
</RouteRule>
<RouteRule name="communications-manager-target">
<TargetEndpoint>communications-manager-target</TargetEndpoint>
</RouteRule>
Expand Down
26 changes: 26 additions & 0 deletions proxies/live/apiproxy/targets/app-response-target.xml
Comment thread
rhyscoxnhs marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<TargetEndpoint name="app-response-target">
<PreFlow>
<Request>
[% include './partials/Partial.Target.PreFlowRequest.xml' %]
</Request>
</PreFlow>
<Flows>
[% include './partials/Partial.Flows.GetMessageResponsesEndpoint.xml' %]
</Flows>
[% include './partials/Partial.Target.PostFlow.xml' %]
<FaultRules>
[% include './partials/Partial.Target.FaultRules.xml' %]
</FaultRules>
<HTTPTargetConnection>
<SSLInfo>
<Enabled>true</Enabled>
</SSLInfo>
<LoadBalancer>
<Server name="app-response-target"/>
</LoadBalancer>
<Path>{requestpath}</Path>
<Properties>
<Property name="io.timeout.millis">29000</Property>
</Properties>
</HTTPTargetConnection>
</TargetEndpoint>
1 change: 1 addition & 0 deletions proxies/sandbox/apiproxy/targets/sandbox.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
[% include './partials/Partial.Flows.CreateMessageEndpoint.xml' %]
[% include './partials/Partial.Flows.GetMessageEndpoint.xml' %]
[% include './partials/Partial.Flows.GetNhsAppAccountsEndpoint.xml' %]
[% include './partials/Partial.Flows.GetMessageResponsesEndpoint.xml' %]
</Flows>
[% include './partials/Partial.Target.PostFlow.xml' %]
<FaultRules>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<Flow name="GetMessageResponsesEndpoint">
<Description>Handle get message responses</Description>
<Request>
<!-- app-response has no ref-tier backend; fail fast instead of routing to an undefined TargetServer -->
<Step>
<Name>RaiseFault.404NotFound</Name>
<Condition>environment.name = "ref"</Condition>
</Step>
<Step>
<Name>ExtractVariables.MessageResponses.Get.Request</Name>
</Step>
<Step>
<Name>JavaScript.MessageResponses.Get.Validate</Name>
</Step>
<Step>
<Name>RaiseFault.4xxGeneric</Name>
<Condition>errors != null</Condition>
</Step>
<Step>
<Name>AssignMessage.MessageResponses.Get.Request</Name>
</Step>
{% if ENVIRONMENT_TYPE != 'sandbox' %}
<Step>
<Name>AssignMessage.AuthenticationDetails</Name>
</Step>
{% endif %}
</Request>
<Response>
<Step>
<Name>AssignMessage.MessageResponses.Get.Response</Name>
</Step>
</Response>
<Condition>
(proxy.pathsuffix MatchesPath "/v1/message-responses/{messageId}") and (request.verb = "GET")
</Condition>
</Flow>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
Sets the backend request path to /api/response/{messageId} and forwards correlation ID.
-->
<AssignMessage async="false" continueOnError="false" enabled="true" name="AssignMessage.MessageResponses.Get.Request">
<DisplayName>AssignMessage.MessageResponses.Get.Request</DisplayName>
<Properties/>
<AssignTo createNew="false" transport="http" type="request"/>
<IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
<AssignVariable>
<Name>target.copy.pathsuffix</Name>
<Value>false</Value>
</AssignVariable>
<AssignVariable>
<Name>requestpath</Name>
<Template>/api/response/{data.messageId}</Template>
</AssignVariable>
<Set>
<Headers>
<Header name="X-Correlation-Id">{backendCorrelationId}</Header>
</Headers>
<Verb>GET</Verb>
</Set>
</AssignMessage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
Passthrough response policy — no body transformation required for this endpoint.
-->
<AssignMessage async="false" continueOnError="false" enabled="true" name="AssignMessage.MessageResponses.Get.Response">
<DisplayName>AssignMessage.MessageResponses.Get.Response</DisplayName>
<Properties/>
<AssignTo createNew="false" transport="http" type="response"/>
<IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
</AssignMessage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
Extracts the messageId from the URI path into data.* flow variables.
-->
<ExtractVariables async="false" continueOnError="false" enabled="true" name="ExtractVariables.MessageResponses.Get.Request">
<VariablePrefix>data</VariablePrefix>
<Source>request</Source>
<URIPath>
<Pattern ignoreCase="true">/v1/message-responses/{messageId}</Pattern>
</URIPath>
<IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
</ExtractVariables>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
Validates the messageId extracted from the get message responses request.
-->
<Javascript async="false" continueOnError="false" enabled="true" timeLimit="5000" name="JavaScript.MessageResponses.Get.Validate">
<DisplayName>JavaScript.MessageResponses.Get.Validate</DisplayName>
<Properties/>
<IncludeURL>jsc://helpers/validationErrors.js</IncludeURL>
Comment thread
rhyscoxnhs marked this conversation as resolved.
<IncludeURL>jsc://helpers/validationChecks.js</IncludeURL>
<ResourceURL>jsc://MessageResponses.Get.Validate.js</ResourceURL>
</Javascript>
13 changes: 13 additions & 0 deletions proxies/shared/resources/jsc/MessageResponses.Get.Validate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* global context, validateUuidParameter */

const errors = []

validateUuidParameter(errors, context.getVariable("data.messageId"), "messageId");

if (errors.length > 0) {
context.setVariable("generic_status_code", errors[0].status);
context.setVariable("errors", JSON.stringify(errors));
} else {
context.setVariable("generic_status_code", null);
context.setVariable("errors", null);
}
4 changes: 4 additions & 0 deletions proxies/shared/resources/jsc/Routing.CheckValid.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ const validPaths = [
{
match: /^\/channels\/nhsapp\/accounts$/,
methods: ['get']
},
{
match: /^\/v1\/message-responses\/.*$/,
methods: ['get']
}
];

Expand Down
11 changes: 11 additions & 0 deletions proxies/shared/resources/jsc/helpers/validationChecks.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ const validateUuid = (errors, fieldValue, fieldPointer) => {
return true
}

const validateUuidParameter = (errors, fieldValue, parameter) => {
if (typeof fieldValue !== "string" || !uuidRegex.test(fieldValue)) {
errors.push(invalidParameterError(
parameter,
"The messageId path parameter is not a valid UUID."
));
return false
}
return true
}

const validateConstantString = (errors, fieldValue, fieldPointer, requiredValue) => {
if (isUndefined(fieldValue)) {
errors.push(missingError(fieldPointer));
Expand Down
16 changes: 16 additions & 0 deletions proxies/shared/resources/jsc/helpers/validationErrors.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@ function invalidError(pointer) {
);
}

function invalidParameterError(parameter, detail) {
return {
"id": messageId + "." + errors.length,
"code": "CM_INVALID_REQUEST",
"links": {
"about": "https://digital.nhs.uk/developer/api-catalogue/nhs-notify"
},
"status": "400",
"title": "Invalid Request",
"detail": detail,
"source": {
"parameter": parameter
}
};
}

function duplicateError(pointer) {
return createErrorObject(
"CM_DUPLICATE_VALUE",
Expand Down
119 changes: 119 additions & 0 deletions sandbox/__test__/message_responses.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import request from "supertest"
import * as uuid from 'uuid';
import { setup } from './helpers.js'

const VALID_MESSAGE_ID = '11111111-1111-4111-8111-111111111111';
const NOT_FOUND_MESSAGE_ID = '00000000-0000-4000-8000-000000000404';
const UUID_VERSIONS = [1, 2, 3, 4, 5, 6, 7, 8];

describe('/api/v1/message-responses/:messageId', () => {
let env;
let server;

beforeEach(() => {
env = process.env;
server = setup();
});

afterEach(() => {
process.env = env;
server.close();
});

it('returns a X-Correlation-Id when provided', (done) => {
const correlationId = uuid.v4();
request(server)
.get(`/api/v1/message-responses/${VALID_MESSAGE_ID}`)
.set('X-Correlation-Id', correlationId)
.expect(200)
.expect('X-Correlation-Id', correlationId, done);
});

it('returns a service ban (403) when the user is banned', (done) => {
request(server)
.get(`/api/v1/message-responses/${VALID_MESSAGE_ID}`)
.set({ Authorization: 'banned' })
.expect(403, {
error: 'Forbidden'
})
.expect('Content-Type', /json/, done);
});

UUID_VERSIONS.forEach((version) => {
it(`returns a 200 for a valid UUID v${version} messageId`, (done) => {
const messageId = `11111111-1111-${version}111-8111-111111111111`;

request(server)
.get(`/api/v1/message-responses/${messageId}`)
.expect(200)
.expect('Content-Type', /json/, done);
});
});

it('returns a 404 when no responses are found', (done) => {
request(server)
.get(`/api/v1/message-responses/${NOT_FOUND_MESSAGE_ID}`)
.expect(404, {
errors: [
{
code: 'CM_NOT_FOUND',
status: '404',
title: 'Resource not found',
detail: 'The resource at the requested URI was not found.'
}
]
})
.expect('Content-Type', /json/, done);
});

it('returns a 415 when the content type is not supported', (done) => {
request(server)
.get(`/api/v1/message-responses/${VALID_MESSAGE_ID}`)
.set('Content-Type', 'text/plain')
.expect(415, {
message: 'Unsupported media type.'
})
.expect('Content-Type', /json/, done);
});

it('returns a 429 when the request is rate limited', (done) => {
request(server)
.get(`/api/v1/message-responses/${VALID_MESSAGE_ID}`)
.set('Prefer', 'code=429')
.expect(429, {
errors: [
{
code: 'CM_QUOTA',
status: '429',
title: 'Too many requests',
detail: 'You have made too many requests. Re-send the request after the time (in seconds) specified `Retry-After` header.'
}
]
})
.expect('Content-Type', /json/, done);
});

it('returns a 200 with correct response structure for a valid messageId', (done) => {
request(server)
.get(`/api/v1/message-responses/${VALID_MESSAGE_ID}`)
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
const { body } = res;
if (!Array.isArray(body.data)) throw new Error('response must contain a data array');
const first = body.data[0];
if (!first.id) throw new Error('missing id');
if (first.type !== 'RecipientResponse') throw new Error('incorrect type');
const { attributes } = first;
if (attributes.messageId !== VALID_MESSAGE_ID) throw new Error('incorrect messageId');
if (!attributes.messageReference) throw new Error('missing messageReference');
if (!attributes.code) throw new Error('missing code');
if (!attributes.channel) throw new Error('missing channel');
if (!attributes.channelStatus) throw new Error('missing channelStatus');
if (!attributes.cascadeType) throw new Error('missing cascadeType');
if (!attributes.authoredAt) throw new Error('missing authoredAt');
if (!attributes.timestamp) throw new Error('missing timestamp');
})
.end(done);
});
});
1 change: 1 addition & 0 deletions sandbox/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ app.post("/api/v1/send", handlers.batchSend);
app.post("/api/v1/messages", handlers.messages);
app.get("/api/v1/messages/:messageId", handlers.getMessage);
app.get("/api/channels/nhsapp/accounts", handlers.nhsappAccounts);
app.get("/api/v1/message-responses/:messageId", handlers.messageResponses);
app.get("/_timeout", handlers.triggerTimeout);
app.get("/_invalid_certificate", handlers.backend403);
app.get("/_timeout_408", handlers.backend408);
Expand Down
1 change: 1 addition & 0 deletions sandbox/handlers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { batchSend } from "./batch_send.js"
export { messages } from "./messages.js"
export { getMessage } from "./get_message.js"
export { nhsappAccounts } from "./nhsapp_accounts.js"
export { messageResponses } from "./message_responses.js"
export { triggerTimeout } from "./trigger_timeout.js"
export { backend403 } from "./responses/backend_403.js"
export { backend408 } from "./responses/backend_408.js"
Expand Down
Loading
Loading