davinci_crd_test_kit 0.9.0

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 (83) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/davinci_crd_test_kit/card_responses/companions_prerequisites.json +58 -0
  4. data/lib/davinci_crd_test_kit/card_responses/create_update_coverage_information.json +20 -0
  5. data/lib/davinci_crd_test_kit/card_responses/external_reference.json +21 -0
  6. data/lib/davinci_crd_test_kit/card_responses/instructions.json +14 -0
  7. data/lib/davinci_crd_test_kit/card_responses/launch_smart_app.json +21 -0
  8. data/lib/davinci_crd_test_kit/card_responses/propose_alternate_request.json +71 -0
  9. data/lib/davinci_crd_test_kit/card_responses/request_form_completion.json +227 -0
  10. data/lib/davinci_crd_test_kit/cards_validation.rb +234 -0
  11. data/lib/davinci_crd_test_kit/client_fhir_api_group.rb +762 -0
  12. data/lib/davinci_crd_test_kit/client_hook_request_validation.rb +15 -0
  13. data/lib/davinci_crd_test_kit/client_hooks_group.rb +706 -0
  14. data/lib/davinci_crd_test_kit/client_tests/appointment_book_receive_request_test.rb +71 -0
  15. data/lib/davinci_crd_test_kit/client_tests/client_display_cards_attest.rb +48 -0
  16. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_create_test.rb +40 -0
  17. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_read_test.rb +39 -0
  18. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_search_test.rb +232 -0
  19. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_update_test.rb +40 -0
  20. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_validation_test.rb +60 -0
  21. data/lib/davinci_crd_test_kit/client_tests/decode_auth_token_test.rb +40 -0
  22. data/lib/davinci_crd_test_kit/client_tests/encounter_discharge_receive_request_test.rb +68 -0
  23. data/lib/davinci_crd_test_kit/client_tests/encounter_start_receive_request_test.rb +68 -0
  24. data/lib/davinci_crd_test_kit/client_tests/hook_request_optional_fields_test.rb +41 -0
  25. data/lib/davinci_crd_test_kit/client_tests/hook_request_required_fields_test.rb +40 -0
  26. data/lib/davinci_crd_test_kit/client_tests/hook_request_valid_context_test.rb +63 -0
  27. data/lib/davinci_crd_test_kit/client_tests/hook_request_valid_prefetch_test.rb +151 -0
  28. data/lib/davinci_crd_test_kit/client_tests/order_dispatch_receive_request_test.rb +79 -0
  29. data/lib/davinci_crd_test_kit/client_tests/order_select_receive_request_test.rb +76 -0
  30. data/lib/davinci_crd_test_kit/client_tests/order_sign_receive_request_test.rb +79 -0
  31. data/lib/davinci_crd_test_kit/client_tests/retrieve_jwks_test.rb +65 -0
  32. data/lib/davinci_crd_test_kit/client_tests/token_header_test.rb +34 -0
  33. data/lib/davinci_crd_test_kit/client_tests/token_payload_test.rb +61 -0
  34. data/lib/davinci_crd_test_kit/crd_client_suite.rb +156 -0
  35. data/lib/davinci_crd_test_kit/crd_jwks.json +59 -0
  36. data/lib/davinci_crd_test_kit/crd_options.rb +9 -0
  37. data/lib/davinci_crd_test_kit/crd_server_suite.rb +115 -0
  38. data/lib/davinci_crd_test_kit/ext/inferno_core/runnable.rb +22 -0
  39. data/lib/davinci_crd_test_kit/hook_request_field_validation.rb +410 -0
  40. data/lib/davinci_crd_test_kit/jwks.rb +25 -0
  41. data/lib/davinci_crd_test_kit/jwt_helper.rb +74 -0
  42. data/lib/davinci_crd_test_kit/mock_service_response.rb +421 -0
  43. data/lib/davinci_crd_test_kit/routes/cds-services.json +74 -0
  44. data/lib/davinci_crd_test_kit/routes/cds_services_discovery_handler.rb +18 -0
  45. data/lib/davinci_crd_test_kit/routes/hook_request_endpoint.rb +93 -0
  46. data/lib/davinci_crd_test_kit/routes/jwk_set_endpoint_handler.rb +15 -0
  47. data/lib/davinci_crd_test_kit/server_appointment_book_group.rb +173 -0
  48. data/lib/davinci_crd_test_kit/server_discovery_group.rb +59 -0
  49. data/lib/davinci_crd_test_kit/server_encounter_discharge_group.rb +144 -0
  50. data/lib/davinci_crd_test_kit/server_encounter_start_group.rb +144 -0
  51. data/lib/davinci_crd_test_kit/server_hook_request_validation.rb +15 -0
  52. data/lib/davinci_crd_test_kit/server_hooks_group.rb +69 -0
  53. data/lib/davinci_crd_test_kit/server_order_dispatch_group.rb +173 -0
  54. data/lib/davinci_crd_test_kit/server_order_select_group.rb +169 -0
  55. data/lib/davinci_crd_test_kit/server_order_sign_group.rb +198 -0
  56. data/lib/davinci_crd_test_kit/server_required_card_response_validation_group.rb +23 -0
  57. data/lib/davinci_crd_test_kit/server_tests/additional_orders_validation_test.rb +70 -0
  58. data/lib/davinci_crd_test_kit/server_tests/card_optional_fields_validation_test.rb +47 -0
  59. data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_across_hooks_validation_test.rb +32 -0
  60. data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_received_test.rb +58 -0
  61. data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_validation_test.rb +121 -0
  62. data/lib/davinci_crd_test_kit/server_tests/create_or_update_coverage_info_response_validation_test.rb +72 -0
  63. data/lib/davinci_crd_test_kit/server_tests/discovery_endpoint_test.rb +88 -0
  64. data/lib/davinci_crd_test_kit/server_tests/discovery_services_validation_test.rb +65 -0
  65. data/lib/davinci_crd_test_kit/server_tests/external_reference_card_across_hooks_validation_test.rb +28 -0
  66. data/lib/davinci_crd_test_kit/server_tests/external_reference_card_validation_test.rb +36 -0
  67. data/lib/davinci_crd_test_kit/server_tests/form_completion_response_validation_test.rb +79 -0
  68. data/lib/davinci_crd_test_kit/server_tests/instructions_card_received_across_hooks_test.rb +25 -0
  69. data/lib/davinci_crd_test_kit/server_tests/instructions_card_received_test.rb +28 -0
  70. data/lib/davinci_crd_test_kit/server_tests/launch_smart_app_card_validation_test.rb +38 -0
  71. data/lib/davinci_crd_test_kit/server_tests/propose_alternate_request_card_validation_test.rb +65 -0
  72. data/lib/davinci_crd_test_kit/server_tests/service_call_test.rb +86 -0
  73. data/lib/davinci_crd_test_kit/server_tests/service_request_context_validation_test.rb +30 -0
  74. data/lib/davinci_crd_test_kit/server_tests/service_request_optional_fields_validation_test.rb +41 -0
  75. data/lib/davinci_crd_test_kit/server_tests/service_request_required_fields_validation_test.rb +43 -0
  76. data/lib/davinci_crd_test_kit/server_tests/service_response_validation_test.rb +82 -0
  77. data/lib/davinci_crd_test_kit/suggestion_actions_validation.rb +123 -0
  78. data/lib/davinci_crd_test_kit/tags.rb +8 -0
  79. data/lib/davinci_crd_test_kit/test_helper.rb +23 -0
  80. data/lib/davinci_crd_test_kit/urls.rb +52 -0
  81. data/lib/davinci_crd_test_kit/version.rb +3 -0
  82. data/lib/davinci_crd_test_kit.rb +2 -0
  83. metadata +170 -0
