diff --git a/proxies/live/apiproxy/targets/target.xml b/proxies/live/apiproxy/targets/target.xml index c9c0bfdd3..ad495a61e 100644 --- a/proxies/live/apiproxy/targets/target.xml +++ b/proxies/live/apiproxy/targets/target.xml @@ -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' %] [% include './partials/Partial.Target.PostFlow.xml' %] diff --git a/proxies/sandbox/apiproxy/targets/sandbox.xml b/proxies/sandbox/apiproxy/targets/sandbox.xml index 4543da831..6a6b53eb9 100644 --- a/proxies/sandbox/apiproxy/targets/sandbox.xml +++ b/proxies/sandbox/apiproxy/targets/sandbox.xml @@ -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' %] [% include './partials/Partial.Target.PostFlow.xml' %] diff --git a/proxies/shared/partials/Partial.Flows.GetMessageResponsesEndpoint.xml b/proxies/shared/partials/Partial.Flows.GetMessageResponsesEndpoint.xml new file mode 100644 index 000000000..bc1a24275 --- /dev/null +++ b/proxies/shared/partials/Partial.Flows.GetMessageResponsesEndpoint.xml @@ -0,0 +1,24 @@ + + Handle get message responses + + + ExtractVariables.MessageResponses.Get.Request + + + AssignMessage.MessageResponses.Get.Request + + {% if ENVIRONMENT_TYPE != 'sandbox' %} + + AssignMessage.AuthenticationDetails + + {% endif %} + + + + AssignMessage.MessageResponses.Get.Response + + + + (proxy.pathsuffix MatchesPath "/v1/message-responses/{messageId}") and (request.verb = "GET") + + diff --git a/proxies/shared/policies/AssignMessage.MessageResponses.Get.Request.xml b/proxies/shared/policies/AssignMessage.MessageResponses.Get.Request.xml new file mode 100644 index 000000000..3fbc72790 --- /dev/null +++ b/proxies/shared/policies/AssignMessage.MessageResponses.Get.Request.xml @@ -0,0 +1,24 @@ + + + + AssignMessage.MessageResponses.Get.Request + + + true + + target.copy.pathsuffix + false + + + requestpath + + + + +
{backendCorrelationId}
+
+ GET +
+
diff --git a/proxies/shared/policies/AssignMessage.MessageResponses.Get.Response.xml b/proxies/shared/policies/AssignMessage.MessageResponses.Get.Response.xml new file mode 100644 index 000000000..36535d230 --- /dev/null +++ b/proxies/shared/policies/AssignMessage.MessageResponses.Get.Response.xml @@ -0,0 +1,10 @@ + + + + AssignMessage.MessageResponses.Get.Response + + + true + diff --git a/proxies/shared/policies/ExtractVariables.MessageResponses.Get.Request.xml b/proxies/shared/policies/ExtractVariables.MessageResponses.Get.Request.xml new file mode 100644 index 000000000..787aceead --- /dev/null +++ b/proxies/shared/policies/ExtractVariables.MessageResponses.Get.Request.xml @@ -0,0 +1,12 @@ + + + + data + request + + /v1/message-responses/{messageId} + + true + diff --git a/proxies/shared/resources/jsc/Routing.CheckValid.js b/proxies/shared/resources/jsc/Routing.CheckValid.js index 1a7087403..e26fabf28 100644 --- a/proxies/shared/resources/jsc/Routing.CheckValid.js +++ b/proxies/shared/resources/jsc/Routing.CheckValid.js @@ -26,6 +26,10 @@ const validPaths = [ { match: /^\/channels\/nhsapp\/accounts$/, methods: ['get'] + }, + { + match: /^\/v1\/message-responses\/.*$/, + methods: ['get'] } ]; diff --git a/sandbox/__test__/message_responses.spec.js b/sandbox/__test__/message_responses.spec.js new file mode 100644 index 000000000..718ed8d4f --- /dev/null +++ b/sandbox/__test__/message_responses.spec.js @@ -0,0 +1,111 @@ +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 TOO_MANY_RESPONSES_MESSAGE_ID = '00000000-0000-4000-8000-000000000422'; + +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); + }); + + it('returns a 400 when messageId is not a UUID', (done) => { + request(server) + .get('/api/v1/message-responses/not-a-valid-uuid') + .expect(400, { + error: 'Invalid messageId format' + }) + .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, { + error: 'No responses found for the specified messageId' + }) + .expect('Content-Type', /json/, done); + }); + + it('returns a 500 when too many responses are returned', (done) => { + request(server) + .get(`/api/v1/message-responses/${TOO_MANY_RESPONSES_MESSAGE_ID}`) + .expect(422, { + error: 'response_too_large', + message: 'There are too many responses to return.' + }) + .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, { + message: 'Too many requests.' + }) + .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)) throw new Error('response must be an array'); + const first = body[0]; + if (!first.responseId) throw new Error('missing responseId'); + if (first.messageId !== VALID_MESSAGE_ID) throw new Error('incorrect messageId'); + if (!first.messageReference) throw new Error('missing messageReference'); + if (!first.code) throw new Error('missing code'); + if (!first.channel) throw new Error('missing channel'); + if (!first.channelStatus) throw new Error('missing channelStatus'); + if (!first.cascadeType) throw new Error('missing cascadeType'); + if (!first.authoredAt) throw new Error('missing authoredAt'); + if (!first.timestamp) throw new Error('missing timestamp'); + }) + .end(done); + }); +}); diff --git a/sandbox/app.js b/sandbox/app.js index 1fe8b6f9a..c977d2c63 100644 --- a/sandbox/app.js +++ b/sandbox/app.js @@ -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); diff --git a/sandbox/handlers/index.js b/sandbox/handlers/index.js index 15b14371b..559a843b0 100644 --- a/sandbox/handlers/index.js +++ b/sandbox/handlers/index.js @@ -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" diff --git a/sandbox/handlers/message_responses.js b/sandbox/handlers/message_responses.js new file mode 100644 index 000000000..7872ca272 --- /dev/null +++ b/sandbox/handlers/message_responses.js @@ -0,0 +1,78 @@ +import { sendError } from './utils.js' + +const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +const notFoundMessageId = '00000000-0000-4000-8000-000000000404'; +const tooManyResponsesMessageId = '00000000-0000-4000-8000-000000000422'; + +export async function messageResponses(req, res, next) { + if (req.headers.authorization === 'banned') { + res.status(403).json({ error: 'Forbidden' }); + next(); + return; + } + + if (req.headers['content-type'] && req.headers['content-type'] !== 'application/json') { + sendError(res, 415, 'Unsupported media type.'); + next(); + return; + } + + if (req.headers.prefer === 'code=429') { + sendError(res, 429, 'Too many requests.'); + next(); + return; + } + + const { messageId } = req.params; + + if (!uuidRegex.test(messageId)) { + res.status(400).json({ error: 'Invalid messageId format' }); + next(); + return; + } + + if (messageId === notFoundMessageId) { + res.status(404).json({ error: 'No responses found for the specified messageId' }); + next(); + return; + } + + if (messageId === tooManyResponsesMessageId) { + res.status(422).json({ + error: 'response_too_large', + message: 'There are too many responses to return.' + }); + next(); + return; + } + + res.type('json').status(200).json(getDefaultResponse(messageId)); +} + +function getDefaultResponse(messageId) { + return [ + { + responseId: '22222222-2222-4222-8222-222222222222', + messageId, + messageReference: 'msg-ref-1', + channel: 'nhsapp', + channelStatus: 'delivered', + cascadeType: 'primary', + code: 'YES', + authoredAt: '2026-01-02T09:00:00.000Z', + timestamp: '2026-01-02T09:00:02.345Z' + }, + { + responseId: '33333333-3333-4333-8333-333333333333', + messageId, + messageReference: 'msg-ref-1', + channel: 'nhsapp', + channelStatus: 'delivered', + cascadeType: 'secondary', + code: 'NO', + authoredAt: '2026-01-02T09:05:00.000Z', + timestamp: '2026-01-02T09:05:01.678Z' + } + ]; +} diff --git a/specification/communications-manager.yaml b/specification/communications-manager.yaml index 4ab713263..aa492d0ad 100644 --- a/specification/communications-manager.yaml +++ b/specification/communications-manager.yaml @@ -32,6 +32,17 @@ paths: description: The unique identifier for the message. get: $ref: endpoints/get_message.yaml + /v1/message-responses/{messageId}: + parameters: + - schema: + type: string + format: uuid + name: messageId + in: path + required: true + description: The unique identifier for the message. + get: + $ref: endpoints/get_responses.yaml /channels/nhsapp/accounts: get: $ref: endpoints/get_nhsapp_account_details.yaml diff --git a/specification/documentation/APIDescription.md b/specification/documentation/APIDescription.md index c4a24db72..d36639bf4 100644 --- a/specification/documentation/APIDescription.md +++ b/specification/documentation/APIDescription.md @@ -255,6 +255,10 @@ In order to present the recipient with answers, include the `answerOptions` fiel If you subscribe to recipient response callbacks, NHS Notify will send you a callback when a recipient responds to a message (currently only NHS App supports this). See [the recipient response callback](#post-/-client-provided-recipient-response-URI-) for more details. +### Retrieving responses via the API + +Use the [get message responses](#get-/v1/message-responses/-messageId-) endpoint to query responses for a given message. Responses are available for up to 9 months after the message was sent. + ## Message character limits Different character limits apply to each of the communication channels as listed below. NHS Notify will validate that any personalisation fields submitted in the send message request do not exceed these limits but it is the client's responsibility to ensure that when personalisation is combined with any templated text, the channel character limit is not exceeded. diff --git a/specification/documentation/GetMessageResponses.md b/specification/documentation/GetMessageResponses.md new file mode 100644 index 000000000..0776de10f --- /dev/null +++ b/specification/documentation/GetMessageResponses.md @@ -0,0 +1,42 @@ +## Overview + +Use this endpoint to retrieve recipient responses associated with a specific message. + +Recipient responses are the keyword answers selected by a recipient for a message sent through the NHS App. For more information, see the [recipient response callback](#post-/-client-provided-recipient-response-URI-). + +Responses are available for retrieval for up to 9 months after the message was sent. + +### Response structure + +A successful response returns an array of response items. + +Each item includes: + +* `responseId` - the unique identifier for this response +* `messageId` - the identifier of the message this response relates to +* `messageReference` - the reference you provided when the message was created +* `channel` - the channel through which the response was received +* `channelStatus` - the status of the channel at the time the response was received +* `channelFailureReasonCode` - the reason code for the channel failure (only present when `channelStatus` is `failed`) +* `cascadeType` - whether this is a `primary` or `secondary` cascade response +* `code` - the keyword code selected by the recipient +* `authoredAt` - the date-time the recipient submitted their response +* `timestamp` - the date-time the response was recorded by NHS Notify + +If no responses exist for the given message, a `404` response is returned. + +### Sandbox + +When sending this request on sandbox you can use any valid UUID v4 message ID. + +To simulate error responses in the sandbox, use the following message IDs: + +* not found - `00000000-0000-4000-8000-000000000404` +* too many responses - `00000000-0000-4000-8000-000000000422` + +Here's an example curl command: + +``` +curl -X GET 'https://sandbox.api.service.nhs.uk/comms/v1/message-responses/11111111-1111-4111-8111-111111111111' \ + --header 'Accept: application/json' +``` diff --git a/specification/endpoints/get_responses.yaml b/specification/endpoints/get_responses.yaml new file mode 100644 index 000000000..d6216371d --- /dev/null +++ b/specification/endpoints/get_responses.yaml @@ -0,0 +1,34 @@ +summary: Get responses for a message +description: + $ref: ../documentation/GetMessageResponses.md +operationId: get-message-responses +parameters: + - $ref: ../snippets/AuthorizationParameter.yaml + - $ref: ../snippets/CorrelationParameter.yaml +responses: + '200': + $ref: ../responses/2xx/200_Responses.yaml + '400': + $ref: ../responses/4xx/message_responses/400_UnableToGetResponses.yaml + '401': + $ref: ../responses/4xx/401_AccessDenied.yaml + '403': + $ref: ../responses/4xx/403_Forbidden.yaml + '404': + $ref: ../responses/4xx/404_NotFound.yaml + '405': + $ref: ../responses/4xx/405_NotAllowed.yaml + '408': + $ref: ../responses/4xx/408_RequestTimeout.yaml + '415': + $ref: ../responses/4xx/415_UnsupportedMedia.yaml + '422': + $ref: ../responses/4xx/message_responses/422_TooManyResponses.yaml + '429': + $ref: ../responses/4xx/429_TooManyRequests.yaml + '500': + $ref: ../responses/5xx/500_InternalServerError.yaml + '503': + $ref: ../responses/5xx/503_ServiceUnavailable.yaml + '504': + $ref: ../responses/5xx/504_ServiceTimeout.yaml diff --git a/specification/responses/2xx/200_Responses.yaml b/specification/responses/2xx/200_Responses.yaml new file mode 100644 index 000000000..d69028d05 --- /dev/null +++ b/specification/responses/2xx/200_Responses.yaml @@ -0,0 +1,7 @@ +description: The responses for the given message have been retrieved successfully. +headers: + $ref: ../../snippets/StandardResponseHeaders.yaml +content: + application/json: + schema: + $ref: ../../schemas/responses/MessageResponses.yaml diff --git a/specification/responses/4xx/406_NotAcceptable.yaml b/specification/responses/4xx/406_NotAcceptable.yaml index d8ad56b3b..34de3ef24 100644 --- a/specification/responses/4xx/406_NotAcceptable.yaml +++ b/specification/responses/4xx/406_NotAcceptable.yaml @@ -15,4 +15,4 @@ content: schema: $ref: ../../schemas/responses/errors/NotAcceptable.yaml headers: - $ref: ../../snippets/StandardResponseHeaders.yaml \ No newline at end of file + $ref: ../../snippets/StandardResponseHeaders.yaml diff --git a/specification/responses/4xx/415_UnsupportedMedia.yaml b/specification/responses/4xx/415_UnsupportedMedia.yaml index dc686705d..e47139f8f 100644 --- a/specification/responses/4xx/415_UnsupportedMedia.yaml +++ b/specification/responses/4xx/415_UnsupportedMedia.yaml @@ -1,16 +1,8 @@ description: |+ - The `Content-Type` of the request is not supported. This endpoint supports: - - * `application/json` - * `application/vnd.api+json` - * `application/json; charset=utf-8` - * `application/vnd.api+json; charset=utf-8` + The `Content-Type` of the request is not supported. This endpoint supports `application/json`. content: - application/vnd.api+json: - schema: - $ref: ../../schemas/responses/errors/UnsupportedMedia.yaml application/json: schema: $ref: ../../schemas/responses/errors/UnsupportedMedia.yaml headers: - $ref: ../../snippets/StandardResponseHeaders.yaml \ No newline at end of file + $ref: ../../snippets/StandardResponseHeaders.yaml diff --git a/specification/responses/4xx/message_responses/400_UnableToGetResponses.yaml b/specification/responses/4xx/message_responses/400_UnableToGetResponses.yaml new file mode 100644 index 000000000..6c5fdca57 --- /dev/null +++ b/specification/responses/4xx/message_responses/400_UnableToGetResponses.yaml @@ -0,0 +1,7 @@ +description: The `messageId` path parameter is not a valid UUID. +content: + application/json: + schema: + $ref: ../../../schemas/responses/errors/message_responses/UnableToGetResponses.yaml +headers: + $ref: ../../../snippets/StandardResponseHeaders.yaml diff --git a/specification/responses/4xx/message_responses/422_TooManyResponses.yaml b/specification/responses/4xx/message_responses/422_TooManyResponses.yaml new file mode 100644 index 000000000..168764700 --- /dev/null +++ b/specification/responses/4xx/message_responses/422_TooManyResponses.yaml @@ -0,0 +1,7 @@ +description: There are too many responses associated with this message to return in a single response. This error occurs when more than 1000 responses exist for the given message ID. +content: + application/json: + schema: + $ref: ../../../schemas/responses/errors/message_responses/TooManyResponses.yaml +headers: + $ref: ../../../snippets/StandardResponseHeaders.yaml diff --git a/specification/schemas/components/ResponseItem.yaml b/specification/schemas/components/ResponseItem.yaml new file mode 100644 index 000000000..1065d51bf --- /dev/null +++ b/specification/schemas/components/ResponseItem.yaml @@ -0,0 +1,66 @@ +type: object +title: ResponseItem +additionalProperties: false +required: + - responseId + - authoredAt + - cascadeType + - channel + - channelStatus + - code + - messageId + - messageReference + - timestamp +properties: + responseId: + type: string + format: uuid + description: The unique identifier for this response. + example: "33333333-3333-4333-8333-333333333333" + messageId: + type: string + description: The unique identifier of the message this response relates to. + format: uuid + example: "11111111-1111-4111-8111-111111111111" + messageReference: + type: string + description: The reference for the message, as provided when the message was created. + example: "da0b1495-c7cb-468c-9d81-07dee089d728" + channel: + type: string + enum: + - nhsapp + example: nhsapp + channelStatus: + type: string + description: The status of the channel at the time the response was received. + enum: + - sending + - delivered + - failed + example: delivered + channelFailureReasonCode: + type: string + description: The reason code for the channel failure. Only present when channelStatus is failed. + example: "CFR_SUPE_0001" + cascadeType: + type: string + description: Whether this is a primary or secondary cascade response. + enum: + - primary + - secondary + example: primary + code: + type: string + description: The keyword code from the recipient's response. + example: "YES" + authoredAt: + type: string + description: The date-time the recipient submitted their response. + format: date-time + example: "2026-06-15T14:30:00.000Z" + timestamp: + type: string + description: The date-time the response was recorded by NHS Notify. + format: date-time + example: "2026-06-15T14:30:05.123Z" diff --git a/specification/schemas/responses/MessageResponses.yaml b/specification/schemas/responses/MessageResponses.yaml new file mode 100644 index 000000000..a2756f09b --- /dev/null +++ b/specification/schemas/responses/MessageResponses.yaml @@ -0,0 +1,4 @@ +title: MessageResponses +type: array +items: + $ref: ../components/ResponseItem.yaml diff --git a/specification/schemas/responses/errors/message_responses/TooManyResponses.yaml b/specification/schemas/responses/errors/message_responses/TooManyResponses.yaml new file mode 100644 index 000000000..0aee3a0aa --- /dev/null +++ b/specification/schemas/responses/errors/message_responses/TooManyResponses.yaml @@ -0,0 +1,17 @@ +type: object +title: Too many responses +additionalProperties: false +required: + - error + - message +properties: + error: + type: string + enum: + - response_too_large + example: response_too_large + message: + type: string + enum: + - There are too many responses to return. + example: There are too many responses to return. diff --git a/specification/schemas/responses/errors/message_responses/UnableToGetResponses.yaml b/specification/schemas/responses/errors/message_responses/UnableToGetResponses.yaml new file mode 100644 index 000000000..a4d689291 --- /dev/null +++ b/specification/schemas/responses/errors/message_responses/UnableToGetResponses.yaml @@ -0,0 +1,11 @@ +type: object +title: Unable to get responses +additionalProperties: false +required: + - error +properties: + error: + type: string + enum: + - Invalid messageId format + example: Invalid messageId format diff --git a/tests/api/message_responses/__init__.py b/tests/api/message_responses/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/api/message_responses/test_200_success.py b/tests/api/message_responses/test_200_success.py new file mode 100644 index 000000000..335311829 --- /dev/null +++ b/tests/api/message_responses/test_200_success.py @@ -0,0 +1,20 @@ +import requests +import pytest +from lib import Assertions, Generators +from lib.constants.message_responses_paths import MESSAGE_RESPONSES_ENDPOINT, VALID_MESSAGE_ID +from lib.fixtures import * # NOSONAR + + +@pytest.mark.devtest +def test_200_success(url, bearer_token): + headers = Generators.generate_valid_headers(bearer_token.value) + + resp = requests.get( + f"{url}{MESSAGE_RESPONSES_ENDPOINT}/{VALID_MESSAGE_ID}", + headers=headers + ) + + assert resp.status_code == 200, f"Response: {resp.status_code}: {resp.text}" + body = resp.json() + assert "messageId" in body + assert isinstance(body.get("responses"), list) diff --git a/tests/api/message_responses/test_404.py b/tests/api/message_responses/test_404.py new file mode 100644 index 000000000..bf9683640 --- /dev/null +++ b/tests/api/message_responses/test_404.py @@ -0,0 +1,22 @@ +import requests +import pytest +from lib import Assertions, Generators +from lib.constants.message_responses_paths import MESSAGE_RESPONSES_ENDPOINT, NOT_FOUND_MESSAGE_ID +from lib.fixtures import * # NOSONAR + + +@pytest.mark.devtest +def test_404_not_found(url, bearer_token): + headers = Generators.generate_valid_headers(bearer_token.value) + + resp = requests.get( + f"{url}{MESSAGE_RESPONSES_ENDPOINT}/{NOT_FOUND_MESSAGE_ID}", + headers=headers + ) + + Assertions.assert_error_with_optional_correlation_id( + resp, + 404, + Generators.generate_not_found_error(), + None + ) diff --git a/tests/lib/constants/message_responses_paths.py b/tests/lib/constants/message_responses_paths.py new file mode 100644 index 000000000..ad56d9173 --- /dev/null +++ b/tests/lib/constants/message_responses_paths.py @@ -0,0 +1,14 @@ +MESSAGE_RESPONSES_ENDPOINT = "/v1/message-responses" + +VALID_MESSAGE_ID = "11111111-1111-4111-8111-111111111111" +NOT_FOUND_MESSAGE_ID = "00000000-0000-4000-8000-000000000404" +BAD_GATEWAY_MESSAGE_ID = "00000000-0000-4000-8000-000000000502" +TOO_MANY_RESPONSES_MESSAGE_ID = "00000000-0000-4000-8000-000000000500" + +INVALID_MESSAGE_IDS = [ + "not-a-uuid", + "12345", + "invalid_id" +] + +CORRELATION_IDS = [None, "228aac39-542d-4803-b28e-5de9e100b9f8"] diff --git a/tests/sandbox/message_responses/__init__.py b/tests/sandbox/message_responses/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/sandbox/message_responses/test_400.py b/tests/sandbox/message_responses/test_400.py new file mode 100644 index 000000000..422947359 --- /dev/null +++ b/tests/sandbox/message_responses/test_400.py @@ -0,0 +1,18 @@ +import requests +import pytest +from lib.constants.message_responses_paths import MESSAGE_RESPONSES_ENDPOINT, CORRELATION_IDS, INVALID_MESSAGE_IDS + + +@pytest.mark.sandboxtest +@pytest.mark.parametrize("correlation_id", CORRELATION_IDS) +@pytest.mark.parametrize("message_id", INVALID_MESSAGE_IDS) +def test_400_invalid_message_id(nhsd_apim_proxy_url, correlation_id, message_id): + resp = requests.get( + f"{nhsd_apim_proxy_url}{MESSAGE_RESPONSES_ENDPOINT}/{message_id}", + headers={ + "X-Correlation-Id": correlation_id, + "Accept": "application/json" + } + ) + + assert resp.status_code == 400, f"Response: {resp.status_code}: {resp.text}" diff --git a/tests/sandbox/message_responses/test_404.py b/tests/sandbox/message_responses/test_404.py new file mode 100644 index 000000000..740ca68f6 --- /dev/null +++ b/tests/sandbox/message_responses/test_404.py @@ -0,0 +1,23 @@ +import requests +import pytest +from lib import Assertions, Generators +from lib.constants.message_responses_paths import MESSAGE_RESPONSES_ENDPOINT, CORRELATION_IDS, NOT_FOUND_MESSAGE_ID + + +@pytest.mark.sandboxtest +@pytest.mark.parametrize("correlation_id", CORRELATION_IDS) +def test_404_message_not_found(nhsd_apim_proxy_url, correlation_id): + resp = requests.get( + f"{nhsd_apim_proxy_url}{MESSAGE_RESPONSES_ENDPOINT}/{NOT_FOUND_MESSAGE_ID}", + headers={ + "X-Correlation-Id": correlation_id, + "Accept": "application/json" + } + ) + + Assertions.assert_error_with_optional_correlation_id( + resp, + 404, + Generators.generate_not_found_error(), + correlation_id + ) diff --git a/tests/sandbox/message_responses/test_500.py b/tests/sandbox/message_responses/test_500.py new file mode 100644 index 000000000..b8fbffda3 --- /dev/null +++ b/tests/sandbox/message_responses/test_500.py @@ -0,0 +1,25 @@ +import requests +import pytest +from lib import Assertions, Generators +from lib.constants.message_responses_paths import ( + MESSAGE_RESPONSES_ENDPOINT, CORRELATION_IDS, TOO_MANY_RESPONSES_MESSAGE_ID +) + + +@pytest.mark.sandboxtest +@pytest.mark.parametrize("correlation_id", CORRELATION_IDS) +def test_500_too_many_responses(nhsd_apim_proxy_url, correlation_id): + resp = requests.get( + f"{nhsd_apim_proxy_url}{MESSAGE_RESPONSES_ENDPOINT}/{TOO_MANY_RESPONSES_MESSAGE_ID}", + headers={ + "X-Correlation-Id": correlation_id, + "Accept": "application/json" + } + ) + + Assertions.assert_error_with_optional_correlation_id( + resp, + 500, + Generators.generate_internal_server_error(), + correlation_id + ) diff --git a/tests/sandbox/message_responses/test_502.py b/tests/sandbox/message_responses/test_502.py new file mode 100644 index 000000000..d32a9e801 --- /dev/null +++ b/tests/sandbox/message_responses/test_502.py @@ -0,0 +1,23 @@ +import requests +import pytest +from lib import Assertions, Generators +from lib.constants.message_responses_paths import MESSAGE_RESPONSES_ENDPOINT, CORRELATION_IDS, BAD_GATEWAY_MESSAGE_ID + + +@pytest.mark.sandboxtest +@pytest.mark.parametrize("correlation_id", CORRELATION_IDS) +def test_502_bad_gateway(nhsd_apim_proxy_url, correlation_id): + resp = requests.get( + f"{nhsd_apim_proxy_url}{MESSAGE_RESPONSES_ENDPOINT}/{BAD_GATEWAY_MESSAGE_ID}", + headers={ + "X-Correlation-Id": correlation_id, + "Accept": "application/json" + } + ) + + Assertions.assert_error_with_optional_correlation_id( + resp, + 502, + Generators.generate_bad_gateway_error(), + correlation_id + ) diff --git a/tests/sandbox/message_responses/test_success.py b/tests/sandbox/message_responses/test_success.py new file mode 100644 index 000000000..01adef8a2 --- /dev/null +++ b/tests/sandbox/message_responses/test_success.py @@ -0,0 +1,34 @@ +import requests +import pytest +from lib import Assertions +from lib.constants.message_responses_paths import MESSAGE_RESPONSES_ENDPOINT, CORRELATION_IDS, VALID_MESSAGE_ID + + +@pytest.mark.sandboxtest +@pytest.mark.parametrize("correlation_id", CORRELATION_IDS) +def test_200_success(nhsd_apim_proxy_url, correlation_id): + resp = requests.get( + f"{nhsd_apim_proxy_url}{MESSAGE_RESPONSES_ENDPOINT}/{VALID_MESSAGE_ID}", + headers={ + "X-Correlation-Id": correlation_id, + "Accept": "application/json" + } + ) + + assert resp.status_code == 200, f"Response: {resp.status_code}: {resp.text}" + body = resp.json() + assert isinstance(body, list) + assert len(body) > 0 + + first = body[0] + assert "responseId" in first + assert first["messageId"] == VALID_MESSAGE_ID + assert "messageReference" in first + assert "code" in first + assert "channel" in first + assert "channelStatus" in first + assert "cascadeType" in first + assert "authoredAt" in first + assert "timestamp" in first + + Assertions.assert_correlation_id(resp.headers.get("X-Correlation-Id"), correlation_id)