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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/config/presets/inferno_crd_client_suite_v221.json.erb +64 -0
  3. data/config/presets/inferno_crd_server_suite_v221.json.erb +2 -2
  4. data/lib/davinci_crd_test_kit/client/endpoints/hook_request_endpoint.rb +12 -6
  5. data/lib/davinci_crd_test_kit/client/endpoints/mocked_card_responses/external_reference.json +1 -1
  6. data/lib/davinci_crd_test_kit/client/endpoints/mocked_card_responses/launch_smart_app.json +0 -7
  7. data/lib/davinci_crd_test_kit/client/v2.2.1/client_appointment_book_group.rb +8 -1
  8. data/lib/davinci_crd_test_kit/client/v2.2.1/client_encounter_discharge_group.rb +7 -1
  9. data/lib/davinci_crd_test_kit/client/v2.2.1/client_encounter_start_group.rb +7 -1
  10. data/lib/davinci_crd_test_kit/client/v2.2.1/client_hooks_group.rb +20 -0
  11. data/lib/davinci_crd_test_kit/client/v2.2.1/client_long_running_hook_group.rb +1 -1
  12. data/lib/davinci_crd_test_kit/client/v2.2.1/client_order_dispatch_group.rb +7 -1
  13. data/lib/davinci_crd_test_kit/client/v2.2.1/client_order_select_group.rb +7 -1
  14. data/lib/davinci_crd_test_kit/client/v2.2.1/client_order_sign_group.rb +7 -1
  15. data/lib/davinci_crd_test_kit/client/v2.2.1/client_registration_group.rb +6 -0
  16. data/lib/davinci_crd_test_kit/client/v2.2.1/crd_client_suite.rb +5 -0
  17. data/lib/davinci_crd_test_kit/client/v2.2.1/long_running/client_skip_long_running_attestation_test.rb +10 -5
  18. data/lib/davinci_crd_test_kit/client/v2.2.1/registration/client_service_registration_attestation_test.rb +2 -1
  19. data/lib/davinci_crd_test_kit/client/v2.2.1/verify_request/hook_request_prefetch_complete_test.rb +6 -0
  20. data/lib/davinci_crd_test_kit/client/v2.2.1/verify_response/client_display_cards_attest.rb +2 -1
  21. data/lib/davinci_crd_test_kit/cross_suite/cards_identification.rb +9 -0
  22. data/lib/davinci_crd_test_kit/cross_suite/prefetch_completeness_checker.rb +25 -11
  23. data/lib/davinci_crd_test_kit/cross_suite/requests_logical_model_validation.rb +2 -1
  24. data/lib/davinci_crd_test_kit/cross_suite/response_logical_model_validation.rb +164 -3
  25. data/lib/davinci_crd_test_kit/cross_suite/tags.rb +4 -0
  26. data/lib/davinci_crd_test_kit/requirements/generated/crd_server_v221_requirements_coverage.csv +38 -38
  27. data/lib/davinci_crd_test_kit/server/jobs/invoke_hook.rb +3 -3
  28. data/lib/davinci_crd_test_kit/server/jobs/invoke_hook_no_special_requests.rb +17 -0
  29. data/lib/davinci_crd_test_kit/server/jobs/invoke_hook_with_bad_auth.rb +14 -0
  30. data/lib/davinci_crd_test_kit/server/server_abstract_invoke_hook_test.rb +24 -6
  31. data/lib/davinci_crd_test_kit/server/server_hook_helper.rb +2 -2
  32. data/lib/davinci_crd_test_kit/server/v2.2.1/crd_server_suite.rb +3 -0
  33. data/lib/davinci_crd_test_kit/server/v2.2.1/discovery/discovery_standard_prefetch_expressions_test.rb +166 -0
  34. data/lib/davinci_crd_test_kit/server/v2.2.1/interaction/server_invoke_hook_single_request_test.rb +22 -0
  35. data/lib/davinci_crd_test_kit/server/v2.2.1/server_appointment_book_group.rb +10 -102
  36. data/lib/davinci_crd_test_kit/server/v2.2.1/server_demonstrate_hook_response_group.rb +1 -0
  37. data/lib/davinci_crd_test_kit/server/v2.2.1/server_discovery_group.rb +2 -0
  38. data/lib/davinci_crd_test_kit/server/v2.2.1/server_encounter_discharge_group.rb +10 -97
  39. data/lib/davinci_crd_test_kit/server/v2.2.1/server_encounter_start_group.rb +10 -97
  40. data/lib/davinci_crd_test_kit/server/v2.2.1/server_order_dispatch_group.rb +10 -103
  41. data/lib/davinci_crd_test_kit/server/v2.2.1/server_order_select_group.rb +12 -116
  42. data/lib/davinci_crd_test_kit/server/v2.2.1/server_order_sign_group.rb +12 -121
  43. data/lib/davinci_crd_test_kit/server/v2.2.1/specific_coverage_response_group.rb +23 -0
  44. data/lib/davinci_crd_test_kit/server/v2.2.1/specific_coverage_responses/coverage_info_reason_test.rb +108 -0
  45. data/lib/davinci_crd_test_kit/server/v2.2.1/specific_coverage_responses/coverage_not_found_group.rb +116 -0
  46. data/lib/davinci_crd_test_kit/server/v2.2.1/specific_coverage_responses/no_active_coverage_group.rb +115 -0
  47. data/lib/davinci_crd_test_kit/server/v2.2.1/specific_coverage_responses/no_member_found_group.rb +117 -0
  48. data/lib/davinci_crd_test_kit/server/v2.2.1/specific_coverage_responses/technical_issues_group.rb +121 -0
  49. data/lib/davinci_crd_test_kit/server/v2.2.1/specific_coverage_responses/technical_issues_invoke_test.rb +22 -0
  50. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_request/service_request_context_validation_test.rb +1 -0
  51. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_request/service_request_optional_fields_validation_test.rb +1 -0
  52. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_request/service_request_required_fields_validation_test.rb +1 -0
  53. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/additional_orders_validation_test.rb +78 -35
  54. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/coverage_information_card_absence_test.rb +76 -0
  55. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/coverage_information_system_action_validation_test.rb +70 -124
  56. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/create_or_update_coverage_info_response_validation_test.rb +83 -39
  57. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/external_reference_card_validation_test.rb +59 -17
  58. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/form_completion_response_validation_test.rb +80 -45
  59. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/hook_request_resource_resolution.rb +37 -0
  60. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/instructions_card_received_test.rb +57 -6
  61. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/launch_smart_app_card_validation_test.rb +56 -17
  62. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/propose_alternate_request_card_validation_test.rb +52 -25
  63. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/service_response_validation_test.rb +17 -51
  64. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/unknown_cds_hooks_elements_test.rb +1 -1
  65. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/unknown_configuration_test.rb +1 -1
  66. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/unknown_context_test.rb +1 -1
  67. data/lib/davinci_crd_test_kit/version.rb +2 -2
  68. metadata +18 -8
  69. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/card_optional_fields_validation_test.rb +0 -57
  70. 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
 