@@ -0,0 +1,32 @@
1
+ require_relative '../test_helper'
2
+
3
+ module DaVinciCRDTestKit
4
+ class CoverageInformationSystemActionAcrossHooksValidationTest < Inferno::Test
5
+ include DaVinciCRDTestKit::TestHelper
6
+
7
+ title 'Valid Coverage Information system actions received across all hooks'
8
+ id :crd_coverage_info_system_action_across_hooks_validation
9
+ description %(
10
+ This test verifies the presence of valid [Coverage Information](https://hl7.org/fhir/us/davinci-crd/STU2/cards.html#coverage-information)
11
+ system action returned by CRD services across all hooks invoked. It verifies the following for each action:
12
+ - The action type is `update`.
13
+ - The resource within the action conforms its respective FHIR profile.
14
+
15
+ Additionally, the test examines the `coverage-info` extensions within the resource to ensure that:
16
+ - Entries referencing differing coverage have distinct `coverage-assertion-ids` and `satisfied-pa-ids`
17
+ (if present).
18
+ - Entries referencing the same coverage have the same `coverage-assertion-ids` and `satisfied-pa-ids`
19
+ (if present).
20
+
21
+ The test will be skipped if no valid Coverage Information system actions are returned across all hooks.
22
+ )
23
+
24
+ run do
25
+ verify_at_least_one_test_passes(
26
+ self.class.parent.parent.groups,
27
+ 'crd_coverage_info_system_action_validation',
28
+ 'None of the hooks invoked returned valid Coverage Info system actions.'
29
+ )
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,58 @@
1
+ require_relative '../test_helper'
2
+ module DaVinciCRDTestKit
3
+ class CoverageInformationSystemActionReceivedTest < Inferno::Test
4
+ include DaVinciCRDTestKit::TestHelper
5
+
6
+ title 'Coverage Information system action was received'
7
+ id :crd_coverage_info_system_action_received
8
+ description %(
9
+ This test validates that a [Coverage Information](https://hl7.org/fhir/us/davinci-crd/STU2/cards.html#coverage-information)
10
+ system action was returned. It does so by:
11
+ - First checking for the presence of actions with a `resource` element of the following types:
12
+ - For `appointment-book`: Appointment
13
+ - For `order-sign` or `order-dispatch`: DeviceRequest, MedicationRequest, NutritionOrder,
14
+ ServiceRequest, or VisionPrescription
15
+ - Then, among the target actions, checking if their resource has the [coverage-information extension](http://hl7.org/fhir/us/davinci-crd/StructureDefinition/ext-coverage-information).
16
+ )
17
+
18
+ input :valid_system_actions
19
+ output :coverage_info
20
+
21
+ def hook_name
22
+ config.options[:hook_name]
23
+ end
24
+
25
+ def resources_by_hook
26
+ shared_resources = [
27
+ 'DeviceRequest', 'MedicationRequest', 'NutritionOrder',
28
+ 'ServiceRequest', 'VisionPrescription'
29
+ ]
30
+ {
31
+ 'appointment-book' => ['Appointment'],
32
+ 'order-sign' => shared_resources,
33
+ 'order-dispatch' => shared_resources
34
+ }
35
+ end
36
+
37
+ run do
38
+ parsed_actions = parse_json(valid_system_actions)
39
+ target_resources = resources_by_hook[hook_name]
40
+
41
+ target_actions = parsed_actions.select do |action|
42
+ resource = FHIR.from_contents(action['resource'].to_json)
43
+ target_resources.include?(resource&.resourceType)
44
+ end
45
+
46
+ coverage_info_system_actions = target_actions.select do |action|
47
+ resource = FHIR.from_contents(action['resource'].to_json)
48
+ coverage_info_ext_url = 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/ext-coverage-information'
49
+ resource.extension.any? { |extension| extension.url == coverage_info_ext_url }
50
+ end
51
+
52
+ assert coverage_info_system_actions.present?,
53
+ "Coverage Information system action was not returned in the #{hook_name} hook response."
54
+
55
+ output coverage_info: coverage_info_system_actions.to_json
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,121 @@
1
+ require_relative '../server_hook_request_validation'
2
+ require_relative '../test_helper'
3
+
4
+ module DaVinciCRDTestKit
5
+ class CoverageInformationSystemActionValidationTest < Inferno::Test
6
+ include DaVinciCRDTestKit::ServerHookRequestValidation
7
+ include DaVinciCRDTestKit::TestHelper
8
+
9
+ title 'All Coverage Information system actions received are valid'
10
+ id :crd_coverage_info_system_action_validation
11
+ description %(
12
+ This test validates all [Coverage Information](https://hl7.org/fhir/us/davinci-crd/STU2/cards.html#coverage-information)
13
+ system actions received. It verifies the following for each action:
14
+ - The action type is `update`.
15
+ - The resource within the action conforms its respective FHIR profile.
16
+
17
+ Additionally, the test examines the `coverage-info` extensions within the resource to ensure that:
18
+ - Entries referencing differing coverage have distinct `coverage-assertion-ids` and `satisfied-pa-ids`
19
+ (if present).
20
+ - Entries referencing the same coverage have the same `coverage-assertion-ids` and `satisfied-pa-ids`
21
+ (if present).
22
+ )
23
+ input :coverage_info
24
+
25
+ def hook_name
26
+ config.options[:hook_name]
27
+ end
28
+
29
+ def find_extension_value(extension, url, *properties)
30
+ found_extension = extension.extension.find { |ext| ext.url == url }
31
+ return nil unless found_extension
32
+
33
+ properties.reduce(found_extension) do |current, prop|
34
+ return current unless current.respond_to?(prop)
35
+
36
+ current.send(prop)
37
+ end
38
+ end
39
+
40
+ def extract_and_group_coverage_info(resource)
41
+ resource.extension.each_with_object({}) do |extension, grouped_extensions|
42
+ next unless extension.url == 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/ext-coverage-information'
43
+
44
+ coverage_key = find_extension_value(extension, 'coverage', 'valueReference', 'reference')
45
+ grouped_extensions[coverage_key] ||= []
46
+ grouped_extensions[coverage_key] << extension
47
+ end
48
+ end
49
+
50
+ # For the same coverage, ensure coverage-assertion-ids and satisfied-pa-ids are the same.
51
+ # For different coverages, ensure coverage-assertion-ids and satisfied-pa-ids are distinct.
52
+ def multiple_extensions_conformance_check(grouped_coverage_info, resource)
53
+ resource_ref = "#{resource.resourceType}/#{resource.id}"
54
+ assertion_ids_across_coverages = Set.new
55
+ pa_ids_across_coverages = Set.new
56
+
57
+ grouped_coverage_info.each do |coverage, extensions|
58
+ coverage_assertion_ids = collect_extensions_id(extensions, 'coverage-assertion-id', 'valueString').uniq
59
+ satisfied_pa_ids = collect_extensions_id(extensions, 'satisfied-pa-id', 'valueString').uniq.compact
60
+ assert coverage_assertion_ids.length == 1,
61
+ same_coverage_conformance_error_msg(resource_ref, coverage, 'coverage-assertion-ids')
62
+
63
+ assert satisfied_pa_ids.length <= 1,
64
+ same_coverage_conformance_error_msg(resource_ref, coverage, 'satisfied-pa-ids')
65
+
66
+ assertion_id = coverage_assertion_ids.first
67
+ assert !assertion_ids_across_coverages.include?(assertion_id),
68
+ different_coverage_conformance_error_msg(resource_ref, 'coverage-assertion-ids')
69
+ assertion_ids_across_coverages.add(assertion_id)
70
+ pa_id = satisfied_pa_ids.first
71
+ next unless pa_id
72
+
73
+ assert !pa_ids_across_coverages.include?(pa_id),
74
+ different_coverage_conformance_error_msg(resource_ref, 'satisfied-pa-ids')
75
+ pa_ids_across_coverages.add(pa_id)
76
+ end
77
+ end
78
+
79
+ def collect_extensions_id(extensions, url, *properties)
80
+ extensions.map do |extension|
81
+ find_extension_value(extension, url, *properties)
82
+ end
83
+ end
84
+
85
+ def same_coverage_conformance_error_msg(resource_ref, coverage, id_name)
86
+ "#{resource_ref}: extension has multiple repetitions of coverage `#{coverage}` with different #{id_name}."
87
+ end
88
+
89
+ def different_coverage_conformance_error_msg(resource_ref, id_name)
90
+ "#{resource_ref}: extensions referencing differing coverage SHALL have distinct #{id_name}."
91
+ end
92
+
93
+ def coverage_info_system_action_check(coverage_info_system_action)
94
+ type = coverage_info_system_action['type']
95
+ assert type, '`type` field is missing.'
96
+ assert type == 'update', "`type` must be `update`, but was `#{type}`"
97
+
98
+ resource = FHIR.from_contents(coverage_info_system_action['resource'].to_json)
99
+ profile_url = structure_definition_map[resource.resourceType]
100
+ assert_valid_resource(resource:, profile_url:)
101
+
102
+ grouped_coverage_info = extract_and_group_coverage_info(resource)
103
+ multiple_extensions_conformance_check(grouped_coverage_info, resource)
104
+ end
105
+
106
+ run do
107
+ parsed_coverage_info = parse_json(coverage_info)
108
+ error_messages = []
109
+ parsed_coverage_info.each do |action|
110
+ coverage_info_system_action_check(action)
111
+ rescue Inferno::Exceptions::AssertionException => e
112
+ error_messages << "Coverage Info system action `#{action}`: #{e.message}"
113
+ end
114
+
115
+ error_messages.each do |msg|
116
+ messages << { type: 'error', message: msg }
117
+ end
118
+ assert error_messages.empty?, 'Some Coverage Info system actions are not valid.'
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,72 @@
1
+ require_relative '../test_helper'
2
+ require_relative '../suggestion_actions_validation'
3
+
4
+ module DaVinciCRDTestKit
5
+ class CreateOrUpdateCoverageInfoResponseValidationTest < Inferno::Test
6
+ include DaVinciCRDTestKit::TestHelper
7
+ include DaVinciCRDTestKit::SuggestionActionsValidation
8
+
9
+ title 'Valid Create or Update Coverage Information cards or system actions received'
10
+ id :crd_create_or_update_coverage_info_response_validation
11
+ description %(
12
+ This test validates the Create or Update Coverage Information cards or system actions received from the
13
+ CRD service, as per the specifications outlined in the [Da Vinci CRD Implementation Guide](https://hl7.org/fhir/us/davinci-crd/STU2/cards.html#create-or-update-coverage-information).
14
+
15
+ - **Checking for Presence:**
16
+ The test first checks if any Create or Update Coverage Information cards or system actions are present in
17
+ the returned valid cards or valid system actions.
18
+ - **For cards**: it ensures there are cards with a `suggestions` array containing a single suggestion,
19
+ and the `actions` array of that suggestion has one `create` or `update` action for the `Coverage` resource.
20
+ - **For system actions**: it checks for the presence of `create` or `update` actions for the `Coverage`
21
+ resource.
22
+
23
+ - **Validating:**
24
+ If any Create or Update Coverage Information cards or system actions are found, each `Coverage` resource is
25
+ validated against the base FHIR Coverage resource.
26
+
27
+ If no Create or Update Coverage Information cards or system actions are received, the test is skipped.
28
+ )
29
+ optional
30
+ input :valid_cards_with_suggestions, :valid_system_actions
31
+
32
+ def hook_name
33
+ config.options[:hook_name]
34
+ end
35
+
36
+ def coverage_actions(actions)
37
+ return [] if actions.nil?
38
+
39
+ valid_types = ['create', 'update']
40
+ actions.filter do |action|
41
+ valid_types.include?(action['type']) && action_resource_type_check(action, ['Coverage'])
42
+ end
43
+ end
44
+
45
+ def create_or_update_coverage_info_card?(card)
46
+ card['suggestions'].one? && coverage_actions(card['suggestions'].first['actions']).one?
47
+ end
48
+
49
+ run do
50
+ parsed_cards = parse_json(valid_cards_with_suggestions)
51
+ parsed_actions = parse_json(valid_system_actions)
52
+
53
+ create_or_update_coverage_info_cards = parsed_cards.filter { |card| create_or_update_coverage_info_card?(card) }
54
+ create_or_update_coverage_info_actions = coverage_actions(parsed_actions)
55
+
56
+ skip_msg = "#{hook_name} hook response does not contain any Create or Update Coverage Information " \
57
+ 'cards or system actions.'
58
+ skip_if create_or_update_coverage_info_cards.blank? && create_or_update_coverage_info_actions.blank?, skip_msg
59
+
60
+ actions_check(create_or_update_coverage_info_actions) if create_or_update_coverage_info_actions.present?
61
+
62
+ if create_or_update_coverage_info_cards.present?
63
+ create_or_update_coverage_info_cards.each do |card|
64
+ actions = card['suggestions'].first['actions']
65
+ actions_check(coverage_actions(actions))
66
+ end
67
+ end
68
+
69
+ no_error_validation('Some Create or Update Coverage Information received are not valid.')
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,88 @@
1
+ module DaVinciCRDTestKit
2
+ class DiscoveryEndpointTest < Inferno::Test
3
+ title 'Server returns a discovery response'
4
+ id :crd_discovery_endpoint_test
5
+ description %(
6
+ A CDS Service provider must expose its discovery endpoint at `{baseURL}/cds-services`
7
+ as specified in the [CDS Hooks Specification](https://cds-hooks.hl7.org/2.0/#discovery).
8
+
9
+ This test checks that the server responds to a GET request at the following endpoint:
10
+
11
+ `GET {baseURL}/cds-services`
12
+
13
+ It does this by checking that the server responds with an HTTP OK 200 status code
14
+ and that the body of the response is a valid JSON object. This test does not
15
+ inspect the structure and content of the response body to see if it contains the required information.
16
+ It only checks to see if the RESTful interaction is supported and returns a valid JSON object.
17
+ )
18
+
19
+ input_order :base_url, :authentication_required, :encryption_method, :jwks_kid
20
+ input :base_url
21
+ input :authentication_required,
22
+ title: 'Discovery endpoint requires authentication?',
23
+ type: 'radio',
24
+ default: 'no',
25
+ options: {
26
+ list_options: [
27
+ {
28
+ label: 'No',
29
+ value: 'no'
30
+ },
31
+ {
32
+ label: 'Yes',
33
+ value: 'yes'
34
+ }
35
+ ]
36
+ }
37
+ input :encryption_method,
38
+ title: 'JWT Signing Algorithm',
39
+ description: <<~DESCRIPTION,
40
+ CDS Hooks recommends ES384 and RS384 for JWT signature verification.
41
+ Select which method to use.
42
+ DESCRIPTION
43
+ type: 'radio',
44
+ default: 'ES384',
45
+ options: {
46
+ list_options: [
47
+ {
48
+ label: 'ES384',
49
+ value: 'ES384'
50
+ },
51
+ {
52
+ label: 'RS384',
53
+ value: 'RS384'
54
+ }
55
+ ]
56
+ }
57
+ input :jwks_kid,
58
+ title: 'CDS Services JWKS kid',
59
+ description: <<~DESCRIPTION,
60
+ The key ID of the JWKS private key to use for signing the JWTs when invoking a CDS service endpoint
61
+ requiring authentication.
62
+ Defaults to the first JWK in the list if no kid is supplied.
63
+ DESCRIPTION
64
+ optional: true
65
+ output :cds_services
66
+
67
+ run do
68
+ discovery_url = "#{base_url.chomp('/')}/cds-services"
69
+ headers = { 'Accept' => 'application/json' }
70
+
71
+ if authentication_required == 'yes'
72
+ token = JwtHelper.build(
73
+ aud: discovery_url,
74
+ iss: inferno_base_url,
75
+ jku: "#{inferno_base_url}/jwks.json",
76
+ kid: jwks_kid,
77
+ encryption_method:
78
+ )
79
+ headers['Authorization'] = "Bearer #{token}"
80
+ end
81
+ get(discovery_url, headers:)
82
+ assert_response_status(200)
83
+ assert_valid_json(request.response_body)
84
+
85
+ output cds_services: request.response_body
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,65 @@
1
+ require_relative '../test_helper'
2
+
3
+ module DaVinciCRDTestKit
4
+ class DiscoveryServicesValidationTest < Inferno::Test
5
+ include DaVinciCRDTestKit::TestHelper
6
+
7
+ title 'Discovery response contains valid services'
8
+ id :crd_discovery_services_validation
9
+ description %(
10
+ As per the [CDS Hooks Spec](https://cds-hooks.hl7.org/2.0/#response),
11
+ the response to the discovery endpoint SHALL be an object containing
12
+ a list of CDS services. If your CDS server hosts no CDS services,
13
+ the discovery endpoint should return a 200 HTTP response with
14
+ an empty array of services.
15
+
16
+ Each CDS service must contain the following required fields:
17
+ `hook`, `description`, and `id`.
18
+
19
+ This test checks for the presence of the required fields and
20
+ validates that they are of the correct type.
21
+
22
+ The test will be skipped if the server hosts no CDS services.
23
+ )
24
+
25
+ input :cds_services
26
+ output :appointment_book_service_ids, :encounter_start_service_ids, :encounter_discharge_service_ids,
27
+ :order_dispatch_service_ids, :order_select_service_ids, :order_sign_service_ids
28
+
29
+ def required_fields
30
+ {
31
+ 'hook' => String,
32
+ 'description' => String,
33
+ 'id' => String
34
+ }
35
+ end
36
+
37
+ run do
38
+ object = parse_json(cds_services)
39
+ assert object['services'], 'Discovery response did not contain `services`'
40
+
41
+ services = object['services']
42
+ assert services.is_a?(Array), 'Services field of the CDS Discovery response object is not an array.'
43
+ skip_if services.empty?, 'Server hosts no CDS Services.'
44
+
45
+ service_hooks_to_ids = services.each_with_object({}) do |service, hash|
46
+ hash[service['hook']] ||= []
47
+ hash[service['hook']] << service['id'] if service['id']
48
+ end
49
+
50
+ output appointment_book_service_ids: service_hooks_to_ids['appointment-book']&.join(', '),
51
+ encounter_start_service_ids: service_hooks_to_ids['encounter-start']&.join(', '),
52
+ encounter_discharge_service_ids: service_hooks_to_ids['encounter-discharge']&.join(', '),
53
+ order_dispatch_service_ids: service_hooks_to_ids['order-dispatch']&.join(', '),
54
+ order_select_service_ids: service_hooks_to_ids['order-select']&.join(', '),
55
+ order_sign_service_ids: service_hooks_to_ids['order-sign']&.join(', ')
56
+
57
+ services.each do |service|
58
+ required_fields.each do |field, type|
59
+ assert(service[field], "Service `#{service}` did not contain required field: `#{field}`")
60
+ assert(service[field].is_a?(type), "Service `#{service}`: field `#{field}` is not of type #{type}")
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,28 @@
1
+ require_relative '../test_helper'
2
+
3
+ module DaVinciCRDTestKit
4
+ class ExternalReferenceCardAcrossHooksValidationTest < Inferno::Test
5
+ include DaVinciCRDTestKit::TestHelper
6
+
7
+ title 'Valid External Reference cards received across all hooks'
8
+ id :crd_external_reference_card_across_hooks_validation
9
+ description %(
10
+ This test verifies the presence of valid External Reference returned by CRD services across all hooks invoked.
11
+ As per the [Da Vinci CRD Implementation Guide](https://hl7.org/fhir/us/davinci-crd/STU2/cards.html#external-reference),
12
+ External Reference cards must contain links with the type set to `absolute`.
13
+ This test checks for the presence of any External Reference cards by verifying:
14
+ - The presence of a `links` array within each card.
15
+ - That every link in the `links` array of a card is of type `absolute`.
16
+
17
+ The test will be skipped if no valid External Reference cards are returned across all hooks.
18
+ )
19
+
20
+ run do
21
+ verify_at_least_one_test_passes(
22
+ self.class.parent.parent.groups,
23
+ 'crd_external_reference_card_validation',
24
+ 'None of the hooks invoked returned an External Reference card.'
25
+ )
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ require_relative '../test_helper'
2
+
3
+ module DaVinciCRDTestKit
4
+ class ExternalReferenceCardValidationTest < Inferno::Test
5
+ include DaVinciCRDTestKit::TestHelper
6
+
7
+ title 'Valid External Reference cards received'
8
+ id :crd_external_reference_card_validation
9
+ description %(
10
+ This test verifies the presence of valid External Reference cards within the list of valid cards
11
+ returned by the CRD service.
12
+ As per the [Da Vinci CRD Implementation Guide](https://hl7.org/fhir/us/davinci-crd/STU2/cards.html#external-reference),
13
+ External Reference cards must contain links with the type set to `absolute`.
14
+ This test checks for the presence of any External Reference cards by verifying:
15
+ - The presence of a `links` array within each card.
16
+ - That every link in the `links` array of a card is of type `absolute`.
17
+ )
18
+
19
+ input :valid_cards_with_links
20
+ optional
21
+
22
+ def hook_name
23
+ config.options[:hook_name]
24
+ end
25
+
26
+ run do
27
+ parsed_cards = parse_json(valid_cards_with_links)
28
+ external_reference_cards = parsed_cards.select do |card|
29
+ links = card['links']
30
+ links.present? && links.all? { |link| link['type'] == 'absolute' }
31
+ end
32
+
33
+ assert external_reference_cards.present?, "#{hook_name} hook response did not contain an External Reference card."
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,79 @@
1
+ require_relative '../test_helper'
2
+ require_relative '../suggestion_actions_validation'
3
+
4
+ module DaVinciCRDTestKit
5
+ class FormCompletionResponseValidationTest < Inferno::Test
6
+ include DaVinciCRDTestKit::TestHelper
7
+ include DaVinciCRDTestKit::SuggestionActionsValidation
8
+
9
+ title 'Valid Request Form Completion cards or system actions received'
10
+ id :crd_request_form_completion_response_validation
11
+ description %(
12
+ This test validates the Request Form Completion cards or system actions received from the CRD service,
13
+ as per the specifications outlined in the [Da Vinci CRD Implementation Guide](https://hl7.org/fhir/us/davinci-crd/STU2/cards.html#request-form-completion).
14
+
15
+ - **Checking for Presence:**
16
+ The test begins by verifying whether any Request Form Completion cards or system actions are present.
17
+ - **For cards:** It ensures that there are cards with `suggestions` containing `create` actions
18
+ for the `Task` resource, specifically:
19
+ - The `Task` must have a `code` of `complete-questionnaire`.
20
+ - The `Task` should include an input of type `text` (`Task.input.type.text`) labeled as `questionnaire`
21
+ and associated with a valid canonical URL (`Task.input.valueCanonical`).
22
+ - **For system actions:** It checks for the presence of `create` actions for the `Task` resource with
23
+ the characteristics described above.
24
+
25
+ - **Validating:**
26
+ If any Request Form Completion cards or system actions are found, the test proceeds to validate them.
27
+ Each `Task` resource is validated against the [CRD Questionnaire Task profile](http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-taskquestionnaire).
28
+
29
+ If no Request Form Completion cards or system actions are received, the test is skipped.
30
+ )
31
+ optional
32
+ input :valid_cards_with_suggestions, :valid_system_actions
33
+
34
+ def hook_name
35
+ config.options[:hook_name]
36
+ end
37
+
38
+ def task_actions(actions)
39
+ actions&.select { |action| action['type'] == 'create' && action_resource_type_check(action, ['Task']) }
40
+ end
41
+
42
+ def task_questionnaire?(task_action)
43
+ task = FHIR.from_contents(task_action['resource'].to_json)
44
+ task.code.coding.any? { |code| code.code == 'complete-questionnaire' } &&
45
+ task.input.any? { |input| input.type.text == 'questionnaire' && valid_url?(input.valueCanonical) }
46
+ end
47
+
48
+ def request_form_completion_card?(card)
49
+ card['suggestions'].all? do |suggestion|
50
+ actions = suggestion['actions']
51
+ task_actions = task_actions(actions)
52
+ task_actions.present? && task_actions.all? { |action| task_questionnaire?(action) }
53
+ end
54
+ end
55
+
56
+ run do
57
+ parsed_cards = parse_json(valid_cards_with_suggestions)
58
+ parsed_actions = parse_json(valid_system_actions)
59
+
60
+ form_completion_cards = parsed_cards.filter { |card| request_form_completion_card?(card) }
61
+ form_completion_actions = task_actions(parsed_actions).select { |action| task_questionnaire?(action) }
62
+
63
+ skip_if form_completion_cards.blank? && form_completion_actions.blank?,
64
+ "#{hook_name} hook response does not contain any Request Form Completion cards or system actions."
65
+
66
+ actions_check(form_completion_actions) if form_completion_actions.present?
67
+
68
+ if form_completion_cards.present?
69
+ form_completion_cards.each do |card|
70
+ card['suggestions'].each do |suggestion|
71
+ actions_check(task_actions(suggestion['actions']))
72
+ end
73
+ end
74
+ end
75
+
76
+ no_error_validation('Some Request Form Completion received are not valid.')
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,25 @@
1
+ require_relative '../test_helper'
2
+
3
+ module DaVinciCRDTestKit
4
+ class InstructionsCardReceivedAcrossHooksTest < Inferno::Test
5
+ include DaVinciCRDTestKit::TestHelper
6
+
7
+ title 'Valid Instructions cards received across all hooks'
8
+ id :crd_valid_instructions_card_received_across_hooks
9
+ description %(
10
+ This test validates that a valid [Instructions card](https://hl7.org/fhir/us/davinci-crd/STU2/cards.html#instructions)
11
+ was received across all hooks responses.
12
+
13
+ The test will be skipped if no valid Instructions cards are returned across all hooks.
14
+ )
15
+
16
+ run do
17
+ verify_at_least_one_test_passes(
18
+ self.class.parent.parent.groups,
19
+ 'crd_valid_instructions_card_received',
20
+ 'None of the hooks invoked returned a valid Instructions card.',
21
+ 'across_hooks'
22
+ )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ require_relative '../test_helper'
2
+
3
+ module DaVinciCRDTestKit
4
+ class InstructionsCardReceivedTest < Inferno::Test
5
+ include DaVinciCRDTestKit::TestHelper
6
+
7
+ title 'Valid Instructions cards received'
8
+ id :crd_valid_instructions_card_received
9
+ description %(
10
+ This test validates that an [Instructions](https://hl7.org/fhir/us/davinci-crd/STU2/cards.html#instructions)
11
+ card was received. It does so by:
12
+ - Checking for the presence of a valid card that does not contain the `links` field and the `suggestions` field.
13
+ )
14
+
15
+ input :valid_cards
16
+ optional
17
+
18
+ def hook_name
19
+ config.options[:hook_name]
20
+ end
21
+
22
+ run do
23
+ parsed_cards = parse_json(valid_cards)
24
+ instructions_card = parsed_cards.find { |card| card['links'].blank? && card['suggestions'].blank? }
25
+ assert instructions_card, 'Hook response did not contain an Instructions card.'
26
+ end
27
+ end
28
+ end