From 6b45d06d75432d07a6a12a36fab80d64f89b7b0c Mon Sep 17 00:00:00 2001 From: TomKinghorn Date: Fri, 4 Sep 2026 15:02:08 +0000 Subject: [PATCH] [RAA-4534]-[KM/AA]-[Add A039 endpoint]-[TK] --- .../policies/AssignMessage.AddCors.xml | 2 +- sandbox/src/app.js | 2 +- .../requests/upload.txt | 2 + .../responses/BinaryResource.json | 15 ++ .../responses/OperationOutcome-422.json | 24 ++ sandbox/src/routes/index.js | 2 + .../r4/services/mockResponseProvider.js | 47 +++- .../routes/r4/uploadFileToDocumentStore.js | 33 +++ .../r4/schemas/A039-OperationOutcome-R4.yaml | 18 ++ .../a039-upload-file-to-document-store.yaml | 214 ++++++++++++++++++ .../200Response.yaml | 81 +++++++ .../422Response.yaml | 44 ++++ specification/e-referrals-service-api.yaml | 3 + .../sandbox/r4/test_a039_upload_attachment.py | 149 ++++++++++++ 14 files changed, 633 insertions(+), 3 deletions(-) create mode 100644 sandbox/src/mocks/r4/uploadFileToDocumentStore/requests/upload.txt create mode 100644 sandbox/src/mocks/r4/uploadFileToDocumentStore/responses/BinaryResource.json create mode 100644 sandbox/src/mocks/r4/uploadFileToDocumentStore/responses/OperationOutcome-422.json create mode 100644 sandbox/src/routes/r4/uploadFileToDocumentStore.js create mode 100644 specification/components/r4/schemas/A039-OperationOutcome-R4.yaml create mode 100644 specification/components/r4/schemas/endpoints/a039-upload-file-to-document-store.yaml create mode 100644 specification/components/r4/schemas/responses/uploadFileToDocumentStore/200Response.yaml create mode 100644 specification/components/r4/schemas/responses/uploadFileToDocumentStore/422Response.yaml create mode 100644 tests/sandbox/r4/test_a039_upload_attachment.py diff --git a/proxies/sandbox/apiproxy/policies/AssignMessage.AddCors.xml b/proxies/sandbox/apiproxy/policies/AssignMessage.AddCors.xml index 52207e199..9e6cbbfd2 100644 --- a/proxies/sandbox/apiproxy/policies/AssignMessage.AddCors.xml +++ b/proxies/sandbox/apiproxy/policies/AssignMessage.AddCors.xml @@ -4,7 +4,7 @@
{request.header.origin}
-
origin, x-requested-with, x-correlation-id, accept, content-type, nhsd-session-urid, nhsd-end-user-organisation-ods, nhsd-ers-business-function, nhsd-ers-on-behalf-of-user-id, authorization, nhsd-ers-comm-rule-org, nhsd-ers-file-name, nhsd-ers-referral-id, if-match
+
origin, x-requested-with, x-correlation-id, accept, content-type, nhsd-session-urid, nhsd-end-user-organisation-ods, nhsd-ers-business-function, nhsd-ers-on-behalf-of-user-id, authorization, nhsd-ers-comm-rule-org, nhsd-ers-file-name, nhsd-ers-file-size, nhsd-ers-file-mime-type, nhsd-ers-referral-id, if-match
x-correlation-id, x-request-id, content-type, Location, ETag, Content-Disposition, Content-Length, Cache-Control
3628800
GET, PUT, POST, DELETE, HEAD
diff --git a/sandbox/src/app.js b/sandbox/src/app.js index da1f56197..d1da23e13 100644 --- a/sandbox/src/app.js +++ b/sandbox/src/app.js @@ -36,7 +36,7 @@ const init = async () => { routes: { cors: { origin: ["*"], - headers: ['origin', 'x-requested-with', 'x-correlation-id', 'accept', 'content-type', 'nhsd-session-urid', 'nhsd-end-user-organisation-ods', 'nhsd-ers-business-function', 'authorization', 'nhsd-ers-comm-rule-org', 'nhsd-ers-file-name', 'nhsd-ers-referral-id', 'if-match', 'nhsd-ers-on-behalf-of-user-id'], + headers: ['origin', 'x-requested-with', 'x-correlation-id', 'accept', 'content-type', 'nhsd-session-urid', 'nhsd-end-user-organisation-ods', 'nhsd-ers-business-function', 'authorization', 'nhsd-ers-comm-rule-org', 'nhsd-ers-file-name', 'nhsd-ers-file-size', 'nhsd-ers-file-mime-type', 'nhsd-ers-referral-id', 'if-match', 'nhsd-ers-on-behalf-of-user-id'], exposedHeaders: ['x-correlation-id', 'x-request-id', 'content-type', 'Location', 'ETag', 'Content-Disposition', 'Content-Length', 'Cache-Control'], maxAge: 3628800, credentials: false diff --git a/sandbox/src/mocks/r4/uploadFileToDocumentStore/requests/upload.txt b/sandbox/src/mocks/r4/uploadFileToDocumentStore/requests/upload.txt new file mode 100644 index 000000000..5b68d50f6 --- /dev/null +++ b/sandbox/src/mocks/r4/uploadFileToDocumentStore/requests/upload.txt @@ -0,0 +1,2 @@ +A simple test upload file - this is only used by the sandbox integration tests and it's not accessible to the 'try it' UI in Bloomreach + diff --git a/sandbox/src/mocks/r4/uploadFileToDocumentStore/responses/BinaryResource.json b/sandbox/src/mocks/r4/uploadFileToDocumentStore/responses/BinaryResource.json new file mode 100644 index 000000000..86dae043e --- /dev/null +++ b/sandbox/src/mocks/r4/uploadFileToDocumentStore/responses/BinaryResource.json @@ -0,0 +1,15 @@ +{ + "id": "d497bbe3-f88b-45f1-b3d4-9c563e4c0f5f", + "resourceType": "Binary", + "meta": { + "lastUpdated": "2026-09-04T09:40:04.656Z" + }, + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/ers-binary-id", + "value": "d497bbe3-f88b-45f1-b3d4-9c563e4c0f5f" + } + ], + "contentType": "text/plain" +} + diff --git a/sandbox/src/mocks/r4/uploadFileToDocumentStore/responses/OperationOutcome-422.json b/sandbox/src/mocks/r4/uploadFileToDocumentStore/responses/OperationOutcome-422.json new file mode 100644 index 000000000..d07519438 --- /dev/null +++ b/sandbox/src/mocks/r4/uploadFileToDocumentStore/responses/OperationOutcome-422.json @@ -0,0 +1,24 @@ +{ + "resourceType": "OperationOutcome", + "meta": { + "lastUpdated": "2026-09-04T10:15:30.000Z", + "profile": [ + "https://www.hl7.org/fhir/R4/operationoutcome.html" + ] + }, + "issue": [ + { + "severity": "error", + "code": "required", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/ers-error-codes", + "code": "MISSING_VALUE" + } + ] + }, + "diagnostics": "Missing required header: NHSD-eRS-File-Name" + } + ] +} diff --git a/sandbox/src/routes/index.js b/sandbox/src/routes/index.js index 8c5096101..2542b99f1 100644 --- a/sandbox/src/routes/index.js +++ b/sandbox/src/routes/index.js @@ -43,6 +43,7 @@ const retrieveHealthcareService = require('./r4/retrieveHealthcareService') const searchForHealthcareServices = require('./r4/searchForHealthcareServices') const searchServiceRequest = require('./r4/searchServiceRequest') const retrieveAttachmentR4 = require('./r4/retrieveAttachment') +const uploadFileToDocumentStoreR4 = require('./r4/uploadFileToDocumentStore') /** * Services @@ -89,6 +90,7 @@ const routes = [].concat( searchServiceRequest, createAdviceAndGuidance, retrieveAttachmentR4, + uploadFileToDocumentStoreR4, objectStore ) diff --git a/sandbox/src/routes/r4/services/mockResponseProvider.js b/sandbox/src/routes/r4/services/mockResponseProvider.js index 6031646da..7f55437c7 100644 --- a/sandbox/src/routes/r4/services/mockResponseProvider.js +++ b/sandbox/src/routes/r4/services/mockResponseProvider.js @@ -1,3 +1,31 @@ +const OBJECT_STORE_FILE_ID = 'd497bbe3-f88b-45f1-b3d4-9c563e4c0f5f' +const DEFAULT_UPLOAD_FILENAME = 'upload.bin' + +function stripTrailingSlash(url) { + return url.replace(/\/+$/, '') +} + +function getObjectStoreLocation(request) { + const explicitBaseUrl = request.headers['x-ers-sandbox-baseurl'] + const baseUrl = typeof explicitBaseUrl === 'string' && explicitBaseUrl.trim() + ? stripTrailingSlash(explicitBaseUrl.trim()) + : stripTrailingSlash(request.server.info.uri) + + return `${baseUrl}/ObjectStore/${OBJECT_STORE_FILE_ID}` +} + +function createContentDisposition(fileNameHeaderValue) { + const inputFileName = String(fileNameHeaderValue || '').replace(/[\r\n]/g, '').trim() + const utf8Filename = inputFileName || DEFAULT_UPLOAD_FILENAME + const asciiFallback = utf8Filename + .replace(/[^\x20-\x7E]/g, '_') + .replace(/["\\]/g, '_') + const safeFallback = asciiFallback || DEFAULT_UPLOAD_FILENAME + const encodedFilename = encodeURIComponent(utf8Filename) + + return `attachment; filename="${safeFallback}"; filename*=UTF-8''${encodedFilename}` +} + module.exports = { getExampleResponseForRetrieveBusinessFunctions: function () { @@ -43,7 +71,7 @@ module.exports = { getExampleResponseForSearchServiceRequest: function (request) { let ubrn; const identifier = request.query.identifier; - + if (identifier.includes('|')) { ubrn = identifier.split('|')[1] } @@ -64,6 +92,23 @@ module.exports = { return 'r4/searchServiceRequest/responses/ResponseExampleEmpty.json' } + return null + }, + + getExampleResponseForUploadFileToDocumentStore: function (request) { + const filename = request.headers['nhsd-ers-file-name'] + const fileSize = request.headers['nhsd-ers-file-size'] + const fileMimeType = request.headers['nhsd-ers-file-mime-type'] + + if (filename && fileSize && fileMimeType) { + return { + responsePath: 'r4/uploadFileToDocumentStore/responses/BinaryResource.json', + responseCode: 200, + location: getObjectStoreLocation(request), + contentDisposition: createContentDisposition(filename) + } + } + return null } } diff --git a/sandbox/src/routes/r4/uploadFileToDocumentStore.js b/sandbox/src/routes/r4/uploadFileToDocumentStore.js new file mode 100644 index 000000000..463bf6671 --- /dev/null +++ b/sandbox/src/routes/r4/uploadFileToDocumentStore.js @@ -0,0 +1,33 @@ +const mockResponseProvider = require('./services/mockResponseProvider') +const validationUtils = require('../common/validationUtils') + +module.exports = [ + /** + * Sandbox implementation for uploadFileToDocumentStore A039 (R4) endpoint + */ + { + method: 'POST', + path: '/FHIR/R4/Binary', + handler: (request, h) => { + const allowedBusinessFunctions = ['REFERRING_CLINICIAN', 'REFERRING_CLINICIAN_ADMIN', 'SERVICE_PROVIDER_CLINICIAN', 'SERVICE_PROVIDER_CLINICIAN_ADMIN'] + + const validationResult = validationUtils.validateBusinessFunction(request, h, allowedBusinessFunctions) + if (validationResult) { + return validationResult + } + + const exampleResponse = mockResponseProvider.getExampleResponseForUploadFileToDocumentStore(request) + if (exampleResponse) { + const { responsePath, responseCode, location, contentDisposition } = exampleResponse + return h.file(responsePath, { etagMethod: false }) + .code(responseCode) + .type('application/fhir+json') + .header('Location', location) + .header('Content-Disposition', contentDisposition) + } + + return h.file('r4/uploadFileToDocumentStore/responses/OperationOutcome-422.json').code(422).type('application/fhir+json') + } + } +] + diff --git a/specification/components/r4/schemas/A039-OperationOutcome-R4.yaml b/specification/components/r4/schemas/A039-OperationOutcome-R4.yaml new file mode 100644 index 000000000..70a77dba7 --- /dev/null +++ b/specification/components/r4/schemas/A039-OperationOutcome-R4.yaml @@ -0,0 +1,18 @@ +allOf: + - $ref: './NHSDigital-OperationOutcome.yaml' + - type: object + properties: + meta: + type: object + required: + - lastUpdated + - profile + properties: + profile: + type: array + minItems: 1 + items: + type: string + enum: + - 'https://www.hl7.org/fhir/R4/operationoutcome.html' + diff --git a/specification/components/r4/schemas/endpoints/a039-upload-file-to-document-store.yaml b/specification/components/r4/schemas/endpoints/a039-upload-file-to-document-store.yaml new file mode 100644 index 000000000..62a992293 --- /dev/null +++ b/specification/components/r4/schemas/endpoints/a039-upload-file-to-document-store.yaml @@ -0,0 +1,214 @@ +security: + - + bearerAuth: [] + +description: | + ## Overview + + Use this endpoint to initiate the upload of a file containing clinical information to e-RS ready to be associated with a referral, an advice and guidance request or an advice and guidance response. + + This end point supersedes A020 and should be used for any new work. + + ## Supported security patterns + - Healthcare worker, user-restricted access + + ## Pre-requisites + In order to use this endpoint you must be an authenticated e-RS user and use one of the following e-RS roles: + - `REFERRING_CLINICIAN` + - `REFERRING_CLINICIAN_ADMIN` + - `SERVICE_PROVIDER_CLINICIAN` + - `SERVICE_PROVIDER_CLINICIAN_ADMIN` + + You can only upload file formats that are supported by e-RS. Currently, the supported file formats are: + + | File type | File extension(s) | MIME Type | + | ------------------ | --------------------------- | ---------------------------------------------------------------------------- | + | Plain Text | `.txt` | `text/plain` | + | PDF | `.pdf` | `application/pdf` | + | XML Text | `.xml` | `text/xml` | + | RTF Text | `.rtf`, `.rtx` | `text/rtf` | + | Basic Audio | `.au` | `audio/basic` | + | MPEG audio layer 3 | `.mp3` | `audio/mpeg` | + | PNG Image | `.png` | `image/png` | + | GIF Image | `.gif` | `image/gif` | + | JPEG Image | `.jpg`, `.jpe`, `.jpeg` | `image/jpeg` | + | TIFF Image | `.tif`, `.tiff` | `image/tiff` | + | MPEG Video | `.mpg`, `.mpeg`, `.mpe` | `video/mpeg` | + | MSWORD | `.doc`, `.docx` | `application/msword`, `application/vnd.openxmlformats-officedocument.wordprocessingml.document` | + | DICOM | `.dcm` | `application/dicom` | + + Currently, the maximum file size supported by e-RS is 5MB. This will be increasing to 100MB inline with the roadmap. + + ## Use case + + As an authenticated user + + I need to upload a referral letter or other clinical information file to e-RS + + So that I can associate the file with an existing referral or advice and guidance request. + + ## Important notes + + This endpoint **only** initiates the file upload and **does not** associate it with a referral. The uploaded file must be explicitly associated with a referral by, for example, using [Maintain referral letter (A012)](#post-/STU3/ReferralRequest/-ubrn-/$ers.maintainReferralLetter). + + The file will not immediately be available for download, it must pass a malware scan first. The file can be associated with referrals during this period, but will have an `AvailabilityStatus` of `Pending` until the scan is complete. + + Request header `NHSD-eRS-Referral-ID` - the ID (typically the UBRN) of the referral which the documents are intended for - is non-mandatory, but must be provided if this file is to be attached to an existing request. + + ## Support for a temporary redirect + + This end point will respond with a 200 code as it returns a body. Note that it behaves like a temporary redirect. The response will contain a `Location` header, which is the URL where the file should be uploaded to and a `Binary` resource in the body, which will contain the Binary Id used to reference the file when using other end points. + + The temporary location is only valid for a short period of time and should be used immediately. + + Callers of this endpoint must ensure they: + - perform a PUT to this location to upload the file + - do not cache the temporary location + - do not attempt to re-use the temporary location + - supply a file size header (when calling A039) that EXACTLY matches the size of the file uploaded + + When performing the PUT operation the following headers are required: + - `Content-Type` - this should be the same value that was provided in the `NHSD-eRS-File-MIME-Type` header when calling A039 + - `Content-Disposition` - this should contain the value returned in the `Content-Disposition` header of the A039 response + - `Content-Length` - this should be the exact length of the file in bytes and must be the same as the value provided in the `NHSD-eRS-File-Size header` when calling A039. + + See the Response HTTP 200 section for further information. + + ## Related endpoints + + You can associate the uploaded file with a specific referral using: + - [Maintain referral letter (A012)](#post-/STU3/ReferralRequest/-ubrn-/$ers.maintainReferralLetter) + - [Cancel appointment, action later (A022)](#post-/STU3/ReferralRequest/-ubrn-/$ers.cancelAppointmentActionLater) + - [Record triage outcome (A028)](#post-/STU3/ReferralRequest/-ubrn-/$ers.recordReviewOutcome) + + You can associate the uploaded file with a specific advice and guidance request using: + - [Create advice and guidance request (A044)](#post-/STU3/CommunicationRequest/$ers.createAdviceAndGuidance) + - [Send advice and guidance response (A026)](#post-/STU3/CommunicationRequest/-ubrn-/$ers.sendCommunicationToRequester) + - [Convert advice and guidance request to referral (A027)](#post-/STU3/ReferralRequest/$ers.createFromCommunicationRequestActionLater) +summary: 'Upload file to document store (A039, FHIR R4)' +operationId: 'a039-upload-file-to-document-store' +tags: + - 'Refer patient' + - 'Request advice' + - 'Arrange appointment' + - 'Arrange triage' +parameters: + - + in: 'header' + name: 'Authorization' + description: | + An [OAuth 2.0 bearer token](https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation#user-restricted-apis). + + Required in all environments except sandbox. + required: true + schema: + type: 'string' + pattern: '^Bearer\s+.+$' + example: 'Bearer g1112R_ccQ1Ebbb4gtHBP1aaaNM' + - + in: 'header' + name: 'NHSD-End-User-Organisation-ODS' + description: | + The ODS code of the caller's Organisation + required: true + schema: + type: 'string' + example: 'R69' + - + in: 'header' + name: 'NHSD-eRS-Business-Function' + description: | + The e-RS Business Function of the caller + required: true + schema: + type: 'string' + enum: + - 'REFERRING_CLINICIAN' + - 'REFERRING_CLINICIAN_ADMIN' + - 'SERVICE_PROVIDER_CLINICIAN_ADMIN' + - 'SERVICE_PROVIDER_CLINICIAN' + example: 'SERVICE_PROVIDER_CLINICIAN_ADMIN' + - + in: 'header' + name: 'NHSD-eRS-On-Behalf-Of-User-ID' + description: | + The (SDS) user ID of the user that the authenticating user wishes to act on behalf of (OBO). + + "On behalf of" is only supported for a Service Provider Clinician Admin (SPCA) acting on behalf of a Service Provider Clinician (SPC). + + Where an OBO User ID is supplied the authenticating user must be an SPCA and the OBO User ID must be that of an appropriate SPC. + required: false + schema: + type: 'string' + example: '021600556514' + - + in: 'header' + name: 'X-Correlation-ID' + description: | + Arbitrary string value provided by API Consumer + + Tends to be unique, but does not have to be + + Returned, unchanged, in the response + required: false + schema: + type: 'string' + example: '11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA' + - + in: 'header' + name: 'NHSD-eRS-File-Name' + description: | + The name of file being uploaded, including the extension + required: true + schema: + type: 'string' + example: 'test.txt' + - + in: 'header' + name: 'NHSD-eRS-File-Size' + description: | + The size of file being uploaded, in bytes + required: true + schema: + type: 'integer' + minimum: 1 + maximum: 5242880 + format: 'int32' + example: 1024 + - + in: 'header' + name: 'NHSD-eRS-File-MIME-Type' + description: | + The mime type of file being uploaded, must be one from the list of supported types above + required: true + schema: + type: 'string' + example: 'text/plain' + - + in: 'header' + name: 'NHSD-eRS-Referral-ID' + description: | + The ID (typically the UBRN) of the referral which the documents are intended for. This must be provided if this file is to be attached to an existing request. + required: false + schema: + type: 'string' + example: '000000070000' +responses: + '200': + $ref: '../responses/uploadFileToDocumentStore/200Response.yaml' + '401': + $ref: '../responses/Unauthorized.yaml' + '403': + $ref: '../responses/ForbiddenOrNoLR.yaml' + '406': + $ref: '../responses/NotAcceptable.yaml' + '415': + $ref: '../responses/UnsupportedMediaType.yaml' + '422': + $ref: '../responses/uploadFileToDocumentStore/422Response.yaml' + '429': + $ref: '../responses/TooManyRequests.yaml' + '500': + $ref: '../responses/InternalServerError.yaml' + '503': + $ref: '../responses/ServiceUnavailable.yaml' diff --git a/specification/components/r4/schemas/responses/uploadFileToDocumentStore/200Response.yaml b/specification/components/r4/schemas/responses/uploadFileToDocumentStore/200Response.yaml new file mode 100644 index 000000000..8d62be34f --- /dev/null +++ b/specification/components/r4/schemas/responses/uploadFileToDocumentStore/200Response.yaml @@ -0,0 +1,81 @@ +description: Response body is a `Binary` resource containing binary id for the file to be uploaded. The file should be uploaded via a PUT to the uri returned in the Location header. +headers: + x-correlation-id: + description: | + The X-Correlation-ID from the request header, if supplied, mirrored back. + required: false + schema: + type: string + example: 11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA + x-request-id: + description: | + The e-RS transaction id + required: false + schema: + type: string + example: 58621d65-d5ad-4c3a-959f-0438e355990e-1 + Location: + description: Location in the object store where the file should be uploaded to + required: true + schema: + type: string + format: uri + example: https://s3.amazonaws.com/nhs-example-bucket/patientfile.txt?X-Amz-Algorithm=AWS4-HMAC... + Content-Disposition: + description: | + Content-Disposition to be supplied in the PUT operation when uploading the file + required: true + schema: + type: string + example: attachment; filename="upload.txt"; filename*=UTF-8''upload.txt +content: + application/fhir+json: + schema: + title: Binary resource + description: A FHIR Binary resource containing binary identifier and metadata. + type: object + required: + - resourceType + - contentType + properties: + id: + type: string + format: uuid + example: d497bbe3-f88b-45f1-b3d4-9c563e4c0f5f + resourceType: + type: string + enum: + - Binary + example: Binary + meta: + type: object + properties: + lastUpdated: + type: string + format: date-time + example: 2026-09-04T09:40:04.656Z + identifier: + type: array + items: + type: object + required: + - system + - value + properties: + system: + type: string + enum: + - https://fhir.nhs.uk/Id/ers-binary-id + example: https://fhir.nhs.uk/Id/ers-binary-id + value: + type: string + example: d497bbe3-f88b-45f1-b3d4-9c563e4c0f5f + contentType: + type: string + example: text/plain + examples: + example-response-body: + summary: Example response body + value: + $ref: '../../../examples/uploadFileToDocumentStore/responses/BinaryResource.json' + diff --git a/specification/components/r4/schemas/responses/uploadFileToDocumentStore/422Response.yaml b/specification/components/r4/schemas/responses/uploadFileToDocumentStore/422Response.yaml new file mode 100644 index 000000000..65e063a4b --- /dev/null +++ b/specification/components/r4/schemas/responses/uploadFileToDocumentStore/422Response.yaml @@ -0,0 +1,44 @@ +description: | + Where status code 422 (Unprocessable Entity) is returned then a FHIR R4 OperationOutcome will be included in the body, as detailed below. + Check diagnostics property for specific information regarding the error. + + | Error code | Description | + | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | + | CONFLICTING_VALUES | Two or more supplied values are contradictory. | + | FILE_SIZE_ERROR | The file is either empty or too large. | + | INAPPROPRIATE_VALUE | A value, which is acceptable under different conditions, is inappropriate in the context of the other information provided. | + | INVALID_VALUE | The input provided does not conform to the expected data types and format. | + | MISSING_VALUE | A field defined as mandatory for an endpoint has not been provided. | +headers: + X-Correlation-ID: + description: | + The X-Correlation-ID from the request header, if supplied, mirrored back. + required: false + schema: + type: string + example: 11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA + X-Request-ID: + description: | + The e-RS transaction id + required: false + schema: + type: string + example: 58621d65-d5ad-4c3a-959f-0438e355990e-1 + Content-Type: + description: | + Indicates the media type of the associated resource. + required: true + schema: + type: string + enum: + - application/fhir+json + example: application/fhir+json +content: + application/fhir+json: + schema: + $ref: '../../A039-OperationOutcome-R4.yaml' + example: + $ref: '../../../examples/uploadFileToDocumentStore/responses/OperationOutcome-422.json' + + + diff --git a/specification/e-referrals-service-api.yaml b/specification/e-referrals-service-api.yaml index 3024e2545..744901bcb 100644 --- a/specification/e-referrals-service-api.yaml +++ b/specification/e-referrals-service-api.yaml @@ -290,6 +290,9 @@ tags: - name: Review advice and guidance requests paths: + /R4/Binary: + post: + $ref: 'components/r4/schemas/endpoints/a039-upload-file-to-document-store.yaml' /STU3/CodeSystem/{codeSystemType}: get: $ref: 'components/stu3/schemas/endpoints/a004-retrieve-reference-data.yaml' diff --git a/tests/sandbox/r4/test_a039_upload_attachment.py b/tests/sandbox/r4/test_a039_upload_attachment.py new file mode 100644 index 000000000..d8cc30f02 --- /dev/null +++ b/tests/sandbox/r4/test_a039_upload_attachment.py @@ -0,0 +1,149 @@ +from typing import Callable, Dict, Iterable + +import pytest +from tests import asserts + +from requests import Response +from tests.sandbox.SandboxTest import SandboxTest +from tests.data import Actor, RenamedHeader +from tests.sandbox.utils import HttpMethod + + +@pytest.mark.sandbox +class TestUploadAttachmentR4(SandboxTest): + allowed_business_function_data = [ + "REFERRING_CLINICIAN", + "REFERRING_CLINICIAN_ADMIN", + "SERVICE_PROVIDER_CLINICIAN", + "SERVICE_PROVIDER_CLINICIAN_ADMIN", + ] + + authorised_actor_data = Actor.all( + required_business_functions=allowed_business_function_data + ) + + @pytest.fixture + def endpoint_url(self) -> str: + return "FHIR/R4/Binary" + + @pytest.fixture + def http_method(self) -> HttpMethod: + return HttpMethod.POST + + @pytest.fixture + def authorised_actors(self) -> Iterable[Actor]: + return TestUploadAttachmentR4.authorised_actor_data + + @pytest.fixture + def allowed_business_functions(self) -> Iterable[str]: + return TestUploadAttachmentR4.allowed_business_function_data + + @pytest.fixture + def default_headers(self) -> Dict[str, str]: + return { + RenamedHeader.FILENAME.original: "upload.txt", + RenamedHeader.REFERRAL_ID.original: "000000070000", + "nhsd-ers-file-size": "128", + "nhsd-ers-file-mime-type": "text/plain", + "content-type": "text/plain", + } + + @pytest.fixture + def call_endpoint( + self, + call_endpoint_url_with_file: Callable[[Actor, str, Dict[str, str]], Response], + ) -> Callable[[Actor, Dict[str, str]], Response]: + return lambda actor, headers={}: call_endpoint_url_with_file( + actor, + "r4/uploadFileToDocumentStore/requests/upload.txt", + headers=headers, + ) + + @pytest.fixture + def call_endpoint_url_with_file( + self, + send_rest_request: Callable[[HttpMethod, str, Actor], Response], + load_file: Callable[[str], bytes], + endpoint_url: str, + http_method: HttpMethod, + ) -> Callable[[Actor, str], Response]: + return lambda actor, file, headers={}: send_rest_request( + http_method, + endpoint_url, + actor, + headers=headers, + data=load_file(file), + ) + + @pytest.mark.parametrize("actor", authorised_actor_data) + def test_success( + self, + call_endpoint_url_with_file: Callable[[Actor, str, Dict[str, str]], Response], + load_json: Callable[[str], Dict[str, str]], + sandbox_url: str, + actor: Actor, + ): + headers = { + RenamedHeader.FILENAME.original: "upload.txt", + RenamedHeader.REFERRAL_ID.original: "000000070000", + "nhsd-ers-file-size": "128", + "nhsd-ers-file-mime-type": "text/plain", + "content-type": "text/plain", + } + + actual_response = call_endpoint_url_with_file( + actor, "r4/uploadFileToDocumentStore/requests/upload.txt", headers + ) + + expected_response = load_json( + "r4/uploadFileToDocumentStore/responses/BinaryResource.json" + ) + + asserts.assert_status_code(200, actual_response.status_code) + asserts.assert_response(expected_response, actual_response) + asserts.assert_json_response_headers( + actual_response, + additional={ + "Location": f"{sandbox_url}/ObjectStore/d497bbe3-f88b-45f1-b3d4-9c563e4c0f5f", + "Content-Disposition": "attachment; filename=\"upload.txt\"; filename*=UTF-8''upload.txt", + }, + ) + + @pytest.mark.parametrize("actor", authorised_actor_data) + def test_success_without_payload( + self, + send_rest_request: Callable[[HttpMethod, str, Actor], Response], + load_json: Callable[[str], Dict[str, str]], + endpoint_url: str, + http_method: HttpMethod, + sandbox_url: str, + actor: Actor, + ): + headers = { + RenamedHeader.FILENAME.original: "upload.txt", + RenamedHeader.REFERRAL_ID.original: "000000070000", + "nhsd-ers-file-size": "128", + "nhsd-ers-file-mime-type": "text/plain", + "content-type": "text/plain", + } + + actual_response = send_rest_request( + http_method, + endpoint_url, + actor, + headers=headers, + ) + + expected_response = load_json( + "r4/uploadFileToDocumentStore/responses/BinaryResource.json" + ) + + asserts.assert_status_code(200, actual_response.status_code) + asserts.assert_response(expected_response, actual_response) + asserts.assert_json_response_headers( + actual_response, + additional={ + "Location": f"{sandbox_url}/ObjectStore/d497bbe3-f88b-45f1-b3d4-9c563e4c0f5f", + "Content-Disposition": "attachment; filename=\"upload.txt\"; filename*=UTF-8''upload.txt", + }, + )