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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_dinner_questionnaire_package_request_test.rb +134 -0
  3. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_questionnaire_response_save_test.rb +2 -2
  4. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_smart_app_questionnaire_workflow_group.rb +3 -2
  5. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/prepopulation_attestation_test.rb +4 -4
  6. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/prepopulation_override_attestation_test.rb +4 -4
  7. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/rendering_enabled_questions_attestation_test.rb +4 -4
  8. data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_questionnaire_package_group.rb +2 -2
  9. data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_questionnaire_rendering_attestation_test.rb +4 -4
  10. data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_questionnaire_response_save_test.rb +2 -2
  11. data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_resp_questionnaire_package_request_test.rb +132 -0
  12. data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_smart_app_questionnaire_workflow_group.rb +4 -3
  13. data/lib/davinci_dtr_test_kit/client_groups/shared/dtr_questionnaire_response_pre_population_test.rb +1 -0
  14. data/lib/davinci_dtr_test_kit/docs/dtr_payer_server_suite_description_v201.md +2 -2
  15. data/lib/davinci_dtr_test_kit/docs/dtr_smart_app_suite_description_v201.md +106 -42
  16. data/lib/davinci_dtr_test_kit/dtr_full_ehr_suite.rb +4 -3
  17. data/lib/davinci_dtr_test_kit/dtr_payer_server_suite.rb +6 -4
  18. data/lib/davinci_dtr_test_kit/dtr_questionnaire_response_validation.rb +26 -10
  19. data/lib/davinci_dtr_test_kit/dtr_smart_app_suite.rb +33 -11
  20. data/lib/davinci_dtr_test_kit/fixtures/dinner_adaptive/dinner_order_adaptive_next_question_hamburger.json +1 -1
  21. data/lib/davinci_dtr_test_kit/fixtures/dinner_static/questionnaire_dinner_order_static.json +2 -2
  22. data/lib/davinci_dtr_test_kit/fixtures/questionnaire_package.json +1 -1
  23. data/lib/davinci_dtr_test_kit/mock_auth_server.rb +145 -0
  24. data/lib/davinci_dtr_test_kit/mock_ehr.rb +37 -12
  25. data/lib/davinci_dtr_test_kit/mock_payer.rb +1 -20
  26. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_request_validation_test.rb +1 -0
  27. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_response_validation_test.rb +1 -0
  28. data/lib/davinci_dtr_test_kit/urls.rb +14 -3
  29. data/lib/davinci_dtr_test_kit/validation_test.rb +1 -0
  30. data/lib/davinci_dtr_test_kit/version.rb +1 -1
  31. metadata +19 -3
  32. 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
- Inferno does not currently include the ability to launch the client. Therefore, clients
56
- must be manually configured to point to Inferno's simulated server endpoints. The endpoints
57
- can be inferred from the URL of the test session which will be of the form `[URL prefix]/dtr_smart_app/[session id]`: (NOTE: both currently use the same URL)
58
- - Payer Server Base FHIR URL: `[URL prefix]/custom/dtr_smart_app/fhir`
59
- - Light EHR Base FHIR URL: `[URL prefix]/custom/dtr_smart_app/fhir`
60
-
61
- In order for Inferno to associate requests sent to locations under these base URLs with this session,
62
- it needs to know the bearer token that the app will send on requests, for which
63
- there are two options.
64
-
65
- 1. If you want to choose your own bearer token, then
66
- 1. Select the "2. Basic Workflows" test from the list on the left (or other target test).
67
- 2. Click the '*Run All Tests*' button on the right.
68
- 3. In the "access_token" field, enter the bearer token that will be sent by the client
69
- under test (as part of the Authorization header - `Bearer <provided value>`).
70
- 4. Click the '*Submit*' button at the bottom of the dialog.
71
- 2. If you want to use a client_id to obtain an access token, then
72
- 1. Click the '*Run All Tests*' button on the right.
73
- 2. Provide the client's registered id "client_id" field of the input (NOTE, Inferno doesn't support the
74
- registration API, so this must be obtained from another system or configured manually).
75
- 3. Click the '*Submit*' button at the bottom of the dialog.
76
- 4. Make a token request that includes the specified client id to the
77
- `[URL prefix]/custom/dtr_smart_app/mock_auth/token` endpoint to get
78
- an access token to use on the request of the requests.
79
-
80
- In either case, the tests will continue from that point. Further executions of tests under
81
- this session will also use the selected bearer token.
82
-
83
- Note: authentication options for these tests have not been finalized and are subject to change.
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
- - **2.1** *Static Questionnaire Workflow*: use requests in the `Static Dinner` folder
94
- - **2.1.1.01** *Invoke the DTR Questionnaire Package operation*: submit request `Questionnaire Package for Dinner (Static)` while this test is waiting.
95
- - **2.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.
96
- - **3.1** *Respiratory Assist Device Questionnaire Workflow*: use requests in the `Respiratory Assist Device` folder
97
- - **3.1.1.01** *Invoke the DTR Questionnaire Package operation*: submit request `Questionnaire Package for Resp Assist Device` while this test is waiting.
98
- - **3.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.
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 SMART app
103
- configuration to only part of it. Future versions of the test suite will test further
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
- The primary limitation on this test suite is that it requires the client under test
109
- to be manually configured to point to the Inferno endpoints and send a bearer token.
110
- In the future, the tests will provide a mechanism for launching the application using
111
- the SMART app launch mechanism. To provide feedback and input on the design of this feature,
112
- submit a ticket [here](https://github.com/inferno-framework/davinci-pas-test-kit/issues).
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, TOKEN_PATH, 'dtr_auth', method(:token_response) do |request|
59
- DTRFullEHRSuite.extract_client_id(request)
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, '/fhir/Questionnaire/$questionnaire-package', QUESTIONNAIRE_PACKAGE_TAG,
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, TOKEN_PATH, 'dtr_auth', method(:token_response) do |request|
108
- DTRPayerServerSuite.extract_client_id(request)
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, '/fhir/Questionnaire/$questionnaire-package', QUESTIONNAIRE_TAG,
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, '/fhir/Questionnaire/$next-question', NEXT_TAG,
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[link_id], error_messages,
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[link_id], error_messages, true)
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.value
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.value
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.value == expected_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.value != expected_answer
121
- error_list << "answer to item `#{item.linkId}` contains unexpected value. Expected: #{expected_answer}. " \
122
- "Found #{answer.value}"
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 MockPayer
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
- record_response_route :post, TOKEN_PATH, 'dtr_auth', method(:token_response) do |request|
56
- DTRSmartAppSuite.extract_client_id(request)
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.extract_bearer_token(request)
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.extract_bearer_token(request)
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.extract_bearer_token(request)
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.extract_bearer_token(request)
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.extract_token_from_query_params(request)
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.extract_token_from_query_params(request)
104
+ DTRSmartAppSuite.extract_client_id_from_query_params(request)
85
105
  end
86
106
 
87
- group from: :oauth2_authentication
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'
@@ -153,7 +153,7 @@
153
153
  },
154
154
  {
155
155
  "valueCoding": {
156
- "code": "Pickels"
156
+ "code": "Pickles"
157
157
  }
158
158
  },
159
159
  {
@@ -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": "Pickels"
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