@@ -16,7 +16,7 @@ module DaVinciCRDTestKit
16
16
 
17
17
  run_as_group
18
18
 
19
- input_order :cds_jwt_iss
19
+ input_order :long_running_pause_time
20
20
 
21
21
  config(
22
22
  options: {
@@ -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 :cds_jwt_iss, :cds_jwk_set
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 :cds_jwt_iss, :cds_jwk_set
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 :cds_jwt_iss, :cds_jwk_set
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 (Attestation)'
11
+ title 'Client allows the user to continue their workflow during long-running requests'
12
12
  description %(
13
- During this test, the tester will confirm that the client system allowed
14
- its user to continue with their workflow while the sytem was waiting for the previous
15
- long-running hook request to return.
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 had the option to continue their workflow
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 (Attestation)'
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,
@@ -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 (Attestation)'
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
- unless prefetched_value['entry'].size == 1
176
- errors << "#{error_prefix} exactly one Coverage must be provided."
177
- return
178
- end
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
- check_coverage(prefetched_value.dig('entry', 0, 'resource'))
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
- target_patient_id = hook_request.dig('context', 'patientId')
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
- resource = FHIR.from_contents(request_body.dig('context', 'draftOrders')&.to_json)
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 DaVinciCRDTestKit::CardsIdentification::FORM_COMPLETION_RESPONSE_TYPE
277
+ when CardsIdentification::FORM_COMPLETION_RESPONSE_TYPE
137
278
  manually_check_form_completion_errors(card, validation_issues, error_prefix)
138
- when DaVinciCRDTestKit::CardsIdentification::PROPOSE_ALTERNATIVE_REQUEST_RESPONSE_TYPE
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 DaVinciCRDTestKit::CardsIdentification::ADDITIONAL_ORDERS_RESPONSE_TYPE
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