onc_certification_g10_test_kit 2.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/lib/inferno/exceptions.rb +31 -0
- data/lib/inferno/ext/bloomer.rb +24 -0
- data/lib/inferno/repositiories/validators.rb +17 -0
- data/lib/inferno/repositiories/value_sets.rb +26 -0
- data/lib/inferno/terminology/bcp47.rb +95 -0
- data/lib/inferno/terminology/bcp_13.rb +26 -0
- data/lib/inferno/terminology/codesystem.rb +49 -0
- data/lib/inferno/terminology/expected_manifest.yml +1123 -0
- data/lib/inferno/terminology/fhir_package_manager.rb +69 -0
- data/lib/inferno/terminology/loader.rb +298 -0
- data/lib/inferno/terminology/tasks/check_built_terminology.rb +77 -0
- data/lib/inferno/terminology/tasks/cleanup.rb +13 -0
- data/lib/inferno/terminology/tasks/cleanup_precursors.rb +23 -0
- data/lib/inferno/terminology/tasks/count_codes_in_value_set.rb +20 -0
- data/lib/inferno/terminology/tasks/create_value_set_validators.rb +34 -0
- data/lib/inferno/terminology/tasks/download_fhir_terminology.rb +27 -0
- data/lib/inferno/terminology/tasks/download_umls.rb +109 -0
- data/lib/inferno/terminology/tasks/download_umls_notice.rb +20 -0
- data/lib/inferno/terminology/tasks/expand_value_set_to_file.rb +36 -0
- data/lib/inferno/terminology/tasks/process_umls.rb +91 -0
- data/lib/inferno/terminology/tasks/process_umls_translations.rb +85 -0
- data/lib/inferno/terminology/tasks/run_umls_jar.rb +75 -0
- data/lib/inferno/terminology/tasks/temp_dir.rb +27 -0
- data/lib/inferno/terminology/tasks/unzip_umls.rb +42 -0
- data/lib/inferno/terminology/tasks/validate_code.rb +36 -0
- data/lib/inferno/terminology/tasks.rb +11 -0
- data/lib/inferno/terminology/terminology_configuration.rb +52 -0
- data/lib/inferno/terminology/terminology_validation.rb +42 -0
- data/lib/inferno/terminology/validator.rb +64 -0
- data/lib/inferno/terminology/value_set.rb +462 -0
- data/lib/inferno/terminology.rb +16 -0
- data/lib/onc_certification_g10_test_kit/authorization_request_builder.rb +87 -0
- data/lib/onc_certification_g10_test_kit/base_token_refresh_group.rb +48 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_authorization.rb +235 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_group_export.rb +255 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_group_export_validation.rb +474 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_jwks.json +58 -0
- data/lib/onc_certification_g10_test_kit/bulk_export_validation_tester.rb +171 -0
- data/lib/onc_certification_g10_test_kit/configuration_checker.rb +104 -0
- data/lib/onc_certification_g10_test_kit/export_kick_off_performer.rb +12 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodyheight.json +3772 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodytemp.json +3772 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodyweight.json +3772 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bp.json +6034 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-heartrate.json +3756 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-resprate.json +3756 -0
- data/lib/onc_certification_g10_test_kit/limited_scope_grant_test.rb +66 -0
- data/lib/onc_certification_g10_test_kit/multi_patient_api.rb +43 -0
- data/lib/onc_certification_g10_test_kit/patient_context_test.rb +30 -0
- data/lib/onc_certification_g10_test_kit/profile_guesser.rb +69 -0
- data/lib/onc_certification_g10_test_kit/resource_access_test.rb +96 -0
- data/lib/onc_certification_g10_test_kit/restricted_access_test.rb +12 -0
- data/lib/onc_certification_g10_test_kit/restricted_resource_type_access_group.rb +303 -0
- data/lib/onc_certification_g10_test_kit/smart_app_launch_invalid_aud_group.rb +136 -0
- data/lib/onc_certification_g10_test_kit/smart_ehr_practitioner_app_group.rb +209 -0
- data/lib/onc_certification_g10_test_kit/smart_invalid_token_group.rb +197 -0
- data/lib/onc_certification_g10_test_kit/smart_limited_app_group.rb +123 -0
- data/lib/onc_certification_g10_test_kit/smart_public_standalone_launch_group.rb +113 -0
- data/lib/onc_certification_g10_test_kit/smart_scopes_test.rb +153 -0
- data/lib/onc_certification_g10_test_kit/smart_standalone_patient_app_group.rb +177 -0
- data/lib/onc_certification_g10_test_kit/terminology_binding_validator.rb +140 -0
- data/lib/onc_certification_g10_test_kit/token_revocation_group.rb +133 -0
- data/lib/onc_certification_g10_test_kit/unauthorized_access_test.rb +25 -0
- data/lib/onc_certification_g10_test_kit/unrestricted_resource_type_access_group.rb +375 -0
- data/lib/onc_certification_g10_test_kit/version.rb +3 -0
- data/lib/onc_certification_g10_test_kit/visual_inspection_and_attestations_group.rb +470 -0
- data/lib/onc_certification_g10_test_kit/well_known_capabilities_test.rb +37 -0
- data/lib/onc_certification_g10_test_kit.rb +223 -0
- metadata +310 -0
@@ -0,0 +1,171 @@
|
|
1
|
+
require_relative 'profile_guesser'
|
2
|
+
|
3
|
+
module ONCCertificationG10TestKit
|
4
|
+
module BulkExportValidationTester
|
5
|
+
include USCoreTestKit::MustSupportTest
|
6
|
+
include ProfileGuesser
|
7
|
+
|
8
|
+
attr_reader :metadata
|
9
|
+
|
10
|
+
MAX_NUM_COLLECTED_LINES = 100
|
11
|
+
MIN_RESOURCE_COUNT = 2
|
12
|
+
|
13
|
+
def observation_metadata
|
14
|
+
[
|
15
|
+
USCoreTestKit::PediatricBmiForAgeGroup.metadata,
|
16
|
+
USCoreTestKit::PediatricWeightForHeightGroup.metadata,
|
17
|
+
USCoreTestKit::ObservationLabGroup.metadata,
|
18
|
+
USCoreTestKit::PulseOximetryGroup.metadata,
|
19
|
+
USCoreTestKit::SmokingstatusGroup.metadata,
|
20
|
+
USCoreTestKit::HeadCircumferenceGroup.metadata,
|
21
|
+
USCoreTestKit::BpGroup.metadata,
|
22
|
+
USCoreTestKit::BodyheightGroup.metadata,
|
23
|
+
USCoreTestKit::BodytempGroup.metadata,
|
24
|
+
USCoreTestKit::BodyweightGroup.metadata,
|
25
|
+
USCoreTestKit::HeartrateGroup.metadata,
|
26
|
+
USCoreTestKit::ResprateGroup.metadata
|
27
|
+
]
|
28
|
+
end
|
29
|
+
|
30
|
+
def diagnostic_metadata
|
31
|
+
[USCoreTestKit::DiagnosticReportLabGroup.metadata, USCoreTestKit::DiagnosticReportNoteGroup.metadata]
|
32
|
+
end
|
33
|
+
|
34
|
+
def determine_metadata
|
35
|
+
return observation_metadata if resource_type == 'Observation'
|
36
|
+
return diagnostic_metadata if resource_type == 'DiagnosticReport'
|
37
|
+
|
38
|
+
if resource_type == 'Location' || resource_type == 'Medication'
|
39
|
+
return Array.wrap(USCoreTestKit::USCoreTestSuite.metadata.find do |meta|
|
40
|
+
meta.resource == resource_type
|
41
|
+
end)
|
42
|
+
end
|
43
|
+
["USCoreTestKit::#{resource_type}Group".constantize.metadata]
|
44
|
+
end
|
45
|
+
|
46
|
+
def metadata_list
|
47
|
+
@metadata_list ||= determine_metadata
|
48
|
+
end
|
49
|
+
|
50
|
+
def patient_ids_seen
|
51
|
+
scratch[:patient_ids_seen] ||= []
|
52
|
+
end
|
53
|
+
|
54
|
+
def build_headers(use_token)
|
55
|
+
headers = { accept: 'application/fhir+ndjson' }
|
56
|
+
headers.merge!({ authorization: "Bearer #{bearer_token}" }) if use_token == 'true'
|
57
|
+
headers
|
58
|
+
end
|
59
|
+
|
60
|
+
def stream_ndjson(endpoint, headers, process_chunk_line, process_response)
|
61
|
+
hanging_chunk = String.new
|
62
|
+
|
63
|
+
process_body = proc { |chunk|
|
64
|
+
hanging_chunk << chunk
|
65
|
+
chunk_by_lines = hanging_chunk.lines
|
66
|
+
|
67
|
+
hanging_chunk = chunk_by_lines.pop || String.new
|
68
|
+
|
69
|
+
chunk_by_lines.each do |elem|
|
70
|
+
process_chunk_line.call(elem)
|
71
|
+
end
|
72
|
+
}
|
73
|
+
|
74
|
+
stream(process_body, endpoint, headers: headers)
|
75
|
+
|
76
|
+
process_chunk_line.call(hanging_chunk)
|
77
|
+
process_response.call(response)
|
78
|
+
end
|
79
|
+
|
80
|
+
def predefined_device_type?(resource) # rubocop:disable Metrics/CyclomaticComplexity
|
81
|
+
return true if bulk_device_types_in_group.blank?
|
82
|
+
|
83
|
+
expected = Set.new(bulk_device_types_in_group.split(',').map(&:strip))
|
84
|
+
|
85
|
+
actual = resource&.type&.coding&.filter_map do |coding|
|
86
|
+
coding.code if coding.system.nil? || coding.system == 'http://snomed.info/sct'
|
87
|
+
end
|
88
|
+
|
89
|
+
(expected & actual).any?
|
90
|
+
end
|
91
|
+
|
92
|
+
def determine_profile(resource)
|
93
|
+
return if resource.resourceType == 'Device' && !predefined_device_type?(resource)
|
94
|
+
|
95
|
+
guess_profile(resource)
|
96
|
+
end
|
97
|
+
|
98
|
+
def validate_conformance(resources)
|
99
|
+
metadata_list.each do |meta|
|
100
|
+
skip_if resources[meta.profile_url].blank?,
|
101
|
+
"No #{resource_type} resources found that conform to profile: #{meta.profile_url}."
|
102
|
+
@metadata = meta
|
103
|
+
@missing_elements = nil
|
104
|
+
@missing_slices = nil
|
105
|
+
begin
|
106
|
+
perform_must_support_test(resources[meta.profile_url])
|
107
|
+
rescue Inferno::Exceptions::PassException
|
108
|
+
next
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def check_file_request(url) # rubocop:disable Metrics/CyclomaticComplexity
|
114
|
+
line_count = 0
|
115
|
+
resources = Hash.new { |h, k| h[k] = [] }
|
116
|
+
|
117
|
+
process_line = proc { |line|
|
118
|
+
next unless lines_to_validate.blank? ||
|
119
|
+
line_count < lines_to_validate.to_i ||
|
120
|
+
(resource_type == 'Patient' && patient_ids_seen.length < MIN_RESOURCE_COUNT)
|
121
|
+
|
122
|
+
line_count += 1
|
123
|
+
|
124
|
+
begin
|
125
|
+
resource = FHIR.from_contents(line)
|
126
|
+
rescue StandardError
|
127
|
+
skip "Server response at line \"#{line_count}\" is not a processable FHIR resource."
|
128
|
+
end
|
129
|
+
|
130
|
+
skip_if resource.resourceType != resource_type,
|
131
|
+
"Resource type \"#{resource.resourceType}\" at line \"#{line_count}\" does not match type " \
|
132
|
+
"defined in output \"#{resource_type}\""
|
133
|
+
|
134
|
+
profile_url = determine_profile(resource)
|
135
|
+
resources[profile_url] << resource
|
136
|
+
scratch[:patient_ids_seen] = patient_ids_seen | [resource.id] if resource_type == 'Patient'
|
137
|
+
|
138
|
+
skip_if !resource_is_valid?(resource: resource, profile_url: profile_url),
|
139
|
+
"Resource at line \"#{line_count}\" does not conform to profile \"#{profile_url}\"."
|
140
|
+
}
|
141
|
+
|
142
|
+
process_headers = proc { |response|
|
143
|
+
value = (response[:headers].find { |header| header.name.downcase == 'content-type' })&.value
|
144
|
+
unless value&.start_with?('application/fhir+ndjson')
|
145
|
+
skip "Content type must have 'application/fhir+ndjson' but found '#{value}'"
|
146
|
+
end
|
147
|
+
}
|
148
|
+
|
149
|
+
stream_ndjson(url, build_headers(requires_access_token), process_line, process_headers)
|
150
|
+
validate_conformance(resources)
|
151
|
+
|
152
|
+
line_count
|
153
|
+
end
|
154
|
+
|
155
|
+
def perform_bulk_export_validation
|
156
|
+
skip_if status_output.blank?, 'Could not verify this functionality when Bulk Status Output is not provided'
|
157
|
+
skip_if (requires_access_token == 'true' && bearer_token.blank?),
|
158
|
+
'Could not verify this functionality when Bearer Token is required and not provided'
|
159
|
+
|
160
|
+
file_list = JSON.parse(status_output).select { |file| file['type'] == resource_type }
|
161
|
+
skip_if file_list.empty?, "No #{resource_type} resource file item returned by server."
|
162
|
+
|
163
|
+
success_count = 0
|
164
|
+
file_list.each do |file|
|
165
|
+
success_count += check_file_request(file['url'])
|
166
|
+
end
|
167
|
+
|
168
|
+
pass "Successfully validated #{success_count} #{resource_type} resource(s)."
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require_relative '../inferno/terminology/tasks/check_built_terminology'
|
2
|
+
|
3
|
+
module ONCCertificationG10TestKit
|
4
|
+
class ConfigurationChecker
|
5
|
+
EXPECTED_VALIDATOR_VERSION = '2.1.0'.freeze
|
6
|
+
|
7
|
+
def configuration_messages
|
8
|
+
validator_version_message + terminology_messages
|
9
|
+
end
|
10
|
+
|
11
|
+
def terminology_checker
|
12
|
+
@terminology_checker ||= Inferno::Terminology::Tasks::CheckBuiltTerminology.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def mismatched_value_sets
|
16
|
+
terminology_checker.mismatched_value_sets
|
17
|
+
end
|
18
|
+
|
19
|
+
def validator_url
|
20
|
+
@validator_url ||= G10CertificationSuite.find_validator(:default).url
|
21
|
+
end
|
22
|
+
|
23
|
+
def validator_version_message
|
24
|
+
response = Faraday.get "#{validator_url}/version"
|
25
|
+
version = response.body
|
26
|
+
|
27
|
+
if version == EXPECTED_VALIDATOR_VERSION
|
28
|
+
[{
|
29
|
+
type: 'info',
|
30
|
+
message: "FHIR validator is the expected version `#{EXPECTED_VALIDATOR_VERSION}`"
|
31
|
+
}]
|
32
|
+
else
|
33
|
+
[{
|
34
|
+
type: 'error',
|
35
|
+
message: "Expected FHIR validator version `#{EXPECTED_VALIDATOR_VERSION}`, but found `#{version}`"
|
36
|
+
}]
|
37
|
+
end
|
38
|
+
rescue StandardError => e
|
39
|
+
[{
|
40
|
+
type: 'error',
|
41
|
+
message: "Unable to connect to Validator: `#{e.message}`"
|
42
|
+
}]
|
43
|
+
end
|
44
|
+
|
45
|
+
def terminology_messages # rubocop:disable Metrics/CyclomaticComplexity
|
46
|
+
success_messages = []
|
47
|
+
warning_messages = []
|
48
|
+
error_messages = []
|
49
|
+
messages = []
|
50
|
+
terminology_checker.expected_manifest.each do |expected_value_set|
|
51
|
+
url = expected_value_set[:url]
|
52
|
+
actual_value_set = terminology_checker.new_value_set(url)
|
53
|
+
|
54
|
+
if actual_value_set == expected_value_set
|
55
|
+
success_messages << "* `#{url}`: #{actual_value_set[:count]} codes"
|
56
|
+
elsif actual_value_set.nil?
|
57
|
+
error_messages << "* `#{url}`: Not loaded"
|
58
|
+
elsif terminology_checker.class::MIME_TYPE_SYSTEMS.include? url
|
59
|
+
warning_messages <<
|
60
|
+
"* `#{url}`: Expected codes: #{expected_value_set[:count]} Actual codes: #{actual_value_set[:count]}"
|
61
|
+
else
|
62
|
+
error_messages <<
|
63
|
+
"* `#{url}`: Expected codes: #{expected_value_set[:count]} Actual codes: #{actual_value_set[:count]}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
if success_messages.present?
|
68
|
+
messages << {
|
69
|
+
type: 'info',
|
70
|
+
message:
|
71
|
+
"The following value sets and code systems have been properly loaded:\n#{success_messages.join("\n")}"
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
if warning_messages.present?
|
76
|
+
warning_message = <<~WARNING
|
77
|
+
Mime-type based terminology did not exactly match. This can be the
|
78
|
+
result of using a slightly different version of the `mime-types-data`
|
79
|
+
gem and does not reflect a problem with the terminology build as long
|
80
|
+
as the expected and actual number of codes are close to each other.
|
81
|
+
WARNING
|
82
|
+
messages << {
|
83
|
+
type: 'warning',
|
84
|
+
message: warning_message + warning_messages.join("\n")
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
if error_messages.present?
|
89
|
+
error_message = <<~ERROR
|
90
|
+
There is a problem with the terminology resources. See the README for
|
91
|
+
the [G10 Certification Test Kit
|
92
|
+
README](https://github.com/inferno-framework/g10-certification-test-kit#terminology-support)
|
93
|
+
for instructions on building the required terminology resources:\n
|
94
|
+
ERROR
|
95
|
+
messages << {
|
96
|
+
type: 'error',
|
97
|
+
message: error_message + error_messages.join("\n")
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
messages
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module ONCCertificationG10TestKit
|
2
|
+
module ExportKickOffPerformer
|
3
|
+
def perform_export_kick_off_request(use_token: true)
|
4
|
+
skip_if use_token && bearer_token.blank?, 'Could not verify this functionality when bearer token is not set'
|
5
|
+
|
6
|
+
headers = { accept: 'application/fhir+json', prefer: 'respond-async' }
|
7
|
+
headers.merge!({ authorization: "Bearer #{bearer_token}" }) if use_token
|
8
|
+
|
9
|
+
get("Group/#{group_id}/$export", client: :bulk_server, name: :export, headers: headers)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|