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,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