diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0dadcff6c..f53c9da90 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,7 +49,7 @@ repos: - "--exclude=.git,__pycache__,dist,.venv,tests" - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 23.12.1 hooks: - id: black language_version: python3 diff --git a/feature_tests/domain/features/device.failure.feature b/feature_tests/domain/features/device.failure.feature index 7a645f420..9c4026105 100644 --- a/feature_tests/domain/features/device.failure.feature +++ b/feature_tests/domain/features/device.failure.feature @@ -1,25 +1,11 @@ Feature: Device Failure Scenarios - Scenario: Device ID is not valid - Given Product Teams - | id | name | ods_code | - | 00702d39-e65f-49f5-b9ef-6570245bfe17 | My Product Team | H8S7A | - When Product Team "00702d39-e65f-49f5-b9ef-6570245bfe17" creates a Device with - | property | value | - | id | not_a_valid_id | - | name | My Device | - | type | product | - Then the operation is not successful - And the error is ValidationError on fields - | Device.id | - Scenario: Device name is not valid Given Product Teams | id | name | ods_code | | 00702d39-e65f-49f5-b9ef-6570245bfe17 | My Product Team | H8S7A | When Product Team "00702d39-e65f-49f5-b9ef-6570245bfe17" creates a Device with | property | value | - | id | XXX-YYY | | name | My Device 🚀 | | type | product | Then the operation is not successful @@ -32,7 +18,6 @@ Feature: Device Failure Scenarios | 00702d39-e65f-49f5-b9ef-6570245bfe17 | My Product Team | H8S7A | When Product Team "00702d39-e65f-49f5-b9ef-6570245bfe17" creates a Device with | property | value | - | id | XXX-YYY | | name | My Device | | type | not_a_type | Then the operation is not successful @@ -45,12 +30,11 @@ Feature: Device Failure Scenarios | 00702d39-e65f-49f5-b9ef-6570245bfe17 | My Product Team | H8S7A | When Product Team "00702d39-e65f-49f5-b9ef-6570245bfe17" creates a Device with | property | value | - | id | not_an_id | | name | My Device 🚀 | | type | not_a_type | Then the operation is not successful And the error is ValidationError on fields - | Device.id | Device.name | Device.type | + | Device.name | Device.type | Scenario: Invalid product key types Given Product Teams @@ -58,7 +42,6 @@ Feature: Device Failure Scenarios | 00702d39-e65f-49f5-b9ef-6570245bfe17 | My Product Team | H8S7A | When Product Team "00702d39-e65f-49f5-b9ef-6570245bfe17" creates a Device with | property | value | - | id | XXX-YYY | | name | My Product | | type | product | | keys.0.key | AAA-CCC-DDD | @@ -73,7 +56,6 @@ Feature: Device Failure Scenarios | 00702d39-e65f-49f5-b9ef-6570245bfe17 | My Product Team | H8S7A | When Product Team "00702d39-e65f-49f5-b9ef-6570245bfe17" creates a Device with | property | value | - | id | XXX-YYY | | name | My Product | | type | product | | keys.0.key | not_a_valid_product_id | @@ -88,12 +70,11 @@ Feature: Device Failure Scenarios | 00702d39-e65f-49f5-b9ef-6570245bfe17 | My Product Team | H8S7A | When Product Team "00702d39-e65f-49f5-b9ef-6570245bfe17" creates a Device with | property | value | - | id | XXX-YYY | | name | My Product | | type | product | - | keys.0.key | AAA-CCC | + | keys.0.key | P.AAA-CCC | | keys.0.type | product_id | - | keys.1.key | AAA-CCC | + | keys.1.key | P.AAA-CCC | | keys.1.type | product_id | Then the operation is not successful And the error is DuplicateError diff --git a/feature_tests/domain/features/device.success.feature b/feature_tests/domain/features/device.success.feature index cde230441..1873cb23d 100644 --- a/feature_tests/domain/features/device.success.feature +++ b/feature_tests/domain/features/device.success.feature @@ -6,13 +6,11 @@ Feature: Device Success Scenarios | 00702d39-e65f-49f5-b9ef-6570245bfe17 | My Product Team | H8S7A | When Product Team "00702d39-e65f-49f5-b9ef-6570245bfe17" creates a Device with: | property | value | - | id | | | name | | | type | | Then the operation is successful And the result is a Device with | property | value | - | id | | | name | | | type | | | status | active | @@ -22,15 +20,14 @@ Feature: Device Success Scenarios | DeviceCreatedEvent | And event #1 of the result is DeviceCreatedEvent with | property | value | - | id | | | name | | | type | | | status | active | | ods_code | H8S7A | Examples: - | id | name | type | - | XXX-YYY | My Product | product | + | name | type | + | My Product | product | # | XXX-YYY | My API | service | # | XXX-YYY | My Service | api | @@ -40,25 +37,23 @@ Feature: Device Success Scenarios | 00702d39-e65f-49f5-b9ef-6570245bfe17 | My Product Team | H8S7A | When Product Team "00702d39-e65f-49f5-b9ef-6570245bfe17" creates a Device with: | property | value | - | id | XXX-YYY | | name | My Product | | type | product | - | keys.0.key | AAA-CCC | + | keys.0.key | P.AAA-CCC | | keys.0.type | product_id | | keys.1.key | 12345 | | keys.1.type | accredited_system_id | Then the operation is successful And the result is a Device with - | property | value | - | id | XXX-YYY | - | name | My Product | - | type | product | - | status | active | - | ods_code | H8S7A | - | keys.AAA-CCC.key | AAA-CCC | - | keys.AAA-CCC.type | product_id | - | keys.12345.key | 12345 | - | keys.12345.type | accredited_system_id | + | property | value | + | name | My Product | + | type | product | + | status | active | + | ods_code | H8S7A | + | keys.P#DOT#AAA-CCC.key | P.AAA-CCC | + | keys.P#DOT#AAA-CCC.type | product_id | + | keys.12345.key | 12345 | + | keys.12345.type | accredited_system_id | And the following events were raised for the result | event | | DeviceCreatedEvent | @@ -66,14 +61,13 @@ Feature: Device Success Scenarios | DeviceKeyCreatedEvent | And event #1 of the result is DeviceCreatedEvent with | property | value | - | id | XXX-YYY | | name | My Product | | type | product | | status | active | | ods_code | H8S7A | And event #2 of the result is DeviceKeyAddedEvent with | property | value | - | key | AAA-CCC | + | key | P.AAA-CCC | | type | product_id | And event #3 of the result is DeviceKeyAddedEvent with | property | value | diff --git a/feature_tests/domain/steps/common.py b/feature_tests/domain/steps/common.py index d89cd8817..36397c6e5 100644 --- a/feature_tests/domain/steps/common.py +++ b/feature_tests/domain/steps/common.py @@ -114,6 +114,7 @@ def _read_value(obj, path: list[str]) -> any: ) head, *tail = path + head = head.replace("#DOT#", ".") if isinstance(obj, dict): obj = obj[head] else: diff --git a/feature_tests/end_to_end/features/createDevice.failure.feature b/feature_tests/end_to_end/features/createDevice.failure.feature index 01c0f01ce..a183577f9 100644 --- a/feature_tests/end_to_end/features/createDevice.failure.feature +++ b/feature_tests/end_to_end/features/createDevice.failure.feature @@ -7,49 +7,6 @@ Feature: Create Device - failure scenarios | version | 1 | | Authorization | letmein | - Scenario: Cannot create a Device that already exists - Given I have already made a "POST" request with "default" headers to "Organization" with body: - | path | value | - | resourceType | Organization | - | identifier.0.system | connecting-party-manager/product-team-id | - | identifier.0.value | ${ uuid(1) } | - | name | My Great Product Team | - | partOf.identifier.system | https://directory.spineservices.nhs.uk/ORD/2-0-0/organisations | - | partOf.identifier.value | F5H1R | - And I have already made a "POST" request with "default" headers to "Device" with body: - | path | value | - | resourceType | Device | - | deviceName.0.name | My Device of type "product" | - | deviceName.0.type | user-friendly-name | - | definition.identifier.system | connecting-party-manager/device-type | - | definition.identifier.value | product | - | identifier.0.system | connecting-party-manager/product_id | - | identifier.0.value | XXX-YYY | - | owner.identifier.system | connecting-party-manager/product-team-id | - | owner.identifier.value | ${ uuid(1) } | - When I make a "POST" request with "default" headers to "Device" with body: - | path | value | - | resourceType | Device | - | deviceName.0.name | My Device of type "product" | - | deviceName.0.type | user-friendly-name | - | definition.identifier.system | connecting-party-manager/device-type | - | definition.identifier.value | product | - | identifier.0.system | connecting-party-manager/product_id | - | identifier.0.value | XXX-YYY | - | owner.identifier.system | connecting-party-manager/product-team-id | - | owner.identifier.value | ${ uuid(1) } | - Then I receive a status code "400" with body - | path | value | - | resourceType | OperationOutcome | - | id | << ignore >> | - | meta.profile.0 | https://fhir.nhs.uk/StructureDefinition/NHSDigital-OperationOutcome | - | issue.0.severity | error | - | issue.0.code | processing | - | issue.0.details.coding.0.system | https://fhir.nhs.uk/StructureDefinition/NHSDigital-OperationOutcome | - | issue.0.details.coding.0.code | VALIDATION_ERROR | - | issue.0.details.coding.0.display | Validation error | - | issue.0.diagnostics | Item already exists | - Scenario: Cannot create a Device with an Device that is missing fields (no owner.identifier.value) Given I have already made a "POST" request with "default" headers to "Organization" with body: | path | value | @@ -66,8 +23,6 @@ Feature: Create Device - failure scenarios | deviceName.0.type | user-friendly-name | | definition.identifier.system | connecting-party-manager/device-type | | definition.identifier.value | product | - | identifier.0.system | connecting-party-manager/product_id | - | identifier.0.value | XXX-YYY | | owner.identifier.system | connecting-party-manager/product-team-id | Then I receive a status code "400" with body | path | value | @@ -102,8 +57,6 @@ Feature: Create Device - failure scenarios | deviceName.0.type | user-friendly-name | | definition.identifier.system | connecting-party-manager/device-type | | definition.identifier.value | product | - | identifier.0.system | connecting-party-manager/product_id | - | identifier.0.value | XXX-YYY | | owner.identifier | connecting-party-manager/product-team-id | Then I receive a status code "400" with body | path | value | @@ -129,43 +82,6 @@ Feature: Create Device - failure scenarios | Content-Type | application/json | | Content-Length | 806 | - Scenario: Cannot create a Device with an invalid ID - Given I have already made a "POST" request with "default" headers to "Organization" with body: - | path | value | - | resourceType | Organization | - | identifier.0.system | connecting-party-manager/product-team-id | - | identifier.0.value | ${ uuid(1) } | - | name | My Great Product Team | - | partOf.identifier.system | https://directory.spineservices.nhs.uk/ORD/2-0-0/organisations | - | partOf.identifier.value | F5H1R | - When I make a "POST" request with "default" headers to "Device" with body: - | path | value | - | resourceType | Device | - | deviceName.0.name | My Device of type "product" | - | deviceName.0.type | user-friendly-name | - | definition.identifier.system | connecting-party-manager/device-type | - | definition.identifier.value | product | - | identifier.0.system | connecting-party-manager/product_id | - | identifier.0.value | not_a_valid_id | - | owner.identifier.system | connecting-party-manager/product-team-id | - | owner.identifier.value | ${ uuid(1) } | - Then I receive a status code "400" with body - | path | value | - | resourceType | OperationOutcome | - | id | << ignore >> | - | meta.profile.0 | https://fhir.nhs.uk/StructureDefinition/NHSDigital-OperationOutcome | - | issue.0.severity | error | - | issue.0.code | processing | - | issue.0.details.coding.0.system | https://fhir.nhs.uk/StructureDefinition/NHSDigital-OperationOutcome | - | issue.0.details.coding.0.code | VALIDATION_ERROR | - | issue.0.details.coding.0.display | Validation error | - | issue.0.diagnostics | Key 'not_a_valid_id' does not match the expected pattern '^[ACDEFGHJKLMNPRTUVWXY34679]{3}-[ACDEFGHJKLMNPRTUVWXY34679]{3}${ dollar() }' associated with key type 'product_id' | - | issue.0.expression.0 | Device.identifier.0 | - And the response headers contain: - | name | value | - | Content-Type | application/json | - | Content-Length | 630 | - Scenario: Cannot create a Device with an invalid name Given I have already made a "POST" request with "default" headers to "Organization" with body: | path | value | @@ -182,8 +98,6 @@ Feature: Create Device - failure scenarios | deviceName.0.type | user-friendly-name | | definition.identifier.system | connecting-party-manager/device-type | | definition.identifier.value | product | - | identifier.0.system | connecting-party-manager/product_id | - | identifier.0.value | XXX-YYY | | owner.identifier.system | connecting-party-manager/product-team-id | | owner.identifier.value | ${ uuid(1) } | Then I receive a status code "400" with body @@ -219,8 +133,6 @@ Feature: Create Device - failure scenarios | deviceName.0.type | not_a_type | | definition.identifier.system | connecting-party-manager/device-type | | definition.identifier.value | product | - | identifier.0.system | connecting-party-manager/product_id | - | identifier.0.value | XXX-YYY | | owner.identifier.system | connecting-party-manager/product-team-id | | owner.identifier.value | ${ uuid(1) } | Then I receive a status code "400" with body @@ -256,8 +168,6 @@ Feature: Create Device - failure scenarios | deviceName.0.type | user-friendly-name | | definition.identifier.system | connecting-party-manager/device-type | | definition.identifier.value | not_a_type | - | identifier.0.system | connecting-party-manager/product_id | - | identifier.0.value | XXX-YYY | | owner.identifier.system | connecting-party-manager/product-team-id | | owner.identifier.value | ${ uuid(1) } | Then I receive a status code "400" with body @@ -294,7 +204,7 @@ Feature: Create Device - failure scenarios | definition.identifier.system | connecting-party-manager/device-type | | definition.identifier.value | product | | identifier.0.system | not_a_key_type | - | identifier.0.value | XXX-YYY | + | identifier.0.value | P.XXX-YYY | | owner.identifier.system | connecting-party-manager/product-team-id | | owner.identifier.value | ${ uuid(1) } | Then I receive a status code "400" with body @@ -335,21 +245,21 @@ Feature: Create Device - failure scenarios | owner.identifier.system | connecting-party-manager/product-team-id | | owner.identifier.value | ${ uuid(1) } | Then I receive a status code "400" with body - | path | value | - | resourceType | OperationOutcome | - | id | << ignore >> | - | meta.profile.0 | https://fhir.nhs.uk/StructureDefinition/NHSDigital-OperationOutcome | - | issue.0.severity | error | - | issue.0.code | processing | - | issue.0.details.coding.0.system | https://fhir.nhs.uk/StructureDefinition/NHSDigital-OperationOutcome | - | issue.0.details.coding.0.code | VALIDATION_ERROR | - | issue.0.details.coding.0.display | Validation error | - | issue.0.diagnostics | Key 'not_a_valid_product_id' does not match the expected pattern '^[ACDEFGHJKLMNPRTUVWXY34679]{3}-[ACDEFGHJKLMNPRTUVWXY34679]{3}${ dollar() }' associated with key type 'product_id' | - | issue.0.expression.0 | Device.identifier.0 | + | path | value | + | resourceType | OperationOutcome | + | id | << ignore >> | + | meta.profile.0 | https://fhir.nhs.uk/StructureDefinition/NHSDigital-OperationOutcome | + | issue.0.severity | error | + | issue.0.code | processing | + | issue.0.details.coding.0.system | https://fhir.nhs.uk/StructureDefinition/NHSDigital-OperationOutcome | + | issue.0.details.coding.0.code | VALIDATION_ERROR | + | issue.0.details.coding.0.display | Validation error | + | issue.0.diagnostics | Key 'not_a_valid_product_id' does not match the expected pattern '^P\\.[ACDEFGHJKLMNPRTUVWXY34679]{3}-[ACDEFGHJKLMNPRTUVWXY34679]{3}${ dollar() }' associated with key type 'product_id' | + | issue.0.expression.0 | Device.identifier.0 | And the response headers contain: | name | value | | Content-Type | application/json | - | Content-Length | 638 | + | Content-Length | 642 | Scenario: Cannot create a Device with a repeated key Given I have already made a "POST" request with "default" headers to "Organization" with body: @@ -368,9 +278,9 @@ Feature: Create Device - failure scenarios | definition.identifier.system | connecting-party-manager/device-type | | definition.identifier.value | product | | identifier.0.system | connecting-party-manager/product_id | - | identifier.0.value | XXX-YYY | + | identifier.0.value | P.XXX-YYY | | identifier.1.system | connecting-party-manager/product_id | - | identifier.1.value | XXX-YYY | + | identifier.1.value | P.XXX-YYY | | owner.identifier.system | connecting-party-manager/product-team-id | | owner.identifier.value | ${ uuid(1) } | Then I receive a status code "400" with body @@ -427,8 +337,6 @@ Feature: Create Device - failure scenarios | deviceName.0.type | user-friendly-name | | definition.identifier.system | connecting-party-manager/device-type | | definition.identifier.value | product | - | identifier.0.system | connecting-party-manager/product_id | - | identifier.0.value | XXX-YYY | | owner.identifier.system | connecting-party-manager/product-team-id | | owner.identifier.value | ${ uuid(1) } | Then I receive a status code "404" with body diff --git a/feature_tests/end_to_end/features/createDevice.success.feature b/feature_tests/end_to_end/features/createDevice.success.feature index f227a73c6..90cb00b6f 100644 --- a/feature_tests/end_to_end/features/createDevice.success.feature +++ b/feature_tests/end_to_end/features/createDevice.success.feature @@ -23,8 +23,6 @@ Feature: Create Device - success scenarios | deviceName.0.type | user-friendly-name | | definition.identifier.system | connecting-party-manager/device-type | | definition.identifier.value | | - | identifier.0.system | connecting-party-manager/product_id | - | identifier.0.value | XXX-YYY | | owner.identifier.system | connecting-party-manager/product-team-id | | owner.identifier.value | ${ uuid(1) } | Then I receive a status code "201" with body @@ -42,16 +40,17 @@ Feature: Create Device - success scenarios | name | value | | Content-Type | application/json | | Content-Length | 456 | - When I make a "GET" request with "default" headers to "Device/XXX-YYY" + When I make a "GET" request with "default" headers to the id in the location response header to the Device endpoint Then I receive a status code "200" with body | path | value | | resourceType | Device | + | id | << ignore >> | | deviceName.0.name | My Device of type "" | | deviceName.0.type | user-friendly-name | | definition.identifier.system | connecting-party-manager/device-type | | definition.identifier.value | | | identifier.0.system | connecting-party-manager/product_id | - | identifier.0.value | XXX-YYY | + | identifier.0.value | << ignore >> | | owner.identifier.system | connecting-party-manager/product-team-id | | owner.identifier.value | ${ uuid(1) } | diff --git a/feature_tests/end_to_end/features/readDevice.success.feature b/feature_tests/end_to_end/features/readDevice.success.feature index a2d2bc9ed..e2a1f69ce 100644 --- a/feature_tests/end_to_end/features/readDevice.success.feature +++ b/feature_tests/end_to_end/features/readDevice.success.feature @@ -23,23 +23,22 @@ Feature: Read Device - success scenarios | deviceName.0.type | user-friendly-name | | definition.identifier.system | connecting-party-manager/device-type | | definition.identifier.value | product | - | identifier.0.system | connecting-party-manager/product_id | - | identifier.0.value | XXX-YYY | | owner.identifier.system | connecting-party-manager/product-team-id | | owner.identifier.value | ${ uuid(1) } | - When I make a "GET" request with "default" headers to "Device/XXX-YYY" + When I make a "GET" request with "default" headers to the id in the location response header to the Device endpoint Then I receive a status code "200" with body | path | value | | resourceType | Device | + | id | << ignore >> | | deviceName.0.name | My Device of type "product" | | deviceName.0.type | user-friendly-name | | definition.identifier.system | connecting-party-manager/device-type | | definition.identifier.value | product | | identifier.0.system | connecting-party-manager/product_id | - | identifier.0.value | XXX-YYY | + | identifier.0.value | << ignore >> | | owner.identifier.system | connecting-party-manager/product-team-id | | owner.identifier.value | ${ uuid(1) } | And the response headers contain: | name | value | | Content-Type | application/json | - | Content-Length | 434 | + | Content-Length | 436 | diff --git a/feature_tests/end_to_end/steps/assertion.py b/feature_tests/end_to_end/steps/assertion.py index b3a84bd3b..5a8fb5c22 100644 --- a/feature_tests/end_to_end/steps/assertion.py +++ b/feature_tests/end_to_end/steps/assertion.py @@ -20,6 +20,18 @@ def _pop_ignore(expected: dict, received: dict): received_value = received.get(key) if isinstance(expected_value, dict) and isinstance(received_value, dict): _pop_ignore(expected=expected_value, received=received_value) + if isinstance(received_value, list) and isinstance(expected_value, list): + for a, b in zip(received_value, expected_value): + if not isinstance(a, dict) and not isinstance(b, dict): + continue + _pop_ignore(expected=b, received=a) + + +def _fix_backslashes(json_data: dict): + if "issue" in json_data and isinstance(json_data["issue"], list): + for issue in json_data["issue"]: + if "diagnostics" in issue and isinstance(issue["diagnostics"], str): + issue["diagnostics"] = issue["diagnostics"].replace("\\\\", "\\") def stringify(item) -> str: @@ -39,6 +51,7 @@ def assert_same_type(expected, received, label=""): def assert_equal(expected, received, label=""): if isinstance(expected, dict): + _fix_backslashes(json_data=expected) _pop_ignore(expected=expected, received=received) assert expected == received, error_message( expected, "does not equal", received, label=label diff --git a/feature_tests/end_to_end/steps/steps.py b/feature_tests/end_to_end/steps/steps.py index b76e94388..61e9c0ae1 100644 --- a/feature_tests/end_to_end/steps/steps.py +++ b/feature_tests/end_to_end/steps/steps.py @@ -27,7 +27,7 @@ def given_made_request( context: Context, http_method: str, header_name: str, endpoint: str ): body = parse_table(table=context.table) - response = make_request( + context.response = make_request( base_url=context.base_url, http_method=http_method, endpoint=endpoint, @@ -36,7 +36,11 @@ def given_made_request( raise_for_status=True, ) context.postman_step.request = PostmanRequest( - url=Url(raw=response.url, host=[context.base_url.rstrip("/")], path=[endpoint]), + url=Url( + raw=context.response.url, + host=[context.base_url.rstrip("/")], + path=[endpoint], + ), method=http_method, header=[ HeaderItem(key=k, value=v) for k, v in context.headers[header_name].items() @@ -51,7 +55,7 @@ def given_made_request( def given_made_request( context: Context, http_method: str, header_name: str, endpoint: str ): - response = make_request( + context.response = make_request( base_url=context.base_url, http_method=http_method, endpoint=endpoint, @@ -59,7 +63,11 @@ def given_made_request( raise_for_status=True, ) context.postman_step.request = PostmanRequest( - url=Url(raw=response.url, host=[context.base_url.rstrip("/")], path=[endpoint]), + url=Url( + raw=context.response.url, + host=[context.base_url.rstrip("/")], + path=[endpoint], + ), method=http_method, header=[ HeaderItem(key=k, value=v) for k, v in context.headers[header_name].items() @@ -118,6 +126,30 @@ def when_make_request( ) +@when( + 'I make a "{http_method}" request with "{header_name}" headers to the id in the location response header to the Device endpoint' +) +def when_make_device_request(context: Context, http_method: str, header_name: str): + endpoint = f"Device/{context.response.headers.get('Location')}" + context.response = make_request( + base_url=context.base_url, + http_method=http_method, + endpoint=endpoint, + headers=context.headers[header_name], + ) + context.postman_step.request = PostmanRequest( + url=Url( + raw=context.response.url, + host=[context.base_url.rstrip("/")], + path=[endpoint], + ), + method=http_method, + header=[ + HeaderItem(key=k, value=v) for k, v in context.headers[header_name].items() + ], + ) + + @then('I receive a status code "{status_code}" with body') def then_response(context: Context, status_code: str): expected_body = parse_table(table=context.table) @@ -131,22 +163,16 @@ def then_response(context: Context, status_code: str): assert_equal, assert_same_type, assert_equal, - assert_is_subset, - assert_is_subset, ), expected=( int(status_code), expected_body, expected_body, - expected_body, - response_body, ), received=( context.response.status_code, response_body, response_body, - response_body, - expected_body, ), ) diff --git a/feature_tests/end_to_end/steps/tests/test_mock_requests.py b/feature_tests/end_to_end/steps/tests/test_mock_requests.py index c1cf32c74..111294a24 100644 --- a/feature_tests/end_to_end/steps/tests/test_mock_requests.py +++ b/feature_tests/end_to_end/steps/tests/test_mock_requests.py @@ -33,6 +33,7 @@ def test__mock_requests(): "Content-Length": str(len(response_body)), "Content-Type": "application/json", "Version": "1", + "Location": None, }, "status_code": 200, "reason": "OK", diff --git a/pyproject.toml b/pyproject.toml index 29a5d2530..6c011223d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ email-validator = "^2.1.0.post1" [tool.poetry.group.dev.dependencies] pre-commit = "^3.4.0" -black = "^23.9.1" +black = "^23.12.1" flake8 = "^6.1.0" behave = "^1.2.6" pytest = "^7.4.2" diff --git a/src/api/createDevice/index.py b/src/api/createDevice/index.py index d77934e53..b3084bdc8 100644 --- a/src/api/createDevice/index.py +++ b/src/api/createDevice/index.py @@ -1,7 +1,17 @@ -from event.api_step_chain import execute_step_chain +from types import ModuleType + from event.aws.client import dynamodb_client from event.environment import BaseEnvironment from event.logging.logger import setup_logger +from event.logging.step_decorators import logging_step_decorators +from event.response.steps import response_steps +from event.step_chain import StepChain +from event.versioning.constants import VERSIONING_STEP_ARGS +from event.versioning.steps import ( + get_largest_possible_version, + get_steps_for_requested_version, + versioning_steps, +) from .src.v1.steps import steps as v1_steps @@ -24,3 +34,46 @@ def handler(event: dict, context=None): cache=cache, versioned_steps=versioned_steps, ) + + +STEP_DECORATORS = [*logging_step_decorators] + + +def lower_case_keys(_dict: dict[str, str]): + return {k.lower(): v for k, v in _dict.items()} + + +def execute_step_chain( + event: dict, cache: dict, versioned_steps: dict[str, ModuleType] +): + event["headers"] = lower_case_keys(event.get("headers", {})) + + version_chain = StepChain( + step_chain=versioning_steps, step_decorators=STEP_DECORATORS + ) + version_chain.run( + init={ + VERSIONING_STEP_ARGS.EVENT: event, + VERSIONING_STEP_ARGS.VERSIONED_STEPS: versioned_steps, + } + ) + + version = None + location = None + if isinstance(version_chain.result, Exception): + result = version_chain.result + else: + version = version_chain.data[get_largest_possible_version] + steps = version_chain.data[get_steps_for_requested_version] + api_chain = StepChain(step_chain=steps, step_decorators=STEP_DECORATORS) + api_chain.run(cache=cache, init=event) + if isinstance(api_chain.result, Exception): + result = api_chain.result + else: + result, location = api_chain.result + + response_chain = StepChain( + step_chain=response_steps, step_decorators=STEP_DECORATORS + ) + response_chain.run(init=(result, version, location)) + return response_chain.result diff --git a/src/api/createDevice/src/v1/steps.py b/src/api/createDevice/src/v1/steps.py index e969909bf..706451caf 100644 --- a/src/api/createDevice/src/v1/steps.py +++ b/src/api/createDevice/src/v1/steps.py @@ -2,7 +2,10 @@ from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEvent from domain.core.device import Device +from domain.core.device_id import generate_device_key +from domain.core.device_key import DeviceKeyType from domain.core.product_team import ProductTeam +from domain.fhir.r4.cpm_model import SYSTEM from domain.fhir.r4.cpm_model import Device as FhirDevice from domain.fhir_translation.device import ( create_domain_device_from_fhir_device, @@ -23,6 +26,14 @@ def parse_event_body(data, cache) -> dict: def parse_fhir_device(data, cache) -> FhirDevice: json_body = data[parse_event_body] + identifier = json_body.get("identifier", []) + identifier.append( + dict( + system=f"{SYSTEM}/{DeviceKeyType.PRODUCT_ID}", + value=generate_device_key(DeviceKeyType.PRODUCT_ID), + ) + ) + json_body["identifier"] = identifier fhir_device = parse_fhir_device_json(fhir_device_json=json_body) return fhir_device @@ -53,7 +64,8 @@ def save_device(data, cache) -> dict: def set_http_status(data, cache) -> HTTPStatus: - return HTTPStatus.CREATED + device: Device = data[create_device] + return HTTPStatus.CREATED, str(device.id) steps = [ diff --git a/src/api/createDevice/tests/test_index.py b/src/api/createDevice/tests/test_index.py index 026b06130..e8633dece 100644 --- a/src/api/createDevice/tests/test_index.py +++ b/src/api/createDevice/tests/test_index.py @@ -8,6 +8,7 @@ from nhs_context_logging import app_logger from test_helpers.dynamodb import mock_table +from test_helpers.response_assertions import _response_assertions from test_helpers.sample_data import DEVICE TABLE_NAME = "hiya" @@ -73,15 +74,19 @@ def test_index(version): ], } ) - assert result == { + expected = { "statusCode": 201, "body": expected_body, "headers": { "Content-Length": str(len(expected_body)), "Content-Type": "application/json", "Version": version, + "Location": "FOO", }, } + _response_assertions( + result=result, expected=expected, check_body=True, check_content_length=True + ) @pytest.mark.parametrize( @@ -160,21 +165,6 @@ def test_index_bad_payload(version): "diagnostics": "field required", "expression": ["Device.definition"], }, - { - "severity": "error", - "code": "processing", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/StructureDefinition/NHSDigital-OperationOutcome", - "code": "MISSING_VALUE", - "display": "Missing value", - } - ] - }, - "diagnostics": "field required", - "expression": ["Device.identifier"], - }, { "severity": "error", "code": "processing", @@ -193,7 +183,7 @@ def test_index_bad_payload(version): ], } ) - assert result == { + expected = { "statusCode": 400, "body": expected_body, "headers": { @@ -202,3 +192,6 @@ def test_index_bad_payload(version): "Version": version, }, } + _response_assertions( + result=result, expected=expected, check_body=True, check_content_length=True + ) diff --git a/src/api/createProductTeam/tests/test_index.py b/src/api/createProductTeam/tests/test_index.py index 406369542..9495fbea7 100644 --- a/src/api/createProductTeam/tests/test_index.py +++ b/src/api/createProductTeam/tests/test_index.py @@ -6,6 +6,7 @@ from nhs_context_logging import app_logger from test_helpers.dynamodb import mock_table +from test_helpers.response_assertions import _response_assertions from test_helpers.sample_data import ORGANISATION TABLE_NAME = "hiya" @@ -59,15 +60,19 @@ def test_index(version): ], } ) - assert result == { + expected = { "statusCode": 201, "body": expected_body, "headers": { "Content-Length": str(len(expected_body)), "Content-Type": "application/json", "Version": version, + "Location": None, }, } + _response_assertions( + result=result, expected=expected, check_body=True, check_content_length=True + ) @pytest.mark.parametrize( @@ -164,12 +169,16 @@ def test_index_bad_payload(version): ], } ) - assert result == { + expected = { "statusCode": 400, "body": expected_body, "headers": { "Content-Length": str(len(expected_body)), "Content-Type": "application/json", "Version": version, + "Location": None, }, } + _response_assertions( + result=result, expected=expected, check_body=True, check_content_length=True + ) diff --git a/src/api/readDevice/tests/test_index.py b/src/api/readDevice/tests/test_index.py index ccf227a34..f5768f6c4 100644 --- a/src/api/readDevice/tests/test_index.py +++ b/src/api/readDevice/tests/test_index.py @@ -3,11 +3,13 @@ from unittest import mock import pytest +from domain.core.device_key import DeviceKeyType from domain.core.root import Root from domain.repository.device_repository import DeviceRepository from nhs_context_logging import app_logger from test_helpers.dynamodb import mock_table +from test_helpers.response_assertions import _response_assertions from test_helpers.uuid import consistent_uuid TABLE_NAME = "hiya" @@ -20,15 +22,13 @@ ], ) def test_index(version): - device_id = "XXX-YYY" + device_key = "P.XXX-YYY" org = Root.create_ods_organisation(ods_code="ABC") product_team = org.create_product_team( id=consistent_uuid(1), name="product-team-name" ) - device = product_team.create_device( - id=device_id, name="device-name", type="product" - ) - device.add_key(type="product_id", key=device_id) + device = product_team.create_device(name="device-name", type="product") + device.add_key(DeviceKeyType.PRODUCT_ID, device_key) with mock_table(TABLE_NAME) as client, mock.patch.dict( os.environ, @@ -46,7 +46,7 @@ def test_index(version): result = handler( event={ "headers": {"version": version}, - "pathParameters": {"id": device_id}, + "pathParameters": {"id": str(device.id)}, } ) @@ -61,7 +61,7 @@ def test_index(version): } }, "identifier": [ - {"system": "connecting-party-manager/product_id", "value": "XXX-YYY"} + {"system": "connecting-party-manager/product_id", "value": "P.XXX-YYY"} ], "owner": { "identifier": { @@ -72,15 +72,19 @@ def test_index(version): } ) - assert result == { + expected = { "statusCode": 200, "body": expected_result, "headers": { "Content-Length": str(len(expected_result)), "Content-Type": "application/json", "Version": version, + "Location": None, }, } + _response_assertions( + result=result, expected=expected, check_body=True, check_content_length=True + ) @pytest.mark.parametrize( @@ -135,12 +139,16 @@ def test_index_no_such_device(version): } ) - assert result == { + expected = { "statusCode": 404, "body": expected_result, "headers": { "Content-Length": str(len(expected_result)), "Content-Type": "application/json", "Version": version, + "Location": None, }, } + _response_assertions( + result=result, expected=expected, check_body=True, check_content_length=True + ) diff --git a/src/api/readProductTeam/tests/test_index.py b/src/api/readProductTeam/tests/test_index.py index ad22c0748..ec91b51b3 100644 --- a/src/api/readProductTeam/tests/test_index.py +++ b/src/api/readProductTeam/tests/test_index.py @@ -8,6 +8,7 @@ from nhs_context_logging import app_logger from test_helpers.dynamodb import mock_table +from test_helpers.response_assertions import _response_assertions from test_helpers.uuid import consistent_uuid TABLE_NAME = "hiya" @@ -65,15 +66,19 @@ def test_index(version): } ) - assert result == { + expected = { "statusCode": 200, "body": expected_result, "headers": { "Content-Length": str(len(expected_result)), "Content-Type": "application/json", "Version": version, + "Location": None, }, } + _response_assertions( + result=result, expected=expected, check_body=True, check_content_length=True + ) @pytest.mark.parametrize( @@ -138,12 +143,16 @@ def test_index_no_such_product_team(version): } ) - assert result == { + expected = { "statusCode": 404, "body": expected_result, "headers": { "Content-Length": str(len(expected_result)), "Content-Type": "application/json", "Version": version, + "Location": None, }, } + _response_assertions( + result=result, expected=expected, check_body=True, check_content_length=True + ) diff --git a/src/api/status/index.py b/src/api/status/index.py index a374f5473..eb37314b2 100644 --- a/src/api/status/index.py +++ b/src/api/status/index.py @@ -26,5 +26,5 @@ def handler(event: dict, context=None): api_chain.run(cache=cache, init=event) response_chain = StepChain(step_chain=post_steps, step_decorators=step_decorators) - response_chain.run(init=(api_chain.result, None)) + response_chain.run(init=(api_chain.result, None, None)) return response_chain.result diff --git a/src/api/status/tests/test_index.py b/src/api/status/tests/test_index.py index 639343a11..777d4caae 100644 --- a/src/api/status/tests/test_index.py +++ b/src/api/status/tests/test_index.py @@ -8,6 +8,7 @@ from nhs_context_logging import app_logger from test_helpers.dynamodb import mock_table +from test_helpers.response_assertions import _response_assertions TABLE_NAME = "hiya" @@ -64,15 +65,19 @@ def test_index(): } ) - assert result == { + expected = { "statusCode": 200, "body": expected_body, "headers": { "Content-Length": str(len(expected_body)), "Content-Type": "application/json", "Version": "null", + "Location": None, }, } + _response_assertions( + result=result, expected=expected, check_body=True, check_content_length=True + ) def test_index_not_ok(): @@ -116,12 +121,16 @@ def test_index_not_ok(): } ) - assert result == { + expected = { "statusCode": 503, "body": expected_body, "headers": { "Content-Length": str(len(expected_body)), "Content-Type": "application/json", "Version": "null", + "Location": None, }, } + _response_assertions( + result=result, expected=expected, check_body=True, check_content_length=True + ) diff --git a/src/layers/domain/core/device.py b/src/layers/domain/core/device.py index 890c8ffc0..59f0a0b0b 100644 --- a/src/layers/domain/core/device.py +++ b/src/layers/domain/core/device.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from enum import StrEnum, auto -from uuid import UUID +from uuid import UUID, uuid4 from pydantic import Field @@ -8,7 +8,6 @@ from .device_key import DeviceKey, DeviceKeyType from .error import DuplicateError from .event import Event -from .product_id import PRODUCT_ID_REGEX from .validation import DEVICE_NAME_REGEX @@ -63,7 +62,7 @@ class Device(AggregateRoot): +-- nrl-producer-api (???) """ - id: str = Field(regex=PRODUCT_ID_REGEX.pattern) + id: UUID = Field(default_factory=uuid4) name: str = Field(regex=DEVICE_NAME_REGEX) type: DeviceType status: DeviceStatus = Field(default=DeviceStatus.ACTIVE) diff --git a/src/layers/domain/core/device_id.py b/src/layers/domain/core/device_id.py new file mode 100644 index 000000000..4d608b6ee --- /dev/null +++ b/src/layers/domain/core/device_id.py @@ -0,0 +1,26 @@ +""" +In order to reduce human error: +1. All values are uppercase +2. similar characters have been removed from the set of available characters. + e.g. 0/O/Q, 1/I, S/5, B/8, and Z/2 +""" +import random +from datetime import datetime + +from .device_key import DeviceKeyType +from .validation import PRODUCT_ID_CHARS + +PART_LENGTH = 3 +N_PARTS = 2 + + +def generate_device_key(device_type: DeviceKeyType) -> str: + rng = random.Random(datetime.now().timestamp()) + + match device_type: + case DeviceKeyType.PRODUCT_ID: + device_key = "-".join( + "".join(rng.choices(PRODUCT_ID_CHARS, k=PART_LENGTH)) + for _ in range(N_PARTS) + ) + return f"P.{device_key}" diff --git a/src/layers/domain/core/device_key.py b/src/layers/domain/core/device_key.py index bde4a1081..21933f3d8 100644 --- a/src/layers/domain/core/device_key.py +++ b/src/layers/domain/core/device_key.py @@ -4,8 +4,7 @@ from domain.core.error import InvalidDeviceKeyError from pydantic import BaseModel, validator -from .product_id import PRODUCT_ID_REGEX -from .validation import ACCREDITED_SYSTEM_ID_REGEX +from .validation import ACCREDITED_SYSTEM_ID_REGEX, PRODUCT_ID_REGEX class DeviceKeyType(StrEnum): diff --git a/src/layers/domain/core/product_id.py b/src/layers/domain/core/product_id.py deleted file mode 100644 index f4ff2c718..000000000 --- a/src/layers/domain/core/product_id.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -In order to reduce human error: -1. All values are uppercase -2. similar characters have been removed from the set of available characters. - e.g. 0/O/Q, 1/I, S/5, B/8, and Z/2 -""" -import random -import re - -PRODUCT_ID_CHARS = "ACDEFGHJKLMNPRTUVWXY34679" -PRODUCT_ID_REGEX = re.compile(rf"^[{PRODUCT_ID_CHARS}]{{3}}-[{PRODUCT_ID_CHARS}]{{3}}$") - -PART_LENGTH = 3 -N_PARTS = 2 - - -def generate_product_id(seed: int | None = None) -> str: - rng = random.Random(seed) - return "-".join( - "".join(rng.choices(PRODUCT_ID_CHARS, k=PART_LENGTH)) for _ in range(N_PARTS) - ) diff --git a/src/layers/domain/core/product_team.py b/src/layers/domain/core/product_team.py index ff5651a9a..414ae23f5 100644 --- a/src/layers/domain/core/product_team.py +++ b/src/layers/domain/core/product_team.py @@ -34,13 +34,11 @@ class ProductTeam(AggregateRoot): def create_device( self, - id: UUID, name: str, type: DeviceType, status: DeviceStatus = DeviceStatus.ACTIVE, ) -> Device: device = Device( - id=id, name=name, type=type, status=status, diff --git a/src/layers/domain/core/tests/test_device.py b/src/layers/domain/core/tests/test_device.py index b1d9f8e5c..3dd5f6c83 100644 --- a/src/layers/domain/core/tests/test_device.py +++ b/src/layers/domain/core/tests/test_device.py @@ -5,10 +5,9 @@ @pytest.mark.parametrize( - ["id", "name", "ods_code", "product_team_id", "type", "status"], + ["name", "ods_code", "product_team_id", "type", "status"], [ [ - "XXX-YYY", "Foo", "AB123", "18934119-5780-4d28-b9be-0e6dff3908ba", @@ -18,7 +17,6 @@ ], ) def test__can_create_device( - id: str, name: str, ods_code: str, product_team_id: str, @@ -26,18 +24,24 @@ def test__can_create_device( status: str, ): device = Device( - id=id, name=name, ods_code=ods_code, product_team_id=product_team_id, type=type, status=status, ) - assert device.dict() == { - "id": id, - "name": name, - "ods_code": ods_code, - "product_team_id": UUID(product_team_id), - "status": "active", - "type": "product", - } + assert isinstance(device.id, UUID) + assert device.id.version == 4 + # Assert the existence of other attributes + assert hasattr(device, "name") + assert hasattr(device, "ods_code") + assert hasattr(device, "product_team_id") + assert hasattr(device, "type") + assert hasattr(device, "status") + + # Assert that the values of the attributes are correct + assert device.name == name + assert device.ods_code == ods_code + assert device.product_team_id == UUID(product_team_id) + assert device.type == "product" + assert device.status == "active" diff --git a/src/layers/domain/core/tests/test_device_key.py b/src/layers/domain/core/tests/test_device_key.py index 949050ed3..df1b7dbf6 100644 --- a/src/layers/domain/core/tests/test_device_key.py +++ b/src/layers/domain/core/tests/test_device_key.py @@ -6,7 +6,7 @@ @pytest.mark.parametrize( ["type", "key"], ( - ("product_id", "XXX-YYY"), + ("product_id", "P.XXX-YYY"), ("accredited_system_id", "12345"), ), ) diff --git a/src/layers/domain/core/tests/test_product_id.py b/src/layers/domain/core/tests/test_product_id.py index d80f6398f..785ea8f73 100644 --- a/src/layers/domain/core/tests/test_product_id.py +++ b/src/layers/domain/core/tests/test_product_id.py @@ -1,16 +1,12 @@ -import pytest -from domain.core.product_id import PRODUCT_ID_REGEX, generate_product_id +from domain.core.device_id import generate_device_key +from domain.core.device_key import DeviceKeyType +from domain.core.validation import PRODUCT_ID_REGEX -@pytest.mark.parametrize("seed", [1, 2, 3]) -def test__deterministic_generate(seed: int): - a = generate_product_id(seed) - b = generate_product_id(seed) +def test__deterministic_generate(): + a = generate_device_key(DeviceKeyType.PRODUCT_ID) + b = generate_device_key(DeviceKeyType.PRODUCT_ID) - assert a == b - assert PRODUCT_ID_REGEX.match(a) is not None - - -def test__deterministic_generate_without_seed(): - a = generate_product_id() + assert a != b assert PRODUCT_ID_REGEX.match(a) is not None + assert PRODUCT_ID_REGEX.match(b) is not None diff --git a/src/layers/domain/core/validation.py b/src/layers/domain/core/validation.py index 32a9dca5f..eea2d4450 100644 --- a/src/layers/domain/core/validation.py +++ b/src/layers/domain/core/validation.py @@ -9,3 +9,7 @@ DEVICE_NAME_REGEX = ( r"^[a-zA-Z]{1}[ -~]+$" # starts with any letter, followed by any sequence of ascii ) +PRODUCT_ID_CHARS = "ACDEFGHJKLMNPRTUVWXY34679" +PRODUCT_ID_REGEX = re.compile( + rf"^P\.[{PRODUCT_ID_CHARS}]{{3}}-[{PRODUCT_ID_CHARS}]{{3}}$" +) diff --git a/src/layers/domain/fhir_translation/device.py b/src/layers/domain/fhir_translation/device.py index 442d3b108..247e0d0b3 100644 --- a/src/layers/domain/fhir_translation/device.py +++ b/src/layers/domain/fhir_translation/device.py @@ -31,7 +31,6 @@ def create_domain_device_from_fhir_device( ) -> DomainDevice: (device_name,) = fhir_device.deviceName device = product_team.create_device( - id=fhir_device.identifier[0].value, name=device_name.name, type=fhir_device.definition.identifier.value, ) diff --git a/src/layers/domain/fhir_translation/tests/data/device-fhir-example-required.json b/src/layers/domain/fhir_translation/tests/data/device-fhir-example-required.json index 01981a132..b1c158520 100644 --- a/src/layers/domain/fhir_translation/tests/data/device-fhir-example-required.json +++ b/src/layers/domain/fhir_translation/tests/data/device-fhir-example-required.json @@ -3,7 +3,7 @@ "identifier": [ { "system": "connecting-party-manager/product_id", - "value": "XXX-YYY" + "value": "P.XXX-YYY" }, { "system": "connecting-party-manager/accredited_system_id", diff --git a/src/layers/domain/repository/tests/test_device_repository.py b/src/layers/domain/repository/tests/test_device_repository.py index eb41a7d13..beb67de61 100644 --- a/src/layers/domain/repository/tests/test_device_repository.py +++ b/src/layers/domain/repository/tests/test_device_repository.py @@ -13,7 +13,6 @@ @pytest.mark.integration def test__device_repository(): - subject_id = "XXX-YYY" table_name = read_terraform_output("dynamodb_table_name.value") org = Root.create_ods_organisation(ods_code="AB123") @@ -21,27 +20,24 @@ def test__device_repository(): id=UUID("6f8c285e-04a2-4194-a84e-dabeba474ff7"), name="Team" ) subject = team.create_device( - id=subject_id, name="Subject", type=DeviceType.PRODUCT, status=DeviceStatus.ACTIVE, ) - subject.add_key(key="WWW-XXX", type=DeviceKeyType.PRODUCT_ID) + subject.add_key(key="P.WWW-XXX", type=DeviceKeyType.PRODUCT_ID) subject.add_key(key="1234567890", type=DeviceKeyType.ACCREDITED_SYSTEM_ID) - device_repo = DeviceRepository( table_name=table_name, dynamodb_client=dynamodb_client(), ) device_repo.write(subject) - result = device_repo.read(subject_id) + result = device_repo.read(subject.id) assert result == subject @pytest.mark.integration def test__device_repository_already_exists(): - subject_id = "XXX-YYY" table_name = read_terraform_output("dynamodb_table_name.value") org = Root.create_ods_organisation(ods_code="AB123") @@ -50,12 +46,11 @@ def test__device_repository_already_exists(): name="Team", ) subject = team.create_device( - id=subject_id, name="Subject", type=DeviceType.PRODUCT, status=DeviceStatus.ACTIVE, ) - subject.add_key(key="WWW-XXX", type=DeviceKeyType.PRODUCT_ID) + subject.add_key(key="P.WWW-XXX", type=DeviceKeyType.PRODUCT_ID) subject.add_key(key="1234567890", type=DeviceKeyType.ACCREDITED_SYSTEM_ID) device_repo = DeviceRepository( @@ -69,7 +64,7 @@ def test__device_repository_already_exists(): @pytest.mark.integration def test__device_repository__device_does_not_exist(): - subject_id = "XXX-YYY" + subject_id = "6f8c285e-04a2-4194-a84e-dabeba474ff7" table_name = read_terraform_output("dynamodb_table_name.value") device_repo = DeviceRepository( @@ -82,22 +77,18 @@ def test__device_repository__device_does_not_exist(): def test__device_repository_local(): - subject_id = "XXX-YYY" - org = Root.create_ods_organisation(ods_code="AB123") team = org.create_product_team( id="6f8c285e-04a2-4194-a84e-dabeba474ff7", name="Team", ) subject = team.create_device( - id=subject_id, name="Subject", type=DeviceType.PRODUCT, status=DeviceStatus.ACTIVE, ) - subject.add_key(key="WWW-XXX", type=DeviceKeyType.PRODUCT_ID) + subject.add_key(key="P.WWW-XXX", type=DeviceKeyType.PRODUCT_ID) subject.add_key(key="1234567890", type=DeviceKeyType.ACCREDITED_SYSTEM_ID) - with mock_table("my_table") as client: device_repo = DeviceRepository( table_name="my_table", @@ -105,12 +96,12 @@ def test__device_repository_local(): ) device_repo.write(subject) - result = device_repo.read(subject_id) + result = device_repo.read(subject.id) assert result == subject def test__device_repository__device_does_not_exist_local(): - subject_id = "XXX-YYY" + subject_id = "6f8c285e-04a2-4194-a84e-dabeba474ff7" with mock_table("my_table") as client: device_repo = DeviceRepository( diff --git a/src/layers/event/api_step_chain/__init__.py b/src/layers/event/api_step_chain/__init__.py index daaafb9a4..2204389f3 100644 --- a/src/layers/event/api_step_chain/__init__.py +++ b/src/layers/event/api_step_chain/__init__.py @@ -45,5 +45,5 @@ def execute_step_chain( response_chain = StepChain( step_chain=response_steps, step_decorators=STEP_DECORATORS ) - response_chain.run(init=(result, version)) + response_chain.run(init=(result, version, None)) return response_chain.result diff --git a/src/layers/event/response/aws_lambda_response.py b/src/layers/event/response/aws_lambda_response.py index 906300fbc..d5f85b8cd 100644 --- a/src/layers/event/response/aws_lambda_response.py +++ b/src/layers/event/response/aws_lambda_response.py @@ -1,5 +1,5 @@ from http import HTTPStatus -from typing import Literal +from typing import Literal, Optional from pydantic import BaseModel, Field, validator @@ -10,6 +10,7 @@ class AwsLambdaResponseHeaders(BaseModel): ) content_length: str = Field(alias="Content-Length", regex=r"^[1-9][0-9]*$") version: str = Field(alias="Version", regex=r"^(null)|([1-9][0-9]*)$") + location: Optional[str] = Field(alias="Location") class Config: allow_population_by_field_name = True @@ -22,6 +23,7 @@ class AwsLambdaResponse(BaseModel): statusCode: HTTPStatus body: str = Field(min_length=1) version: None | str = Field(exclude=True) + location: None | str = Field(exclude=True, default=None) headers: AwsLambdaResponseHeaders = None @validator("headers", always=True) @@ -30,9 +32,11 @@ def generate_response_headers(headers, values): return headers body: str = values["body"] version: None | str = values["version"] + location: None | str = values.get("location") headers = AwsLambdaResponseHeaders( content_length=len(body), version="null" if version is None else version, + location=location, ) return headers diff --git a/src/layers/event/response/render_response.py b/src/layers/event/response/render_response.py index 90e1bcb0d..795129441 100644 --- a/src/layers/event/response/render_response.py +++ b/src/layers/event/response/render_response.py @@ -19,6 +19,7 @@ def render_response( response: JsonSerialisable | HTTPStatus | Exception, id: str = None, version: str = None, + location: str = None, ) -> AwsLambdaResponse: if id is None: id = app_logger.service_name @@ -46,4 +47,6 @@ def render_response( outcome = response body = json.dumps(outcome) - return AwsLambdaResponse(statusCode=http_status, body=body, version=version) + return AwsLambdaResponse( + statusCode=http_status, body=body, version=version, location=location + ) diff --git a/src/layers/event/response/steps.py b/src/layers/event/response/steps.py index 6a12b3a52..a88e5c519 100644 --- a/src/layers/event/response/steps.py +++ b/src/layers/event/response/steps.py @@ -5,8 +5,8 @@ def render_response(data, cache) -> dict: - result, version = data[StepChain.INIT] - response = _render_response(response=result, version=version) + result, version, location = data[StepChain.INIT] + response = _render_response(response=result, version=version, location=location) return response.dict() diff --git a/src/layers/event/response/tests/test_render_response.py b/src/layers/event/response/tests/test_render_response.py index 0937d9ed8..2cfd34420 100644 --- a/src/layers/event/response/tests/test_render_response.py +++ b/src/layers/event/response/tests/test_render_response.py @@ -9,20 +9,28 @@ _get_validation_error, ) +from test_helpers.response_assertions import _response_assertions + NON_SUCCESS_STATUSES = set(HTTPStatus._member_map_.values()) - SUCCESS_STATUSES def test_render_response_of_json_serialisable(): aws_lambda_response = render_response(response={"dict": "of things"}) - assert aws_lambda_response.dict() == { + expected = { "statusCode": HTTPStatus.OK, "body": '{"dict": "of things"}', "headers": { "Content-Type": "application/json", - "Content-Length": "21", + "Content-Length": "14", "Version": "null", }, } + _response_assertions( + result=aws_lambda_response.dict(), + expected=expected, + check_body=True, + check_content_length=True, + ) def test_render_response_of_success_http_status_created(): @@ -56,7 +64,7 @@ def test_render_response_of_success_http_status_created(): aws_lambda_response = render_response(response=HTTPStatus.CREATED, id="foo") - assert aws_lambda_response.dict() == { + expected = { "statusCode": 201, "body": expected_body, "headers": { @@ -66,6 +74,13 @@ def test_render_response_of_success_http_status_created(): }, } + _response_assertions( + result=aws_lambda_response.dict(), + expected=expected, + check_body=True, + check_content_length=True, + ) + @pytest.mark.parametrize("http_status", NON_SUCCESS_STATUSES) def test_render_response_of_non_success_http_status(http_status: HTTPStatus): @@ -98,7 +113,7 @@ def test_render_response_of_non_success_http_status(http_status: HTTPStatus): ) aws_lambda_response = render_response(response=http_status, id="foo") - assert aws_lambda_response.dict() == { + expected = { "statusCode": HTTPStatus.INTERNAL_SERVER_ERROR, "body": expected_body, "headers": { @@ -108,6 +123,13 @@ def test_render_response_of_non_success_http_status(http_status: HTTPStatus): }, } + _response_assertions( + result=aws_lambda_response.dict(), + expected=expected, + check_body=True, + check_content_length=True, + ) + def test_render_response_of_non_json_serialisable(): expected_body = json.dumps( @@ -139,7 +161,7 @@ def test_render_response_of_non_json_serialisable(): ) aws_lambda_response = render_response(object(), id="foo") - assert aws_lambda_response.dict() == { + expected = { "statusCode": HTTPStatus.INTERNAL_SERVER_ERROR, "body": expected_body, "headers": { @@ -148,6 +170,12 @@ def test_render_response_of_non_json_serialisable(): "Version": "null", }, } + _response_assertions( + result=aws_lambda_response.dict(), + expected=expected, + check_body=True, + check_content_length=True, + ) @pytest.mark.parametrize( @@ -161,7 +189,7 @@ def test_render_response_of_non_json_serialisable(): ) def test_render_response_of_json_serialisable(response, expected_body): aws_lambda_response = render_response(response, id="foo") - assert aws_lambda_response.dict() == { + expected = { "statusCode": HTTPStatus.OK, "body": expected_body, "headers": { @@ -170,6 +198,12 @@ def test_render_response_of_json_serialisable(response, expected_body): "Version": "null", }, } + _response_assertions( + result=aws_lambda_response.dict(), + expected=expected, + check_body=True, + check_content_length=True, + ) def test_render_response_of_blank_exception(): @@ -201,7 +235,7 @@ def test_render_response_of_blank_exception(): ], } ) - assert aws_lambda_response.dict() == { + expected = { "statusCode": 500, "body": expected_body, "headers": { @@ -210,6 +244,12 @@ def test_render_response_of_blank_exception(): "Version": "null", }, } + _response_assertions( + result=aws_lambda_response.dict(), + expected=expected, + check_body=True, + check_content_length=True, + ) def test_render_response_of_general_exception(): @@ -241,7 +281,7 @@ def test_render_response_of_general_exception(): ], } ) - assert aws_lambda_response.dict() == { + expected = { "statusCode": 500, "body": expected_body, "headers": { @@ -250,6 +290,12 @@ def test_render_response_of_general_exception(): "Version": "null", }, } + _response_assertions( + result=aws_lambda_response.dict(), + expected=expected, + check_body=True, + check_content_length=True, + ) def test_render_response_of_general_validation_error(): @@ -323,7 +369,7 @@ def test_render_response_of_general_validation_error(): ], } ) - assert aws_lambda_response.dict() == { + expected = { "statusCode": 500, "body": expected_body, "headers": { @@ -332,6 +378,12 @@ def test_render_response_of_general_validation_error(): "Version": "null", }, } + _response_assertions( + result=aws_lambda_response.dict(), + expected=expected, + check_body=True, + check_content_length=True, + ) def test_render_response_of_internal_validation_error(): @@ -380,7 +432,7 @@ def test_render_response_of_internal_validation_error(): ], } ) - assert aws_lambda_response.dict() == { + expected = { "statusCode": 400, "body": expected_body, "headers": { @@ -389,3 +441,9 @@ def test_render_response_of_internal_validation_error(): "Version": "null", }, } + _response_assertions( + result=aws_lambda_response.dict(), + expected=expected, + check_body=True, + check_content_length=True, + ) diff --git a/src/test_helpers/response_assertions.py b/src/test_helpers/response_assertions.py new file mode 100644 index 000000000..4ba5c6a63 --- /dev/null +++ b/src/test_helpers/response_assertions.py @@ -0,0 +1,15 @@ +def _response_assertions( + result, expected, check_body=False, check_content_length=False +): + for key, value in expected.items(): + assert key in result + if isinstance(value, dict): + _response_assertions(result=result.get(key, {}), expected=value) + if key != "Location" and key != "headers": + if key != "Content-Length" and key != "body": + assert result[key] == value + else: + if key == "Content-Length" and check_content_length: + assert result[key] == value + if key == "body" and check_body: + assert result[key] == value