davinci_crd_test_kit 0.14.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/config/presets/inferno_crd_client_suite_v221.json.erb +64 -0
- data/config/presets/inferno_crd_server_suite_v221.json.erb +2 -2
- data/lib/davinci_crd_test_kit/client/endpoints/hook_request_endpoint.rb +12 -6
- data/lib/davinci_crd_test_kit/client/endpoints/mocked_card_responses/external_reference.json +1 -1
- data/lib/davinci_crd_test_kit/client/endpoints/mocked_card_responses/launch_smart_app.json +0 -7
- data/lib/davinci_crd_test_kit/client/v2.2.1/client_appointment_book_group.rb +8 -1
- data/lib/davinci_crd_test_kit/client/v2.2.1/client_encounter_discharge_group.rb +7 -1
- data/lib/davinci_crd_test_kit/client/v2.2.1/client_encounter_start_group.rb +7 -1
- data/lib/davinci_crd_test_kit/client/v2.2.1/client_hooks_group.rb +20 -0
- data/lib/davinci_crd_test_kit/client/v2.2.1/client_long_running_hook_group.rb +1 -1
- data/lib/davinci_crd_test_kit/client/v2.2.1/client_order_dispatch_group.rb +7 -1
- data/lib/davinci_crd_test_kit/client/v2.2.1/client_order_select_group.rb +7 -1
- data/lib/davinci_crd_test_kit/client/v2.2.1/client_order_sign_group.rb +7 -1
- data/lib/davinci_crd_test_kit/client/v2.2.1/client_registration_group.rb +6 -0
- data/lib/davinci_crd_test_kit/client/v2.2.1/crd_client_suite.rb +5 -0
- data/lib/davinci_crd_test_kit/client/v2.2.1/long_running/client_skip_long_running_attestation_test.rb +10 -5
- data/lib/davinci_crd_test_kit/client/v2.2.1/registration/client_service_registration_attestation_test.rb +2 -1
- data/lib/davinci_crd_test_kit/client/v2.2.1/verify_request/hook_request_prefetch_complete_test.rb +6 -0
- data/lib/davinci_crd_test_kit/client/v2.2.1/verify_response/client_display_cards_attest.rb +2 -1
- data/lib/davinci_crd_test_kit/cross_suite/cards_identification.rb +9 -0
- data/lib/davinci_crd_test_kit/cross_suite/prefetch_completeness_checker.rb +25 -11
- data/lib/davinci_crd_test_kit/cross_suite/requests_logical_model_validation.rb +2 -1
- data/lib/davinci_crd_test_kit/cross_suite/response_logical_model_validation.rb +164 -3
- data/lib/davinci_crd_test_kit/cross_suite/tags.rb +4 -0
- data/lib/davinci_crd_test_kit/requirements/generated/crd_server_v221_requirements_coverage.csv +38 -38
- data/lib/davinci_crd_test_kit/server/jobs/invoke_hook.rb +3 -3
- data/lib/davinci_crd_test_kit/server/jobs/invoke_hook_no_special_requests.rb +17 -0
- data/lib/davinci_crd_test_kit/server/jobs/invoke_hook_with_bad_auth.rb +14 -0
- data/lib/davinci_crd_test_kit/server/server_abstract_invoke_hook_test.rb +24 -6
- data/lib/davinci_crd_test_kit/server/server_hook_helper.rb +2 -2
- data/lib/davinci_crd_test_kit/server/v2.2.1/crd_server_suite.rb +3 -0
- data/lib/davinci_crd_test_kit/server/v2.2.1/discovery/discovery_standard_prefetch_expressions_test.rb +166 -0
- data/lib/davinci_crd_test_kit/server/v2.2.1/interaction/server_invoke_hook_single_request_test.rb +22 -0
- data/lib/davinci_crd_test_kit/server/v2.2.1/server_appointment_book_group.rb +10 -102
- data/lib/davinci_crd_test_kit/server/v2.2.1/server_demonstrate_hook_response_group.rb +1 -0
- data/lib/davinci_crd_test_kit/server/v2.2.1/server_discovery_group.rb +2 -0
- data/lib/davinci_crd_test_kit/server/v2.2.1/server_encounter_discharge_group.rb +10 -97
- data/lib/davinci_crd_test_kit/server/v2.2.1/server_encounter_start_group.rb +10 -97
- data/lib/davinci_crd_test_kit/server/v2.2.1/server_order_dispatch_group.rb +10 -103
- data/lib/davinci_crd_test_kit/server/v2.2.1/server_order_select_group.rb +12 -116
- data/lib/davinci_crd_test_kit/server/v2.2.1/server_order_sign_group.rb +12 -121
- data/lib/davinci_crd_test_kit/server/v2.2.1/specific_coverage_response_group.rb +23 -0
- data/lib/davinci_crd_test_kit/server/v2.2.1/specific_coverage_responses/coverage_info_reason_test.rb +108 -0
- data/lib/davinci_crd_test_kit/server/v2.2.1/specific_coverage_responses/coverage_not_found_group.rb +116 -0
- data/lib/davinci_crd_test_kit/server/v2.2.1/specific_coverage_responses/no_active_coverage_group.rb +115 -0
- data/lib/davinci_crd_test_kit/server/v2.2.1/specific_coverage_responses/no_member_found_group.rb +117 -0
- data/lib/davinci_crd_test_kit/server/v2.2.1/specific_coverage_responses/technical_issues_group.rb +121 -0
- data/lib/davinci_crd_test_kit/server/v2.2.1/specific_coverage_responses/technical_issues_invoke_test.rb +22 -0
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_request/service_request_context_validation_test.rb +1 -0
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_request/service_request_optional_fields_validation_test.rb +1 -0
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_request/service_request_required_fields_validation_test.rb +1 -0
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/additional_orders_validation_test.rb +78 -35
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/coverage_information_card_absence_test.rb +76 -0
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/coverage_information_system_action_validation_test.rb +70 -124
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/create_or_update_coverage_info_response_validation_test.rb +83 -39
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/external_reference_card_validation_test.rb +59 -17
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/form_completion_response_validation_test.rb +80 -45
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/hook_request_resource_resolution.rb +37 -0
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/instructions_card_received_test.rb +57 -6
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/launch_smart_app_card_validation_test.rb +56 -17
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/propose_alternate_request_card_validation_test.rb +52 -25
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/service_response_validation_test.rb +17 -51
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/unknown_cds_hooks_elements_test.rb +1 -1
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/unknown_configuration_test.rb +1 -1
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/unknown_context_test.rb +1 -1
- data/lib/davinci_crd_test_kit/version.rb +2 -2
- metadata +18 -8
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/card_optional_fields_validation_test.rb +0 -57
- data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/coverage_information_system_action_received_test.rb +0 -65
|
@@ -4,6 +4,7 @@ require_relative 'client_encounter_start_group'
|
|
|
4
4
|
require_relative 'client_order_dispatch_group'
|
|
5
5
|
require_relative 'client_order_select_group'
|
|
6
6
|
require_relative 'client_order_sign_group'
|
|
7
|
+
require_relative 'client_urls'
|
|
7
8
|
|
|
8
9
|
module DaVinciCRDTestKit
|
|
9
10
|
module V221
|
|
@@ -28,6 +29,25 @@ module DaVinciCRDTestKit
|
|
|
28
29
|
The CRD IG does not require support for any specific hook, so all the hook-specific sub-groups are
|
|
29
30
|
optional. A conformant CRD client will have implemented at least one hook and will run and pass
|
|
30
31
|
the hook-specific groups corresponding to each hook that it implements.
|
|
32
|
+
|
|
33
|
+
Inferno simulates two CRD discovery endpoints, each with service endpoints for all six CRD hooks
|
|
34
|
+
but with different service ids:
|
|
35
|
+
- Discovery endpoint for services requesting the complete [standard prefetch data set](https://hl7.org/fhir/us/davinci-crd/2.2.1/en/foundation.html#standard-prefetch):
|
|
36
|
+
`#{ClientURLs.discovery_url}`
|
|
37
|
+
* `appointment-book` service id: `appointment-book-service`
|
|
38
|
+
* `encounter-start` service id: `encounter-start-service`
|
|
39
|
+
* `encounter-discharge` service id: `encounter-discharge-service`
|
|
40
|
+
* `order-select` service id: `order-select-service`
|
|
41
|
+
* `order-dispatch` service id: `order-dispatch-service`
|
|
42
|
+
* `order-sign` service id: `order-sign-service`
|
|
43
|
+
- Discovery endpoint for services requesting the a subset of the [standard prefetch data set](https://hl7.org/fhir/us/davinci-crd/2.2.1/en/foundation.html#standard-prefetch):
|
|
44
|
+
`#{ClientURLs.prefetch_subset_discovery_url}`
|
|
45
|
+
* `appointment-book` service id: `appointment-book-subset`
|
|
46
|
+
* `encounter-start` service id: `encounter-start-subset`
|
|
47
|
+
* `encounter-discharge` service id: `encounter-discharge-subset`
|
|
48
|
+
* `order-select` service id: `order-select-subset`
|
|
49
|
+
* `order-dispatch` service id: `order-dispatch-subset`
|
|
50
|
+
* `order-sign` service id: `order-sign-subset`
|
|
31
51
|
DESCRIPTION
|
|
32
52
|
id :crd_v221_client_hooks
|
|
33
53
|
|
|
@@ -26,12 +26,18 @@ module DaVinciCRDTestKit
|
|
|
26
26
|
that was not tied to a specific performer. The CRD IG places [additional constraints on the use
|
|
27
27
|
of the order-dispatch hook](https://hl7.org/fhir/us/davinci-crd/2.2.1/en/hooks.html#order-dispatch),
|
|
28
28
|
including the profiles that resources in each request must conform to.
|
|
29
|
+
|
|
30
|
+
Inferno's simulated `order-dispatch` hook endpoints are available at:
|
|
31
|
+
- Complete Prefetch: `#{ClientURLs.base_url}#{ORDER_DISPATCH_PATH}`
|
|
32
|
+
- Subset Prefetch: `#{ClientURLs.base_url}#{ORDER_DISPATCH_PREFETCH_SUBSET_PATH}`
|
|
29
33
|
DESCRIPTION
|
|
30
34
|
run_as_group
|
|
31
35
|
|
|
32
36
|
verifies_requirements 'hl7.fhir.us.davinci-crd_2.2.1@hook-2-A', 'hl7.fhir.us.davinci-crd_2.2.1@hook-2-B'
|
|
33
37
|
|
|
34
|
-
input_order :
|
|
38
|
+
input_order :order_dispatch_response_approach,
|
|
39
|
+
:order_dispatch_selected_response_types,
|
|
40
|
+
:order_dispatch_custom_response_template
|
|
35
41
|
|
|
36
42
|
config(
|
|
37
43
|
inputs: {
|
|
@@ -25,12 +25,18 @@ module DaVinciCRDTestKit
|
|
|
25
25
|
procedures, labs and other orders). The CRD IG places [additional constraints on the use
|
|
26
26
|
of the order-select hook](https://hl7.org/fhir/us/davinci-crd/2.2.1/en/hooks.html#order-select),
|
|
27
27
|
including the profiles that resources in each request must conform to.
|
|
28
|
+
|
|
29
|
+
Inferno's simulated `order-select` hook endpoints are available at:
|
|
30
|
+
- Complete Prefetch: `#{ClientURLs.base_url}#{ORDER_SELECT_PATH}`
|
|
31
|
+
- Subset Prefetch: `#{ClientURLs.base_url}#{ORDER_SELECT_PREFETCH_SUBSET_PATH}`
|
|
28
32
|
DESCRIPTION
|
|
29
33
|
run_as_group
|
|
30
34
|
|
|
31
35
|
verifies_requirements 'hl7.fhir.us.davinci-crd_2.2.1@hook-2-A', 'hl7.fhir.us.davinci-crd_2.2.1@hook-2-B'
|
|
32
36
|
|
|
33
|
-
input_order :
|
|
37
|
+
input_order :order_select_response_approach,
|
|
38
|
+
:order_select_selected_response_types,
|
|
39
|
+
:order_select_custom_response_template
|
|
34
40
|
|
|
35
41
|
config(
|
|
36
42
|
inputs: {
|
|
@@ -26,12 +26,18 @@ module DaVinciCRDTestKit
|
|
|
26
26
|
for medications, procedures, labs and other orders). The CRD IG places [additional constraints on the use
|
|
27
27
|
of the order-sign hook](https://hl7.org/fhir/us/davinci-crd/2.2.1/en/hooks.html#order-sign),
|
|
28
28
|
including the profiles that resources in each request must conform to.
|
|
29
|
+
|
|
30
|
+
Inferno's simulated `order-sign` hook endpoints are available at:
|
|
31
|
+
- Complete Prefetch: `#{ClientURLs.base_url}#{ORDER_SIGN_PATH}`
|
|
32
|
+
- Subset Prefetch: `#{ClientURLs.base_url}#{ORDER_SIGN_PREFETCH_SUBSET_PATH}`
|
|
29
33
|
DESCRIPTION
|
|
30
34
|
run_as_group
|
|
31
35
|
|
|
32
36
|
verifies_requirements 'hl7.fhir.us.davinci-crd_2.2.1@hook-2-A', 'hl7.fhir.us.davinci-crd_2.2.1@hook-2-B'
|
|
33
37
|
|
|
34
|
-
input_order :
|
|
38
|
+
input_order :order_sign_response_approach,
|
|
39
|
+
:order_sign_selected_response_types,
|
|
40
|
+
:order_sign_custom_response_template
|
|
35
41
|
|
|
36
42
|
config(
|
|
37
43
|
inputs: {
|
|
@@ -12,6 +12,12 @@ module DaVinciCRDTestKit
|
|
|
12
12
|
and vice-versa. Tests in this group confirm the registration of the partner
|
|
13
13
|
system on both ends.
|
|
14
14
|
|
|
15
|
+
Inferno simulates two CRD discovery endpoints for the client to connect to:
|
|
16
|
+
- Discovery endpoint for services requesting the complete [standard prefetch data set](https://hl7.org/fhir/us/davinci-crd/2.2.1/en/foundation.html#standard-prefetch):
|
|
17
|
+
`#{ClientURLs.discovery_url}`
|
|
18
|
+
- Discovery endpoint for services requesting the a subset of the [standard prefetch data set](https://hl7.org/fhir/us/davinci-crd/2.2.1/en/foundation.html#standard-prefetch):
|
|
19
|
+
`#{ClientURLs.prefetch_subset_discovery_url}`
|
|
20
|
+
|
|
15
21
|
This group must be run before any other tests in the "Hook Invocation" group because inputs
|
|
16
22
|
provided to this group will be used by Inferno for the remainder of the tests
|
|
17
23
|
to identify client requests and verify behavior. The inputs will appear as locked and
|
|
@@ -23,6 +23,11 @@ module DaVinciCRDTestKit
|
|
|
23
23
|
capabilities of a CRD client as described in [version 2.2.1](https://hl7.org/fhir/us/davinci-crd/2.2.1)
|
|
24
24
|
of the Da Vinci Coverage Requirements Discovery (CRD) Implementation Guide.
|
|
25
25
|
|
|
26
|
+
These tests are a **DRAFT** intended to allow CRD implementers to perform
|
|
27
|
+
preliminary checks of their implementations against the CRD IG requirements and
|
|
28
|
+
[provide feedback](https://github.com/inferno-framework/davinci-crd-test-kit/issues) on the tests.
|
|
29
|
+
Future versions of these tests may validate other requirements and may change how these are tested.
|
|
30
|
+
|
|
26
31
|
Detailed information about this test suite can be found in the
|
|
27
32
|
[client section](https://github.com/inferno-framework/davinci-crd-test-kit/wiki/Client-Details) of the
|
|
28
33
|
[CRD Test Kit Wiki](https://github.com/inferno-framework/davinci-crd-test-kit/wiki), including:
|
|
@@ -8,12 +8,17 @@ module DaVinciCRDTestKit
|
|
|
8
8
|
include DaVinciCRDTestKit::TaggedRequestLoadHelper
|
|
9
9
|
|
|
10
10
|
id :crd_v221_client_skip_long_running_attestation_test
|
|
11
|
-
title 'Client allows the user to continue their workflow during long-running requests
|
|
11
|
+
title 'Client allows the user to continue their workflow during long-running requests'
|
|
12
12
|
description %(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
The CRD IG [requires](https://hl7.org/fhir/us/davinci-crd/2.2.1/en/foundation.html#ci-c-found-6)
|
|
14
|
+
that client systems not block their users while waiting for long-running CRD Hook calls.
|
|
15
|
+
During this test, the tester will confirm that the client system allows users to continue
|
|
16
|
+
with their workflow when a hook request is long-running. This could be accomplished,
|
|
17
|
+
for example, by providing a bypass/continue mechanism when a CRD server is taking too long
|
|
18
|
+
to respond, or by always running hooks requests in the background and notifying users when they
|
|
19
|
+
return pertinent information.
|
|
16
20
|
)
|
|
21
|
+
attestation
|
|
17
22
|
|
|
18
23
|
verifies_requirements 'hl7.fhir.us.davinci-crd_2.2.1@found-6'
|
|
19
24
|
|
|
@@ -35,7 +40,7 @@ module DaVinciCRDTestKit
|
|
|
35
40
|
message: <<~MESSAGE
|
|
36
41
|
**Long Running Request Attestation**:
|
|
37
42
|
|
|
38
|
-
I attest that the user
|
|
43
|
+
I attest that the user was able to continue their workflow
|
|
39
44
|
while waiting for the long-running hook request to return a response:
|
|
40
45
|
|
|
41
46
|
[Click here](#{attest_true_url}) if the above statement is **true**.
|
|
@@ -4,7 +4,7 @@ module DaVinciCRDTestKit
|
|
|
4
4
|
include ClientURLs
|
|
5
5
|
|
|
6
6
|
id :crd_v221_client_service_registration_attestation
|
|
7
|
-
title 'CRD client registers Inferno
|
|
7
|
+
title 'CRD client registers Inferno'
|
|
8
8
|
description %(
|
|
9
9
|
Inferno simulates two CRD servers which can be discovered at the following endpoints:
|
|
10
10
|
- Complete Prefetch Service Discovery Endpoint: #{ClientURLs.discovery_url}
|
|
@@ -21,6 +21,7 @@ module DaVinciCRDTestKit
|
|
|
21
21
|
of the US Core FHIR API and used to verify the `fhirAuthorization.scope`
|
|
22
22
|
hook request field).
|
|
23
23
|
)
|
|
24
|
+
attestation
|
|
24
25
|
verifies_requirements 'cds-hooks_3.0.0-ballot@174'
|
|
25
26
|
|
|
26
27
|
input :complete_prefetch_service_organization_id,
|
data/lib/davinci_crd_test_kit/client/v2.2.1/verify_request/hook_request_prefetch_complete_test.rb
CHANGED
|
@@ -29,6 +29,12 @@ module DaVinciCRDTestKit
|
|
|
29
29
|
During this test, Inferno will verify that each hook request body includes a `prefetch` field populated with
|
|
30
30
|
valid JSON that contains exactly the requested prefetch keys and data sets described in the service description
|
|
31
31
|
for the invoked service as calculated by Inferno based on the resources provided in the request.
|
|
32
|
+
|
|
33
|
+
Note that Inferno requires **all** requested prefetch keys to be present in the request even if there is no
|
|
34
|
+
data for that key. This because the [CDS Hook specification requires](https://cds-hooks.hl7.org/2026Jan/en/#example-of-pagination-in-prefetch)
|
|
35
|
+
that if "the CDS Client has no data to populate a template prefetch key, the prefetch template key MUST have a
|
|
36
|
+
value of null." For templates that request the results of a FHIR search interaction, an empty Bundle is also
|
|
37
|
+
acceptable.
|
|
32
38
|
)
|
|
33
39
|
verifies_requirements 'cds-hooks_3.0.0-ballot@30', 'cds-hooks_3.0.0-ballot@231', 'cds-hooks_3.0.0-ballot@45',
|
|
34
40
|
'cds-hooks_3.0.0-ballot@46', 'cds-hooks_3.0.0-ballot@47', 'cds-hooks_3.0.0-ballot@232',
|
|
@@ -10,12 +10,13 @@ module DaVinciCRDTestKit
|
|
|
10
10
|
include DaVinciCRDTestKit::TaggedRequestLoadHelper
|
|
11
11
|
|
|
12
12
|
id :crd_v221_card_display_attest_test
|
|
13
|
-
title 'Client displays returned decision support details to the user
|
|
13
|
+
title 'Client displays returned decision support details to the user'
|
|
14
14
|
description %(
|
|
15
15
|
During this test, the tester will confirm that the received cards and actions in the
|
|
16
16
|
hook responses have been displayed or otherwise made available to users of the client system
|
|
17
17
|
in an appopriate way that allows for consideration and action if warranted.
|
|
18
18
|
)
|
|
19
|
+
attestation
|
|
19
20
|
|
|
20
21
|
def responded_card_types
|
|
21
22
|
list_card_types_in_requests(requests)
|
|
@@ -88,6 +88,14 @@ module DaVinciCRDTestKit
|
|
|
88
88
|
[cards, actions]
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
def coverage_info_system_action_response?(response_body)
|
|
92
|
+
return false unless response_body.is_a?(Hash)
|
|
93
|
+
|
|
94
|
+
return false unless response_body['systemActions'].is_a?(Array)
|
|
95
|
+
|
|
96
|
+
response_body['systemActions'].any? { |action| coverage_information_response_type?(action) }
|
|
97
|
+
end
|
|
98
|
+
|
|
91
99
|
def coverage_info_response?(response_body)
|
|
92
100
|
cards, actions = coverage_info_content(response_body)
|
|
93
101
|
|
|
@@ -123,6 +131,7 @@ module DaVinciCRDTestKit
|
|
|
123
131
|
|
|
124
132
|
def coverage_information_response_type?(action) # rubocop:disable Metrics/CyclomaticComplexity
|
|
125
133
|
return false unless action.respond_to?(:[])
|
|
134
|
+
return false unless action['type'] == 'update'
|
|
126
135
|
|
|
127
136
|
resource = action['resource']&.to_hash
|
|
128
137
|
return false unless COVERAGE_INFO_EXPECTED_RESOURCE_TYPES.include? resource&.dig('resourceType')
|
|
@@ -172,12 +172,16 @@ module DaVinciCRDTestKit
|
|
|
172
172
|
end
|
|
173
173
|
|
|
174
174
|
check_is_fhir_resource(prefetched_value, target_resource_type: 'Bundle')
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
if prefetched_value['resourceType'] == 'Bundle'
|
|
176
|
+
unless prefetched_value['entry']&.size == 1
|
|
177
|
+
errors << "#{error_prefix} exactly one Coverage must be provided."
|
|
178
|
+
return
|
|
179
|
+
end
|
|
179
180
|
|
|
180
|
-
|
|
181
|
+
check_coverage(prefetched_value.dig('entry', 0, 'resource'))
|
|
182
|
+
elsif prefetched_value['resourceType'] == 'Coverage'
|
|
183
|
+
check_coverage(prefetched_value)
|
|
184
|
+
end
|
|
181
185
|
end
|
|
182
186
|
|
|
183
187
|
def check_coverage(prefetched_coverage)
|
|
@@ -197,16 +201,26 @@ module DaVinciCRDTestKit
|
|
|
197
201
|
"expected active, got #{prefetched_coverage['status']}."
|
|
198
202
|
end
|
|
199
203
|
|
|
200
|
-
|
|
201
|
-
unless prefetched_coverage.dig('beneficiary', 'reference') == "Patient/#{target_patient_id}"
|
|
202
|
-
errors << "#{error_prefix} prefetched Coverage has an unexpected beneficiary reference: " \
|
|
203
|
-
"expected Patient/#{target_patient_id}, got #{prefetched_coverage.dig('beneficiary',
|
|
204
|
-
'reference')}."
|
|
205
|
-
end
|
|
204
|
+
check_coverage_beneficiary(prefetched_coverage)
|
|
206
205
|
|
|
207
206
|
nil
|
|
208
207
|
end
|
|
209
208
|
|
|
209
|
+
def check_coverage_beneficiary(prefetched_coverage)
|
|
210
|
+
target_patient_id = hook_request.dig('context', 'patientId')
|
|
211
|
+
patient_id = target_patient_id&.delete_prefix('Patient/')
|
|
212
|
+
relative_ref = "Patient/#{patient_id}"
|
|
213
|
+
fhir_server = hook_request['fhirServer']&.chomp('/')
|
|
214
|
+
absolute_ref = "#{fhir_server}/#{relative_ref}" if fhir_server.present?
|
|
215
|
+
|
|
216
|
+
actual_ref = prefetched_coverage.dig('beneficiary', 'reference')
|
|
217
|
+
return if actual_ref == relative_ref || (absolute_ref && actual_ref == absolute_ref)
|
|
218
|
+
|
|
219
|
+
expected_refs = [relative_ref, absolute_ref].compact.join(' or ')
|
|
220
|
+
errors << "#{error_prefix} prefetched Coverage has an unexpected beneficiary reference: " \
|
|
221
|
+
"expected #{expected_refs}, got #{actual_ref}."
|
|
222
|
+
end
|
|
223
|
+
|
|
210
224
|
def check_read(prefetched_value, instantiated_request)
|
|
211
225
|
resource_type, resource_id = instantiated_request.split('/')
|
|
212
226
|
|
|
@@ -78,7 +78,8 @@ module DaVinciCRDTestKit
|
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
def draft_orders_conform_to_profiles?(request_body, request_index, ig_semver)
|
|
81
|
-
|
|
81
|
+
draft_orders = request_body.dig('context', 'draftOrders').to_json
|
|
82
|
+
resource = FHIR.from_contents(draft_orders)
|
|
82
83
|
resource_is_valid?(resource:, profile_url: "http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-bundle-request|#{ig_semver}",
|
|
83
84
|
message_prefix: "(Request #{request_index + 1}) context.draftOrders - ")
|
|
84
85
|
end
|
|
@@ -73,12 +73,56 @@ module DaVinciCRDTestKit
|
|
|
73
73
|
conforms_to_logical_model?({ 'cards' => [card] }, logical_model_url(profile_name),
|
|
74
74
|
add_messages_to_runnable: false, validator_response_details: validation_issues)
|
|
75
75
|
|
|
76
|
+
validate_summary_length(card, label)
|
|
77
|
+
validate_suggestion_uuid_presence(card, label) unless ['v201', '2.0.1'].include? ig_semver
|
|
78
|
+
validate_no_smart_suggestions(card, label) if card_type == CardsIdentification::LAUNCH_SMART_APP_RESPONSE_TYPE
|
|
79
|
+
|
|
76
80
|
error_prefix = "#{label} (#{card_type || 'uncategorized'}): "
|
|
77
81
|
filtered_issues = manually_check_card_specific_errors(card, validation_issues, card_type,
|
|
78
82
|
request_body, error_prefix, ig_semver)
|
|
79
83
|
add_messages_not_excluded(filtered_issues, error_prefix)
|
|
80
84
|
end
|
|
81
85
|
|
|
86
|
+
def validate_summary_length(card, label)
|
|
87
|
+
return unless card['summary'].is_a? String
|
|
88
|
+
|
|
89
|
+
summary_length = card['summary'].length
|
|
90
|
+
|
|
91
|
+
return if summary_length < 140
|
|
92
|
+
|
|
93
|
+
add_message(
|
|
94
|
+
'error',
|
|
95
|
+
"#{label} summary length of #{summary_length} characters is longer than " \
|
|
96
|
+
'the maximum allowed of 139 characters.'
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def validate_suggestion_uuid_presence(card, label)
|
|
101
|
+
return unless card['suggestions'].presence.is_a? Array
|
|
102
|
+
return unless card['suggestions'].all?(Hash)
|
|
103
|
+
|
|
104
|
+
card['suggestions'].each_with_index do |suggestion, index|
|
|
105
|
+
next if suggestion['uuid'].present?
|
|
106
|
+
|
|
107
|
+
add_message(
|
|
108
|
+
'error',
|
|
109
|
+
"#{label} suggestion #{index + 1} does not contain a `uuid`"
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def validate_no_smart_suggestions(card, label)
|
|
115
|
+
suggestion_count = card['suggestions']&.length || 0
|
|
116
|
+
|
|
117
|
+
return if suggestion_count.zero?
|
|
118
|
+
|
|
119
|
+
add_message(
|
|
120
|
+
'error',
|
|
121
|
+
"#{label} CDSHooksResponse.cards.suggestions: max allowed = 0, but found #{suggestion_count} " \
|
|
122
|
+
'(from http://hl7.org/fhir/us/davinci-crd/StructureDefinition/CRDHooksResponse-launchSMART|2.2.1)'
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
82
126
|
def validate_system_action_against_logical_model(action, response_index, request_body, action_index, ig_semver)
|
|
83
127
|
label = logical_model_entity_label(response_index, action_index, 'systemAction')
|
|
84
128
|
unless action.is_a?(Hash)
|
|
@@ -104,12 +148,109 @@ module DaVinciCRDTestKit
|
|
|
104
148
|
conforms_to_logical_model?({ 'systemActions' => [action] }, logical_model_url(profile_name),
|
|
105
149
|
add_messages_to_runnable: false, validator_response_details: validation_issues)
|
|
106
150
|
|
|
151
|
+
validation_issues
|
|
152
|
+
.reject! do |issue|
|
|
153
|
+
issue.message.match?(%r{The extension definition http://hl7\.org/fhir/us/davinci-crd/StructureDefinition/CDSHookServiceResponseExtensionIfNoneExist|2\.2\.1 defines the contexts of use as.*CDSHooksResponse.systemActions.extension\z}) # rubocop:disable Layout/LineLength
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
if action_type == CardsIdentification::COVERAGE_INFORMATION_RESPONSE_TYPE
|
|
157
|
+
check_multiple_coverage_info_extension_conformance(action)
|
|
158
|
+
end
|
|
107
159
|
error_prefix = "#{label} (#{action_type || 'uncategorized'}): "
|
|
108
160
|
filtered_issues = manually_check_action_specific_errors(action, validation_issues, action_type,
|
|
109
161
|
request_body, error_prefix, ig_semver)
|
|
110
162
|
add_messages_not_excluded(filtered_issues, error_prefix)
|
|
111
163
|
end
|
|
112
164
|
|
|
165
|
+
def check_multiple_coverage_info_extension_conformance(action)
|
|
166
|
+
resource = FHIR.from_contents(action['resource'].to_json)
|
|
167
|
+
|
|
168
|
+
grouped_coverage_info = extract_and_group_coverage_info(resource)
|
|
169
|
+
multiple_extensions_conformance_check(grouped_coverage_info, resource)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def extract_and_group_coverage_info(resource)
|
|
173
|
+
resource.extension.each_with_object({}) do |extension, grouped_extensions|
|
|
174
|
+
next unless extension.url == COVERAGE_INFO_EXT_URL
|
|
175
|
+
|
|
176
|
+
coverage_key = find_extension_value(extension, 'coverage', 'valueReference', 'reference')
|
|
177
|
+
grouped_extensions[coverage_key] ||= []
|
|
178
|
+
grouped_extensions[coverage_key] << extension
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# For the same coverage, ensure coverage-assertion-ids and satisfied-pa-ids are the same.
|
|
183
|
+
# For different coverages, ensure coverage-assertion-ids and satisfied-pa-ids are distinct.
|
|
184
|
+
def multiple_extensions_conformance_check(grouped_coverage_info, resource)
|
|
185
|
+
resource_ref = "#{resource.resourceType}/#{resource.id}"
|
|
186
|
+
assertion_ids_across_coverages = Set.new
|
|
187
|
+
pa_ids_across_coverages = Set.new
|
|
188
|
+
|
|
189
|
+
grouped_coverage_info.each do |coverage, extensions|
|
|
190
|
+
coverage_assertion_ids = collect_extensions_id(extensions, 'coverage-assertion-id', 'valueString').uniq
|
|
191
|
+
satisfied_pa_ids = collect_extensions_id(extensions, 'satisfied-pa-id', 'valueString').uniq.compact
|
|
192
|
+
if coverage_assertion_ids.length != 1
|
|
193
|
+
add_message(
|
|
194
|
+
'error',
|
|
195
|
+
same_coverage_conformance_error_msg(resource_ref, coverage, 'coverage-assertion-ids')
|
|
196
|
+
)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
if satisfied_pa_ids.length > 1
|
|
200
|
+
add_message(
|
|
201
|
+
'error',
|
|
202
|
+
same_coverage_conformance_error_msg(resource_ref, coverage, 'satisfied-pa-ids')
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
assertion_id = coverage_assertion_ids.first
|
|
207
|
+
if assertion_ids_across_coverages.include?(assertion_id)
|
|
208
|
+
add_message(
|
|
209
|
+
'error',
|
|
210
|
+
different_coverage_conformance_error_msg(resource_ref, 'coverage-assertion-ids')
|
|
211
|
+
)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
assertion_ids_across_coverages.add(assertion_id)
|
|
215
|
+
pa_id = satisfied_pa_ids.first
|
|
216
|
+
next unless pa_id
|
|
217
|
+
|
|
218
|
+
if pa_ids_across_coverages.include?(pa_id)
|
|
219
|
+
add_message(
|
|
220
|
+
'error',
|
|
221
|
+
different_coverage_conformance_error_msg(resource_ref, 'satisfied-pa-ids')
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
pa_ids_across_coverages.add(pa_id)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def collect_extensions_id(extensions, url, *properties)
|
|
230
|
+
extensions.map do |extension|
|
|
231
|
+
find_extension_value(extension, url, *properties)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def find_extension_value(extension, url, *properties)
|
|
236
|
+
found_extension = extension.extension.find { |ext| ext.url == url }
|
|
237
|
+
return nil unless found_extension
|
|
238
|
+
|
|
239
|
+
properties.reduce(found_extension) do |current, prop|
|
|
240
|
+
return current unless current.respond_to?(prop)
|
|
241
|
+
|
|
242
|
+
current.send(prop)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def same_coverage_conformance_error_msg(resource_ref, coverage, id_name)
|
|
247
|
+
"#{resource_ref}: extension has multiple repetitions of coverage `#{coverage}` with different #{id_name}."
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def different_coverage_conformance_error_msg(resource_ref, id_name)
|
|
251
|
+
"#{resource_ref}: extensions referencing differing coverage SHALL have distinct #{id_name}."
|
|
252
|
+
end
|
|
253
|
+
|
|
113
254
|
def add_messages_not_excluded(issues, error_prefix)
|
|
114
255
|
issues.each do |issue|
|
|
115
256
|
next if issue.filtered || logical_model_extension_issue?(issue)
|
|
@@ -133,14 +274,16 @@ module DaVinciCRDTestKit
|
|
|
133
274
|
def manually_check_card_specific_errors(card, validation_issues, card_type, request_body, error_prefix,
|
|
134
275
|
ig_semver)
|
|
135
276
|
case card_type
|
|
136
|
-
when
|
|
277
|
+
when CardsIdentification::FORM_COMPLETION_RESPONSE_TYPE
|
|
137
278
|
manually_check_form_completion_errors(card, validation_issues, error_prefix)
|
|
138
|
-
when
|
|
279
|
+
when CardsIdentification::PROPOSE_ALTERNATIVE_REQUEST_RESPONSE_TYPE
|
|
139
280
|
manually_check_propose_alternative_errors(card, validation_issues, request_body,
|
|
140
281
|
error_prefix, ig_semver)
|
|
141
|
-
when
|
|
282
|
+
when CardsIdentification::ADDITIONAL_ORDERS_RESPONSE_TYPE
|
|
142
283
|
manually_check_additional_orders_errors(card, validation_issues, request_body,
|
|
143
284
|
error_prefix, ig_semver)
|
|
285
|
+
when CardsIdentification::LAUNCH_SMART_APP_RESPONSE_TYPE
|
|
286
|
+
manually_check_launch_smart_app_errors(validation_issues)
|
|
144
287
|
else
|
|
145
288
|
validation_issues
|
|
146
289
|
end
|
|
@@ -194,6 +337,12 @@ module DaVinciCRDTestKit
|
|
|
194
337
|
add_message('error', "#{message_prefix}Questionnaire must have an id.")
|
|
195
338
|
end
|
|
196
339
|
|
|
340
|
+
def manually_check_launch_smart_app_errors(validation_issues)
|
|
341
|
+
validation_issues.reject do |issue|
|
|
342
|
+
issue.message.match?(/CDSHooksResponse.cards.suggestions: minimum required = 1, but only found 0/)
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
197
346
|
def manually_check_propose_alternative_errors(card, validation_issues, request_body,
|
|
198
347
|
error_prefix, ig_semver)
|
|
199
348
|
no_resource_issues = manually_check_action_resources_for_order_profile_conformance(card,
|
|
@@ -201,6 +350,18 @@ module DaVinciCRDTestKit
|
|
|
201
350
|
request_body,
|
|
202
351
|
error_prefix,
|
|
203
352
|
ig_semver)
|
|
353
|
+
|
|
354
|
+
no_resource_issues.each do |issue|
|
|
355
|
+
next unless issue.message.match?(/Constraint failed: crd-respar-1/)
|
|
356
|
+
|
|
357
|
+
new_message =
|
|
358
|
+
"#{issue.message}. In some cases [additional create " \
|
|
359
|
+
'actions](https://hl7.org/fhir/us/davinci-crd/2.2.1/en/cards.html#propose-alternate-request-response-type) ' \
|
|
360
|
+
'are permitted. Manually check the suggestion to verify whether this is an actual error.'
|
|
361
|
+
issue.instance_variable_set(:@severity, 'warning')
|
|
362
|
+
issue.instance_variable_set(:@message, new_message)
|
|
363
|
+
end
|
|
364
|
+
|
|
204
365
|
no_resource_issues.reject do |issue|
|
|
205
366
|
issue.message.match?(/but is fixed to 'create' in the profile/)
|
|
206
367
|
end
|
|
@@ -15,6 +15,10 @@ module DaVinciCRDTestKit
|
|
|
15
15
|
LONG_RUNNING_GROUP_TAG = 'long_running_request'.freeze
|
|
16
16
|
DUPLICATED_HOOK_INSTANCE_TAG = 'duplicate_hook_instance'.freeze
|
|
17
17
|
COVERAGE_INFO_DISABLED_TAG = 'coverage-info-disabled'.freeze
|
|
18
|
+
TECHNICAL_ISSUES_TAG = 'technical-issues'.freeze
|
|
19
|
+
NO_MEMBER_FOUND_TAG = 'no-member-found'.freeze
|
|
20
|
+
COVERAGE_NOT_FOUND_TAG = 'coverage-not-found'.freeze
|
|
21
|
+
NO_ACTIVE_COVERAGE_TAG = 'no-active-coverage'.freeze
|
|
18
22
|
UNKNOWN_CONFIGURATION_TAG = 'unknown-configuration'.freeze
|
|
19
23
|
UNKNOWN_CONTEXT_TAG = 'unknown-context'.freeze
|
|
20
24
|
UNKNOWN_ELEMENT_TAG = 'unknown-element'.freeze
|