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.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/lib/davinci_pdex_test_kit/docs/payer_client_suite_description_v200.md +91 -0
- data/lib/davinci_pdex_test_kit/docs/payer_server_suite_description_v200.md +119 -0
- data/lib/davinci_pdex_test_kit/ext/inferno_core/record_response_route.rb +98 -0
- data/lib/davinci_pdex_test_kit/ext/inferno_core/request.rb +19 -0
- data/lib/davinci_pdex_test_kit/ext/inferno_core/runnable.rb +18 -0
- data/lib/davinci_pdex_test_kit/fhir_resource_navigation.rb +154 -0
- data/lib/davinci_pdex_test_kit/group_metadata.rb +109 -0
- data/lib/davinci_pdex_test_kit/metadata/mock_capability_statement.json +1052 -0
- data/lib/davinci_pdex_test_kit/metadata/mock_operation_outcome_resource.json +16 -0
- data/lib/davinci_pdex_test_kit/mock_server.rb +247 -0
- data/lib/davinci_pdex_test_kit/must_support_test.rb +252 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/client_member_match_tests/client_member_match_submit_test.rb +24 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/client_member_match_tests/client_member_match_validation_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/client_must_support_tests/client_member_match_must_support_submit_test.rb +26 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/client_must_support_tests/client_member_match_must_support_validation_test.rb +32 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/client_validation_test.rb +94 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/allergyintolerance_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/careplan_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/careteam_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/condition_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/device_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/diagnosticreport_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/documentreference_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/encounter_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/explanationofbenefit_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/goal_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/immunization_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/initial_scratch_storing.rb +34 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/initial_wait_test.rb +28 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/location_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/medicationdispense_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/medicationrequest_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/observation_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/organization_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/patient_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/practitioner_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/practitionerrole_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/clinical_data_request_tests/procedure_clinical_data_request_test.rb +23 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client/collection.rb +46 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_client_suite.rb +152 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/abstract_member_match_request_conformance_test.rb +33 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/abstract_member_match_request_local_references_test.rb +35 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/coverage_to_link_has_minimal_data_test.rb +52 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/coverage_to_link_must_support_test.rb +28 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_id_search_test.rb +58 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_identifier_search_test.rb +58 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_must_support_test.rb +40 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_patient_last_updated_search_test.rb +63 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_patient_service_date_search_test.rb +63 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_patient_type_search_test.rb +63 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_patient_use_search_test.rb +68 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_provenance_revinclude_search_test.rb +52 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_read_test.rb +26 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_reference_resolution_test.rb +43 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit/explanation_of_benefit_validation_test.rb +40 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/explanation_of_benefit_group.rb +105 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/export_patient_group.rb +103 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/export_validation_group.rb +59 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/multiple_member_matches_group.rb +66 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/no_member_matches_group.rb +69 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/workflow_clinical_data.rb +66 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/workflow_everything.rb +184 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/workflow_export.rb +67 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server/workflow_member_match.rb +171 -0
- data/lib/davinci_pdex_test_kit/pdex_payer_server_suite.rb +158 -0
- data/lib/davinci_pdex_test_kit/pdex_provider_client_suite.rb +36 -0
- data/lib/davinci_pdex_test_kit/tags.rb +9 -0
- data/lib/davinci_pdex_test_kit/urls.rb +67 -0
- data/lib/davinci_pdex_test_kit/user_input_response.rb +32 -0
- data/lib/davinci_pdex_test_kit/version.rb +5 -0
- data/lib/davinci_pdex_test_kit.rb +8 -0
- 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
|