davinci_crd_test_kit 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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