davinci_pdex_test_kit 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/davinci_pdex_test_kit/docs/payer_client_suite_description_v200.md +91 -0
  4. data/lib/davinci_pdex_test_kit/docs/payer_server_suite_description_v200.md +119 -0
  5. data/lib/davinci_pdex_test_kit/ext/inferno_core/record_response_route.rb +98 -0
  6. data/lib/davinci_pdex_test_kit/ext/inferno_core/request.rb +19 -0
  7. data/lib/davinci_pdex_test_kit/ext/inferno_core/runnable.rb +18 -0
  8. data/lib/davinci_pdex_test_kit/fhir_resource_navigation.rb +154 -0
  9. data/lib/davinci_pdex_test_kit/group_metadata.rb +109 -0
  10. data/lib/davinci_pdex_test_kit/metadata/mock_capability_statement.json +1052 -0
  11. data/lib/davinci_pdex_test_kit/metadata/mock_operation_outcome_resource.json +16 -0
  12. data/lib/davinci_pdex_test_kit/mock_server.rb +247 -0
  13. data/lib/davinci_pdex_test_kit/must_support_test.rb +252 -0
  14. data/lib/davinci_pdex_test_kit/pdex_payer_client/client_member_match_tests/client_member_match_submit_test.rb +24 -0
  15. data/lib/davinci_pdex_test_kit/pdex_payer_client/client_member_match_tests/client_member_match_validation_test.rb +23 -0
  16. data/lib/davinci_pdex_test_kit/pdex_payer_client/client_must_support_tests/client_member_match_must_support_submit_test.rb +26 -0
  17. data/lib/davinci_pdex_test_kit/pdex_payer_client/client_must_support_tests/client_member_match_must_support_validation_test.rb +32 -0
  18. data/lib/davinci_pdex_test_kit/pdex_payer_client/client_validation_test.rb +94 -0
  19. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/allergyintolerance_clinical_data_request_test.rb +23 -0
  20. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/careplan_clinical_data_request_test.rb +23 -0
  21. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/careteam_clinical_data_request_test.rb +23 -0
  22. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/condition_clinical_data_request_test.rb +23 -0
  23. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/device_clinical_data_request_test.rb +23 -0
  24. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/diagnosticreport_clinical_data_request_test.rb +23 -0
  25. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/documentreference_clinical_data_request_test.rb +23 -0
  26. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/encounter_clinical_data_request_test.rb +23 -0
  27. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/explanationofbenefit_clinical_data_request_test.rb +23 -0
  28. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/goal_clinical_data_request_test.rb +23 -0
  29. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/immunization_clinical_data_request_test.rb +23 -0
  30. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/initial_scratch_storing.rb +34 -0
  31. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/initial_wait_test.rb +28 -0
  32. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/location_clinical_data_request_test.rb +23 -0
  33. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/medicationdispense_clinical_data_request_test.rb +23 -0
  34. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/medicationrequest_clinical_data_request_test.rb +23 -0
  35. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/observation_clinical_data_request_test.rb +23 -0
  36. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/organization_clinical_data_request_test.rb +23 -0
  37. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/patient_clinical_data_request_test.rb +23 -0
  38. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/practitioner_clinical_data_request_test.rb +23 -0
  39. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/practitionerrole_clinical_data_request_test.rb +23 -0
  40. data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/procedure_clinical_data_request_test.rb +23 -0
  41. data/lib/davinci_pdex_test_kit/pdex_payer_client/collection.rb +46 -0
  42. data/lib/davinci_pdex_test_kit/pdex_payer_client_suite.rb +152 -0
  43. data/lib/davinci_pdex_test_kit/pdex_payer_server/abstract_member_match_request_conformance_test.rb +33 -0
  44. data/lib/davinci_pdex_test_kit/pdex_payer_server/abstract_member_match_request_local_references_test.rb +35 -0
  45. data/lib/davinci_pdex_test_kit/pdex_payer_server/coverage_to_link_has_minimal_data_test.rb +52 -0
  46. data/lib/davinci_pdex_test_kit/pdex_payer_server/coverage_to_link_must_support_test.rb +28 -0
  47. data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_id_search_test.rb +58 -0
  48. data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_identifier_search_test.rb +58 -0
  49. data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_must_support_test.rb +40 -0
  50. data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_patient_last_updated_search_test.rb +63 -0
  51. data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_patient_service_date_search_test.rb +63 -0
  52. data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_patient_type_search_test.rb +63 -0
  53. data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_patient_use_search_test.rb +68 -0
  54. data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_provenance_revinclude_search_test.rb +52 -0
  55. data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_read_test.rb +26 -0
  56. data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_reference_resolution_test.rb +43 -0
  57. data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_validation_test.rb +40 -0
  58. data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit_group.rb +105 -0
  59. data/lib/davinci_pdex_test_kit/pdex_payer_server/export_patient_group.rb +103 -0
  60. data/lib/davinci_pdex_test_kit/pdex_payer_server/export_validation_group.rb +59 -0
  61. data/lib/davinci_pdex_test_kit/pdex_payer_server/multiple_member_matches_group.rb +66 -0
  62. data/lib/davinci_pdex_test_kit/pdex_payer_server/no_member_matches_group.rb +69 -0
  63. data/lib/davinci_pdex_test_kit/pdex_payer_server/workflow_clinical_data.rb +66 -0
  64. data/lib/davinci_pdex_test_kit/pdex_payer_server/workflow_everything.rb +184 -0
  65. data/lib/davinci_pdex_test_kit/pdex_payer_server/workflow_export.rb +67 -0
  66. data/lib/davinci_pdex_test_kit/pdex_payer_server/workflow_member_match.rb +171 -0
  67. data/lib/davinci_pdex_test_kit/pdex_payer_server_suite.rb +158 -0
  68. data/lib/davinci_pdex_test_kit/pdex_provider_client_suite.rb +36 -0
  69. data/lib/davinci_pdex_test_kit/tags.rb +9 -0
  70. data/lib/davinci_pdex_test_kit/urls.rb +67 -0
  71. data/lib/davinci_pdex_test_kit/user_input_response.rb +32 -0
  72. data/lib/davinci_pdex_test_kit/version.rb +5 -0
  73. data/lib/davinci_pdex_test_kit.rb +8 -0
  74. metadata +218 -0
