Skip to content

Commit bb81418

Browse files
committed
NRL-1933 add feature test framework for org and app v2 perms
1 parent bcf6bfe commit bb81418

5 files changed

Lines changed: 254 additions & 27 deletions

File tree

tests/features/producer/createDocumentReference-failure.feature

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,76 @@ Feature: Producer - createDocumentReference - Failure Scenarios
292292
}
293293
"""
294294

295+
# Credentials - type not allowed at application level (v2 permissions)
296+
Scenario: Producer lacks permissions to create specified type at the application level
297+
Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API
298+
And the application has 'producer' permissions for pointer types:
299+
| system | value |
300+
| http://snomed.info/sct | 736253002 |
301+
When producer v2 'ANGY1' creates a DocumentReference with values:
302+
| property | value |
303+
| subject | 9999999999 |
304+
| status | current |
305+
| type | 887701000000100 |
306+
| category | 734163000 |
307+
| custodian | ANGY1 |
308+
| author | HAR1 |
309+
| url | https://example.org/my-doc.pdf |
310+
Then the response status code is 403
311+
And the response is an OperationOutcome with 1 issue
312+
And the OperationOutcome contains the issue:
313+
"""
314+
{
315+
"severity": "error",
316+
"code": "forbidden",
317+
"details": {
318+
"coding": [
319+
{
320+
"system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode",
321+
"code": "ACCESS DENIED",
322+
"display": "Access has been denied to process this request"
323+
}
324+
]
325+
},
326+
"diagnostics": "Your organisation 'ANGY1' does not have permission to access this resource. Contact the onboarding team."
327+
}
328+
"""
329+
330+
# Credentials - type not allowed at org level (v2 permissions)
331+
Scenario: Producer lacks permissions to create specified type at the org level
332+
Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API
333+
And the organisation 'ANGY1' is authorised as a Producer for pointer types:
334+
| system | value |
335+
| http://snomed.info/sct | 736253002 |
336+
When producer v2 'ANGY1' creates a DocumentReference with values:
337+
| property | value |
338+
| subject | 9999999999 |
339+
| status | current |
340+
| type | 887701000000100 |
341+
| category | 734163000 |
342+
| custodian | ANGY1 |
343+
| author | HAR1 |
344+
| url | https://example.org/my-doc.pdf |
345+
Then the response status code is 403
346+
And the response is an OperationOutcome with 1 issue
347+
And the OperationOutcome contains the issue:
348+
"""
349+
{
350+
"severity": "error",
351+
"code": "forbidden",
352+
"details": {
353+
"coding": [
354+
{
355+
"system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode",
356+
"code": "ACCESS DENIED",
357+
"display": "Access has been denied to process this request"
358+
}
359+
]
360+
},
361+
"diagnostics": "Your organisation 'ANGY1' does not have permission to access this resource. Contact the onboarding team."
362+
}
363+
"""
364+
295365
Scenario: Invalid status
296366
Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API
297367
And the organisation 'X26' is authorised to access pointer types:

tests/features/producer/createDocumentReference-success.feature

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,100 @@ Feature: Producer - createDocumentReference - Success Scenarios
4747
| url | https://example.org/my-doc.pdf |
4848
| practiceSetting | 788002001 |
4949

50+
Scenario: Successfully create a Document Pointer with org-level permissions (v2 permissions model)
51+
Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API
52+
And the organisation 'ANGY1' is authorised as a Producer for pointer types:
53+
| system | value |
54+
| http://snomed.info/sct | 736253002 |
55+
When producer v2 'ANGY1' creates a DocumentReference with values:
56+
| property | value |
57+
| subject | 9278693472 |
58+
| status | current |
59+
| type | 736253002 |
60+
| category | 734163000 |
61+
| custodian | ANGY1 |
62+
| author | HAR1 |
63+
| url | https://example.org/my-doc.pdf |
64+
| practiceSetting | 788002001 |
65+
Then the response status code is 201
66+
And the response is an OperationOutcome with 1 issue
67+
And the OperationOutcome contains the issue:
68+
"""
69+
{
70+
"severity": "information",
71+
"code": "informational",
72+
"details": {
73+
"coding": [
74+
{
75+
"system": "https://fhir.nhs.uk/ValueSet/NRL-ResponseCode",
76+
"code": "RESOURCE_CREATED",
77+
"display": "Resource created"
78+
}
79+
]
80+
},
81+
"diagnostics": "The document has been created"
82+
}
83+
"""
84+
And the response has a Location header
85+
And the Location header starts with '/DocumentReference/ANGY1-'
86+
And the resource in the Location header exists with values:
87+
| property | value |
88+
| subject | 9278693472 |
89+
| status | current |
90+
| type | 736253002 |
91+
| category | 734163000 |
92+
| custodian | ANGY1 |
93+
| author | HAR1 |
94+
| url | https://example.org/my-doc.pdf |
95+
| practiceSetting | 788002001 |
96+
97+
Scenario: Successfully create a Document Pointer with app-level permissions (v2 permissions model)
98+
Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API
99+
And the application has 'producer' permissions for pointer types:
100+
| system | value |
101+
| http://snomed.info/sct | 736253002 |
102+
When producer v2 'ANGY1' creates a DocumentReference with values:
103+
| property | value |
104+
| subject | 9278693472 |
105+
| status | current |
106+
| type | 736253002 |
107+
| category | 734163000 |
108+
| custodian | ANGY1 |
109+
| author | HAR1 |
110+
| url | https://example.org/my-doc.pdf |
111+
| practiceSetting | 788002001 |
112+
Then the response status code is 201
113+
And the response is an OperationOutcome with 1 issue
114+
And the OperationOutcome contains the issue:
115+
"""
116+
{
117+
"severity": "information",
118+
"code": "informational",
119+
"details": {
120+
"coding": [
121+
{
122+
"system": "https://fhir.nhs.uk/ValueSet/NRL-ResponseCode",
123+
"code": "RESOURCE_CREATED",
124+
"display": "Resource created"
125+
}
126+
]
127+
},
128+
"diagnostics": "The document has been created"
129+
}
130+
"""
131+
And the response has a Location header
132+
And the Location header starts with '/DocumentReference/ANGY1-'
133+
And the resource in the Location header exists with values:
134+
| property | value |
135+
| subject | 9278693472 |
136+
| status | current |
137+
| type | 736253002 |
138+
| category | 734163000 |
139+
| custodian | ANGY1 |
140+
| author | HAR1 |
141+
| url | https://example.org/my-doc.pdf |
142+
| practiceSetting | 788002001 |
143+
50144
# # NRL-766 Resolve custodian suffix issues
51145
# Scenario: Successfully create a Document Pointer (care plan) with custodian suffix
52146
# Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API

tests/features/steps/1_setup.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,24 @@ def add_pointer_types(self, ods_code: str, context: Context):
2525
s3_client.put_object(Bucket=bucket, Key=key, Body=json.dumps(pointer_types))
2626
context.add_cleanup(lambda: s3_client.delete_object(Bucket=bucket, Key=key))
2727

28+
def add_v2_permissions(
29+
self, api_side: str, context: Context, ods_code: Optional[str] = None
30+
):
31+
if not context.table:
32+
raise ValueError("No permissions table provided")
33+
34+
pointer_types = [f"{system}|{value}" for system, value in context.table]
35+
perms_body = {"types": pointer_types}
36+
bucket = f"nhsd-nrlf--{context.stack_name}-authorization-store"
37+
if ods_code: # org-level permissions
38+
key = f"{api_side}/{self.app_id}/{ods_code}.json"
39+
else: # app-level permissions
40+
key = f"{api_side}/{self.app_id}.json"
41+
42+
s3_client = get_s3_client()
43+
s3_client.put_object(Bucket=bucket, Key=key, Body=json.dumps(pointer_types))
44+
context.add_cleanup(lambda: s3_client.delete_object(Bucket=bucket, Key=key))
45+
2846

2947
@given("the application '{app_name}' (ID '{app_id}') is registered to access the API")
3048
def register_application_step(context: Context, app_name: str, app_id: str):
@@ -39,6 +57,31 @@ def register_org_permissions_step(context: Context, ods_code: str):
3957
context.application.add_pointer_types(ods_code, context)
4058

4159

60+
@given("the organisation '{ods_code}' is authorised as a Producer for pointer types")
61+
def register_v2_producer_org_permissions_step(context: Context, ods_code: str):
62+
if not context.table:
63+
raise ValueError("No permissions table provided")
64+
65+
context.application.add_v2_permissions("producer", context, ods_code)
66+
67+
68+
@given("the organisation '{ods_code}' is authorised as a Consumer for pointer types")
69+
def register_v2_consumer_org_permissions_step(context: Context, ods_code: str):
70+
if not context.table:
71+
raise ValueError("No permissions table provided")
72+
73+
context.application.add_v2_permissions("consumer", context, ods_code)
74+
75+
76+
@given("the application has '{use_type}' permissions for pointer types")
77+
def register_app_level_permissions_step(context: Context, use_type: str):
78+
if not context.table:
79+
raise ValueError("No permissions table provided")
80+
if use_type not in {"producer", "consumer"}:
81+
raise ValueError("Producer or Consumer side must be specified")
82+
context.application.add_v2_permissions(use_type, context)
83+
84+
4285
@given("a DocumentReference resource exists with values")
4386
def create_document_reference_step(context: Context):
4487
if not context.table:

tests/features/steps/2_request.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,11 @@ def consumer_read_document_reference_step(
7676
context.response = client.read(doc_ref_id)
7777

7878

79-
@when("producer '{ods_code}' creates a DocumentReference with values")
80-
def create_post_document_reference_step(context: Context, ods_code: str):
81-
client = producer_client_from_context(context, ods_code)
79+
@when(
80+
"producer {version} '{ods_code}' using v2 permissioning creates a DocumentReference with values"
81+
)
82+
def create_post_document_reference_step(context: Context, version: str, ods_code: str):
83+
client = producer_client_from_context(context, ods_code, v2=(version == "v2"))
8284

8385
if not context.table:
8486
raise ValueError("No document reference data table provided")

tests/utilities/api_clients.py

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
6868

6969
class ConsumerTestClient:
7070

71-
def __init__(self, config: ClientConfig):
71+
def __init__(self, config: ClientConfig, use_v2: bool = False):
7272
self.config = config
7373
self.api_url = f"{self.config.base_url}consumer{self.config.api_path}"
7474

@@ -78,17 +78,26 @@ def __init__(self, config: ClientConfig):
7878
}
7979

8080
if self.config.client_cert:
81-
connection_metadata = self.config.connection_metadata.model_dump(
82-
by_alias=True
83-
)
84-
client_rp_details = connection_metadata.pop("client_rp_details")
85-
self.request_headers.update(
86-
{
87-
"NHSD-Connection-Metadata": json.dumps(connection_metadata),
88-
"NHSD-Client-RP-Details": json.dumps(client_rp_details),
89-
"NHSD-Correlation-Id": "test-correlation-id",
90-
}
91-
)
81+
if use_v2:
82+
self.request_headers.update(
83+
{
84+
"NHSD-End-User-Organisation-ODS": self.config.connection_metadata.ods_code,
85+
"NHSD-NRL-App-Id": self.config.connection_metadata.nrl_app_id,
86+
"NHSD-Correlation-Id": "test-correlation-id",
87+
}
88+
)
89+
else:
90+
connection_metadata = self.config.connection_metadata.model_dump(
91+
by_alias=True
92+
)
93+
client_rp_details = connection_metadata.pop("client_rp_details")
94+
self.request_headers.update(
95+
{
96+
"NHSD-Connection-Metadata": json.dumps(connection_metadata),
97+
"NHSD-Client-RP-Details": json.dumps(client_rp_details),
98+
"NHSD-Correlation-Id": "test-correlation-id",
99+
}
100+
)
92101

93102
self.request_headers.update(self.config.custom_headers)
94103

@@ -210,7 +219,7 @@ def read_capability_statement(self) -> Response:
210219

211220

212221
class ProducerTestClient:
213-
def __init__(self, config: ClientConfig):
222+
def __init__(self, config: ClientConfig, use_v2: bool = False):
214223
self.config = config
215224
self.api_url = f"{self.config.base_url}producer{self.config.api_path}"
216225

@@ -220,17 +229,26 @@ def __init__(self, config: ClientConfig):
220229
}
221230

222231
if self.config.client_cert:
223-
connection_metadata = self.config.connection_metadata.model_dump(
224-
by_alias=True
225-
)
226-
client_rp_details = connection_metadata.pop("client_rp_details")
227-
self.request_headers.update(
228-
{
229-
"NHSD-Connection-Metadata": json.dumps(connection_metadata),
230-
"NHSD-Client-RP-Details": json.dumps(client_rp_details),
231-
"NHSD-Correlation-Id": "test-correlation-id",
232-
}
233-
)
232+
if use_v2:
233+
self.request_headers.update(
234+
{
235+
"NHSD-End-User-Organisation-ODS": self.config.connection_metadata.ods_code,
236+
"NHSD-NRL-App-Id": self.config.connection_metadata.nrl_app_id,
237+
"NHSD-Correlation-Id": "test-correlation-id",
238+
}
239+
)
240+
else:
241+
connection_metadata = self.config.connection_metadata.model_dump(
242+
by_alias=True
243+
)
244+
client_rp_details = connection_metadata.pop("client_rp_details")
245+
self.request_headers.update(
246+
{
247+
"NHSD-Connection-Metadata": json.dumps(connection_metadata),
248+
"NHSD-Client-RP-Details": json.dumps(client_rp_details),
249+
"NHSD-Correlation-Id": "test-correlation-id",
250+
}
251+
)
234252

235253
self.request_headers.update(self.config.custom_headers)
236254

0 commit comments

Comments
 (0)