davinci_pdex_test_kit 0.9.1

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