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,113 @@
|
|
|
1
|
+
module ONCCertificationG10TestKit
|
|
2
|
+
class SMARTPublicStandaloneLaunchGroup < SMARTAppLaunch::StandaloneLaunchGroup
|
|
3
|
+
title 'Public Client Standalone Launch with OpenID Connect'
|
|
4
|
+
short_title 'SMART Public Client Launch'
|
|
5
|
+
description %(
|
|
6
|
+
# Background
|
|
7
|
+
|
|
8
|
+
The [Standalone
|
|
9
|
+
Launch](http://hl7.org/fhir/smart-app-launch/#standalone-launch-sequence)
|
|
10
|
+
Sequence allows an app, like Inferno, to be launched independent of an
|
|
11
|
+
existing EHR session. It is one of the two launch methods described in
|
|
12
|
+
the SMART App Launch Framework alongside EHR Launch. The app will
|
|
13
|
+
request authorization for the provided scope from the authorization
|
|
14
|
+
endpoint, ultimately receiving an authorization token which can be
|
|
15
|
+
used to gain access to resources on the FHIR server.
|
|
16
|
+
|
|
17
|
+
# Test Methodology
|
|
18
|
+
|
|
19
|
+
Inferno will redirect the user to the the authorization endpoint so
|
|
20
|
+
that they may provide any required credentials and authorize the
|
|
21
|
+
application. Upon successful authorization, Inferno will exchange the
|
|
22
|
+
authorization code provided for an access token.
|
|
23
|
+
|
|
24
|
+
For more information on the #{title}:
|
|
25
|
+
|
|
26
|
+
* [Standalone Launch Sequence](http://hl7.org/fhir/smart-app-launch/#standalone-launch-sequence)
|
|
27
|
+
)
|
|
28
|
+
id :g10_public_standalone_launch
|
|
29
|
+
run_as_group
|
|
30
|
+
|
|
31
|
+
config(
|
|
32
|
+
inputs: {
|
|
33
|
+
client_id: {
|
|
34
|
+
name: :public_client_id
|
|
35
|
+
},
|
|
36
|
+
client_secret: {
|
|
37
|
+
name: :public_client_secret,
|
|
38
|
+
default: nil,
|
|
39
|
+
optional: true,
|
|
40
|
+
locked: true
|
|
41
|
+
},
|
|
42
|
+
requested_scopes: {
|
|
43
|
+
name: :public_requested_scopes
|
|
44
|
+
},
|
|
45
|
+
url: {
|
|
46
|
+
title: 'Standalone FHIR Endpoint',
|
|
47
|
+
description: 'URL of the FHIR endpoint used by standalone applications'
|
|
48
|
+
},
|
|
49
|
+
code: {
|
|
50
|
+
name: :public_code
|
|
51
|
+
},
|
|
52
|
+
state: {
|
|
53
|
+
name: :public_state
|
|
54
|
+
},
|
|
55
|
+
smart_authorization_url: {
|
|
56
|
+
title: 'OAuth 2.0 Authorize Endpoint',
|
|
57
|
+
description: 'OAuth 2.0 Authorize Endpoint provided during the patient standalone launch'
|
|
58
|
+
},
|
|
59
|
+
smart_token_url: {
|
|
60
|
+
title: 'OAuth 2.0 Token Endpoint',
|
|
61
|
+
description: 'OAuth 2.0 Token Endpoint provided during the patient standalone launch'
|
|
62
|
+
},
|
|
63
|
+
smart_credentials: {
|
|
64
|
+
name: :public_smart_credentials
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
outputs: {
|
|
68
|
+
code: { name: :public_code },
|
|
69
|
+
token_retrieval_time: { name: :public_token_retrieval_time },
|
|
70
|
+
state: { name: :public_state },
|
|
71
|
+
id_token: { name: :public_id_token },
|
|
72
|
+
refresh_token: { name: :public_refresh_token },
|
|
73
|
+
access_token: { name: :public_access_token },
|
|
74
|
+
expires_in: { name: :public_expires_in },
|
|
75
|
+
patient_id: { name: :public_patient_id },
|
|
76
|
+
encounter_id: { name: :public_encounter_id },
|
|
77
|
+
received_scopes: { name: :public_received_scopes },
|
|
78
|
+
intent: { name: :public_intent },
|
|
79
|
+
smart_credentials: { name: :public_smart_credentials }
|
|
80
|
+
},
|
|
81
|
+
requests: {
|
|
82
|
+
redirect: { name: :public_redirect },
|
|
83
|
+
token: { name: :public_token }
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
test from: :g10_patient_context,
|
|
88
|
+
config: {
|
|
89
|
+
inputs: {
|
|
90
|
+
patient_id: { name: :public_patient_id },
|
|
91
|
+
smart_credentials: { name: :public_smart_credentials }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
test do
|
|
96
|
+
title 'OAuth token exchange response contains OpenID Connect id_token'
|
|
97
|
+
description %(
|
|
98
|
+
This test requires that an OpenID Connect id_token is provided to
|
|
99
|
+
demonstrate authentication capabilies for public clients.
|
|
100
|
+
)
|
|
101
|
+
id :g10_public_launch_id_token
|
|
102
|
+
|
|
103
|
+
input :id_token,
|
|
104
|
+
name: :public_id_token,
|
|
105
|
+
locked: true,
|
|
106
|
+
optional: true
|
|
107
|
+
|
|
108
|
+
run do
|
|
109
|
+
assert id_token.present?, 'Token response did not provide an id_token as required.'
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
module ONCCertificationG10TestKit
|
|
2
|
+
class SMARTScopesTest < Inferno::Test
|
|
3
|
+
title 'Patient-level access with OpenID Connect and Refresh Token scopes used.'
|
|
4
|
+
description %(
|
|
5
|
+
The scopes being input must follow the guidelines specified in the
|
|
6
|
+
smart-app-launch guide. All scopes requested are expected to be granted.
|
|
7
|
+
)
|
|
8
|
+
id :g10_smart_scopes
|
|
9
|
+
input :requested_scopes, :received_scopes
|
|
10
|
+
uses_request :token
|
|
11
|
+
|
|
12
|
+
def valid_resource_types
|
|
13
|
+
[
|
|
14
|
+
'*',
|
|
15
|
+
'Patient',
|
|
16
|
+
'AllergyIntolerance',
|
|
17
|
+
'Binary',
|
|
18
|
+
'CarePlan',
|
|
19
|
+
'CareTeam',
|
|
20
|
+
'Condition',
|
|
21
|
+
'Device',
|
|
22
|
+
'DiagnosticReport',
|
|
23
|
+
'DocumentReference',
|
|
24
|
+
'Encounter',
|
|
25
|
+
'Goal',
|
|
26
|
+
'Immunization',
|
|
27
|
+
'Location',
|
|
28
|
+
'Medication',
|
|
29
|
+
'MedicationOrder',
|
|
30
|
+
'MedicationRequest',
|
|
31
|
+
'MedicationStatement',
|
|
32
|
+
'Observation',
|
|
33
|
+
'Organization',
|
|
34
|
+
'Person',
|
|
35
|
+
'Practitioner',
|
|
36
|
+
'PractitionerRole',
|
|
37
|
+
'Procedure',
|
|
38
|
+
'Provenance',
|
|
39
|
+
'RelatedPerson'
|
|
40
|
+
]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def requested_scope_test(scopes, patient_compartment_resource_types)
|
|
44
|
+
patient_scope_found = false
|
|
45
|
+
|
|
46
|
+
scopes.each do |scope|
|
|
47
|
+
bad_format_message =
|
|
48
|
+
"Requested scope '#{scope}' does not follow the format: `#{scope_type}" \
|
|
49
|
+
'/[ resource | * ].[ read | * ]`'
|
|
50
|
+
|
|
51
|
+
scope_pieces = scope.split('/')
|
|
52
|
+
assert scope_pieces.count == 2, bad_format_message
|
|
53
|
+
|
|
54
|
+
resource_access = scope_pieces[1].split('.')
|
|
55
|
+
bad_resource_message = "'#{resource_access[0]}' must be either a valid resource type or '*'"
|
|
56
|
+
|
|
57
|
+
if scope_type == 'patient' && patient_compartment_resource_types.exclude?(resource_access[0])
|
|
58
|
+
assert ['user', 'patient'].include?(scope_pieces[0]),
|
|
59
|
+
"Requested scope '#{scope}' must begin with either 'user/' or 'patient/'"
|
|
60
|
+
else
|
|
61
|
+
assert scope_pieces[0] == scope_type, bad_format_message
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
assert resource_access.count == 2, bad_format_message
|
|
65
|
+
assert valid_resource_types.include?(resource_access[0]), bad_resource_message
|
|
66
|
+
assert resource_access[1] =~ /^(\*|read)/, bad_format_message
|
|
67
|
+
|
|
68
|
+
patient_scope_found = true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
assert patient_scope_found,
|
|
72
|
+
"#{scope_type.capitalize}-level scope in the format: " \
|
|
73
|
+
"`#{scope_type}/[ resource | * ].[ read | *]` was not requested."
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def received_scope_test(scopes, patient_compartment_resource_types)
|
|
77
|
+
granted_resource_types = []
|
|
78
|
+
|
|
79
|
+
scopes.each do |scope|
|
|
80
|
+
scope_pieces = scope.split('/')
|
|
81
|
+
next unless scope_pieces.count == 2
|
|
82
|
+
|
|
83
|
+
resource_access = scope_pieces[1].split('.')
|
|
84
|
+
next unless resource_access.count == 2
|
|
85
|
+
|
|
86
|
+
granted_resource_types << resource_access[0] if resource_access[1] =~ /^(\*|read)/
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
missing_resource_types =
|
|
90
|
+
if granted_resource_types.include?('*')
|
|
91
|
+
[]
|
|
92
|
+
else
|
|
93
|
+
patient_compartment_resource_types - granted_resource_types - ['*']
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
assert missing_resource_types.empty?,
|
|
97
|
+
"Request scopes #{missing_resource_types.join(', ')} were not granted by authorization server."
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
run do
|
|
101
|
+
skip_if request.status != 200, 'Token exchange was unsuccessful'
|
|
102
|
+
|
|
103
|
+
patient_compartment_resource_types = [
|
|
104
|
+
'*',
|
|
105
|
+
'Patient',
|
|
106
|
+
'AllergyIntolerance',
|
|
107
|
+
'CarePlan',
|
|
108
|
+
'CareTeam',
|
|
109
|
+
'Condition',
|
|
110
|
+
'DiagnosticReport',
|
|
111
|
+
'DocumentReference',
|
|
112
|
+
'Goal',
|
|
113
|
+
'Immunization',
|
|
114
|
+
'MedicationRequest',
|
|
115
|
+
'Observation',
|
|
116
|
+
'Procedure',
|
|
117
|
+
'Provenance'
|
|
118
|
+
].freeze
|
|
119
|
+
|
|
120
|
+
[
|
|
121
|
+
{
|
|
122
|
+
scopes: requested_scopes,
|
|
123
|
+
received_or_requested: 'requested'
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
scopes: received_scopes,
|
|
127
|
+
received_or_requested: 'received'
|
|
128
|
+
}
|
|
129
|
+
].each do |metadata|
|
|
130
|
+
scopes = metadata[:scopes].split
|
|
131
|
+
received_or_requested = metadata[:received_or_requested]
|
|
132
|
+
|
|
133
|
+
missing_scopes = required_scopes - scopes
|
|
134
|
+
assert missing_scopes.empty?,
|
|
135
|
+
"Required scopes were not #{received_or_requested}: #{missing_scopes.join(', ')}"
|
|
136
|
+
|
|
137
|
+
scopes -= required_scopes
|
|
138
|
+
|
|
139
|
+
# Other 'okay' scopes. Also scopes may include both 'launch' and
|
|
140
|
+
# 'launch/patient' for EHR launch and Standalone launch.
|
|
141
|
+
# 'launch/encounter' is mentioned by SMART App Launch though not in
|
|
142
|
+
# (g)(10) test procedure
|
|
143
|
+
scopes -= ['online_access', 'launch', 'launch/patient', 'launch/encounter']
|
|
144
|
+
|
|
145
|
+
if received_or_requested == 'requested'
|
|
146
|
+
requested_scope_test(scopes, patient_compartment_resource_types)
|
|
147
|
+
else
|
|
148
|
+
received_scope_test(scopes, patient_compartment_resource_types)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
require_relative 'base_token_refresh_group'
|
|
2
|
+
require_relative 'patient_context_test'
|
|
3
|
+
require_relative 'smart_scopes_test'
|
|
4
|
+
require_relative 'unauthorized_access_test'
|
|
5
|
+
require_relative 'unrestricted_resource_type_access_group'
|
|
6
|
+
require_relative 'well_known_capabilities_test'
|
|
7
|
+
|
|
8
|
+
module ONCCertificationG10TestKit
|
|
9
|
+
class SmartStandalonePatientAppGroup < Inferno::TestGroup
|
|
10
|
+
title 'Standalone Patient App - Full Access'
|
|
11
|
+
short_title 'Standalone Patient App'
|
|
12
|
+
|
|
13
|
+
input_instructions %(
|
|
14
|
+
Register Inferno as a standalone application using the following information:
|
|
15
|
+
|
|
16
|
+
* Redirect URI: `#{SMARTAppLaunch::AppRedirectTest.config.options[:redirect_uri]}`
|
|
17
|
+
|
|
18
|
+
Enter in the appropriate scope to enable patient-level access to all
|
|
19
|
+
relevant resources. In addition, support for the OpenID Connect (openid
|
|
20
|
+
fhirUser), refresh tokens (offline_access), and patient context
|
|
21
|
+
(launch/patient) are required.
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
description %(
|
|
25
|
+
This scenario demonstrates the ability of a system to perform a Patient
|
|
26
|
+
Standalone Launch to a [SMART on
|
|
27
|
+
FHIR](http://www.hl7.org/fhir/smart-app-launch/) confidential client
|
|
28
|
+
with a patient context, refresh token, 1 1 and [OpenID Connect
|
|
29
|
+
(OIDC)](https://openid.net/specs/openid-connect-core-1_0.html) identity
|
|
30
|
+
token. After launch, a simple Patient resource read is performed on the
|
|
31
|
+
patient in context. The access token is then refreshed, and the Patient
|
|
32
|
+
resource is read using the new access token to ensure that the refresh
|
|
33
|
+
was successful. The authentication information provided by OpenID
|
|
34
|
+
Connect is decoded and validated, and simple queries are performed to
|
|
35
|
+
ensure that access is granted to all USCDI data elements.
|
|
36
|
+
)
|
|
37
|
+
id :g10_smart_standalone_patient_app
|
|
38
|
+
run_as_group
|
|
39
|
+
|
|
40
|
+
config(
|
|
41
|
+
inputs: {
|
|
42
|
+
client_secret: {
|
|
43
|
+
optional: false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
group from: :smart_discovery do
|
|
49
|
+
test from: 'g10_smart_well_known_capabilities'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
group from: :smart_standalone_launch do
|
|
53
|
+
title 'Standalone Launch With Patient Scope'
|
|
54
|
+
description %(
|
|
55
|
+
# Background
|
|
56
|
+
|
|
57
|
+
The [Standalone
|
|
58
|
+
Launch](http://hl7.org/fhir/smart-app-launch/#standalone-launch-sequence)
|
|
59
|
+
Sequence allows an app, like Inferno, to be launched independent of an
|
|
60
|
+
existing EHR session. It is one of the two launch methods described in
|
|
61
|
+
the SMART App Launch Framework alongside EHR Launch. The app will
|
|
62
|
+
request authorization for the provided scope from the authorization
|
|
63
|
+
endpoint, ultimately receiving an authorization token which can be used
|
|
64
|
+
to gain access to resources on the FHIR server.
|
|
65
|
+
|
|
66
|
+
# Test Methodology
|
|
67
|
+
|
|
68
|
+
Inferno will redirect the user to the the authorization endpoint so that
|
|
69
|
+
they may provide any required credentials and authorize the application.
|
|
70
|
+
Upon successful authorization, Inferno will exchange the authorization
|
|
71
|
+
code provided for an access token.
|
|
72
|
+
|
|
73
|
+
For more information on the #{title}:
|
|
74
|
+
|
|
75
|
+
* [Standalone Launch
|
|
76
|
+
Sequence](http://hl7.org/fhir/smart-app-launch/#standalone-launch-sequence)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
test from: :g10_smart_scopes do
|
|
80
|
+
config(
|
|
81
|
+
inputs: {
|
|
82
|
+
requested_scopes: { name: :standalone_requested_scopes },
|
|
83
|
+
received_scopes: { name: :standalone_received_scopes }
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def required_scopes
|
|
88
|
+
['openid', 'fhirUser', 'launch/patient', 'offline_access']
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def scope_type
|
|
92
|
+
'patient'
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
test from: :g10_unauthorized_access,
|
|
97
|
+
config: {
|
|
98
|
+
inputs: {
|
|
99
|
+
patient_id: { name: :standalone_patient_id }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
test from: :g10_patient_context,
|
|
104
|
+
config: {
|
|
105
|
+
inputs: {
|
|
106
|
+
patient_id: { name: :standalone_patient_id },
|
|
107
|
+
smart_credentials: { name: :standalone_smart_credentials }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
group from: :smart_openid_connect,
|
|
113
|
+
config: {
|
|
114
|
+
inputs: {
|
|
115
|
+
id_token: { name: :standalone_id_token },
|
|
116
|
+
client_id: { name: :standalone_client_id },
|
|
117
|
+
requested_scopes: { name: :standalone_requested_scopes },
|
|
118
|
+
smart_credentials: { name: :standalone_smart_credentials }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
group from: :g10_token_refresh do
|
|
123
|
+
id :g10_smart_standalone_token_refresh
|
|
124
|
+
|
|
125
|
+
config(
|
|
126
|
+
inputs: {
|
|
127
|
+
refresh_token: { name: :standalone_refresh_token },
|
|
128
|
+
client_id: { name: :standalone_client_id },
|
|
129
|
+
client_secret: { name: :standalone_client_secret },
|
|
130
|
+
received_scopes: { name: :standalone_received_scopes }
|
|
131
|
+
},
|
|
132
|
+
outputs: {
|
|
133
|
+
refresh_token: { name: :standalone_refresh_token },
|
|
134
|
+
received_scopes: { name: :standalone_received_scopes },
|
|
135
|
+
access_token: { name: :standalone_access_token },
|
|
136
|
+
token_retrieval_time: { name: :standalone_token_retrieval_time },
|
|
137
|
+
expires_in: { name: :standalone_expires_in },
|
|
138
|
+
smart_credentials: { name: :standalone_smart_credentials }
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
test from: :g10_patient_context do
|
|
143
|
+
config(
|
|
144
|
+
inputs: {
|
|
145
|
+
patient_id: { name: :standalone_patient_id },
|
|
146
|
+
smart_credentials: { name: :standalone_smart_credentials }
|
|
147
|
+
},
|
|
148
|
+
options: {
|
|
149
|
+
refresh_test: true
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
uses_request :token_refresh
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
group from: :g10_unrestricted_resource_type_access,
|
|
157
|
+
config: {
|
|
158
|
+
inputs: {
|
|
159
|
+
received_scopes: { name: :standalone_received_scopes },
|
|
160
|
+
patient_id: { name: :standalone_patient_id },
|
|
161
|
+
smart_credentials: { name: :standalone_smart_credentials }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
test do
|
|
166
|
+
id :g10_standalone_credentials_export
|
|
167
|
+
title 'Set SMART Credentials to Standalone Launch Credentials'
|
|
168
|
+
|
|
169
|
+
input :standalone_smart_credentials, type: :oauth_credentials
|
|
170
|
+
output :smart_credentials
|
|
171
|
+
|
|
172
|
+
run do
|
|
173
|
+
output smart_credentials: standalone_smart_credentials.to_s
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
require_relative '../inferno/terminology/terminology_validation'
|
|
2
|
+
require_relative '../inferno/exceptions'
|
|
3
|
+
|
|
4
|
+
module ONCCertificationG10TestKit
|
|
5
|
+
class TerminologyBindingValidator
|
|
6
|
+
include USCoreTestKit::FHIRResourceNavigation
|
|
7
|
+
include Inferno::Terminology::TerminologyValidation
|
|
8
|
+
|
|
9
|
+
def self.validate(...)
|
|
10
|
+
new(...).validate
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :resource, :binding_definition, :validation_messages
|
|
14
|
+
|
|
15
|
+
def initialize(resource, binding_definition)
|
|
16
|
+
@resource = resource
|
|
17
|
+
@binding_definition = binding_definition
|
|
18
|
+
@validation_messages = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def validate
|
|
22
|
+
add_error(element_with_invalid_binding) if element_with_invalid_binding.present?
|
|
23
|
+
|
|
24
|
+
validation_messages
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def path_source
|
|
28
|
+
return resource if binding_definition[:extensions].blank?
|
|
29
|
+
|
|
30
|
+
binding_definition[:extensions].reduce(Array.wrap(resource)) do |elements, extension_url|
|
|
31
|
+
elements.flat_map do |element|
|
|
32
|
+
element.extension.select { |extension| extension.url == extension_url }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def element_with_invalid_binding
|
|
38
|
+
@element_with_invalid_binding ||=
|
|
39
|
+
find_a_value_at(path_source, binding_definition[:path]) do |element|
|
|
40
|
+
invalid_binding? element
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def add_error(element)
|
|
45
|
+
validation_messages << {
|
|
46
|
+
type: 'error',
|
|
47
|
+
message: invalid_binding_message(element)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def add_warning(message)
|
|
52
|
+
validation_messages << {
|
|
53
|
+
type: 'warning',
|
|
54
|
+
message: message
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def element_code(element)
|
|
59
|
+
case element
|
|
60
|
+
when FHIR::CodeableConcept
|
|
61
|
+
element&.coding&.map do |coding|
|
|
62
|
+
"`#{coding.system}|#{coding.code}`"
|
|
63
|
+
end&.join(' or ')
|
|
64
|
+
when FHIR::Coding, FHIR::Quantity
|
|
65
|
+
"`#{element.system}|#{element.code}`"
|
|
66
|
+
else
|
|
67
|
+
"`#{element}`"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def resource_type
|
|
72
|
+
resource.resourceType
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def invalid_binding_message(element)
|
|
76
|
+
system = binding_definition[:system].presence || 'the declared CodeSystem'
|
|
77
|
+
|
|
78
|
+
%(
|
|
79
|
+
#{resource_type}/#{resource.id} at #{resource_type}.#{binding_definition[:path]}
|
|
80
|
+
with code #{element_code(element)} is not in #{system}.
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def invalid_binding?(element)
|
|
85
|
+
case binding_definition[:type]
|
|
86
|
+
when 'CodeableConcept'
|
|
87
|
+
invalid_codeable_concept? element
|
|
88
|
+
when 'Quantity', 'Coding'
|
|
89
|
+
invalid_coding? element
|
|
90
|
+
when 'code'
|
|
91
|
+
invalid_code? element
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def invalid_codeable_concept?(element)
|
|
96
|
+
return unless element.is_a? FHIR::CodeableConcept
|
|
97
|
+
|
|
98
|
+
if binding_definition[:system].present?
|
|
99
|
+
element.coding.none? do |coding|
|
|
100
|
+
validate_code(
|
|
101
|
+
value_set_url: binding_definition[:system],
|
|
102
|
+
code: coding.code,
|
|
103
|
+
system: coding.system
|
|
104
|
+
)
|
|
105
|
+
rescue Inferno::ProhibitedSystemException => e
|
|
106
|
+
add_warning(e.message)
|
|
107
|
+
false
|
|
108
|
+
end
|
|
109
|
+
# If we're validating a codesystem (AKA if there's no 'system' URL)
|
|
110
|
+
# We want all of the codes to be in their respective systems
|
|
111
|
+
else
|
|
112
|
+
el.coding.any? do |coding|
|
|
113
|
+
!validate_code(
|
|
114
|
+
value_set_url: nil,
|
|
115
|
+
code: coding.code,
|
|
116
|
+
system: coding.system
|
|
117
|
+
)
|
|
118
|
+
rescue Inferno::ProhibitedSystemException => e
|
|
119
|
+
add_warning(e.message)
|
|
120
|
+
false
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def invalid_coding?(element)
|
|
126
|
+
!validate_code(
|
|
127
|
+
value_set_url: binding_definition[:system],
|
|
128
|
+
code: element.code,
|
|
129
|
+
system: element.system
|
|
130
|
+
)
|
|
131
|
+
rescue Inferno::ProhibitedSystemException => e
|
|
132
|
+
add_warning(e.message)
|
|
133
|
+
false
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def invalid_code?(element)
|
|
137
|
+
!validate_code(value_set_url: binding_definition[:system], code: element)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|