davinci_dtr_test_kit 0.10.0 → 0.11.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 +4 -4
- data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_dinner_questionnaire_package_request_test.rb +134 -0
- data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_questionnaire_response_save_test.rb +2 -2
- data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_smart_app_questionnaire_workflow_group.rb +3 -2
- data/lib/davinci_dtr_test_kit/client_groups/dinner_static/prepopulation_attestation_test.rb +4 -4
- data/lib/davinci_dtr_test_kit/client_groups/dinner_static/prepopulation_override_attestation_test.rb +4 -4
- data/lib/davinci_dtr_test_kit/client_groups/dinner_static/rendering_enabled_questions_attestation_test.rb +4 -4
- data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_questionnaire_package_group.rb +2 -2
- data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_questionnaire_rendering_attestation_test.rb +4 -4
- data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_questionnaire_response_save_test.rb +2 -2
- data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_resp_questionnaire_package_request_test.rb +132 -0
- data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_smart_app_questionnaire_workflow_group.rb +4 -3
- data/lib/davinci_dtr_test_kit/client_groups/shared/dtr_questionnaire_response_pre_population_test.rb +1 -0
- data/lib/davinci_dtr_test_kit/docs/dtr_payer_server_suite_description_v201.md +2 -2
- data/lib/davinci_dtr_test_kit/docs/dtr_smart_app_suite_description_v201.md +106 -42
- data/lib/davinci_dtr_test_kit/dtr_full_ehr_suite.rb +4 -3
- data/lib/davinci_dtr_test_kit/dtr_payer_server_suite.rb +6 -4
- data/lib/davinci_dtr_test_kit/dtr_questionnaire_response_validation.rb +26 -10
- data/lib/davinci_dtr_test_kit/dtr_smart_app_suite.rb +33 -11
- data/lib/davinci_dtr_test_kit/fixtures/dinner_adaptive/dinner_order_adaptive_next_question_hamburger.json +1 -1
- data/lib/davinci_dtr_test_kit/fixtures/dinner_static/questionnaire_dinner_order_static.json +2 -2
- data/lib/davinci_dtr_test_kit/fixtures/questionnaire_package.json +1 -1
- data/lib/davinci_dtr_test_kit/mock_auth_server.rb +145 -0
- data/lib/davinci_dtr_test_kit/mock_ehr.rb +37 -12
- data/lib/davinci_dtr_test_kit/mock_payer.rb +1 -20
- data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_request_validation_test.rb +1 -0
- data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_response_validation_test.rb +1 -0
- data/lib/davinci_dtr_test_kit/urls.rb +14 -3
- data/lib/davinci_dtr_test_kit/validation_test.rb +1 -0
- data/lib/davinci_dtr_test_kit/version.rb +1 -1
- metadata +19 -3
- data/lib/davinci_dtr_test_kit/client_groups/shared/dtr_questionnaire_package_request_test.rb +0 -36
@@ -52,35 +52,50 @@ validated with the Java validator using `tx.fhir.org` as the terminology server.
|
|
52
52
|
|
53
53
|
### Quick Start
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
55
|
+
This test suite can be run in two modes, each described below:
|
56
|
+
1. [EHR launch mode](#ehr-launch)
|
57
|
+
2. [Standalone launch mode](#standalone-launch)
|
58
|
+
|
59
|
+
At this time, Inferno's simulation of the payer server that provides the questionnaires
|
60
|
+
uses the same base server url and access token, and apps will need to be configured to
|
61
|
+
connect to it as well. See the "Combined payer and EHR FHIR servers" section below for details.
|
62
|
+
|
63
|
+
The DTR specification allows apps and their partners significant leeway in terms of
|
64
|
+
what information is provided on launch and how that information gets used by the app
|
65
|
+
to determine the `$questionnaire-package` endpoint and what details to submit as a
|
66
|
+
part of that operation. Inferno cannot know ahead of time what data needs to be
|
67
|
+
available for the app under test to successfully request, pre-populate, and render
|
68
|
+
a questionnaire. See the "`fhirContext` and available instances"
|
69
|
+
section below for details on how to enable Inferno to meet the needs of your application.
|
70
|
+
|
71
|
+
#### EHR Launch
|
72
|
+
|
73
|
+
In this mode Inferno will launch the app under test using the SMART App Launch
|
74
|
+
[EHR launch](https://hl7.org/fhir/smart-app-launch/STU2.1/app-launch.html#launch-app-ehr-launch)
|
75
|
+
flow.
|
76
|
+
|
77
|
+
The tester must provide
|
78
|
+
1. a `client_id`: can be any string and will uniquely identify the testing session.
|
79
|
+
2. a `launch_uri`: will be used by Inferno to generate a link to launch the app under test.
|
80
|
+
|
81
|
+
All the details needed to access clinical data from Inferno's simulated EHR are provided
|
82
|
+
as a part of the SMART flow, including
|
83
|
+
- the FHIR base server URL to request data from
|
84
|
+
- a bearer token to provide on all requests
|
85
|
+
|
86
|
+
#### Standalone Launch
|
87
|
+
|
88
|
+
In this mode the app under test will launch and on its own and reach out to Inferno to
|
89
|
+
begin the workflow as described in the
|
90
|
+
[standalone launch section](https://hl7.org/fhir/smart-app-launch/STU2.1/app-launch.html#launch-app-standalone-launch).
|
91
|
+
|
92
|
+
The tester must provide
|
93
|
+
1. a `client_id`: can be any string and will uniquely identify the testing session.
|
94
|
+
|
95
|
+
The app will then need to connect to Inferno as directed to initiate the SMART and DTR
|
96
|
+
workflow. The FHIR base server url that app will connect to is
|
97
|
+
`[URL prefix]/custom/dtr_smart_app/fhir` where `[URL prefix]` comes from the URL of the
|
98
|
+
test session which will be of the form `[URL prefix]/dtr_smart_app/[session id]`
|
84
99
|
|
85
100
|
### Postman-based Demo
|
86
101
|
|
@@ -90,26 +105,75 @@ to make requests against Inferno. This does not include the capability to render
|
|
90
105
|
questionnaires, but does have samples of correctly and incorrectly completed QuestionnaireResponses.
|
91
106
|
The following is a list of tests with the Postman requests that can be used with them:
|
92
107
|
|
93
|
-
- **
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
- **
|
108
|
+
- **Standalone launch sequence**: use requests in the `SMART App Launch` folder during
|
109
|
+
tests **1.1.1.01** or **2.1.1.01** to simulate the SMART Launch flow and obtain an access
|
110
|
+
token to use for subsequent requests. See the collection's Overview for details on the
|
111
|
+
access token's generation.
|
112
|
+
- **1.1** *Static Questionnaire Workflow*: use requests in the `Static Dinner` folder
|
113
|
+
- **1.1.1.01** *Invoke the DTR Questionnaire Package operation*: submit request `Questionnaire Package for Dinner (Static)` while this test is waiting.
|
114
|
+
- **1.1.3.01** *Save the QuestionnaireResponse after completing it*: submit request `Save QuestionnaireResponse for Dinner (Static)` while this test is waiting. If you want to see a failure, submit request `Save QuestionnaireResponse for Dinner (Static) - missing origin extension` instead.
|
115
|
+
- **2.1** *Respiratory Assist Device Questionnaire Workflow*: use requests in the `Respiratory Assist Device` folder
|
116
|
+
- **2.1.1.01** *Invoke the DTR Questionnaire Package operation*: submit request `Questionnaire Package for Resp Assist Device` while this test is waiting.
|
117
|
+
- **2.1.3.01** *Save the QuestionnaireResponse after completing it*: submit request `Save Questionnaire Response for Resp Assist Device` while this test is waiting. If you want to see a failure, submit request `Save Questionnaire Response for Resp Assist Device - unexpected override` instead.
|
118
|
+
|
119
|
+
## Configuration Details
|
120
|
+
|
121
|
+
### `fhirContext` and available instances
|
122
|
+
|
123
|
+
Once they have launched, DTR SMART Apps obtain details that drive their retrieval of questionnaires
|
124
|
+
and relevant clinical data from the payer and the EHR from [context that is passed with
|
125
|
+
the access token](https://hl7.org/fhir/smart-app-launch/STU2.1/scopes-and-launch-context.html)
|
126
|
+
provided by the EHR. Inferno cannot know ahead of time what information to provide and
|
127
|
+
what instances to make available to direct the app under test to request and render a
|
128
|
+
particular questionnaire.
|
129
|
+
|
130
|
+
Therefore, use of this test suite requests that the tester provide this information so that the
|
131
|
+
app can demonstrate its capabilities based on whatever business logic is present. These tests
|
132
|
+
currently support two context parameters that contain references to instance in the EHR and provides
|
133
|
+
testers with a way to provide those instances to Inferno so it can serve them to the app. These are
|
134
|
+
controlled by the following inputs present on each group associated with a questionnaire:
|
135
|
+
|
136
|
+
- **SMART App Launch Patient ID**: provide an `id` for the subject Patient FHIR instance.
|
137
|
+
- **SMART App Launch `fhirContext`**: provide a JSON object containing FHIR references to instances
|
138
|
+
relevant to the DTR workflow, e.g.
|
139
|
+
|
140
|
+
```
|
141
|
+
[{reference: 'Coverage/cov015'}, {reference: 'DeviceRequest/devreqe0470'}]
|
142
|
+
```
|
143
|
+
This will be included under the `fhirContext` key of the token response.
|
144
|
+
- **EHR-available resources**: provide a Bundle containing FHIR instances referenced in and from the
|
145
|
+
previous two inputs. Each instance must include an `id` element that Inferno will use in conjunction
|
146
|
+
with the `resourceType` to make the instances available at the `[server base url]/[resourceType]/[id]`.
|
147
|
+
|
148
|
+
Each questionnaire workflow group description includes a link to the questionnaire package that Inferno will return
|
149
|
+
(e.g., [here](https://github.com/inferno-framework/davinci-dtr-test-kit/blob/main/lib/davinci_dtr_test_kit/fixtures/dinner_static/questionnaire_dinner_order_static.json))
|
150
|
+
where you can find `id` and `url` values and any other details needed to determine what inputs
|
151
|
+
will allow the app under test to work with that questionnaire. Note additionally that Inferno will always
|
152
|
+
return that questionnaire in response to `$questionnaire-package` requests made during that test.
|
153
|
+
|
154
|
+
These inputs can be cumbersome to create and if you have suggestions about how to improve this process
|
155
|
+
while keeping the flexibility of Inferno to run with any app, submit a ticket
|
156
|
+
[here](https://github.com/inferno-framework/davinci-pas-test-kit/issues).
|
99
157
|
|
100
158
|
## Limitations
|
101
159
|
|
102
|
-
The DTR IG is a complex specification and these tests currently validate
|
103
|
-
|
160
|
+
The DTR IG is a complex specification and these tests currently validate conformance to only
|
161
|
+
a subset of IG requirements. Future versions of the test suite will test further
|
104
162
|
features. A few specific features of interest are listed below.
|
105
163
|
|
106
164
|
### Launching and security
|
107
165
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
166
|
+
This test kit contains basic SMART App Launch cabilities that may not be complete. In particular,
|
167
|
+
refresh tokens are not currently supported and scopes are not precise. To provide feedback and
|
168
|
+
input on the design of this feature and help us priortize improvements, submit a ticket
|
169
|
+
[here](https://github.com/inferno-framework/davinci-pas-test-kit/issues).
|
170
|
+
|
171
|
+
### Combined payer and EHR FHIR servers
|
172
|
+
|
173
|
+
At this time, the test suite simulates a single FHIR server that uses the same access token
|
174
|
+
for both the payer server and the EHR server. Apps under test must use the FHIR server base url
|
175
|
+
and access token identified during the SMART App Launch sequence when making requests
|
176
|
+
to retrieve questionnaires.
|
113
177
|
|
114
178
|
### Questionnaire Feature Coverage
|
115
179
|
|
@@ -55,11 +55,12 @@ module DaVinciDTRTestKit
|
|
55
55
|
|
56
56
|
allow_cors QUESTIONNAIRE_PACKAGE_PATH
|
57
57
|
|
58
|
-
record_response_route :post,
|
59
|
-
|
58
|
+
record_response_route :post, PAYER_TOKEN_PATH, 'dtr_full_ehr_payer_token',
|
59
|
+
method(:payer_token_response) do |request|
|
60
|
+
DTRFullEHRSuite.extract_client_id_from_form_params(request)
|
60
61
|
end
|
61
62
|
|
62
|
-
record_response_route :post,
|
63
|
+
record_response_route :post, QUESTIONNAIRE_PACKAGE_PATH, QUESTIONNAIRE_PACKAGE_TAG,
|
63
64
|
method(:questionnaire_package_response) do |request|
|
64
65
|
DTRFullEHRSuite.extract_bearer_token(request)
|
65
66
|
end
|
@@ -5,10 +5,12 @@ require_relative 'payer_server_groups/payer_server_static_group'
|
|
5
5
|
require_relative 'payer_server_groups/payer_server_adaptive_group'
|
6
6
|
require_relative 'tags'
|
7
7
|
require_relative 'mock_payer'
|
8
|
+
require_relative 'mock_auth_server'
|
8
9
|
require_relative 'version'
|
9
10
|
|
10
11
|
module DaVinciDTRTestKit
|
11
12
|
class DTRPayerServerSuite < Inferno::TestSuite
|
13
|
+
extend MockAuthServer
|
12
14
|
extend MockPayer
|
13
15
|
|
14
16
|
id :dtr_payer_server
|
@@ -104,16 +106,16 @@ module DaVinciDTRTestKit
|
|
104
106
|
|
105
107
|
allow_cors QUESTIONNAIRE_PACKAGE_PATH, NEXT_PATH
|
106
108
|
|
107
|
-
record_response_route :post,
|
108
|
-
DTRPayerServerSuite.
|
109
|
+
record_response_route :post, PAYER_TOKEN_PATH, 'dtr_payer_auth', method(:payer_token_response) do |request|
|
110
|
+
DTRPayerServerSuite.extract_client_id_from_form_params(request)
|
109
111
|
end
|
110
112
|
|
111
|
-
record_response_route :post,
|
113
|
+
record_response_route :post, QUESTIONNAIRE_PACKAGE_PATH, QUESTIONNAIRE_TAG,
|
112
114
|
method(:payer_questionnaire_response), resumes: method(:test_resumes?) do |request|
|
113
115
|
DTRPayerServerSuite.extract_bearer_token(request)
|
114
116
|
end
|
115
117
|
|
116
|
-
record_response_route :post,
|
118
|
+
record_response_route :post, NEXT_PATH, NEXT_TAG,
|
117
119
|
method(:questionnaire_next_response), resumes: method(:test_resumes?) do |request|
|
118
120
|
DTRPayerServerSuite.extract_bearer_token(request)
|
119
121
|
end
|
@@ -36,6 +36,16 @@ module DaVinciDTRTestKit
|
|
36
36
|
validate_cql_executed(questionnaire_response.item, questionnaire_cql_expression_link_ids,
|
37
37
|
template_prepopulation_expectations, template_override_expectations, validation_errors)
|
38
38
|
|
39
|
+
if template_prepopulation_expectations.size.positive?
|
40
|
+
validation_errors << 'Items expected to be pre-populated not found: ' \
|
41
|
+
"#{template_prepopulation_expectations.keys.join(', ')}"
|
42
|
+
end
|
43
|
+
|
44
|
+
if template_override_expectations.size.positive?
|
45
|
+
validation_errors << 'Items expected to be pre-poplated and overridden not found: ' \
|
46
|
+
"#{template_override_expectations.keys.join(', ')}"
|
47
|
+
end
|
48
|
+
|
39
49
|
validation_errors.each { |msg| messages << { type: 'error', message: msg } }
|
40
50
|
assert validation_errors.blank?, 'QuestionnaireResponse is not conformant. Check messages for issues found.'
|
41
51
|
end
|
@@ -47,10 +57,11 @@ module DaVinciDTRTestKit
|
|
47
57
|
link_id = item_to_validate.linkId
|
48
58
|
if questionnaire_cql_expression_link_ids.include?(link_id)
|
49
59
|
if template_prepopulation_expectations.key?(link_id)
|
50
|
-
check_item_prepopulation(item_to_validate, template_prepopulation_expectations
|
51
|
-
false)
|
60
|
+
check_item_prepopulation(item_to_validate, template_prepopulation_expectations.delete(link_id),
|
61
|
+
error_messages, false)
|
52
62
|
elsif template_override_expectations.include?(link_id)
|
53
|
-
check_item_prepopulation(item_to_validate, template_override_expectations
|
63
|
+
check_item_prepopulation(item_to_validate, template_override_expectations.delete(link_id), error_messages,
|
64
|
+
true)
|
54
65
|
else
|
55
66
|
raise "template missing expectation for question `#{link_id}`"
|
56
67
|
end
|
@@ -84,11 +95,10 @@ module DaVinciDTRTestKit
|
|
84
95
|
raise "Template QuestionnaireResponse item `#{target_link_id}` missing the `origin.source` extension"
|
85
96
|
end
|
86
97
|
|
87
|
-
# TODO: handle other data types
|
88
98
|
if source_extension.value == 'auto'
|
89
|
-
expected_prepopulated[target_link_id] = target_item_answer
|
99
|
+
expected_prepopulated[target_link_id] = target_item_answer
|
90
100
|
elsif source_extension.value == 'override'
|
91
|
-
expected_overrides[target_link_id] = target_item_answer
|
101
|
+
expected_overrides[target_link_id] = target_item_answer
|
92
102
|
else
|
93
103
|
raise "`origin.source` extension for item `#{target_link_id}` has unexpected value: #{source_extension.value}"
|
94
104
|
end
|
@@ -114,12 +124,12 @@ module DaVinciDTRTestKit
|
|
114
124
|
answer = item.answer.first
|
115
125
|
if answer&.value&.present?
|
116
126
|
# check answer
|
117
|
-
if override && answer
|
127
|
+
if override && answer_value_equal?(expected_answer, answer)
|
118
128
|
error_list << "Answer to item `#{item.linkId}` was not overriden from the pre-populated value. " \
|
119
129
|
"Found #{expected_answer}, but should be different"
|
120
|
-
elsif !override && answer
|
121
|
-
error_list << "answer to item `#{item.linkId}` contains unexpected value. Expected:
|
122
|
-
|
130
|
+
elsif !override && !answer_value_equal?(expected_answer, answer)
|
131
|
+
error_list << "answer to item `#{item.linkId}` contains unexpected value. Expected: \
|
132
|
+
#{value_for_display(expected_answer)}. Found #{value_for_display(answer)}"
|
123
133
|
end
|
124
134
|
|
125
135
|
# check origin.source extension
|
@@ -176,5 +186,11 @@ module DaVinciDTRTestKit
|
|
176
186
|
def coding_equal?(expected, actual)
|
177
187
|
expected.system == actual&.system && expected.code == actual&.code
|
178
188
|
end
|
189
|
+
|
190
|
+
def value_for_display(answer)
|
191
|
+
return "#{answer.value&.system}|#{answer.value&.code}" if answer.valueCoding.present?
|
192
|
+
|
193
|
+
answer.value
|
194
|
+
end
|
179
195
|
end
|
180
196
|
end
|
@@ -11,8 +11,9 @@ require_relative 'version'
|
|
11
11
|
|
12
12
|
module DaVinciDTRTestKit
|
13
13
|
class DTRSmartAppSuite < Inferno::TestSuite
|
14
|
-
extend
|
14
|
+
extend MockAuthServer
|
15
15
|
extend MockEHR
|
16
|
+
extend MockPayer
|
16
17
|
|
17
18
|
id :dtr_smart_app
|
18
19
|
title 'Da Vinci DTR SMART App Test Suite'
|
@@ -48,43 +49,64 @@ module DaVinciDTRTestKit
|
|
48
49
|
end
|
49
50
|
end
|
50
51
|
|
51
|
-
allow_cors QUESTIONNAIRE_PACKAGE_PATH, QUESTIONNAIRE_RESPONSE_PATH, FHIR_RESOURCE_PATH, FHIR_SEARCH_PATH
|
52
|
+
allow_cors QUESTIONNAIRE_PACKAGE_PATH, QUESTIONNAIRE_RESPONSE_PATH, FHIR_RESOURCE_PATH, FHIR_SEARCH_PATH,
|
53
|
+
EHR_AUTHORIZE_PATH, EHR_TOKEN_PATH
|
52
54
|
|
53
55
|
route(:get, '/fhir/metadata', method(:metadata_handler))
|
54
56
|
|
55
|
-
|
56
|
-
|
57
|
+
route(:get, SMART_CONFIG_PATH, method(:ehr_smart_config))
|
58
|
+
|
59
|
+
record_response_route :get, EHR_AUTHORIZE_PATH, 'dtr_smart_app_ehr_authorize', method(:ehr_authorize),
|
60
|
+
resumes: ->(_) { false } do |request|
|
61
|
+
DTRSmartAppSuite.extract_client_id_from_query_params(request)
|
62
|
+
end
|
63
|
+
|
64
|
+
record_response_route :post, EHR_AUTHORIZE_PATH, 'dtr_smart_app_ehr_authorize', method(:ehr_authorize),
|
65
|
+
resumes: ->(_) { false } do |request|
|
66
|
+
DTRSmartAppSuite.extract_client_id_from_form_params(request)
|
67
|
+
end
|
68
|
+
|
69
|
+
record_response_route :post, EHR_TOKEN_PATH, 'dtr_smart_app_ehr_token', method(:ehr_token_response),
|
70
|
+
resumes: ->(_) { false } do |request|
|
71
|
+
DTRSmartAppSuite.extract_client_id_from_token_request(request)
|
72
|
+
end
|
73
|
+
|
74
|
+
record_response_route :post, PAYER_TOKEN_PATH, 'dtr_smart_app_payer_token',
|
75
|
+
method(:payer_token_response) do |request|
|
76
|
+
DTRSmartAppSuite.extract_client_id_from_client_assertion(request)
|
57
77
|
end
|
58
78
|
|
59
79
|
record_response_route :post, QUESTIONNAIRE_PACKAGE_PATH, QUESTIONNAIRE_PACKAGE_TAG,
|
60
80
|
method(:questionnaire_package_response), resumes: ->(_) { false } do |request|
|
61
|
-
DTRSmartAppSuite.
|
81
|
+
DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
|
62
82
|
end
|
63
83
|
|
64
84
|
record_response_route :post, QUESTIONNAIRE_RESPONSE_PATH, 'dtr_smart_app_questionnaire_response',
|
65
85
|
method(:questionnaire_response_response) do |request|
|
66
|
-
DTRSmartAppSuite.
|
86
|
+
DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
|
67
87
|
end
|
68
88
|
|
69
89
|
record_response_route :get, FHIR_RESOURCE_PATH, SMART_APP_EHR_REQUEST_TAG, method(:get_fhir_resource),
|
70
90
|
resumes: ->(_) { false } do |request|
|
71
|
-
DTRSmartAppSuite.
|
91
|
+
DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
|
72
92
|
end
|
73
93
|
|
74
94
|
record_response_route :get, FHIR_SEARCH_PATH, SMART_APP_EHR_REQUEST_TAG, method(:get_fhir_resource),
|
75
95
|
resumes: ->(_) { false } do |request|
|
76
|
-
DTRSmartAppSuite.
|
96
|
+
DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
|
77
97
|
end
|
78
98
|
|
79
99
|
resume_test_route :get, RESUME_PASS_PATH do |request|
|
80
|
-
DTRSmartAppSuite.
|
100
|
+
DTRSmartAppSuite.extract_client_id_from_query_params(request)
|
81
101
|
end
|
82
102
|
|
83
103
|
resume_test_route :get, RESUME_FAIL_PATH, result: 'fail' do |request|
|
84
|
-
DTRSmartAppSuite.
|
104
|
+
DTRSmartAppSuite.extract_client_id_from_query_params(request)
|
85
105
|
end
|
86
106
|
|
87
|
-
|
107
|
+
# TODO: Update based on SMART Launch changes. Do we even want to have this group now?
|
108
|
+
# group from: :oauth2_authentication
|
109
|
+
|
88
110
|
group do
|
89
111
|
id :dtr_smart_app_basic_workflows
|
90
112
|
title 'Basic Workflows'
|
@@ -7,6 +7,7 @@
|
|
7
7
|
"resource": {
|
8
8
|
"resourceType": "Questionnaire",
|
9
9
|
"id": "DinnerOrderStatic",
|
10
|
+
"url": "urn:inferno:dtr-test-kit:dinner-order-static",
|
10
11
|
"meta": {
|
11
12
|
"profile": [
|
12
13
|
"http://hl7.org/fhir/StructureDefinition/cqf-questionnaire",
|
@@ -21,7 +22,6 @@
|
|
21
22
|
],
|
22
23
|
"name": "DinnerOrderStatic",
|
23
24
|
"title": "Dinner Order (Static)",
|
24
|
-
"url": "urn:inferno:dtr-test-kit:dinner-order-static",
|
25
25
|
"status": "draft",
|
26
26
|
"subjectType": [
|
27
27
|
"Patient"
|
@@ -156,7 +156,7 @@
|
|
156
156
|
},
|
157
157
|
{
|
158
158
|
"valueCoding": {
|
159
|
-
"code": "
|
159
|
+
"code": "Pickles"
|
160
160
|
}
|
161
161
|
},
|
162
162
|
{
|
@@ -5,6 +5,7 @@
|
|
5
5
|
"resource": {
|
6
6
|
"resourceType": "Questionnaire",
|
7
7
|
"id": "RespiratoryAssistDevices",
|
8
|
+
"url": "urn:inferno:dtr-test-kit:respiratory-assist-devices",
|
8
9
|
"meta": {
|
9
10
|
"profile": [
|
10
11
|
"http://hl7.org/fhir/StructureDefinition/cqf-questionnaire",
|
@@ -453,7 +454,6 @@
|
|
453
454
|
],
|
454
455
|
"name": "RespiratoryAssistDevices",
|
455
456
|
"title": "Respiratory Assist Device Questionnaire",
|
456
|
-
"url": "urn:fake",
|
457
457
|
"status": "draft",
|
458
458
|
"subjectType": [
|
459
459
|
"Patient"
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require_relative 'urls'
|
2
|
+
require_relative 'fixtures'
|
3
|
+
|
4
|
+
module DaVinciDTRTestKit
|
5
|
+
module MockAuthServer
|
6
|
+
include Fixtures
|
7
|
+
|
8
|
+
def ehr_smart_config(env)
|
9
|
+
protocol = env['rack.url_scheme']
|
10
|
+
host = env['HTTP_HOST']
|
11
|
+
path = env['REQUEST_PATH'] || env['PATH_INFO']
|
12
|
+
path.gsub!(%r{#{SMART_CONFIG_PATH}(/)?}, '')
|
13
|
+
base_url = "#{protocol}://#{host + path}"
|
14
|
+
response_body =
|
15
|
+
{
|
16
|
+
authorization_endpoint: base_url + EHR_AUTHORIZE_PATH,
|
17
|
+
token_endpoint: base_url + EHR_TOKEN_PATH,
|
18
|
+
token_endpoint_auth_methods_supported: ['private_key_jwt'],
|
19
|
+
token_endpoint_auth_signing_alg_values_supported: ['RS256'],
|
20
|
+
grant_types_supported: ['authorization_code'],
|
21
|
+
scopes_supported: ['launch', 'patient/*.rs', 'user/*.rs', 'offline_access'],
|
22
|
+
response_types_supported: ['code'],
|
23
|
+
code_challenge_methods_supported: ['S256'],
|
24
|
+
capabilities: [
|
25
|
+
'launch-ehr',
|
26
|
+
'permission-patient',
|
27
|
+
'permission-user',
|
28
|
+
'client-public',
|
29
|
+
'client-confidential-symmetric',
|
30
|
+
'client-confidential-asymmetric'
|
31
|
+
]
|
32
|
+
}.to_json
|
33
|
+
|
34
|
+
[200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]]
|
35
|
+
end
|
36
|
+
|
37
|
+
def ehr_authorize(request, _test = nil, _test_result = nil)
|
38
|
+
# Authorization requests can bet GET or POST
|
39
|
+
params = request.verb == 'get' ? request.query_parameters : URI.decode_www_form(request.request_body)&.to_h
|
40
|
+
if params['redirect_uri'].present?
|
41
|
+
redirect_uri = "#{params['redirect_uri']}?" \
|
42
|
+
"code=#{SecureRandom.hex}&" \
|
43
|
+
"state=#{params['state']}"
|
44
|
+
request.status = 302
|
45
|
+
request.response_headers = { 'Location' => redirect_uri }
|
46
|
+
else
|
47
|
+
request.status = 400
|
48
|
+
request.response_headers = { 'Content-Type': 'application/json' }
|
49
|
+
request.response_body = FHIR::OperationOutcome.new(
|
50
|
+
issue: FHIR::OperationOutcome::Issue.new(severity: 'fatal', code: 'required',
|
51
|
+
details: FHIR::CodeableConcept.new(
|
52
|
+
text: 'No redirect_uri provided'
|
53
|
+
))
|
54
|
+
).to_json
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def ehr_token_response(request, _test = nil, test_result = nil)
|
59
|
+
client_id = extract_client_id_from_token_request(request)
|
60
|
+
token = JWT.encode({ inferno_client_id: client_id }, nil, 'none')
|
61
|
+
response = { access_token: token, token_type: 'bearer', expires_in: 3600 }
|
62
|
+
test_input = JSON.parse(test_result.input_json)
|
63
|
+
|
64
|
+
fhir_context_input = test_input.find { |input| input['name'] == 'smart_fhir_context' }
|
65
|
+
fhir_context_input_value = fhir_context_input['value'] if fhir_context_input.present?
|
66
|
+
begin
|
67
|
+
fhir_context = JSON.parse(fhir_context_input_value)
|
68
|
+
rescue StandardError
|
69
|
+
fhir_context = nil
|
70
|
+
end
|
71
|
+
response.merge!({ fhirContext: fhir_context }) if fhir_context
|
72
|
+
|
73
|
+
smart_patient_input = test_input.find { |input| input['name'] == 'smart_patient_id' }
|
74
|
+
smart_patient_input_value = smart_patient_input['value'] if smart_patient_input.present?
|
75
|
+
response.merge!({ patient: smart_patient_input_value }) if smart_patient_input_value
|
76
|
+
|
77
|
+
request.response_body = response.to_json
|
78
|
+
request.response_headers = { 'Access-Control-Allow-Origin' => '*' }
|
79
|
+
request.status = 200
|
80
|
+
end
|
81
|
+
|
82
|
+
def payer_token_response(request, _test = nil, _test_result = nil)
|
83
|
+
# Placeholder for a more complete mock token endpoint
|
84
|
+
request.response_body = { access_token: SecureRandom.hex, token_type: 'bearer', expires_in: 3600 }.to_json
|
85
|
+
request.status = 200
|
86
|
+
end
|
87
|
+
|
88
|
+
def extract_client_id_from_token_request(request)
|
89
|
+
# Public client || confidential client asymmetric || confidential client symmetric
|
90
|
+
extract_client_id_from_form_params(request) ||
|
91
|
+
extract_client_id_from_client_assertion(request) ||
|
92
|
+
extract_client_id_from_basic_auth(request)
|
93
|
+
end
|
94
|
+
|
95
|
+
def extract_client_id_from_form_params(request)
|
96
|
+
URI.decode_www_form(request.request_body).to_h['client_id']
|
97
|
+
end
|
98
|
+
|
99
|
+
def extract_client_id_from_client_assertion(request)
|
100
|
+
encoded_jwt = URI.decode_www_form(request.request_body).to_h['client_assertion']
|
101
|
+
return unless encoded_jwt.present?
|
102
|
+
|
103
|
+
jwt_payload =
|
104
|
+
begin
|
105
|
+
JWT.decode(encoded_jwt, nil, false)&.first # skip signature verification
|
106
|
+
rescue StandardError
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
|
110
|
+
jwt_payload['iss'] || jwt_payload['sub'] if jwt_payload.present?
|
111
|
+
end
|
112
|
+
|
113
|
+
def extract_client_id_from_basic_auth(request)
|
114
|
+
encoded_credentials = request.request_header('Authorization')&.value&.split&.last
|
115
|
+
return unless encoded_credentials.present?
|
116
|
+
|
117
|
+
decoded_credentials = Base64.decode64(encoded_credentials)
|
118
|
+
decoded_credentials&.split(':')&.first
|
119
|
+
end
|
120
|
+
|
121
|
+
def extract_client_id_from_query_params(request)
|
122
|
+
request.query_parameters['client_id']
|
123
|
+
end
|
124
|
+
|
125
|
+
def extract_client_id_from_bearer_token(request)
|
126
|
+
token = extract_bearer_token(request)
|
127
|
+
jwt =
|
128
|
+
begin
|
129
|
+
JWT.decode(token, nil, false)
|
130
|
+
rescue StandardError
|
131
|
+
nil
|
132
|
+
end
|
133
|
+
jwt&.first&.dig('inferno_client_id')
|
134
|
+
end
|
135
|
+
|
136
|
+
# Header expected to be a bearer token of the form "Bearer: <token>"
|
137
|
+
def extract_bearer_token(request)
|
138
|
+
request.request_header('Authorization')&.value&.split&.last
|
139
|
+
end
|
140
|
+
|
141
|
+
def extract_token_from_query_params(request)
|
142
|
+
request.query_parameters['token']
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|