@@ -0,0 +1,16 @@
1
+ {
2
+ "resourceType" : "OperationOutcome",
3
+ "id" : "warning",
4
+ "text" : {
5
+ "status" : "generated",
6
+ "div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\">Inferno does not support a query of this kind.</div>"
7
+ },
8
+ "issue" : [{
9
+ "severity" : "warning",
10
+ "code" : "not-found",
11
+ "details" : {
12
+ "text" : "Inferno does not support a query of this kind."
13
+ }
14
+ }]
15
+ }
16
+
@@ -0,0 +1,247 @@
1
+ require_relative 'user_input_response'
2
+ require_relative 'urls'
3
+ require_relative 'pdex_payer_client/collection'
4
+ require_relative 'pdex_payer_client/client_validation_test'
5
+ #require_relative 'metadata/mock_capability_statement'
6
+
7
+
8
+ module DaVinciPDexTestKit
9
+ # Serve responses to PAS requests
10
+ #
11
+ # Note that there are numerous expected validation issues that can safely be ignored.
12
+ # See here for full list: https://hl7.org/fhir/us/davinci-pas/STU2/qa.html#suppressed
13
+ module MockServer
14
+ include URLs
15
+
16
+ def server_proxy
17
+ @server_proxy ||= Faraday.new(
18
+ url: ENV.fetch('FHIR_REFERENCE_SERVER'),
19
+ params: {},
20
+ headers: {'Content-Type' => 'application/json', 'Authorization' => 'Bearer SAMPLE_TOKEN'},
21
+ )
22
+ end
23
+
24
+ def token_response(request, _test = nil, _test_result = nil)
25
+ # Placeholder for a more complete mock token endpoint
26
+ request.response_body = { access_token: SecureRandom.hex, token_type: 'bearer', expires_in: 300 }.to_json
27
+ request.status = 200
28
+ end
29
+
30
+ def claim_response(request, test = nil, test_result = nil)
31
+ endpoint = resource_endpoint(request.url)
32
+ params = match_request_to_expectation(endpoint, request.query_parameters)
33
+ if params
34
+ response = server_proxy.get(endpoint, params)
35
+ request.status = response.status
36
+ response_resource = replace_bundle_urls(FHIR.from_contents(response.body))
37
+ request.response_headers = response.headers.reject!{|key, value| key == "transfer-encoding"} # chunked causes problems for client
38
+ request.response_body = response_resource.to_json
39
+ else
40
+ response = server_proxy.get('Patient', {_id: 999})
41
+ response_resource = FHIR.from_contents(response.body)
42
+ response_resource.entry = [{fullUrl: 'urn:uuid:2866af9c-137d-4458-a8a9-eeeec0ce5583', resource: mock_operation_outcome_resource, search: {mode: 'outcome'}}]
43
+ response_resource.link.first.url = request.url #specific case for Operation Outcome handling
44
+ request.status = 400
45
+ request.response_body = response_resource.to_json
46
+ end
47
+ end
48
+
49
+ def read_next_page(request, test = nil, test_result = nil)
50
+ response = server_proxy.get('', request.query_parameters)
51
+ request.status = response.status
52
+ request.response_headers = response.headers.reject!{|key, value| key == "transfer-encoding"}
53
+ request.response_body = replace_bundle_urls(FHIR.from_contents(response.body)).to_json
54
+ end
55
+
56
+ def everything_response(request, test = nil, test_result = nil)
57
+ response = server_proxy.get('Patient/999/$everything') #TODO: Change from static response
58
+ request.status = response.status
59
+ request.response_headers = response.headers.reject!{|key, value| key == "transfer-encoding"}
60
+ request.response_body = replace_bundle_urls(FHIR.from_contents(response.body)).to_json
61
+ end
62
+
63
+ # def export_response(request, test = nil, test_result = nil)
64
+ # response = server_proxy.get do |req|
65
+ # req.url 'Group/pdex-Group/$export' #TODO: change from static response
66
+ # req.headers['Prefer'] = 'respond-async'
67
+ # req.headers['Accept'] = 'application/fhir+json'
68
+ # end
69
+ # request.status = response.status
70
+ # request.response_headers = response.env.response_headers
71
+ # request.response_body = response.body
72
+ # end
73
+
74
+ def member_match_response(request, test = nil, test_result = nil)
75
+ #remove token from request as well
76
+ original_request_as_hash = JSON.parse(request.request_body).to_h
77
+ request.request_body = original_request_as_hash.to_json
78
+ #TODO: Change from static response
79
+ request.response_body = {
80
+ resourceType: "Parameters",
81
+ parameter: [
82
+ {
83
+ name: "MemberIdentifier",
84
+ valueIdentifier: {
85
+ type: {
86
+ coding: [
87
+ {
88
+ system: "http://terminology.hl7.org/CodeSystem/v2-0203",
89
+ code: "MB"
90
+ }
91
+ ]
92
+ },
93
+ system: "https://github.com/inferno-framework/target-payer/identifiers/member",
94
+ value: "99999",
95
+ assigner: {
96
+ display: "Old Payer"
97
+ }
98
+ }
99
+ }
100
+ ]
101
+ }.to_json
102
+ request.status = 200
103
+ end
104
+
105
+ def get_metadata
106
+ proc { [200, {'Content-Type' => 'application/fhir+json;charset=utf-8'}, [File.read("lib/davinci_pdex_test_kit/metadata/mock_capability_statement.json")]] }
107
+ end
108
+
109
+ def match_request_to_expectation(endpoint, params)
110
+ matched_search = SEARCHES_BY_PRIORITY[endpoint.to_sym].find {|expectation| (params.keys.map{|key| key.to_s} & expectation).sort == expectation}
111
+ # matched_search_without_patient = SEARCHES_BY_PRIORITY[endpoint.to_sym].find {|expectation| (params.keys.map{|key| key.to_s} << "patient" & expectation) == expectation}
112
+
113
+ if matched_search
114
+ params.select {|key, value| matched_search.include?(key.to_s) || key == "_revInclude" || key == "_include"}
115
+ else
116
+ nil
117
+ end
118
+ # else
119
+ # new_params = params.select {|key, value| matched_search_without_patient.include?(key.to_s) || key == "_revInclude" || key == "_include"}
120
+ # new_params["patient"] = patient_id_from_match_request
121
+ # new_params
122
+ # end
123
+ end
124
+
125
+ def extract_client_id(request)
126
+ URI.decode_www_form(request.request_body).to_h['client_id']
127
+ end
128
+
129
+ # Header expected to be a bearer token of the form "Bearer: <token>"
130
+ def extract_bearer_token(request)
131
+ request.request_header('Authorization')&.value&.split&.last
132
+ end
133
+
134
+ def extract_token_from_query_params(request)
135
+ request.query_parameters['token']
136
+ end
137
+
138
+ # Drop the last two segments of a URL, i.e. the resource type and ID of a FHIR resource
139
+ # e.g. http://example.org/fhir/Patient/123 -> http://example.org/fhir
140
+ # @private
141
+ def base_url(url)
142
+ return unless url.start_with?('http://', 'https://')
143
+
144
+ # Drop everything after the second to last '/', ignoring a trailing slash
145
+ url.sub(%r{/[^/]*/[^/]*(/)?\z}, '')
146
+ end
147
+
148
+ # Pull resource type from url
149
+ # e.g. http://example.org/fhir/Patient/123 -> Patient
150
+ # @private
151
+ def resource_endpoint(url)
152
+ return unless url.start_with?('http://', 'https://')
153
+
154
+ /custom\/pdex_payer_client\/fhir\/(.*)\?/.match(url)[1]
155
+ end
156
+
157
+ # @private
158
+ def referenced_entities(resource, entries, root_url)
159
+ matches = []
160
+ attributes = resource&.source_hash&.keys
161
+ attributes.each do |attr|
162
+ value = resource.send(attr.to_sym)
163
+ if value.is_a?(FHIR::Reference) && value.reference.present?
164
+ match = find_matching_entry(value.reference, entries, root_url)
165
+ if match.present? && matches.none?(match)
166
+ value.reference = match.fullUrl
167
+ matches.concat([match], referenced_entities(match.resource, entries, root_url))
168
+ end
169
+ elsif value.is_a?(Array) && value.all? { |elmt| elmt.is_a?(FHIR::Model) }
170
+ value.each { |val| matches.concat(referenced_entities(val, entries, root_url)) }
171
+ end
172
+ end
173
+
174
+ matches
175
+ end
176
+
177
+ def mock_operation_outcome_resource
178
+ FHIR.from_contents(File.read("lib/davinci_pdex_test_kit/metadata/mock_operation_outcome_resource.json"))
179
+ end
180
+
181
+ def replace_bundle_urls(bundle)
182
+ reference_server_base = ENV.fetch('FHIR_REFERENCE_SERVER')
183
+ bundle.link.map! {|link| {relation: link.relation, url: link.url.gsub(reference_server_base, 'http://localhost:4567/custom/pdex_payer_client/fhir')}}
184
+ bundle&.entry&.map! do |bundled_resource|
185
+ {fullUrl: bundled_resource.fullUrl.gsub(reference_server_base, 'http://localhost:4567/custom/pdex_payer_client/fhir'),
186
+ resource: bundled_resource.resource,
187
+ search: bundled_resource.search
188
+ }
189
+ end
190
+ bundle
191
+ end
192
+
193
+ # @private
194
+ def absolute_reference(ref, entries, root_url)
195
+ url = find_matching_entry(ref&.reference, entries, root_url)&.fullUrl
196
+ ref.reference = url if url
197
+ ref
198
+ end
199
+
200
+ def fetch_all_bundled_resources(
201
+ reply_handler: nil,
202
+ max_pages: 0,
203
+ additional_resource_types: [],
204
+ resource_type: self.resource_type
205
+ )
206
+ page_count = 1
207
+ resources = []
208
+ bundle = resource
209
+ resources += bundle&.entry&.map { |entry| entry&.resource }
210
+
211
+ until bundle.nil? || (page_count == max_pages && max_pages != 0)
212
+
213
+ next_bundle_link = bundle&.link&.find { |link| link.relation == 'next' }&.url
214
+ reply_handler&.call(response)
215
+
216
+ break if next_bundle_link.blank?
217
+
218
+ reply = fhir_client.raw_read_url(next_bundle_link)
219
+
220
+ store_request('outgoing') { reply }
221
+ error_message = cant_resolve_next_bundle_message(next_bundle_link)
222
+
223
+ assert_response_status(200)
224
+ assert_valid_json(reply.body, error_message)
225
+
226
+ bundle = fhir_client.parse_reply(FHIR::Bundle, fhir_client.default_format, reply)
227
+ resources += bundle&.entry&.map { |entry| entry&.resource }
228
+
229
+ page_count += 1
230
+ end
231
+ valid_resource_types = [resource_type, 'OperationOutcome'].concat(additional_resource_types)
232
+ resources
233
+ end
234
+
235
+ # @private
236
+ def find_matching_entry(ref, entries, root_url = '')
237
+ ref = "#{root_url}/#{ref}" if relative_reference?(ref) && root_url&.present?
238
+
239
+ entries&.find { |entry| entry&.fullUrl == ref }
240
+ end
241
+
242
+ # @private
243
+ def relative_reference?(ref)
244
+ ref&.count('/') == 1
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,252 @@
1
+ require_relative 'fhir_resource_navigation'
2
+
3
+ module DaVinciPDexTestKit
4
+ module MustSupportTest
5
+ extend Forwardable
6
+ include FHIRResourceNavigation
7
+
8
+ def_delegators 'self.class', :metadata
9
+
10
+ def all_scratch_resources
11
+ scratch_resources[:all] ||= []
12
+ end
13
+
14
+ def tagged_resources(tag)
15
+ resources = []
16
+ load_tagged_requests(tag)
17
+ return resources if requests.empty?
18
+
19
+ requests.each do |req|
20
+ begin
21
+ bundle = FHIR.from_contents(req.request_body)
22
+ rescue StandardError
23
+ next
24
+ end
25
+
26
+ next unless bundle.is_a?(FHIR::Bundle)
27
+
28
+ resources << bundle
29
+ entry_resources = bundle.entry.map(&:resource)
30
+ resources.concat(entry_resources)
31
+ end
32
+
33
+ resources
34
+ end
35
+
36
+ def all_must_support_errors
37
+ @all_must_support_errors ||= []
38
+ end
39
+
40
+ def validate_must_support
41
+ assert all_must_support_errors.empty?, all_must_support_errors.join("\n")
42
+ end
43
+
44
+ def perform_must_support_test(resources)
45
+ assert resources.present?, "No #{resource_type} resources were found"
46
+
47
+ missing_elements(resources)
48
+ missing_slices(resources)
49
+ missing_extensions(resources)
50
+
51
+ handle_must_support_choices if metadata.must_supports[:choices].present?
52
+
53
+ return unless (missing_elements + missing_slices + missing_extensions).compact.reject(&:empty?).present?
54
+
55
+ pass if (missing_elements + missing_slices + missing_extensions).empty?
56
+ assert false, "Could not find #{missing_must_support_strings.join(', ')} in the #{resources.length} " \
57
+ "provided #{resource_type} resource(s)"
58
+ end
59
+
60
+ def handle_must_support_choices
61
+ missing_elements.delete_if do |element|
62
+ choices = metadata.must_supports[:choices].find { |choice| choice[:paths]&.include?(element[:path]) }
63
+ is_any_choice_supported?(choices)
64
+ end
65
+
66
+ missing_extensions.delete_if do |extension|
67
+ choices = metadata.must_supports[:choices].find { |choice| choice[:extension_ids]&.include?(extension[:id]) }
68
+ is_any_choice_supported?(choices)
69
+ end
70
+
71
+ missing_slices.delete_if do |slice|
72
+ choices = metadata.must_supports[:choices].find { |choice| choice[:slice_names]&.include?(slice[:slice_id]) }
73
+ is_any_choice_supported?(choices)
74
+ end
75
+ end
76
+
77
+ def is_any_choice_supported?(choices)
78
+ choices.present? &&
79
+ (
80
+ choices[:paths]&.any? { |path| missing_elements.none? { |element| element[:path] == path } } ||
81
+ choices[:extension_ids]&.any? do |extension_id|
82
+ missing_extensions.none? do |extension|
83
+ extension[:id] == extension_id
84
+ end
85
+ end ||
86
+ choices[:slice_names]&.any? { |slice_name| missing_slices.none? { |slice| slice[:slice_id] == slice_name } }
87
+ )
88
+ end
89
+
90
+ def missing_must_support_strings
91
+ missing_elements.map { |element_definition| missing_element_string(element_definition) } +
92
+ missing_slices.map { |slice_definition| slice_definition[:slice_id] } +
93
+ missing_extensions.map { |extension_definition| extension_definition[:id] }
94
+ end
95
+
96
+ def missing_element_string(element_definition)
97
+ if element_definition[:fixed_value].present?
98
+ "#{element_definition[:path]}:#{element_definition[:fixed_value]}"
99
+ else
100
+ element_definition[:path]
101
+ end
102
+ end
103
+
104
+ def exclude_uscdi_only_test?
105
+ config.options[:exclude_uscdi_only_test] == true
106
+ end
107
+
108
+ def must_support_extensions
109
+ if exclude_uscdi_only_test?
110
+ metadata.must_supports[:extensions].reject { |extension| extension[:uscdi_only] }
111
+ else
112
+ metadata.must_supports[:extensions]
113
+ end
114
+ end
115
+
116
+ def missing_extensions(resources = [])
117
+ @missing_extensions ||=
118
+ must_support_extensions.select do |extension_definition|
119
+ resources.none? do |resource|
120
+ path = extension_definition[:path]
121
+ if extension_definition[:path] == 'extension'
122
+ resource.extension.any? { |extension| extension.url == extension_definition[:url] }
123
+ else
124
+ extension = find_a_value_at(resource, path) do |el|
125
+ el.url == extension_definition[:url]
126
+ end
127
+
128
+ extension&.url == extension_definition[:url]
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ def must_support_elements
135
+ if exclude_uscdi_only_test?
136
+ metadata.must_supports[:elements].reject { |element| element[:uscdi_only] }
137
+ else
138
+ metadata.must_supports[:elements]
139
+ end
140
+ end
141
+
142
+ def missing_elements(resources = [])
143
+ @missing_elements ||=
144
+ must_support_elements.select do |element_definition|
145
+ # PAS: The MS Claim.supportingInfo slices do not have timing[x]
146
+ next if resource_type == 'Claim' && element_definition[:path] == 'supportingInfo.timing[x]'
147
+
148
+ resources.none? do |resource|
149
+ path = element_definition[:path] # .delete_suffix('[x]')
150
+ value_found = find_a_value_at(resource, path) do |value|
151
+ value_without_extensions =
152
+ value.respond_to?(:to_hash) ? value.to_hash.except('extension') : value
153
+
154
+ (value_without_extensions.present? || value_without_extensions == false) &&
155
+ (element_definition[:fixed_value].blank? || value == element_definition[:fixed_value])
156
+ end
157
+ # Note that false.present? => false, which is why we need to add this extra check
158
+ value_found.present? || value_found == false
159
+ end
160
+ end
161
+ @missing_elements.compact
162
+ end
163
+
164
+ def must_support_slices
165
+ metadata.must_supports[:slices]
166
+ end
167
+
168
+ def missing_slices(resources = [])
169
+ @missing_slices ||=
170
+ must_support_slices.select do |slice|
171
+ resources.none? do |resource|
172
+ path = slice[:path] # .delete_suffix('[x]')
173
+ find_slice(resource, path, slice[:discriminator]).present?
174
+ end
175
+ end
176
+ end
177
+
178
+ def find_slice(resource, path, discriminator)
179
+ find_a_value_at(resource, path) do |element|
180
+ case discriminator[:type]
181
+ when 'patternCodeableConcept'
182
+ coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'
183
+ find_a_value_at(element, coding_path) do |coding|
184
+ coding.code == discriminator[:code] && coding.system == discriminator[:system]
185
+ end
186
+ when 'patternCoding'
187
+ coding_path = discriminator[:path].present? ? discriminator[:path] : ''
188
+ find_a_value_at(element, coding_path) do |coding|
189
+ coding.code == discriminator[:code] && coding.system == discriminator[:system]
190
+ end
191
+ when 'patternIdentifier'
192
+ find_a_value_at(element, discriminator[:path]) { |identifier| identifier.system == discriminator[:system] }
193
+ when 'value'
194
+ values = discriminator[:values].map { |value| value.merge(path: value[:path].split('.')) }
195
+ find_slice_by_values(element, values)
196
+ when 'type'
197
+ case discriminator[:code]
198
+ when 'Date'
199
+ begin
200
+ Date.parse(element)
201
+ rescue ArgumentError
202
+ false
203
+ end
204
+ when 'DateTime'
205
+ begin
206
+ DateTime.parse(element)
207
+ rescue ArgumentError
208
+ false
209
+ end
210
+ when 'String'
211
+ element.is_a? String
212
+ else
213
+ res = element.try(:resource) || element
214
+ res.is_a? FHIR.const_get(discriminator[:code])
215
+ end
216
+ when 'requiredBinding'
217
+ coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'
218
+ find_a_value_at(element, coding_path) { |coding| discriminator[:values].include?(coding.code) }
219
+ end
220
+ end
221
+ end
222
+
223
+ def find_slice_by_values(element, value_definitions)
224
+ path_prefixes = value_definitions.map { |value_definition| value_definition[:path].first }.uniq
225
+ Array.wrap(element).find do |el|
226
+ path_prefixes.all? do |path_prefix|
227
+ value_definitions_for_path =
228
+ value_definitions
229
+ .select { |value_definition| value_definition[:path].first == path_prefix }
230
+ .each { |value_definition| value_definition[:path].shift }
231
+ find_a_value_at(el, path_prefix) do |el_found|
232
+ child_element_value_definitions, current_element_value_definitions =
233
+ value_definitions_for_path.partition { |value_definition| value_definition[:path].present? }
234
+ current_element_values_match = current_element_value_definitions.all? do |value_definition|
235
+ (value_definition[:value].present? && value_definition[:value] == el_found) ||
236
+ (value_definition[:value].blank? && el_found.present?)
237
+ end
238
+
239
+ child_element_values_match =
240
+ if child_element_value_definitions.present?
241
+ find_slice_by_values(el_found, child_element_value_definitions)
242
+ else
243
+ true
244
+ end
245
+
246
+ current_element_values_match && child_element_values_match
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,24 @@
1
+ require_relative '../../urls'
2
+
3
+ module DaVinciPDexTestKit
4
+ class PDexClientMemberMatchSubmitTest < Inferno::Test
5
+ include URLs
6
+
7
+ id :initial_member_match_submit_test
8
+ title 'Client makes a $member-match request'
9
+ description %(
10
+ This test will await a $member-match request and proceed once a request is received.
11
+ )
12
+ input :access_token
13
+
14
+ run do
15
+ wait(
16
+ identifier: access_token,
17
+ message: %(
18
+ Access Token: #{access_token} \n
19
+ Submit a PDex $member-match request to `#{member_match_url}`.
20
+ )
21
+ )
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ require_relative '../client_validation_test.rb'
2
+
3
+ module DaVinciPDexTestKit
4
+ class PDexInitialMemberMatchValidationTest < Inferno::Test
5
+ include DaVinciPDexTestKit::ClientValidationTest
6
+ include URLs
7
+
8
+ id :initial_member_match_validation_test
9
+ title 'Client provides a valid $member-match request'
10
+ description %(
11
+ This test will validate the received $member-match-request input, ensuring it corresponds to the [HRex member-match-in profile](http://hl7.org/fhir/us/davinci-hrex/StructureDefinition/hrex-parameters-member-match-in)
12
+ )
13
+ input :access_token
14
+
15
+ run do
16
+ skip_if !member_match_request.present?, "No previous $member-match request received"
17
+
18
+ parameters = FHIR.from_contents(member_match_request.request_body)
19
+ assert_resource_type(:parameters, resource: parameters)
20
+ assert_valid_resource(resource: parameters, profile_url: 'http://hl7.org/fhir/us/davinci-hrex/StructureDefinition/hrex-parameters-member-match-in')
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ require_relative '../../urls'
2
+
3
+ module DaVinciPDexTestKit
4
+ class PDexClientMemberMatchMustSupportSubmitTest < Inferno::Test
5
+ include URLs
6
+
7
+ id :initial_member_match_must_support_submit_test
8
+ title '$member-match requests span all Must Supports'
9
+ description %(
10
+ This test will receive $member-match requests until the user specifies they are done. It then checks all received $member-match requests for Must Support coverage.
11
+ )
12
+ input :access_token
13
+ config options: { accepts_multiple_requests: true }
14
+
15
+ run do
16
+ wait(
17
+ identifier: access_token,
18
+ message: %(
19
+ Access Token: #{access_token} \n
20
+ Submit PDex $member-match request(s) to `#{member_match_url}`, and [click here](#{resume_pass_url}?token=#{access_token}) when all Must Support
21
+ elements have been covered.
22
+ )
23
+ )
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ require_relative '../client_validation_test.rb'
2
+ require_relative '../../group_metadata.rb'
3
+
4
+ module DaVinciPDexTestKit
5
+ class PDexInitialMemberMatchMustSupportValidationTest < Inferno::Test
6
+ include DaVinciPDexTestKit::MustSupportTest
7
+ include DaVinciPDexTestKit::ClientValidationTest
8
+ include URLs
9
+
10
+ id :initial_member_match_must_support_validation_test
11
+ title 'All must support elements are provided in the received $member-match requests'
12
+ description %(
13
+ This test verifies that the client is capable of making $member-match requests
14
+ )
15
+ input :access_token
16
+
17
+ def resource_type
18
+ 'Parameters'
19
+ end
20
+
21
+ def self.metadata
22
+ @metadata ||= DaVinciPDexTestKit::GroupMetadata.new(YAML.load_file(File.join(__dir__, 'metadata.yml'), aliases: true))
23
+ end
24
+
25
+ run do
26
+
27
+ assert all_member_match_requests, "No previous $member-match requests received"
28
+
29
+ perform_must_support_test(all_member_match_requests.map {|match_request| FHIR::Parameters.new(JSON.parse(match_request.request_body).to_h)})
30
+ end
31
+ end
32
+ end