onc_certification_g10_test_kit 2.0.0.rc1
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/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
|