From e0f10270b75a6c88a50c91a3d154eed98a38b1c4 Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Thu, 26 Feb 2026 14:46:01 +0000 Subject: [PATCH 1/9] NRL-1949 Add consumer & producer objects to auth store --- .../modules/permissions-store-bucket/s3.tf | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/terraform/account-wide-infrastructure/modules/permissions-store-bucket/s3.tf b/terraform/account-wide-infrastructure/modules/permissions-store-bucket/s3.tf index ab7fe77aa..80c9515ca 100644 --- a/terraform/account-wide-infrastructure/modules/permissions-store-bucket/s3.tf +++ b/terraform/account-wide-infrastructure/modules/permissions-store-bucket/s3.tf @@ -60,3 +60,13 @@ resource "aws_s3_bucket_versioning" "authorization-store" { status = "Enabled" } } + +resource "aws_s3_object" "consumer-object" { + bucket = aws_s3_bucket.authorization-store.id + key = "consumer/" +} + +resource "aws_s3_object" "producer-object" { + bucket = aws_s3_bucket.authorization-store.id + key = "producer/" +} From 7eeb57f3d103bcf2c9f1c89d0d8b2f0667c6097a Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Thu, 5 Mar 2026 15:26:30 +0000 Subject: [PATCH 2/9] NRL-1949 Add V2 consumer pointer type feature tests --- scripts/get_s3_permissions.py | 24 ++ .../readDocumentReference-failure.feature | 12 +- .../readDocumentReference-success.feature | 6 +- .../searchDocumentReference-failure.feature | 22 +- .../searchDocumentReference-success.feature | 22 +- ...earchPostDocumentReference-failure.feature | 20 +- ...earchPostDocumentReference-success.feature | 16 +- .../features/consumer/status-success.feature | 1 - .../v2-permissions-by-pointer-type.feature | 215 ++++++++++++++++++ tests/features/steps/2_request.py | 24 +- tests/features/utils/api_client.py | 9 +- tests/utilities/api_clients.py | 32 +++ 12 files changed, 342 insertions(+), 61 deletions(-) delete mode 100644 tests/features/consumer/status-success.feature create mode 100644 tests/features/consumer/v2-permissions-by-pointer-type.feature diff --git a/scripts/get_s3_permissions.py b/scripts/get_s3_permissions.py index 626ded1d6..2d5f52e20 100644 --- a/scripts/get_s3_permissions.py +++ b/scripts/get_s3_permissions.py @@ -49,6 +49,29 @@ def add_test_files(folder, file_name, local_path): json.dump(PointerTypes.list(), f) +def add_feature_test_files(local_path): + """Bake in v2 permissions for the feature test application so that the + v2 permissions model can be proven via feature tests without + requiring a dynamic layer rebuild between test setup and test execution. + """ + + # TODO NEED TO ADD PRODUCER PERMS + print("Adding feature test v2 permissions to temporary directory...") + feature_test_app_id = "z00z-y11y-x22x" + consumer_entries = [ + ( + "RX898", + [PointerTypes.MENTAL_HEALTH_PLAN.value], + ), # http://snomed.info/sct|736253002 + ] + for ods_code, pointer_types in consumer_entries: + folder_path = Path.joinpath(local_path, "consumer", feature_test_app_id) + folder_path.mkdir(parents=True, exist_ok=True) + file_path = folder_path / f"{ods_code}.json" + with open(file_path, "w") as f: + json.dump({"types": pointer_types}, f) + + def download_files(s3_client, bucket_name, local_path, file_names, folders): print(f"Downloading {len(file_names)} S3 files to temporary directory...") local_path = Path(local_path) @@ -65,6 +88,7 @@ def download_files(s3_client, bucket_name, local_path, file_names, folders): s3_client.download_file(bucket_name, file_name, str(file_path)) add_test_files("K6PerformanceTest", "Y05868.json", local_path) + add_feature_test_files(local_path) def main(use_shared_resources: str, env: str, workspace: str, path_to_store: str): diff --git a/tests/features/consumer/readDocumentReference-failure.feature b/tests/features/consumer/readDocumentReference-failure.feature index f6ef1e0f4..dd37af613 100644 --- a/tests/features/consumer/readDocumentReference-failure.feature +++ b/tests/features/consumer/readDocumentReference-failure.feature @@ -6,7 +6,7 @@ Feature: Consumer - readDocumentReference - Failure Scenarios And the organisation 'RX898' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When consumer 'RX898' reads a DocumentReference with ID 'X26-000000000-000000000' + When consumer v1 'RX898' reads a DocumentReference with ID 'X26-000000000-000000000' Then the response status code is 404 And the response is an OperationOutcome with 1 issue And the OperationOutcome contains the issue: @@ -33,7 +33,7 @@ Feature: Consumer - readDocumentReference - Failure Scenarios And the organisation 'RX898' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When consumer 'RX898' reads a DocumentReference with ID 'X26`DROP TABLE 'pointers';--Something-000000000-000000000' + When consumer v1 'RX898' reads a DocumentReference with ID 'X26`DROP TABLE 'pointers';--Something-000000000-000000000' Then the response status code is 404 And the response is an OperationOutcome with 1 issue And the OperationOutcome contains the issue: @@ -58,7 +58,7 @@ Feature: Consumer - readDocumentReference - Failure Scenarios Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API And the organisation 'RX898' is authorised to access pointer types: | system | value | - When consumer 'RX898' reads a DocumentReference with ID 'X26-000000000-000000000' + When consumer v1 'RX898' reads a DocumentReference with ID 'X26-000000000-000000000' Then the response status code is 403 And the response is an OperationOutcome with 1 issue And the OperationOutcome contains the issue: @@ -95,7 +95,7 @@ Feature: Consumer - readDocumentReference - Failure Scenarios | url | https://example.org/my-doc.pdf | | custodian | 02V | | author | 02V | - When consumer 'RX898' reads a DocumentReference with ID '02V-1111111111-ReadDocRefNoAuthForType' + When consumer v1 'RX898' reads a DocumentReference with ID '02V-1111111111-ReadDocRefNoAuthForType' Then the response status code is 403 And the response is an OperationOutcome with 1 issue And the OperationOutcome contains the issue: @@ -120,7 +120,7 @@ Feature: Consumer - readDocumentReference - Failure Scenarios Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API And the organisation 'RX898' is authorised to access pointer types: | system | value | - When consumer 'RX898' reads a DocumentReference with ID 'X26-000000000-000000000' + When consumer v1 'RX898' reads a DocumentReference with ID 'X26-000000000-000000000' Then the response status code is 403 And the response is an OperationOutcome with 1 issue And the OperationOutcome contains the issue: @@ -157,7 +157,7 @@ Feature: Consumer - readDocumentReference - Failure Scenarios | url | https://example.org/my-doc.pdf | | custodian | 02V | | author | 02V | - When consumer 'RX898' reads a DocumentReference with ID '02V-1111111111-ReadDocRefNoAuthForTypeS3' + When consumer v1 'RX898' reads a DocumentReference with ID '02V-1111111111-ReadDocRefNoAuthForTypeS3' Then the response status code is 403 And the response is an OperationOutcome with 1 issue And the OperationOutcome contains the issue: diff --git a/tests/features/consumer/readDocumentReference-success.feature b/tests/features/consumer/readDocumentReference-success.feature index 9a3c81d55..5de6b44cd 100644 --- a/tests/features/consumer/readDocumentReference-success.feature +++ b/tests/features/consumer/readDocumentReference-success.feature @@ -16,7 +16,7 @@ Feature: Consumer - readDocumentReference - Success Scenarios | url | https://example.org/my-doc.pdf | | custodian | RX898 | | author | RX898 | - When consumer 'RX898' reads a DocumentReference with ID 'RX898-9999999999-ReadDocRefSameCustodian' + When consumer v1 'RX898' reads a DocumentReference with ID 'RX898-9999999999-ReadDocRefSameCustodian' Then the response status code is 200 And the response is a DocumentReference with JSON value: """ @@ -133,7 +133,7 @@ Feature: Consumer - readDocumentReference - Success Scenarios | url | https://example.org/my-doc.pdf | | custodian | X26 | | author | RX898 | - When consumer 'RX898' reads a DocumentReference with ID 'X26-9999999999-ReadDocRefDiffCustodian' + When consumer v1 'RX898' reads a DocumentReference with ID 'X26-9999999999-ReadDocRefDiffCustodian' Then the response status code is 200 And the response is a DocumentReference with JSON value: """ @@ -250,5 +250,5 @@ Feature: Consumer - readDocumentReference - Success Scenarios | url | https://example.org/my-doc.pdf | | custodian | RX898\|001 | | author | RX898 | - When consumer 'RX898' reads a DocumentReference with ID 'RX898%7C001-1234567890-ReadDocRefUrlEncoded' + When consumer v1 'RX898' reads a DocumentReference with ID 'RX898%7C001-1234567890-ReadDocRefUrlEncoded' Then the response status code is 200 diff --git a/tests/features/consumer/searchDocumentReference-failure.feature b/tests/features/consumer/searchDocumentReference-failure.feature index 6113b714c..48d0e7882 100644 --- a/tests/features/consumer/searchDocumentReference-failure.feature +++ b/tests/features/consumer/searchDocumentReference-failure.feature @@ -5,7 +5,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios And the organisation 'RX898' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | | extra | parameter | @@ -33,7 +33,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios And the organisation 'RX898' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | Then the response status code is 400 And the response is an OperationOutcome with 1 issue @@ -59,7 +59,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios And the organisation 'RX898' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | | type | http://incorrect.info/sct\|736253002 | @@ -87,7 +87,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios And the organisation 'RX898' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | | type | http://snomed.info/sct\|887701000000100 | @@ -115,7 +115,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios And the organisation 'RX898' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 123 | Then the response status code is 400 @@ -141,7 +141,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API And the organisation 'RX898' is authorised to access pointer types: | system | value | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | Then the response status code is 403 @@ -166,7 +166,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API And the organisation 'RX898' is authorised to access pointer types: | system | value | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | | category | http://snomed.info/sct\|734163000 | @@ -204,7 +204,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios | url | https://example.org/my-doc.pdf | | custodian | 8FW23 | | author | 8FW23 | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | | category | http://snomed.info/sct\|1102421000000108 | @@ -227,7 +227,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios | url | https://example.org/my-doc.pdf | | custodian | 8FW23 | | author | 8FW23 | - When consumer 'Z26' searches for DocumentReferences with parameters: + When consumer v1 'Z26' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | | type | 736253002 | @@ -254,7 +254,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios And the organisation 'RX898' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | | category | http://incorrect.info/sct\|736253002 | @@ -282,7 +282,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios And the organisation 'RX898' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | | category | http://snomed.info/sct\|734163000,http://snomed.info/sct\|invalid | diff --git a/tests/features/consumer/searchDocumentReference-success.feature b/tests/features/consumer/searchDocumentReference-success.feature index a35fa3273..41ec35367 100644 --- a/tests/features/consumer/searchDocumentReference-success.feature +++ b/tests/features/consumer/searchDocumentReference-success.feature @@ -16,7 +16,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc.pdf | | custodian | 02V | | author | 02V | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | Then the response status code is 200 @@ -53,7 +53,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | custodian | 02V | | author | 02V | | identifier | 02V.123456789 | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | Then the response status code is 200 @@ -90,7 +90,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc.pdf | | custodian | 02V | | author | 02V | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | | custodian | 02V | @@ -138,7 +138,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc.pdf | | custodian | DK94 | | author | DK94 | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | | custodian | 02V | @@ -186,7 +186,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc.pdf | | custodian | 02V | | author | 02V | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | | custodian | 02V | @@ -245,7 +245,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc-3.pdf | | custodian | 02V | | author | 02V | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | Then the response status code is 200 @@ -299,7 +299,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | custodian | DK94 | | author | N64 | When producer 'DK94' requests to delete DocumentReference with id 'DK94-111-DeleteDocRefTest1' - And consumer 'RX898' searches for DocumentReferences with parameters: + And consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | | custodian | RX898 | @@ -348,7 +348,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc-3.pdf | | custodian | 02V | | author | 02V | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | | category | http://snomed.info/sct\|734163000 | @@ -409,7 +409,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc-3.pdf | | custodian | 02V | | author | 02V | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | | category | http://snomed.info/sct\|1102421000000108 | @@ -450,7 +450,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc-3.pdf | | custodian | 02V | | author | 02V | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | | _summary | count | @@ -510,7 +510,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc-4.pdf | | custodian | 02V | | author | 02V | - When consumer 'RX898' searches for DocumentReferences with parameters: + When consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | | category | http://snomed.info/sct\|734163000,http://snomed.info/sct\|823651000000106 | diff --git a/tests/features/consumer/searchPostDocumentReference-failure.feature b/tests/features/consumer/searchPostDocumentReference-failure.feature index 2aa60a5b1..53fd41d39 100644 --- a/tests/features/consumer/searchPostDocumentReference-failure.feature +++ b/tests/features/consumer/searchPostDocumentReference-failure.feature @@ -5,7 +5,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios And the organisation 'RX898' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When consumer 'RX898' searches for DocumentReferences using POST with request body: + When consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | | extra | parameter | @@ -33,7 +33,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios And the organisation 'RX898' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When consumer 'RX898' searches for DocumentReferences using POST with request body: + When consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | Then the response status code is 400 And the response is an OperationOutcome with 1 issue @@ -59,7 +59,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios And the organisation 'RX898' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When consumer 'RX898' searches for DocumentReferences using POST with request body: + When consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | | type | http://incorrect.info/sct\|736253002 | @@ -87,7 +87,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios And the organisation 'RX898' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When consumer 'RX898' searches for DocumentReferences using POST with request body: + When consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | | type | http://snomed.info/sct\|887701000000100 | @@ -115,7 +115,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios And the organisation 'RX898' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When consumer 'RX898' searches for DocumentReferences using POST with request body: + When consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 123 | Then the response status code is 400 @@ -141,7 +141,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API And the organisation 'RX898' is authorised to access pointer types: | system | value | - When consumer 'RX898' searches for DocumentReferences using POST with request body: + When consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | Then the response status code is 403 @@ -175,7 +175,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios | url | https://example.org/my-doc.pdf | | custodian | 8FW23 | | author | 8FW23 | - When consumer 'X26' searches for DocumentReferences using POST with request body: + When consumer v1 'X26' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | | type | 736253002 | @@ -199,7 +199,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios Scenario: Search rejects request if the organisation has no registered pointer types Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API - When consumer 'RX898' searches for DocumentReferences using POST with request body: + When consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | Then the response status code is 403 @@ -225,7 +225,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios And the organisation 'RX898' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When consumer 'RX898' searches for DocumentReferences using POST with request body: + When consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | | category | http://incorrect.info/sct\|736253002 | @@ -253,7 +253,7 @@ Feature: Consumer - searchDocumentReference - Failure Scenarios And the organisation 'RX898' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When consumer 'RX898' searches for DocumentReferences using POST with request body: + When consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | | category | http://snomed.info/sct\|734163000,http://snomed.info/sct\|invalid | diff --git a/tests/features/consumer/searchPostDocumentReference-success.feature b/tests/features/consumer/searchPostDocumentReference-success.feature index c8fcecaa5..e601b1e82 100644 --- a/tests/features/consumer/searchPostDocumentReference-success.feature +++ b/tests/features/consumer/searchPostDocumentReference-success.feature @@ -16,7 +16,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc.pdf | | custodian | 8FW23 | | author | 8FW23 | - When consumer 'RX898' searches for DocumentReferences using POST with request body: + When consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | Then the response status code is 200 @@ -52,7 +52,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc.pdf | | custodian | RX898 | | author | RX898 | - When consumer 'RX898' searches for DocumentReferences using POST with request body: + When consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | | custodian | RX898 | @@ -111,7 +111,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc-3.pdf | | custodian | X26 | | author | X26 | - When consumer 'RX898' searches for DocumentReferences using POST with request body: + When consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | Then the response status code is 200 @@ -180,7 +180,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc-3.pdf | | custodian | X26 | | author | X26 | - When consumer 'RX898' searches for DocumentReferences using POST with request body: + When consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | Then the response status code is 200 @@ -233,7 +233,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | custodian | DK94 | | author | N64 | When producer 'DK94' requests to delete DocumentReference with id 'DK94-111-DeleteDocRefTest1' - And consumer 'RX898' searches for DocumentReferences using POST with request body: + And consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | | custodian | RX898 | @@ -282,7 +282,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc-3.pdf | | custodian | x26 | | author | x26 | - When consumer 'RX898' searches for DocumentReferences using POST with request body: + When consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | | category | http://snomed.info/sct\|734163000 | @@ -353,7 +353,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc-3.pdf | | custodian | x26 | | author | x26 | - When consumer 'RX898' searches for DocumentReferences using POST with request body: + When consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | | _summary | count | @@ -412,7 +412,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc-4.pdf | | custodian | X26 | | author | X26 | - When consumer 'RX898' searches for DocumentReferences using POST with request body: + When consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | | category | http://snomed.info/sct\|734163000,http://snomed.info/sct\|823651000000106 | diff --git a/tests/features/consumer/status-success.feature b/tests/features/consumer/status-success.feature deleted file mode 100644 index c4f2cc781..000000000 --- a/tests/features/consumer/status-success.feature +++ /dev/null @@ -1 +0,0 @@ -# Status returns 200 diff --git a/tests/features/consumer/v2-permissions-by-pointer-type.feature b/tests/features/consumer/v2-permissions-by-pointer-type.feature new file mode 100644 index 000000000..829078493 --- /dev/null +++ b/tests/features/consumer/v2-permissions-by-pointer-type.feature @@ -0,0 +1,215 @@ +Feature: Consumer readDocumentReference - v2 Permissions Model - Success and Failure Scenarios + For the v2 permissions model, permissions are resolved from a JSON file stored in the + nrlf_permissions Lambda layer at the path + {actorType}/{app_id}/{ods_code}.json, which must contain a "types" array. + Permissions for the feature test application (ID 'z00z-y11y-x22x') and + ODS code 'RX898' are baked into the layer by `scripts/get_s3_permissions.py` + at build time, so no dynamic seeding step is required for + success scenarios. + + Background: + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + + Scenario: V2 Permissions with access for pointer type - readDocumentReference + Given a DocumentReference resource exists with values: + | property | value | + | id | RX898-9999999999-ReadDocRefV2SameCustodian | + | subject | 9999999999 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc.pdf | + | custodian | RX898 | + | author | RX898 | + When consumer v2 'RX898' reads a DocumentReference with ID 'RX898-9999999999-ReadDocRefV2SameCustodian' + Then the response status code is 200 + And the response is a DocumentReference with JSON value: + """ + { + "resourceType": "DocumentReference", + "id": "RX898-9999999999-ReadDocRefV2SameCustodian", + "status": "current", + "type": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "736253002", + "display": "Mental health crisis plan" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "734163000", + "display": "Care plan" + } + ] + } + ], + "subject": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9999999999" + } + }, + "custodian": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "RX898" + } + }, + "author": [ + { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "RX898" + } + } + ], + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-NRLRetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLRetrievalMechanism", + "code": "Direct", + "display": "Direct" + } + ] + } + } + ] + } + ], + "context": { + "practiceSetting": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "788007007", + "display": "General practice service" + } + ] + } + } + } + """ + + Scenario: V2 permissions with access for pointer type retrieves expected document references - searchPostDocumentReference + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And a DocumentReference resource exists with values: + | property | value | + | id | X26-1111111111-SearchMultipleRefTest1 | + | subject | 9278693472 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc-1.pdf | + | custodian | X26 | + | author | X26 | + And a DocumentReference resource exists with values: + | property | value | + | id | X26-1111111111-SearchMultipleRefTest2 | + | subject | 9278693472 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc-2.pdf | + | custodian | X26 | + | author | X26 | + And a DocumentReference resource exists with values: + | property | value | + | id | X26-1111111111-SearchMultipleRefTestDifferentType | + | subject | 9278693472 | + | status | current | + | type | 887701000000100 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc-3.pdf | + | custodian | X26 | + | author | X26 | + When consumer v2 'RX898' searches for DocumentReferences using POST with request body: + | key | value | + | subject | 9278693472 | + Then the response status code is 200 + And the response is a searchset Bundle + And the Bundle has a total of 2 + And the Bundle has 2 entries + And the Bundle contains an DocumentReference with values + | property | value | + | id | X26-1111111111-SearchMultipleRefTest1 | + | subject | 9278693472 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc-1.pdf | + | custodian | X26 | + | author | X26 | + And the Bundle contains an DocumentReference with values + | property | value | + | id | X26-1111111111-SearchMultipleRefTest2 | + | subject | 9278693472 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc-2.pdf | + | custodian | X26 | + | author | X26 | + And the Bundle does not contain a DocumentReference with ID 'X26-1111111111-SearchMultipleRefTestDifferentType' + + Scenario: V2 permissions with no access for pointer type - searchDocumentReference + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + When consumer v2 'RX898' searches for DocumentReferences with parameters: + | parameter | value | + | subject | 9278693472 | + | type | http://snomed.info/sct\|887701000000100 | + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "code-invalid", + "details": { + "coding": [{ + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "INVALID_CODE_SYSTEM", + "display": "Invalid code system" + }] + }, + "diagnostics": "Invalid query parameter (The provided type does not match the allowed types for this organisation)", + "expression": ["type"] + } + """ diff --git a/tests/features/steps/2_request.py b/tests/features/steps/2_request.py index 304228fec..7ac95e7d5 100644 --- a/tests/features/steps/2_request.py +++ b/tests/features/steps/2_request.py @@ -24,9 +24,11 @@ def consumer_count_document_references_step(context: Context, ods_code: str): context.response = client.count(items) -@when("consumer '{ods_code}' searches for DocumentReferences with parameters") -def consumer_search_document_reference_step(context: Context, ods_code: str): - client = consumer_client_from_context(context, ods_code) +@when("consumer {version} '{ods_code}' searches for DocumentReferences with parameters") +def consumer_search_document_reference_step( + context: Context, version: str, ods_code: str +): + client = consumer_client_from_context(context, ods_code, v2=(version == "v2")) if not context.table: raise ValueError("No search query table provided") @@ -46,10 +48,12 @@ def consumer_search_document_reference_step(context: Context, ods_code: str): @when( - "consumer '{ods_code}' searches for DocumentReferences using POST with request body" + "consumer {version} '{ods_code}' searches for DocumentReferences using POST with request body" ) -def consumer_search_post_document_reference_step(context: Context, ods_code: str): - client = consumer_client_from_context(context, ods_code) +def consumer_search_post_document_reference_step( + context: Context, version: str, ods_code: str +): + client = consumer_client_from_context(context, ods_code, v2=(version == "v2")) if not context.table: raise ValueError("No search query table provided") @@ -68,11 +72,13 @@ def consumer_search_post_document_reference_step(context: Context, ods_code: str ) -@when("consumer '{ods_code}' reads a DocumentReference with ID '{doc_ref_id}'") +@when( + "consumer {version} '{ods_code}' reads a DocumentReference with ID '{doc_ref_id}'" +) def consumer_read_document_reference_step( - context: Context, ods_code: str, doc_ref_id: str + context: Context, version: str, ods_code: str, doc_ref_id: str ): - client = consumer_client_from_context(context, ods_code) + client = consumer_client_from_context(context, ods_code, v2=(version == "v2")) context.response = client.read(doc_ref_id) diff --git a/tests/features/utils/api_client.py b/tests/features/utils/api_client.py index 4c8f8a8cc..ad468767c 100644 --- a/tests/features/utils/api_client.py +++ b/tests/features/utils/api_client.py @@ -4,6 +4,7 @@ from tests.utilities.api_clients import ( ClientConfig, ConsumerTestClient, + ConsumerV2TestClient, ProducerTestClient, ) @@ -33,9 +34,13 @@ def _config_from_context(context: Context, ods_code: str): ) -def consumer_client_from_context(context: Context, ods_code: str): +def consumer_client_from_context(context: Context, ods_code: str, v2: bool = False): client_config = _config_from_context(context, ods_code) - return ConsumerTestClient(config=client_config) + return ( + ConsumerV2TestClient(config=client_config) + if v2 + else ConsumerTestClient(config=client_config) + ) def producer_client_from_context(context: Context, ods_code: str): diff --git a/tests/utilities/api_clients.py b/tests/utilities/api_clients.py index bb522cbe3..8c42e99ab 100644 --- a/tests/utilities/api_clients.py +++ b/tests/utilities/api_clients.py @@ -209,6 +209,38 @@ def read_capability_statement(self) -> Response: ) +class ConsumerV2TestClient(ConsumerTestClient): + """ + Consumer test client that uses the v2 permissions model. + + Sends NHSD-End-User-Organisation-ODS and NHSD-NRL-App-Id instead of + NHSD-Connection-Metadata / NHSD-Client-RP-Details, triggering the + _use_v2_permissions_model path in the request handler. + """ + + def __init__(self, config: ClientConfig): + self.config = config + self.api_url = f"{self.config.base_url}consumer{self.config.api_path}" + + self.request_headers = { + "Authorization": f"Bearer {self.config.auth_token}", + "X-Request-Id": "test-request-id", + } + + if self.config.client_cert: + ods_code = self.config.connection_metadata.ods_code + app_id = self.config.connection_metadata.nrl_app_id + self.request_headers.update( + { + "NHSD-End-User-Organisation-ODS": ods_code, + "NHSD-NRL-App-Id": app_id, + "NHSD-Correlation-Id": "test-correlation-id", + } + ) + + self.request_headers.update(self.config.custom_headers) + + class ProducerTestClient: def __init__(self, config: ClientConfig): self.config = config From ac060ab373b4c7060c06669e4861eb131bab5c42 Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Thu, 5 Mar 2026 15:31:20 +0000 Subject: [PATCH 3/9] NRL-1949 Add todo --- .../modules/permissions-store-bucket/s3.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/account-wide-infrastructure/modules/permissions-store-bucket/s3.tf b/terraform/account-wide-infrastructure/modules/permissions-store-bucket/s3.tf index 80c9515ca..2c7f43d1e 100644 --- a/terraform/account-wide-infrastructure/modules/permissions-store-bucket/s3.tf +++ b/terraform/account-wide-infrastructure/modules/permissions-store-bucket/s3.tf @@ -60,7 +60,7 @@ resource "aws_s3_bucket_versioning" "authorization-store" { status = "Enabled" } } - +# Need to pull these into state if they already exist resource "aws_s3_object" "consumer-object" { bucket = aws_s3_bucket.authorization-store.id key = "consumer/" From e5d4edd316ec67c1d22d6b05a164a69c6ba42433 Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Thu, 5 Mar 2026 17:30:03 +0000 Subject: [PATCH 4/9] NRL-1949 Add producer v2 WIP --- scripts/get_s3_permissions.py | 14 ++++- .../v2-permissions-by-pointer-type.feature | 2 +- .../createDocumentReference-failure.feature | 24 ++++----- .../createDocumentReference-success.feature | 12 ++--- .../v2-permissions-by-pointer-type.feature | 54 +++++++++++++++++++ tests/features/steps/2_request.py | 6 +-- tests/features/utils/api_client.py | 9 +++- tests/utilities/api_clients.py | 33 ++++++++++++ 8 files changed, 129 insertions(+), 25 deletions(-) create mode 100644 tests/features/producer/v2-permissions-by-pointer-type.feature diff --git a/scripts/get_s3_permissions.py b/scripts/get_s3_permissions.py index 2d5f52e20..84ed11471 100644 --- a/scripts/get_s3_permissions.py +++ b/scripts/get_s3_permissions.py @@ -49,13 +49,13 @@ def add_test_files(folder, file_name, local_path): json.dump(PointerTypes.list(), f) +# TODO MAKE THIS NEATER def add_feature_test_files(local_path): """Bake in v2 permissions for the feature test application so that the v2 permissions model can be proven via feature tests without requiring a dynamic layer rebuild between test setup and test execution. """ - # TODO NEED TO ADD PRODUCER PERMS print("Adding feature test v2 permissions to temporary directory...") feature_test_app_id = "z00z-y11y-x22x" consumer_entries = [ @@ -64,12 +64,24 @@ def add_feature_test_files(local_path): [PointerTypes.MENTAL_HEALTH_PLAN.value], ), # http://snomed.info/sct|736253002 ] + producer_entries = [ + ( + "RX898", + [PointerTypes.MENTAL_HEALTH_PLAN.value], + ), # http://snomed.info/sct|736253002 + ] for ods_code, pointer_types in consumer_entries: folder_path = Path.joinpath(local_path, "consumer", feature_test_app_id) folder_path.mkdir(parents=True, exist_ok=True) file_path = folder_path / f"{ods_code}.json" with open(file_path, "w") as f: json.dump({"types": pointer_types}, f) + for ods_code, pointer_types in producer_entries: + folder_path = Path.joinpath(local_path, "producer", feature_test_app_id) + folder_path.mkdir(parents=True, exist_ok=True) + file_path = folder_path / f"{ods_code}.json" + with open(file_path, "w") as f: + json.dump({"types": pointer_types}, f) def download_files(s3_client, bucket_name, local_path, file_names, folders): diff --git a/tests/features/consumer/v2-permissions-by-pointer-type.feature b/tests/features/consumer/v2-permissions-by-pointer-type.feature index 829078493..3ac0e9510 100644 --- a/tests/features/consumer/v2-permissions-by-pointer-type.feature +++ b/tests/features/consumer/v2-permissions-by-pointer-type.feature @@ -1,4 +1,4 @@ -Feature: Consumer readDocumentReference - v2 Permissions Model - Success and Failure Scenarios +Feature: Consumer v2 permissions by pointer type - Success and Failure Scenarios For the v2 permissions model, permissions are resolved from a JSON file stored in the nrlf_permissions Lambda layer at the path {actorType}/{app_id}/{ods_code}.json, which must contain a "types" array. diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index ff1d2bfda..905bea341 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -16,7 +16,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios And the organisation 'ANGY1' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When producer 'ANGY1' creates a DocumentReference with values: + When producer v1 'ANGY1' creates a DocumentReference with values: | property | value | | subject | 9999999999 | | status | current | @@ -53,7 +53,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios And the organisation 'ANGY1' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When producer 'ANGY1' creates a DocumentReference with values: + When producer v1 'ANGY1' creates a DocumentReference with values: | property | value | | subject | 1234567890 | | status | current | @@ -212,7 +212,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios | url | https://example.org/my-doc.pdf | | custodian | N0TANGY | | author | HAR1 | - When producer 'ANGY1' creates a DocumentReference with values: + When producer v1 'ANGY1' creates a DocumentReference with values: | property | value | | subject | 9278693472 | | status | current | @@ -263,7 +263,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API And the organisation 'ANGY1' is authorised to access pointer types: | system | value | - When producer 'ANGY1' creates a DocumentReference with values: + When producer v1 'ANGY1' creates a DocumentReference with values: | property | value | | subject | 9999999999 | | status | current | @@ -298,7 +298,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios | system | value | | http://snomed.info/sct | 1363501000000100 | | http://snomed.info/sct | 736253002 | - When producer 'X26' creates a DocumentReference with values: + When producer v1 'X26' creates a DocumentReference with values: | property | value | | subject | 9999999999 | | type | 736253002 | @@ -335,7 +335,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios And the organisation 'ANGY1' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When producer 'ANGY1' creates a DocumentReference with values: + When producer v1 'ANGY1' creates a DocumentReference with values: | property | value | | subject | 9999999999 | | status | current | @@ -373,7 +373,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios | system | value | | http://snomed.info/sct | 1363501000000100 | | http://snomed.info/sct | 736253002 | - When producer 'X26' creates a DocumentReference with values: + When producer v1 'X26' creates a DocumentReference with values: | property | value | | subject | 9999999999 | | status | current | @@ -544,7 +544,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios And the organisation 'ANGY1' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When producer 'ANGY1' creates a DocumentReference with values: + When producer v1 'ANGY1' creates a DocumentReference with values: | property | value | | subject | 9278693472 | | status | current | @@ -581,7 +581,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios And the organisation 'ANGY1' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When producer 'ANGY1' creates a DocumentReference with values: + When producer v1 'ANGY1' creates a DocumentReference with values: | property | value | | subject | 9999999999 | | status | current | @@ -620,7 +620,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios | system | value | | https://nicip.nhs.uk | MAULR | | https://nicip.nhs.uk | MAXIB | - When producer 'ANGY1' creates a DocumentReference with values: + When producer v1 'ANGY1' creates a DocumentReference with values: | property | value | | subject | 9999999999 | | status | current | @@ -665,7 +665,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios | system | value | | http://snomed.info/sct | 1363501000000100 | | http://snomed.info/sct | 736253002 | - When producer 'X26' creates a DocumentReference with values: + When producer v1 'X26' creates a DocumentReference with values: | property | value | | subject | 9999999999 | | status | current | @@ -814,7 +814,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios And the organisation 'ANGY1' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When producer 'ANGY1' creates a DocumentReference with values: + When producer v1 'ANGY1' creates a DocumentReference with values: | property | value | | subject | 9999999999 | | status | current | diff --git a/tests/features/producer/createDocumentReference-success.feature b/tests/features/producer/createDocumentReference-success.feature index e7ea87d8c..03f4f65c7 100644 --- a/tests/features/producer/createDocumentReference-success.feature +++ b/tests/features/producer/createDocumentReference-success.feature @@ -5,7 +5,7 @@ Feature: Producer - createDocumentReference - Success Scenarios And the organisation 'ANGY1' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When producer 'ANGY1' creates a DocumentReference with values: + When producer v1 'ANGY1' creates a DocumentReference with values: | property | value | | subject | 9278693472 | | status | current | @@ -110,7 +110,7 @@ Feature: Producer - createDocumentReference - Success Scenarios | custodian | ANGY1 | | author | HAR1 | | practiceSetting | 788002001 | - When producer 'ANGY1' creates a DocumentReference with values: + When producer v1 'ANGY1' creates a DocumentReference with values: | property | value | | subject | 9278693472 | | status | current | @@ -161,7 +161,7 @@ Feature: Producer - createDocumentReference - Success Scenarios And the organisation 'ANGY1' is authorised to access pointer types: | system | value | | http://snomed.info/sct | | - When producer 'ANGY1' creates a DocumentReference with values: + When producer v1 'ANGY1' creates a DocumentReference with values: | property | value | | subject | 9278693472 | | status | current | @@ -239,7 +239,7 @@ Feature: Producer - createDocumentReference - Success Scenarios | system | value | | https://nicip.nhs.uk | MAULR | | https://nicip.nhs.uk | MAXIB | - When producer 'ANGY1' creates a DocumentReference with values: + When producer v1 'ANGY1' creates a DocumentReference with values: | property | value | | subject | 9278693472 | | status | current | @@ -282,7 +282,7 @@ Feature: Producer - createDocumentReference - Success Scenarios | author | HAR1 | | url | https://example.org/my-doc.pdf | | practiceSetting | 788002001 | - When producer 'ANGY1' creates a DocumentReference with values: + When producer v1 'ANGY1' creates a DocumentReference with values: | property | value | | subject | 9278693472 | | status | current | @@ -330,7 +330,7 @@ Feature: Producer - createDocumentReference - Success Scenarios And the organisation 'BARS1' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 749001000000101 | - When producer 'BARS1' creates a DocumentReference with values: + When producer v1 'BARS1' creates a DocumentReference with values: | property | value | | subject | 9278693472 | | status | current | diff --git a/tests/features/producer/v2-permissions-by-pointer-type.feature b/tests/features/producer/v2-permissions-by-pointer-type.feature new file mode 100644 index 000000000..0f17a8151 --- /dev/null +++ b/tests/features/producer/v2-permissions-by-pointer-type.feature @@ -0,0 +1,54 @@ +Feature: Producer v2 permissions by pointer type - Success and Failure Scenarios + For the v2 permissions model, permissions are resolved from a JSON file stored in the + nrlf_permissions Lambda layer at the path + {actorType}/{app_id}/{ods_code}.json, which must contain a "types" array. + Permissions for the feature test application (ID 'z00z-y11y-x22x') and + ODS code 'RX898' are baked into the layer by `scripts/get_s3_permissions.py` + at build time, so no dynamic seeding step is required for + success scenarios. + + Background: + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + + Scenario: Successfully create a Document Pointer (care plan) + When producer v2 'RX898' creates a DocumentReference with values: + | property | value | + | subject | 9278693472 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | custodian | RX898 | + | author | HAR1 | + | url | https://example.org/my-doc.pdf | + | practiceSetting | 788002001 | + Then the response status code is 201 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "information", + "code": "informational", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/NRL-ResponseCode", + "code": "RESOURCE_CREATED", + "display": "Resource created" + } + ] + }, + "diagnostics": "The document has been created" + } + """ + And the response has a Location header + And the Location header starts with '/DocumentReference/RX898-' + And the resource in the Location header exists with values: + | property | value | + | subject | 9278693472 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | custodian | RX898 | + | author | HAR1 | + | url | https://example.org/my-doc.pdf | + | practiceSetting | 788002001 | diff --git a/tests/features/steps/2_request.py b/tests/features/steps/2_request.py index 7ac95e7d5..f37699991 100644 --- a/tests/features/steps/2_request.py +++ b/tests/features/steps/2_request.py @@ -82,9 +82,9 @@ def consumer_read_document_reference_step( context.response = client.read(doc_ref_id) -@when("producer '{ods_code}' creates a DocumentReference with values") -def create_post_document_reference_step(context: Context, ods_code: str): - client = producer_client_from_context(context, ods_code) +@when("producer {version} '{ods_code}' creates a DocumentReference with values") +def create_post_document_reference_step(context: Context, version: str, ods_code: str): + client = producer_client_from_context(context, ods_code, v2=(version == "v2")) if not context.table: raise ValueError("No document reference data table provided") diff --git a/tests/features/utils/api_client.py b/tests/features/utils/api_client.py index ad468767c..3e90966a2 100644 --- a/tests/features/utils/api_client.py +++ b/tests/features/utils/api_client.py @@ -6,6 +6,7 @@ ConsumerTestClient, ConsumerV2TestClient, ProducerTestClient, + ProducerV2TestClient, ) @@ -43,6 +44,10 @@ def consumer_client_from_context(context: Context, ods_code: str, v2: bool = Fal ) -def producer_client_from_context(context: Context, ods_code: str): +def producer_client_from_context(context: Context, ods_code: str, v2: bool = False): client_config = _config_from_context(context, ods_code) - return ProducerTestClient(config=client_config) + return ( + ProducerV2TestClient(config=client_config) + if v2 + else ProducerTestClient(config=client_config) + ) diff --git a/tests/utilities/api_clients.py b/tests/utilities/api_clients.py index 8c42e99ab..4f3b10218 100644 --- a/tests/utilities/api_clients.py +++ b/tests/utilities/api_clients.py @@ -241,6 +241,7 @@ def __init__(self, config: ClientConfig): self.request_headers.update(self.config.custom_headers) +# TODO MAKE THIS NEATER class ProducerTestClient: def __init__(self, config: ClientConfig): self.config = config @@ -417,3 +418,35 @@ def head( headers=headers, cert=self.config.client_cert, ) + + +class ProducerV2TestClient(ProducerTestClient): + """ + Producer test client that uses the v2 permissions model. + + Sends NHSD-End-User-Organisation-ODS and NHSD-NRL-App-Id instead of + NHSD-Connection-Metadata / NHSD-Client-RP-Details, triggering the + _use_v2_permissions_model path in the request handler. + """ + + def __init__(self, config: ClientConfig): + self.config = config + self.api_url = f"{self.config.base_url}producer{self.config.api_path}" + + self.request_headers = { + "Authorization": f"Bearer {self.config.auth_token}", + "X-Request-Id": "test-request-id", + } + + if self.config.client_cert: + ods_code = self.config.connection_metadata.ods_code + app_id = self.config.connection_metadata.nrl_app_id + self.request_headers.update( + { + "NHSD-End-User-Organisation-ODS": ods_code, + "NHSD-NRL-App-Id": app_id, + "NHSD-Correlation-Id": "test-correlation-id", + } + ) + + self.request_headers.update(self.config.custom_headers) From db6588348dba28709feda58b7817ad74896a12e1 Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Fri, 6 Mar 2026 12:24:08 +0000 Subject: [PATCH 5/9] NRL-1949 V2 Producer tests for pointer type --- .../searchDocumentReference-success.feature | 2 +- ...earchPostDocumentReference-success.feature | 2 +- .../deleteDocumentReference-failure.feature | 2 +- .../deleteDocumentReference-success.feature | 4 +- .../searchDocumentReference-failure.feature | 0 .../searchDocumentReference-success.feature | 4 +- ...earchPostDocumentReference-failure.feature | 0 ...earchPostDocumentReference-success.feature | 0 .../v2-permissions-by-pointer-type.feature | 79 ++++++++++++++++++- tests/features/steps/2_request.py | 16 ++-- 10 files changed, 95 insertions(+), 14 deletions(-) delete mode 100644 tests/features/producer/searchDocumentReference-failure.feature delete mode 100644 tests/features/producer/searchPostDocumentReference-failure.feature delete mode 100644 tests/features/producer/searchPostDocumentReference-success.feature diff --git a/tests/features/consumer/searchDocumentReference-success.feature b/tests/features/consumer/searchDocumentReference-success.feature index 41ec35367..b3d9c4f7b 100644 --- a/tests/features/consumer/searchDocumentReference-success.feature +++ b/tests/features/consumer/searchDocumentReference-success.feature @@ -298,7 +298,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc.pdf | | custodian | DK94 | | author | N64 | - When producer 'DK94' requests to delete DocumentReference with id 'DK94-111-DeleteDocRefTest1' + When producer v1 'DK94' requests to delete DocumentReference with id 'DK94-111-DeleteDocRefTest1' And consumer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | diff --git a/tests/features/consumer/searchPostDocumentReference-success.feature b/tests/features/consumer/searchPostDocumentReference-success.feature index e601b1e82..af09f03d6 100644 --- a/tests/features/consumer/searchPostDocumentReference-success.feature +++ b/tests/features/consumer/searchPostDocumentReference-success.feature @@ -232,7 +232,7 @@ Feature: Consumer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc.pdf | | custodian | DK94 | | author | N64 | - When producer 'DK94' requests to delete DocumentReference with id 'DK94-111-DeleteDocRefTest1' + When producer v1 'DK94' requests to delete DocumentReference with id 'DK94-111-DeleteDocRefTest1' And consumer v1 'RX898' searches for DocumentReferences using POST with request body: | key | value | | subject | 9278693472 | diff --git a/tests/features/producer/deleteDocumentReference-failure.feature b/tests/features/producer/deleteDocumentReference-failure.feature index 52fb6c3fc..105398075 100644 --- a/tests/features/producer/deleteDocumentReference-failure.feature +++ b/tests/features/producer/deleteDocumentReference-failure.feature @@ -19,7 +19,7 @@ Feature: Producer - deleteDocumentReference - Failure Scenarios | url | https://example.org/my-doc.pdf | | custodian | OC84 | | author | N64 | - When producer 'DK94' requests to delete DocumentReference with id 'OC84-111-DeleteTest-NotYourPointer' + When producer v1 'DK94' requests to delete DocumentReference with id 'OC84-111-DeleteTest-NotYourPointer' Then the response status code is 403 And the response is an OperationOutcome with 1 issue And the OperationOutcome contains the issue: diff --git a/tests/features/producer/deleteDocumentReference-success.feature b/tests/features/producer/deleteDocumentReference-success.feature index 440de277b..f1543a8e5 100644 --- a/tests/features/producer/deleteDocumentReference-success.feature +++ b/tests/features/producer/deleteDocumentReference-success.feature @@ -18,7 +18,7 @@ Feature: Producer - deleteDocumentReference - Success Scenarios | url | https://example.org/my-doc.pdf | | custodian | DK94 | | author | N64 | - When producer 'DK94' requests to delete DocumentReference with id 'DK94-111-DeleteDocRefTest1' + When producer v1 'DK94' requests to delete DocumentReference with id 'DK94-111-DeleteDocRefTest1' Then the response status code is 200 And the response is an OperationOutcome with 1 issue And the OperationOutcome contains the issue: @@ -47,7 +47,7 @@ Feature: Producer - deleteDocumentReference - Success Scenarios And the organisation 'DK94' is authorised to access pointer types: | system | value | | http://snomed.info/sct | 736253002 | - When producer 'DK94' requests to delete DocumentReference with id 'DK94-000-NoPointerHere' + When producer v1 'DK94' requests to delete DocumentReference with id 'DK94-000-NoPointerHere' Then the response status code is 200 And the response is an OperationOutcome with 1 issue And the OperationOutcome contains the issue: diff --git a/tests/features/producer/searchDocumentReference-failure.feature b/tests/features/producer/searchDocumentReference-failure.feature deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/features/producer/searchDocumentReference-success.feature b/tests/features/producer/searchDocumentReference-success.feature index 2fe5e31df..c8823bce5 100644 --- a/tests/features/producer/searchDocumentReference-success.feature +++ b/tests/features/producer/searchDocumentReference-success.feature @@ -27,7 +27,7 @@ Feature: Producer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc.pdf | | custodian | RX898 | | author | X26 | - When producer 'RX898' searches for DocumentReferences with parameters: + When producer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | subject | 9278693472 | Then the response status code is 200 @@ -86,7 +86,7 @@ Feature: Producer - searchDocumentReference - Success Scenarios | url | https://example.org/my-doc.pdf | | custodian | RX898 | | author | X26 | - When producer 'RX898' searches for DocumentReferences with parameters: + When producer v1 'RX898' searches for DocumentReferences with parameters: | parameter | value | | pointer_type | 736253002 | | subject | 9999999999 | diff --git a/tests/features/producer/searchPostDocumentReference-failure.feature b/tests/features/producer/searchPostDocumentReference-failure.feature deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/features/producer/searchPostDocumentReference-success.feature b/tests/features/producer/searchPostDocumentReference-success.feature deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/features/producer/v2-permissions-by-pointer-type.feature b/tests/features/producer/v2-permissions-by-pointer-type.feature index 0f17a8151..cb2f51beb 100644 --- a/tests/features/producer/v2-permissions-by-pointer-type.feature +++ b/tests/features/producer/v2-permissions-by-pointer-type.feature @@ -10,7 +10,7 @@ Feature: Producer v2 permissions by pointer type - Success and Failure Scenarios Background: Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API - Scenario: Successfully create a Document Pointer (care plan) + Scenario: V2 Permissions with access for pointer type - createDocumentReference When producer v2 'RX898' creates a DocumentReference with values: | property | value | | subject | 9278693472 | @@ -52,3 +52,80 @@ Feature: Producer v2 permissions by pointer type - Success and Failure Scenarios | author | HAR1 | | url | https://example.org/my-doc.pdf | | practiceSetting | 788002001 | + + Scenario: V2 Permissions with access for pointer type - deleteDocumentReference + Given a DocumentReference resource exists with values + | property | value | + | id | RX898-111-DeleteDocRefTest1 | + | subject | 9278693472 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc.pdf | + | custodian | RX898 | + | author | RX898 | + When producer v2 'RX898' requests to delete DocumentReference with id 'RX898-111-DeleteDocRefTest1' + Then the response status code is 200 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "information", + "code": "informational", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/NRL-ResponseCode", + "code": "RESOURCE_DELETED", + "display": "Resource deleted" + } + ] + }, + "diagnostics": "The requested DocumentReference has been deleted" + } + """ + And the resource with id 'DK94-111-DeleteDocRefTest1' does not exist + + Scenario: V2 Permissions with no access for pointer type - searchDocumentReference + Given a DocumentReference resource exists with values: + | property | value | + | id | RX898-1111111111-SearchNHSDocRefTest1 | + | subject | 9999999999 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc.pdf | + | custodian | RX898 | + | author | X26 | + And a DocumentReference resource exists with values: + | property | value | + | id | SG4-1111111111-SearchNHSDocRefTest3 | + | subject | 9999999999 | + | status | current | + | type | 1363501000000100 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc.pdf | + | custodian | SG4 | + | author | X26 | + When producer v2 'RX898' searches for DocumentReferences with parameters: + | parameter | value | + | subject | 9999999999 | + Then the response status code is 200 + And the response is a searchset Bundle + And the Bundle has a total of 1 + And the Bundle has 1 entry + And the Bundle contains an DocumentReference with values + | property | value | + | id | RX898-1111111111-SearchNHSDocRefTest1 | + | subject | 9999999999 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc.pdf | + | custodian | RX898 | + | author | X26 | + And the Bundle does not contain a DocumentReference with ID 'SG4-1111111111-SearchNHSDocRefTest3' diff --git a/tests/features/steps/2_request.py b/tests/features/steps/2_request.py index f37699991..733988d83 100644 --- a/tests/features/steps/2_request.py +++ b/tests/features/steps/2_request.py @@ -221,10 +221,12 @@ def update_put_document_reference_step( @when( - "producer '{ods_code}' requests to delete DocumentReference with id '{doc_ref_id}'" + "producer {version} '{ods_code}' requests to delete DocumentReference with id '{doc_ref_id}'" ) -def delete_document_reference_step(context: Context, ods_code: str, doc_ref_id: str): - client = producer_client_from_context(context, ods_code) +def delete_document_reference_step( + context: Context, version: str, ods_code: str, doc_ref_id: str +): + client = producer_client_from_context(context, ods_code, v2=(version == "v2")) context.response = client.delete(doc_ref_id) @@ -236,9 +238,11 @@ def producer_read_document_reference_step( context.response = client.read(doc_ref_id) -@when("producer '{ods_code}' searches for DocumentReferences with parameters") -def producer_search_document_reference_step(context: Context, ods_code: str): - client = producer_client_from_context(context, ods_code) +@when("producer {version} '{ods_code}' searches for DocumentReferences with parameters") +def producer_search_document_reference_step( + context: Context, version: str, ods_code: str +): + client = producer_client_from_context(context, ods_code, v2=(version == "v2")) if not context.table: raise ValueError("No search query table provided") From dddea426a9c656b1bff1a2f25749d2ef75e4d3be Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Fri, 6 Mar 2026 14:42:00 +0000 Subject: [PATCH 6/9] NRL-1949 Tidy up client classes --- tests/features/utils/api_client.py | 14 +-- tests/utilities/api_clients.py | 131 +++++++++-------------------- 2 files changed, 44 insertions(+), 101 deletions(-) diff --git a/tests/features/utils/api_client.py b/tests/features/utils/api_client.py index 3e90966a2..3b00c5938 100644 --- a/tests/features/utils/api_client.py +++ b/tests/features/utils/api_client.py @@ -4,9 +4,7 @@ from tests.utilities.api_clients import ( ClientConfig, ConsumerTestClient, - ConsumerV2TestClient, ProducerTestClient, - ProducerV2TestClient, ) @@ -37,17 +35,9 @@ def _config_from_context(context: Context, ods_code: str): def consumer_client_from_context(context: Context, ods_code: str, v2: bool = False): client_config = _config_from_context(context, ods_code) - return ( - ConsumerV2TestClient(config=client_config) - if v2 - else ConsumerTestClient(config=client_config) - ) + return ConsumerTestClient(config=client_config, use_v2=v2) def producer_client_from_context(context: Context, ods_code: str, v2: bool = False): client_config = _config_from_context(context, ods_code) - return ( - ProducerV2TestClient(config=client_config) - if v2 - else ProducerTestClient(config=client_config) - ) + return ProducerTestClient(config=client_config, use_v2=v2) diff --git a/tests/utilities/api_clients.py b/tests/utilities/api_clients.py index 4f3b10218..85cf41d36 100644 --- a/tests/utilities/api_clients.py +++ b/tests/utilities/api_clients.py @@ -68,7 +68,7 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: class ConsumerTestClient: - def __init__(self, config: ClientConfig): + def __init__(self, config: ClientConfig, use_v2: bool = False): self.config = config self.api_url = f"{self.config.base_url}consumer{self.config.api_path}" @@ -78,17 +78,26 @@ def __init__(self, config: ClientConfig): } if self.config.client_cert: - connection_metadata = self.config.connection_metadata.model_dump( - by_alias=True - ) - client_rp_details = connection_metadata.pop("client_rp_details") - self.request_headers.update( - { - "NHSD-Connection-Metadata": json.dumps(connection_metadata), - "NHSD-Client-RP-Details": json.dumps(client_rp_details), - "NHSD-Correlation-Id": "test-correlation-id", - } - ) + if use_v2: + self.request_headers.update( + { + "NHSD-End-User-Organisation-ODS": self.config.connection_metadata.ods_code, + "NHSD-NRL-App-Id": self.config.connection_metadata.nrl_app_id, + "NHSD-Correlation-Id": "test-correlation-id", + } + ) + else: + connection_metadata = self.config.connection_metadata.model_dump( + by_alias=True + ) + client_rp_details = connection_metadata.pop("client_rp_details") + self.request_headers.update( + { + "NHSD-Connection-Metadata": json.dumps(connection_metadata), + "NHSD-Client-RP-Details": json.dumps(client_rp_details), + "NHSD-Correlation-Id": "test-correlation-id", + } + ) self.request_headers.update(self.config.custom_headers) @@ -209,41 +218,8 @@ def read_capability_statement(self) -> Response: ) -class ConsumerV2TestClient(ConsumerTestClient): - """ - Consumer test client that uses the v2 permissions model. - - Sends NHSD-End-User-Organisation-ODS and NHSD-NRL-App-Id instead of - NHSD-Connection-Metadata / NHSD-Client-RP-Details, triggering the - _use_v2_permissions_model path in the request handler. - """ - - def __init__(self, config: ClientConfig): - self.config = config - self.api_url = f"{self.config.base_url}consumer{self.config.api_path}" - - self.request_headers = { - "Authorization": f"Bearer {self.config.auth_token}", - "X-Request-Id": "test-request-id", - } - - if self.config.client_cert: - ods_code = self.config.connection_metadata.ods_code - app_id = self.config.connection_metadata.nrl_app_id - self.request_headers.update( - { - "NHSD-End-User-Organisation-ODS": ods_code, - "NHSD-NRL-App-Id": app_id, - "NHSD-Correlation-Id": "test-correlation-id", - } - ) - - self.request_headers.update(self.config.custom_headers) - - -# TODO MAKE THIS NEATER class ProducerTestClient: - def __init__(self, config: ClientConfig): + def __init__(self, config: ClientConfig, use_v2: bool = False): self.config = config self.api_url = f"{self.config.base_url}producer{self.config.api_path}" @@ -253,17 +229,26 @@ def __init__(self, config: ClientConfig): } if self.config.client_cert: - connection_metadata = self.config.connection_metadata.model_dump( - by_alias=True - ) - client_rp_details = connection_metadata.pop("client_rp_details") - self.request_headers.update( - { - "NHSD-Connection-Metadata": json.dumps(connection_metadata), - "NHSD-Client-RP-Details": json.dumps(client_rp_details), - "NHSD-Correlation-Id": "test-correlation-id", - } - ) + if use_v2: + self.request_headers.update( + { + "NHSD-End-User-Organisation-ODS": self.config.connection_metadata.ods_code, + "NHSD-NRL-App-Id": self.config.connection_metadata.nrl_app_id, + "NHSD-Correlation-Id": "test-correlation-id", + } + ) + else: + connection_metadata = self.config.connection_metadata.model_dump( + by_alias=True + ) + client_rp_details = connection_metadata.pop("client_rp_details") + self.request_headers.update( + { + "NHSD-Connection-Metadata": json.dumps(connection_metadata), + "NHSD-Client-RP-Details": json.dumps(client_rp_details), + "NHSD-Correlation-Id": "test-correlation-id", + } + ) self.request_headers.update(self.config.custom_headers) @@ -418,35 +403,3 @@ def head( headers=headers, cert=self.config.client_cert, ) - - -class ProducerV2TestClient(ProducerTestClient): - """ - Producer test client that uses the v2 permissions model. - - Sends NHSD-End-User-Organisation-ODS and NHSD-NRL-App-Id instead of - NHSD-Connection-Metadata / NHSD-Client-RP-Details, triggering the - _use_v2_permissions_model path in the request handler. - """ - - def __init__(self, config: ClientConfig): - self.config = config - self.api_url = f"{self.config.base_url}producer{self.config.api_path}" - - self.request_headers = { - "Authorization": f"Bearer {self.config.auth_token}", - "X-Request-Id": "test-request-id", - } - - if self.config.client_cert: - ods_code = self.config.connection_metadata.ods_code - app_id = self.config.connection_metadata.nrl_app_id - self.request_headers.update( - { - "NHSD-End-User-Organisation-ODS": ods_code, - "NHSD-NRL-App-Id": app_id, - "NHSD-Correlation-Id": "test-correlation-id", - } - ) - - self.request_headers.update(self.config.custom_headers) From 833e404b8dd5e2a3edadcb712db5dd08ea59a0c6 Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Fri, 6 Mar 2026 15:39:24 +0000 Subject: [PATCH 7/9] NRL-1949 Tidy up write permissions --- scripts/get_s3_permissions.py | 53 ++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/scripts/get_s3_permissions.py b/scripts/get_s3_permissions.py index 84ed11471..68e82c5de 100644 --- a/scripts/get_s3_permissions.py +++ b/scripts/get_s3_permissions.py @@ -49,7 +49,12 @@ def add_test_files(folder, file_name, local_path): json.dump(PointerTypes.list(), f) -# TODO MAKE THIS NEATER +def _write_permission_file(folder_path, ods_code, pointer_types): + folder_path.mkdir(parents=True, exist_ok=True) + with open(folder_path / f"{ods_code}.json", "w") as f: + json.dump({"types": pointer_types}, f) + + def add_feature_test_files(local_path): """Bake in v2 permissions for the feature test application so that the v2 permissions model can be proven via feature tests without @@ -57,31 +62,29 @@ def add_feature_test_files(local_path): """ print("Adding feature test v2 permissions to temporary directory...") - feature_test_app_id = "z00z-y11y-x22x" - consumer_entries = [ - ( - "RX898", - [PointerTypes.MENTAL_HEALTH_PLAN.value], - ), # http://snomed.info/sct|736253002 + permissions = { + "consumer": [ + ( + "z00z-y11y-x22x", + "RX898", + [PointerTypes.MENTAL_HEALTH_PLAN.value], + ), # http://snomed.info/sct|736253002 + ], + "producer": [ + ( + "z00z-y11y-x22x", + "RX898", + [PointerTypes.MENTAL_HEALTH_PLAN.value], + ), # http://snomed.info/sct|736253002 + ], + } + [ + _write_permission_file( + Path.joinpath(local_path, actor_type, app_id), ods_code, pointer_types + ) + for actor_type, entries in permissions.items() + for app_id, ods_code, pointer_types in entries ] - producer_entries = [ - ( - "RX898", - [PointerTypes.MENTAL_HEALTH_PLAN.value], - ), # http://snomed.info/sct|736253002 - ] - for ods_code, pointer_types in consumer_entries: - folder_path = Path.joinpath(local_path, "consumer", feature_test_app_id) - folder_path.mkdir(parents=True, exist_ok=True) - file_path = folder_path / f"{ods_code}.json" - with open(file_path, "w") as f: - json.dump({"types": pointer_types}, f) - for ods_code, pointer_types in producer_entries: - folder_path = Path.joinpath(local_path, "producer", feature_test_app_id) - folder_path.mkdir(parents=True, exist_ok=True) - file_path = folder_path / f"{ods_code}.json" - with open(file_path, "w") as f: - json.dump({"types": pointer_types}, f) def download_files(s3_client, bucket_name, local_path, file_names, folders): From 791f3a3441979fc72ee12a6eff7937f234de789c Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Fri, 6 Mar 2026 15:59:13 +0000 Subject: [PATCH 8/9] NRL-1949 Amend comments --- .../consumer/v2-permissions-by-pointer-type.feature | 7 ++----- .../producer/v2-permissions-by-pointer-type.feature | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/features/consumer/v2-permissions-by-pointer-type.feature b/tests/features/consumer/v2-permissions-by-pointer-type.feature index 3ac0e9510..7c5fe6bbd 100644 --- a/tests/features/consumer/v2-permissions-by-pointer-type.feature +++ b/tests/features/consumer/v2-permissions-by-pointer-type.feature @@ -1,10 +1,7 @@ Feature: Consumer v2 permissions by pointer type - Success and Failure Scenarios For the v2 permissions model, permissions are resolved from a JSON file stored in the - nrlf_permissions Lambda layer at the path - {actorType}/{app_id}/{ods_code}.json, which must contain a "types" array. - Permissions for the feature test application (ID 'z00z-y11y-x22x') and - ODS code 'RX898' are baked into the layer by `scripts/get_s3_permissions.py` - at build time, so no dynamic seeding step is required for + nrlf_permissions Lambda layer. Permissions for the feature tests are baked into the layer + by `scripts/get_s3_permissions.py` at build time, so no dynamic seeding step is required for success scenarios. Background: diff --git a/tests/features/producer/v2-permissions-by-pointer-type.feature b/tests/features/producer/v2-permissions-by-pointer-type.feature index cb2f51beb..080cea943 100644 --- a/tests/features/producer/v2-permissions-by-pointer-type.feature +++ b/tests/features/producer/v2-permissions-by-pointer-type.feature @@ -1,10 +1,7 @@ Feature: Producer v2 permissions by pointer type - Success and Failure Scenarios For the v2 permissions model, permissions are resolved from a JSON file stored in the - nrlf_permissions Lambda layer at the path - {actorType}/{app_id}/{ods_code}.json, which must contain a "types" array. - Permissions for the feature test application (ID 'z00z-y11y-x22x') and - ODS code 'RX898' are baked into the layer by `scripts/get_s3_permissions.py` - at build time, so no dynamic seeding step is required for + nrlf_permissions Lambda layer. Permissions for the feature tests are baked into the layer by + `scripts/get_s3_permissions.py` at build time, so no dynamic seeding step is required for success scenarios. Background: From 2caf29abf55363df601f13e18b1a7a1390f5956b Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Mon, 9 Mar 2026 12:19:47 +0000 Subject: [PATCH 9/9] NRL-1949 Address pr comments --- scripts/get_s3_permissions.py | 4 +- .../v2-permissions-by-pointer-type.feature | 79 +++++++++++++++++-- 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/scripts/get_s3_permissions.py b/scripts/get_s3_permissions.py index 68e82c5de..bbd08a66b 100644 --- a/scripts/get_s3_permissions.py +++ b/scripts/get_s3_permissions.py @@ -74,8 +74,8 @@ def add_feature_test_files(local_path): ( "z00z-y11y-x22x", "RX898", - [PointerTypes.MENTAL_HEALTH_PLAN.value], - ), # http://snomed.info/sct|736253002 + [PointerTypes.EOL_CARE_PLAN.value], + ), # http://snomed.info/sct|736373009 ], } [ diff --git a/tests/features/producer/v2-permissions-by-pointer-type.feature b/tests/features/producer/v2-permissions-by-pointer-type.feature index 080cea943..673be9d48 100644 --- a/tests/features/producer/v2-permissions-by-pointer-type.feature +++ b/tests/features/producer/v2-permissions-by-pointer-type.feature @@ -12,7 +12,7 @@ Feature: Producer v2 permissions by pointer type - Success and Failure Scenarios | property | value | | subject | 9278693472 | | status | current | - | type | 736253002 | + | type | 736373009 | | category | 734163000 | | custodian | RX898 | | author | HAR1 | @@ -40,6 +40,18 @@ Feature: Producer v2 permissions by pointer type - Success and Failure Scenarios And the response has a Location header And the Location header starts with '/DocumentReference/RX898-' And the resource in the Location header exists with values: + | property | value | + | subject | 9278693472 | + | status | current | + | type | 736373009 | + | category | 734163000 | + | custodian | RX898 | + | author | HAR1 | + | url | https://example.org/my-doc.pdf | + | practiceSetting | 788002001 | + + Scenario: V2 Permissions with no access for pointer type - createDocumentReference + When producer v2 'RX898' creates a DocumentReference with values: | property | value | | subject | 9278693472 | | status | current | @@ -49,6 +61,28 @@ Feature: Producer v2 permissions by pointer type - Success and Failure Scenarios | author | HAR1 | | url | https://example.org/my-doc.pdf | | practiceSetting | 788002001 | + Then the response status code is 403 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "forbidden", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "AUTHOR_CREDENTIALS_ERROR", + "display": "Author credentials error" + } + ] + }, + "diagnostics": "The type of the provided DocumentReference is not in the list of allowed types for this organisation", + "expression": [ + "type.coding[0].code" + ] + } + """ Scenario: V2 Permissions with access for pointer type - deleteDocumentReference Given a DocumentReference resource exists with values @@ -56,7 +90,7 @@ Feature: Producer v2 permissions by pointer type - Success and Failure Scenarios | id | RX898-111-DeleteDocRefTest1 | | subject | 9278693472 | | status | current | - | type | 736253002 | + | type | 736373009 | | category | 734163000 | | contentType | application/pdf | | url | https://example.org/my-doc.pdf | @@ -82,7 +116,7 @@ Feature: Producer v2 permissions by pointer type - Success and Failure Scenarios "diagnostics": "The requested DocumentReference has been deleted" } """ - And the resource with id 'DK94-111-DeleteDocRefTest1' does not exist + And the resource with id 'RX898-111-DeleteDocRefTest1' does not exist Scenario: V2 Permissions with no access for pointer type - searchDocumentReference Given a DocumentReference resource exists with values: @@ -90,7 +124,7 @@ Feature: Producer v2 permissions by pointer type - Success and Failure Scenarios | id | RX898-1111111111-SearchNHSDocRefTest1 | | subject | 9999999999 | | status | current | - | type | 736253002 | + | type | 736373009 | | category | 734163000 | | contentType | application/pdf | | url | https://example.org/my-doc.pdf | @@ -119,10 +153,45 @@ Feature: Producer v2 permissions by pointer type - Success and Failure Scenarios | id | RX898-1111111111-SearchNHSDocRefTest1 | | subject | 9999999999 | | status | current | - | type | 736253002 | + | type | 736373009 | | category | 734163000 | | contentType | application/pdf | | url | https://example.org/my-doc.pdf | | custodian | RX898 | | author | X26 | And the Bundle does not contain a DocumentReference with ID 'SG4-1111111111-SearchNHSDocRefTest3' + + Scenario: V2 Permissions with no access for org - searchDocumentReference + Given a DocumentReference resource exists with values: + | property | value | + | id | RX898-1111111111-SearchNHSDocRefTest1 | + | subject | 9999999999 | + | status | current | + | type | 736373009 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc.pdf | + | custodian | RX898 | + | author | X26 | + When producer v2 'N00RG1' searches for DocumentReferences with parameters: + | parameter | value | + | subject | 9999999999 | + Then the response status code is 403 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "forbidden", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "ACCESS DENIED", + "display": "Access has been denied to process this request" + } + ] + }, + "diagnostics": "Your organisation 'N00RG1' does not have permission to access this resource. Contact the onboarding team." + } + """