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,71 @@
1
+ require_relative '../urls'
2
+
3
+ module DaVinciCRDTestKit
4
+ class AppointmentBookReceiveRequestTest < Inferno::Test
5
+ include URLs
6
+
7
+ id :crd_appointment_book_request
8
+ title 'Request received for appointment-book hook'
9
+ description %(
10
+ This test waits for an incoming [appointment-book](https://hl7.org/fhir/us/davinci-crd/STU2/hooks.html#appointment-book)
11
+ hook request and responds to the client with the response types selected as an input. This hook is a 'primary'
12
+ hook, meaning that CRD Servers SHALL, at minimum, return a [Coverage Information](https://hl7.org/fhir/us/davinci-crd/STU2/StructureDefinition-ext-coverage-information.html)
13
+ system action for these hooks, even if the response indicates that further information is needed or that the
14
+ level of detail provided is insufficient to determine coverage.
15
+ )
16
+ receives_request :appointment_book
17
+
18
+ input :iss
19
+ input :appointment_book_selected_response_types,
20
+ title: 'Response types to return from appointment-book hook requests',
21
+ description: %(
22
+ Select the cards/action response types that the Inferno hook request endpoints will return. The default
23
+ response type that will be returned for this hook is the `Coverage Information` card type.
24
+ ),
25
+ type: 'checkbox',
26
+ default: ['coverage_information'],
27
+ options: {
28
+ list_options: [
29
+ {
30
+ label: 'External Reference',
31
+ value: 'external_reference'
32
+ },
33
+ {
34
+ label: 'Instructions',
35
+ value: 'instructions'
36
+ },
37
+ {
38
+ label: 'Coverage Information',
39
+ value: 'coverage_information'
40
+ },
41
+ {
42
+ label: 'Request Form Completion',
43
+ value: 'request_form_completion'
44
+ },
45
+ {
46
+ label: 'Create/Update Coverage Information',
47
+ value: 'create_update_coverage_info'
48
+ },
49
+ {
50
+ label: 'Launch SMART Application',
51
+ value: 'launch_smart_app'
52
+ }
53
+ ]
54
+ }
55
+
56
+ run do
57
+ wait(
58
+ identifier: "appointment-book #{iss}",
59
+ message: %(
60
+ **Appointment Book CDS Service Test**:
61
+
62
+ Invoke the appointment-book hook and send a request to:
63
+
64
+ `#{appointment_book_url}`
65
+
66
+ Inferno will process the request and return CDS cards if successful.
67
+ )
68
+ )
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,48 @@
1
+ require_relative '../urls'
2
+
3
+ module DaVinciCRDTestKit
4
+ class ClientCardDisplayAttest < Inferno::Test
5
+ include URLs
6
+
7
+ id :crd_card_display_attest_test
8
+ title 'Check that Cards returned are displayed to the user'
9
+
10
+ input :selected_response_types,
11
+ type: 'checkbox'
12
+
13
+ def format_selected_response_types
14
+ selected_response_types
15
+ .map do |response_type|
16
+ response_type_string =
17
+ response_type.split('_')
18
+ .map(&:capitalize)
19
+ .join(' ')
20
+ .prepend('- ')
21
+ .sub('Smart', 'SMART')
22
+ .sub('Create Update', 'Create/Update')
23
+ .sub('Companions Prerequisites', 'Companions/Prerequisites')
24
+ response_type_string
25
+ end
26
+ .join("\n")
27
+ end
28
+
29
+ run do
30
+ identifier = SecureRandom.hex(32)
31
+ wait(
32
+ identifier:,
33
+ message: <<~MESSAGE
34
+ **Approval Workflow Test**:
35
+
36
+ I attest that the following CDS response types were returned and that the client system displays
37
+ each of the CDS Service Cards to the user:
38
+
39
+ #{format_selected_response_types}
40
+
41
+ [Click here](#{resume_pass_url}?token=#{identifier}) if the above statement is **true**.
42
+
43
+ [Click here](#{resume_fail_url}?token=#{identifier}) if the above statement is **false**.
44
+ MESSAGE
45
+ )
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,40 @@
1
+ module DaVinciCRDTestKit
2
+ class ClientFHIRApiCreateTest < Inferno::Test
3
+ id :crd_client_fhir_api_create_test
4
+ title 'Create Interaction'
5
+ description %(
6
+ Verify that the CRD client supports the create interaction for the given resource. The capabilities required
7
+ by each resource can be found here: https://hl7.org/fhir/us/davinci-crd/CapabilityStatement-crd-client.html#resourcesSummary1
8
+ )
9
+
10
+ input :create_resources,
11
+ type: 'textarea',
12
+ description:
13
+ 'Provide a list of resources to create. e.g., [json_resource_1, json_resource_2]'
14
+
15
+ def resource_type
16
+ config.options[:resource_type]
17
+ end
18
+
19
+ run do
20
+ assert_valid_json(create_resources)
21
+ create_resources_list = JSON.parse(create_resources)
22
+ skip_if(!create_resources_list.is_a?(Array), 'Resources to create not inputted in list format, skipping test.')
23
+
24
+ valid_create_resources =
25
+ create_resources_list
26
+ .compact_blank
27
+ .map { |resource| FHIR.from_contents(resource.to_json) }
28
+ .select { |resource| resource.resourceType == resource_type }
29
+ .select { |resource| resource_is_valid?(resource:) }
30
+
31
+ skip_if(valid_create_resources.blank?,
32
+ %(No valid #{resource_type} resources were provided to send in Create requests, skipping test.))
33
+
34
+ valid_create_resources.each do |create_resource|
35
+ fhir_create(create_resource)
36
+ assert_response_status(201)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,39 @@
1
+ module DaVinciCRDTestKit
2
+ class ClientFHIRApiReadTest < Inferno::Test
3
+ id :crd_client_fhir_api_read_test
4
+ title 'Read Interaction'
5
+ description %(
6
+ Verify that the CRD client supports the read interaction for the given resource. The capabilities required by
7
+ each resource can be found here: https://hl7.org/fhir/us/davinci-crd/CapabilityStatement-crd-client.html#resourcesSummary1
8
+ )
9
+
10
+ input :resource_ids
11
+
12
+ def resource_type
13
+ config.options[:resource_type]
14
+ end
15
+
16
+ def no_resources_skip_message
17
+ "No #{resource_type} resource ids were provided, skipping test. "
18
+ end
19
+
20
+ def bad_resource_id_message(expected_id)
21
+ "Expected resource to have id: `#{expected_id}`, but found `#{resource.id}`"
22
+ end
23
+
24
+ run do
25
+ skip_if resource_ids.blank?, no_resources_skip_message
26
+
27
+ resource_id_list = resource_ids.split(',').map(&:strip)
28
+ assert resource_id_list.present?, "No #{resource_type} id provided."
29
+
30
+ resource_id_list.each do |resource_id_to_read|
31
+ fhir_read resource_type, resource_id_to_read, tags: [resource_type, 'read']
32
+
33
+ assert_response_status(200)
34
+ assert_resource_type(resource_type)
35
+ assert resource.id.present? && resource.id == resource_id_to_read, bad_resource_id_message(resource_id_to_read)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,232 @@
1
+ module DaVinciCRDTestKit
2
+ class ClientFHIRApiSearchTest < Inferno::Test
3
+ id :crd_client_fhir_api_search_test
4
+ title 'Search Interaction'
5
+ description %(
6
+ Verify that the CRD client supports the specified search interaction for the given resource. The capabilities
7
+ required by each resource can be found here: https://hl7.org/fhir/us/davinci-crd/CapabilityStatement-crd-client.html#resourcesSummary1
8
+ )
9
+
10
+ input :search_param_values,
11
+ optional: true
12
+
13
+ attr_accessor :successful_search
14
+
15
+ def resource_type
16
+ config.options[:resource_type]
17
+ end
18
+
19
+ def search_type
20
+ config.options[:search_type]
21
+ end
22
+
23
+ def include_searches
24
+ ['organization_include', 'practitioner_include', 'location_include']
25
+ end
26
+
27
+ def reference_search_paramaters
28
+ ['organization', 'practitioner', 'patient']
29
+ end
30
+
31
+ def bad_resource_id_message(expected_id, actual_id)
32
+ "Expected resource to have id: `#{expected_id}`, but found `#{actual_id}`"
33
+ end
34
+
35
+ def perform_fhir_search(search_params, tags)
36
+ fhir_search(resource_type, params: search_params, tags:)
37
+ assert_response_status(200)
38
+ assert_resource_type(:bundle)
39
+ resource
40
+ end
41
+
42
+ def status_search_result_check(bundle, status)
43
+ return if bundle.entry.empty?
44
+
45
+ self.successful_search = true
46
+
47
+ bundle.entry
48
+ .reject { |entry| entry&.resource&.resourceType == 'OperationOutcome' }
49
+ .map(&:resource)
50
+ .each do |resource|
51
+ assert_resource_type(resource_type, resource:)
52
+ assert(resource.status == status, %(
53
+ Each #{resource_type} resource in search result bundle should have a status of `#{status}`, instead got:
54
+ `#{resource.status}` for resource with id: `#{resource.id}`
55
+ ))
56
+ end
57
+ end
58
+
59
+ def check_id_search_result_entry(bundle_entry, search_id, entry_resource_type)
60
+ assert_resource_type(entry_resource_type, resource: bundle_entry)
61
+
62
+ assert bundle_entry.id.present?, "Expected id field in returned #{entry_resource_type} resource"
63
+
64
+ assert bundle_entry.id == search_id,
65
+ bad_resource_id_message(search_id, bundle_entry.id)
66
+ end
67
+
68
+ def id_search_result_check(bundle, search_id)
69
+ warning do
70
+ assert bundle.entry.any?,
71
+ "Search result bundle is empty for #{resource_type} _id search with an id of `#{search_id}`"
72
+ end
73
+ return if bundle.entry.empty?
74
+
75
+ self.successful_search = true
76
+
77
+ bundle.entry
78
+ .reject { |entry| entry&.resource&.resourceType == 'OperationOutcome' }
79
+ .map(&:resource)
80
+ .each do |resource|
81
+ check_id_search_result_entry(resource, search_id, resource_type)
82
+ end
83
+ end
84
+
85
+ def check_include_reference(base_resource_entry, include_resource_id, include_resource_type)
86
+ base_resource_references = Array.wrap(get_reference_field(include_resource_type, base_resource_entry)).compact
87
+
88
+ assert(base_resource_references.present?, %(
89
+ #{resource_type} resource with id #{base_resource_entry.id} did not include the field that references a
90
+ #{include_resource_type} resource}
91
+ ))
92
+
93
+ base_resource_reference_match_found = base_resource_references.any? do |base_resource_reference|
94
+ base_resource_reference.reference_id == include_resource_id
95
+ end
96
+
97
+ assert(base_resource_reference_match_found, %(
98
+ The #{resource_type} resource in search result bundle with id #{base_resource_entry.id} did not have a
99
+ #{include_resource_type} reference with an id of `#{include_resource_id}`.`
100
+ ))
101
+ end
102
+
103
+ def include_search_result_check(bundle, search_id, included_resource_type) # rubocop:disable Metrics/CyclomaticComplexity
104
+ warning do
105
+ assert bundle.entry.any?,
106
+ "Search result bundle is empty for #{resource_type} _include #{search_type} search with an id
107
+ of `#{search_id}`"
108
+ end
109
+ return if bundle.entry.empty?
110
+
111
+ self.successful_search = true
112
+
113
+ base_resource_entry_list = bundle.entry.select do |entry|
114
+ entry.resource&.resourceType == resource_type
115
+ end
116
+
117
+ assert(base_resource_entry_list.length == 1, %(
118
+ The #{included_resource_type} _include search for #{resource_type} resource with id #{search_id}
119
+ should include exactly 1 #{resource_type} resource, instead got #{base_resource_entry_list.length}.
120
+ ))
121
+
122
+ base_resource_entry = base_resource_entry_list.first.resource
123
+
124
+ bundle.entry
125
+ .map(&:resource)
126
+ .each do |resource|
127
+ entry_resource_type = resource.resourceType
128
+
129
+ if entry_resource_type == resource_type
130
+ check_id_search_result_entry(resource, search_id, entry_resource_type)
131
+ elsif entry_resource_type != 'OperationOutcome'
132
+ entry_resource_type = included_resource_type.capitalize
133
+ assert_resource_type(entry_resource_type, resource:)
134
+
135
+ included_resource_id = resource.id
136
+ assert included_resource_id.present?, "Expected id field in returned #{entry_resource_type} resource"
137
+ check_include_reference(base_resource_entry, included_resource_id, included_resource_type)
138
+ end
139
+ end
140
+ end
141
+
142
+ def get_reference_field(reference_type, entry)
143
+ case reference_type
144
+ when 'patient'
145
+ entry.beneficiary
146
+ when 'practitioner'
147
+ entry.practitioner
148
+ when 'organization'
149
+ if resource_type == 'Encounter'
150
+ entry.serviceProvider
151
+ else
152
+ entry.organization
153
+ end
154
+ when 'location'
155
+ locations = entry.location
156
+ locations.map(&:location)
157
+ end
158
+ end
159
+
160
+ def reference_search_result_check(bundle, reference_id, reference_type)
161
+ warning do
162
+ assert bundle.entry.any?, %(
163
+ Search result bundle is empty for #{resource_type} #{reference_type} search with a #{reference_type} id
164
+ `#{reference_id}`
165
+ )
166
+ end
167
+ return if bundle.entry.empty?
168
+
169
+ self.successful_search = true
170
+
171
+ bundle.entry
172
+ .reject { |entry| entry&.resource&.resourceType == 'OperationOutcome' }
173
+ .map(&:resource)
174
+ .each do |resource|
175
+ assert_resource_type(resource_type, resource:)
176
+
177
+ entry_reference_field = get_reference_field(reference_type, resource)
178
+ assert(
179
+ entry_reference_field.present?,
180
+ %(
181
+ #{resource_type} resource with id #{resource.id} did not include the field that references
182
+ a #{reference_type} resource
183
+ )
184
+ )
185
+
186
+ entry_reference_id = entry_reference_field.reference_id
187
+ assert(
188
+ entry_reference_id == reference_id,
189
+ %(
190
+ The #{resource_type} resource in search result bundle with id #{resource.id} should have a
191
+ #{reference_type} reference with an id of `#{reference_id}`, instead got: `#{entry_reference_id}`
192
+ )
193
+ )
194
+ end
195
+ end
196
+
197
+ run do
198
+ if search_type == 'status'
199
+ coverage_status = ['active', 'cancelled', 'draft', 'entered-in-error']
200
+ coverage_status.each do |status|
201
+ bundle = perform_fhir_search({ status: }, [resource_type, 'status_search'])
202
+ status_search_result_check(bundle, status)
203
+ end
204
+ else
205
+ skip_if search_param_values.blank?, 'No search parameters passed in, skipping test.'
206
+
207
+ search_id_list = search_param_values.split(',').map(&:strip)
208
+ search_id_list.each do |search_id|
209
+ if search_type == '_id'
210
+ bundle = perform_fhir_search({ _id: search_id }, [resource_type, 'id_search'])
211
+ id_search_result_check(bundle, search_id)
212
+ elsif reference_search_paramaters.include?(search_type)
213
+ search_params = {}
214
+ search_params[search_type] = search_id
215
+ bundle = perform_fhir_search(search_params, [resource_type, "#{search_type}_search"])
216
+ reference_search_result_check(bundle, search_id, search_type)
217
+ elsif include_searches.include?(search_type)
218
+ include_resource_type = search_type.gsub('_include', '')
219
+ bundle = perform_fhir_search({ _id: search_id, _include: "#{resource_type}:#{include_resource_type}" },
220
+ [resource_type, "include_#{include_resource_type}_search"])
221
+ include_search_result_check(bundle, search_id, include_resource_type)
222
+ else
223
+ raise StandardError,
224
+ 'Passed in search_type does not match to any of the search types handled by this search test.'
225
+ end
226
+ end
227
+ end
228
+ skip_if !successful_search,
229
+ 'No resources returned in any of the search result bundles.'
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,40 @@
1
+ module DaVinciCRDTestKit
2
+ class ClientFHIRApiUpdateTest < Inferno::Test
3
+ id :crd_client_fhir_api_update_test
4
+ title 'Update Interaction'
5
+ description %(
6
+ Verify that the CRD client supports the update interaction for the given resource. The capabilities required by
7
+ each resource can be found here: https://hl7.org/fhir/us/davinci-crd/CapabilityStatement-crd-client.html#resourcesSummary1
8
+ )
9
+
10
+ input :update_resources,
11
+ type: 'textarea',
12
+ description:
13
+ 'Provide a list of resources to update. e.g., [json_resource_1, json_resource_2]'
14
+
15
+ def resource_type
16
+ config.options[:resource_type]
17
+ end
18
+
19
+ run do
20
+ assert_valid_json(update_resources)
21
+ update_resources_list = JSON.parse(update_resources)
22
+ skip_if(!update_resources_list.is_a?(Array), 'Resources to update not inputted in list format, skipping test.')
23
+
24
+ valid_update_resources =
25
+ update_resources_list
26
+ .compact_blank
27
+ .map { |resource| FHIR.from_contents(resource.to_json) }
28
+ .select { |resource| resource.resourceType == resource_type }
29
+ .select { |resource| resource_is_valid?(resource:) }
30
+
31
+ skip_if(valid_update_resources.blank?,
32
+ %(No valid #{resource_type} resources were provided to send in Update requests, skipping test.))
33
+
34
+ valid_update_resources.each do |update_resource|
35
+ fhir_update(update_resource, update_resource.id)
36
+ assert_response_status([200, 201])
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,60 @@
1
+ module DaVinciCRDTestKit
2
+ class ClientFHIRApiValidationTest < Inferno::Test
3
+ id :crd_client_fhir_api_validation_test
4
+ title 'FHIR Resource Validation'
5
+ description %(
6
+ Verify that the given resources returned from the previous client API interactions are valid resources. Each
7
+ resource is validated against its corresponding [CRD resorce profile](https://hl7.org/fhir/us/davinci-crd/STU2/artifacts.html).
8
+ )
9
+
10
+ def resource_type
11
+ config.options[:resource_type]
12
+ end
13
+
14
+ def structure_definition_map
15
+ {
16
+ 'Practitioner' => 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-practitioner',
17
+ 'PractitionerRole' => 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-practitionerrole',
18
+ 'Patient' => 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-patient',
19
+ 'Encounter' => 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-encounter',
20
+ 'Coverage' => 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-coverage',
21
+ 'Device' => 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-device',
22
+ 'Location' => 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-location',
23
+ 'Organization' => 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-organization'
24
+ }.freeze
25
+ end
26
+
27
+ def profile_url
28
+ structure_definition_map[resource_type]
29
+ end
30
+
31
+ run do
32
+ load_tagged_requests(resource_type)
33
+ skip_if requests.empty?, 'No FHIR api requests were made'
34
+
35
+ requests.keep_if { |req| req.status == 200 }
36
+ skip_if(requests.blank?,
37
+ 'There were no successful FHIR API requests made in previous tests to use in validation.')
38
+
39
+ validated_resources =
40
+ requests
41
+ .map(&:resource)
42
+ .compact
43
+ .flat_map { |resource| resource.is_a?(FHIR::Bundle) ? resource.entry.map(&:resource) : resource }
44
+ .select { |resource| resource.resourceType == resource_type }
45
+ .uniq { |resource| resource.to_reference.reference }
46
+ .each { |resource| resource_is_valid?(resource:, profile_url:) }
47
+
48
+ skip_if(validated_resources.blank?,
49
+ %(No #{resource_type} resources were returned from any of the FHIR API requests made in previous tests
50
+ that could be validated.))
51
+
52
+ validation_error_count = messages.count { |msg| msg[:type] == 'error' }
53
+ assert(validation_error_count.zero?,
54
+ %(#{validation_error_count}/#{validated_resources.length} #{resource_type} resources returned from previous
55
+ test's FHIR API requests failed validation.))
56
+
57
+ skip_if validated_resources.blank?, 'No FHIR resources were made in previous tests that could be validated.'
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,40 @@
1
+ module DaVinciCRDTestKit
2
+ class DecodeAuthTokenTest < Inferno::Test
3
+ id :crd_decode_auth_token
4
+ title 'Bearer token can be decoded'
5
+ description %(
6
+ Verify that the Bearer token is a properly constructed JWT. As per the [CDS hooks specification](https://cds-hooks.hl7.org/2.0#trusting-cds-clients),
7
+ each time a CDS Client transmits a request to a CDS Service which requires authentication, the request MUST
8
+ include an Authorization header presenting the JWT as a "Bearer" token.
9
+ )
10
+
11
+ output :auth_token, :auth_token_payload_json, :auth_token_header_json
12
+
13
+ uses_request :hook_request
14
+
15
+ run do
16
+ authorization_header = request.request_header('Authorization')&.value
17
+ skip_if authorization_header.blank?, 'Request does not include an Authorization header'
18
+
19
+ assert(authorization_header.start_with?('Bearer '),
20
+ 'Authorization token must be a JWT presented as a `Bearer` token')
21
+
22
+ auth_token = authorization_header.delete_prefix('Bearer ')
23
+ output(auth_token:)
24
+
25
+ begin
26
+ payload, header =
27
+ JWT.decode(
28
+ auth_token,
29
+ nil,
30
+ false
31
+ )
32
+
33
+ output auth_token_payload_json: payload.to_json,
34
+ auth_token_header_json: header.to_json
35
+ rescue StandardError => e
36
+ assert false, "Token is not a properly constructed JWT: #{e.message}"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,68 @@
1
+ require_relative '../urls'
2
+
3
+ module DaVinciCRDTestKit
4
+ class EncounterDischargeReceiveRequestTest < Inferno::Test
5
+ include URLs
6
+
7
+ id :crd_encounter_discharge_request
8
+ title 'Request received for encounter-discharge hook'
9
+ description %(
10
+ This test waits for an incoming [encounter-discharge](https://hl7.org/fhir/us/davinci-crd/STU2/hooks.html#encounter-discharge)
11
+ hook request and responds to the client with the response types selected as an input.
12
+ )
13
+ receives_request :encounter_discharge
14
+
15
+ input :iss
16
+ input :encounter_discharge_selected_response_types,
17
+ title: 'Response types to return from encounter-discharge hook requests',
18
+ description: %(
19
+ Select the cards/action response types that the Inferno hook request endpoints will return. The default
20
+ response type that will be returned for this hook is the `Instructions` card type.
21
+ ),
22
+ type: 'checkbox',
23
+ default: ['instructions'],
24
+ options: {
25
+ list_options: [
26
+ {
27
+ label: 'External Reference',
28
+ value: 'external_reference'
29
+ },
30
+ {
31
+ label: 'Instructions',
32
+ value: 'instructions'
33
+ },
34
+ {
35
+ label: 'Coverage Information',
36
+ value: 'coverage_information'
37
+ },
38
+ {
39
+ label: 'Request Form Completion',
40
+ value: 'request_form_completion'
41
+ },
42
+ {
43
+ label: 'Create/Update Coverage Information',
44
+ value: 'create_update_coverage_info'
45
+ },
46
+ {
47
+ label: 'Launch SMART Application',
48
+ value: 'launch_smart_app'
49
+ }
50
+ ]
51
+ }
52
+
53
+ run do
54
+ wait(
55
+ identifier: "encounter-discharge #{iss}",
56
+ message: %(
57
+ **Encounter Discharge CDS Service Test**:
58
+
59
+ Invoke the encounter-discharge hook and send a request to:
60
+
61
+ `#{encounter_discharge_url}`
62
+
63
+ Inferno will process the request and return CDS cards if successful.
64
+ )
65
+ )
66
+ end
67
+ end
68
+ end