davinci_dtr_test_kit 0.10.0 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_dinner_questionnaire_package_request_test.rb +97 -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 +95 -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/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_static/questionnaire_dinner_order_static.json +1 -1
- data/lib/davinci_dtr_test_kit/fixtures/questionnaire_package.json +1 -1
- data/lib/davinci_dtr_test_kit/mock_auth_server.rb +135 -0
- data/lib/davinci_dtr_test_kit/mock_ehr.rb +32 -12
- data/lib/davinci_dtr_test_kit/mock_payer.rb +1 -20
- data/lib/davinci_dtr_test_kit/urls.rb +14 -3
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e95ac1a81ec266133aa196823f9c4af450b6ff0c628b2ca9dccbb0fcd3b32863
|
4
|
+
data.tar.gz: d4ac1d46eadaa7f1067d07b4811e00803308f8d1c91cf9165f6e17534a280449
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 22b65864b0795e2355b17d19b78d262524b29a9c0b1352603cd226a8d5c135a5bb0566c6c8449ce4ea0516ebedbee1e7a94965d7448e98a927b07716bd65c049
|
7
|
+
data.tar.gz: 0daaae716b201d20f99c768e5a7bc9d6ae997f953ab2d238862298ab00a39d82746b5e1a71537f13eeb09ea260c097955a22487405128aa5cbe9339cc1860fce
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require_relative '../../urls'
|
3
|
+
|
4
|
+
module DaVinciDTRTestKit
|
5
|
+
class DTRDinnerQuestionnairePackageRequestTest < Inferno::Test
|
6
|
+
include URLs
|
7
|
+
|
8
|
+
id :dtr_dinner_questionnaire_package_request
|
9
|
+
title 'Invoke the DTR Questionnaire Package operation'
|
10
|
+
description %(
|
11
|
+
Inferno will wait for a DTR questionnaire package request from the client. Upon receipt, Inferno will generate and
|
12
|
+
send a response.
|
13
|
+
)
|
14
|
+
input :smart_app_launch, type: 'radio', title: 'SMART App Launch',
|
15
|
+
description: 'How will the DTR SMART App launch?',
|
16
|
+
options: { list_options: [{ label: 'Launch from Inferno', value: 'inferno' },
|
17
|
+
{ label: 'Launch from EHR', value: 'ehr' }] }
|
18
|
+
input :client_id
|
19
|
+
input :launch_uri, optional: true, description: 'Required if "Launch from Inferno" is selected'
|
20
|
+
input :smart_patient_id, optional: true, title: 'SMART App Launch Patient ID (Dinner Static)',
|
21
|
+
type: 'text',
|
22
|
+
description: %(
|
23
|
+
Patient instance id to be provided by Inferno as the `patient` as a part of the SMART app
|
24
|
+
launch.
|
25
|
+
)
|
26
|
+
input :smart_fhir_context, optional: true, title: 'SMART App Launch fhirContext (Dinner Static)',
|
27
|
+
type: 'textarea',
|
28
|
+
description: %(
|
29
|
+
References to be provided by Inferno as the `fhirContext` as a part of the SMART app
|
30
|
+
launch. These references help determine the behavior of the app. Referenced instances
|
31
|
+
may be providedin the "EHR-available resources" input.
|
32
|
+
)
|
33
|
+
input :ehr_bundle, optional: true, title: 'EHR-available resources (Dinner Static)', type: 'textarea',
|
34
|
+
description: %(
|
35
|
+
Resources available from the EHR needed to drive the dinner static workflow.
|
36
|
+
Formatted as a FHIR bundle that contains resources, each with an `id` property populated. Each
|
37
|
+
instance present will be available for retrieval from Inferno at the endpoint
|
38
|
+
`[fhir-base]/[resource type]/[instance id].`
|
39
|
+
)
|
40
|
+
|
41
|
+
def example_client_jwt_payload_part
|
42
|
+
Base64.strict_encode64({ inferno_client_id: client_id }.to_json).delete('=')
|
43
|
+
end
|
44
|
+
|
45
|
+
run do
|
46
|
+
launch_prompt = if smart_app_launch == 'inferno'
|
47
|
+
%(Launch the DTR SMART App from Inferno by right clicking
|
48
|
+
[this link](#{launch_uri}?iss=#{fhir_base_url}&launch=#{launch_uri})
|
49
|
+
and selecting or "Open in new window" or "Open in new tab".)
|
50
|
+
else
|
51
|
+
%(Launch the SMART App from your EHR.)
|
52
|
+
end
|
53
|
+
inferno_prompt_cont = %(As the DTR app steps through the launch steps, Inferno will wait and respond to the app's
|
54
|
+
requests for SMART configuration, authorization and access token.)
|
55
|
+
|
56
|
+
wait(
|
57
|
+
identifier: client_id,
|
58
|
+
message: %(
|
59
|
+
### SMART App Launch
|
60
|
+
|
61
|
+
#{launch_prompt}
|
62
|
+
|
63
|
+
#{inferno_prompt_cont if smart_app_launch == 'inferno'}
|
64
|
+
|
65
|
+
Then, Inferno will expect the SMART App to invoke the DTR Questionnaire Package operation by sending a POST
|
66
|
+
request to
|
67
|
+
|
68
|
+
`#{questionnaire_package_url}`
|
69
|
+
|
70
|
+
A questionnaire package generated by Inferno will be returned.
|
71
|
+
|
72
|
+
### Pre-population
|
73
|
+
|
74
|
+
Inferno will then wait for the client to complete Questionnaire pre-population. The client should make FHIR
|
75
|
+
GET requests using service base path:
|
76
|
+
|
77
|
+
`#{fhir_base_url}`
|
78
|
+
|
79
|
+
### Request Identification
|
80
|
+
|
81
|
+
In order to identify requests for this session, Inferno will look for
|
82
|
+
an `Authorization` header with value:
|
83
|
+
|
84
|
+
```
|
85
|
+
Bearer eyJhbGcmOiJub25lIn0.#{example_client_jwt_payload_part}.
|
86
|
+
```
|
87
|
+
|
88
|
+
### Continuing the Tests
|
89
|
+
|
90
|
+
When the DTR application has finished loading the Questionnaire,
|
91
|
+
including any clinical data requests to support pre-population,
|
92
|
+
[Click here](#{resume_pass_url}?client_id=#{client_id}) to continue.
|
93
|
+
)
|
94
|
+
)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_questionnaire_response_save_test.rb
CHANGED
@@ -9,11 +9,11 @@ module DaVinciDTRTestKit
|
|
9
9
|
description %(
|
10
10
|
Inferno, acting as the EHR, will wait for a request to save the QuestionnaireResponse from the client.
|
11
11
|
)
|
12
|
-
input :
|
12
|
+
input :client_id
|
13
13
|
|
14
14
|
run do
|
15
15
|
wait(
|
16
|
-
identifier:
|
16
|
+
identifier: client_id,
|
17
17
|
message: %(
|
18
18
|
Complete the questionnaire, leaving the following items unmodified, because a subsequent test will expect
|
19
19
|
their pre-populated values:
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require_relative '
|
1
|
+
require_relative 'dtr_dinner_questionnaire_package_request_test'
|
2
2
|
require_relative '../shared/dtr_questionnaire_package_request_validation_test'
|
3
3
|
require_relative 'prepopulation_attestation_test'
|
4
4
|
require_relative 'prepopulation_override_attestation_test'
|
@@ -17,6 +17,7 @@ module DaVinciDTRTestKit
|
|
17
17
|
demonstrate their ability to:
|
18
18
|
|
19
19
|
1. Fetch the static questionnaire package
|
20
|
+
([DinnerOrderStatic](https://github.com/inferno-framework/davinci-dtr-test-kit/blob/main/lib/davinci_dtr_test_kit/fixtures/dinner_static/questionnaire_dinner_order_static.json))
|
20
21
|
2. Render and pre-populate the questionnaire appropriately, including:
|
21
22
|
- fetch additional data needed for pre-population
|
22
23
|
- pre-populate data as directed by the questionnaire
|
@@ -36,7 +37,7 @@ module DaVinciDTRTestKit
|
|
36
37
|
run_as_group
|
37
38
|
|
38
39
|
# Test 1: wait for the $questionnaire-package request
|
39
|
-
test from: :
|
40
|
+
test from: :dtr_dinner_questionnaire_package_request
|
40
41
|
# Test 2: validate the $questionnaire-package body
|
41
42
|
test from: :dtr_questionnaire_package_request_validation
|
42
43
|
end
|
@@ -9,19 +9,19 @@ module DaVinciDTRTestKit
|
|
9
9
|
description %(
|
10
10
|
Validate that pre-population of patient name information occurs as expected.
|
11
11
|
)
|
12
|
-
input :
|
12
|
+
input :client_id
|
13
13
|
|
14
14
|
run do
|
15
15
|
wait(
|
16
|
-
identifier:
|
16
|
+
identifier: client_id,
|
17
17
|
message: %(
|
18
18
|
I attest that the client application pre-populates the following questions with the respective values:
|
19
19
|
- Last Name: Oster
|
20
20
|
- First Name: William
|
21
21
|
|
22
|
-
[Click here](#{resume_pass_url}?
|
22
|
+
[Click here](#{resume_pass_url}?client_id=#{client_id}) if the above statement is **true**.
|
23
23
|
|
24
|
-
[Click here](#{resume_fail_url}?
|
24
|
+
[Click here](#{resume_fail_url}?client_id=#{client_id}) if the above statement is **false**.
|
25
25
|
)
|
26
26
|
)
|
27
27
|
end
|
data/lib/davinci_dtr_test_kit/client_groups/dinner_static/prepopulation_override_attestation_test.rb
CHANGED
@@ -9,20 +9,20 @@ module DaVinciDTRTestKit
|
|
9
9
|
description %(
|
10
10
|
Validate that the user can edit a pre-populated item and replace it with another value.
|
11
11
|
)
|
12
|
-
input :
|
12
|
+
input :client_id
|
13
13
|
|
14
14
|
run do
|
15
15
|
wait(
|
16
|
-
identifier:
|
16
|
+
identifier: client_id,
|
17
17
|
message: %(
|
18
18
|
I attest that
|
19
19
|
|
20
20
|
1. The client pre-populated an answer for question 'Location'.
|
21
21
|
2. I have changed the answer to a different value.
|
22
22
|
|
23
|
-
[Click here](#{resume_pass_url}?
|
23
|
+
[Click here](#{resume_pass_url}?client_id=#{client_id}) if the above statement is **true**.
|
24
24
|
|
25
|
-
[Click here](#{resume_fail_url}?
|
25
|
+
[Click here](#{resume_fail_url}?client_id=#{client_id}) if the above statement is **false**.
|
26
26
|
)
|
27
27
|
)
|
28
28
|
end
|
@@ -10,19 +10,19 @@ module DaVinciDTRTestKit
|
|
10
10
|
Validate that the rendering of the questionnaire includes only the "What would you like on..."
|
11
11
|
question appropriate for the dinner selection, if made.
|
12
12
|
)
|
13
|
-
input :
|
13
|
+
input :client_id
|
14
14
|
|
15
15
|
run do
|
16
16
|
wait(
|
17
|
-
identifier:
|
17
|
+
identifier: client_id,
|
18
18
|
message: %(
|
19
19
|
I attest that the client application does not display any "What would you like on..."
|
20
20
|
questions until I have selected a dinner choice and then only displays the
|
21
21
|
"What would you like on..." question relevant for the dinner request:
|
22
22
|
|
23
|
-
[Click here](#{resume_pass_url}?
|
23
|
+
[Click here](#{resume_pass_url}?client_id=#{client_id}) if the above statement is **true**.
|
24
24
|
|
25
|
-
[Click here](#{resume_fail_url}?
|
25
|
+
[Click here](#{resume_fail_url}?client_id=#{client_id}) if the above statement is **false**.
|
26
26
|
)
|
27
27
|
)
|
28
28
|
end
|
data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_questionnaire_package_group.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require_relative '
|
1
|
+
require_relative 'dtr_resp_questionnaire_package_request_test'
|
2
2
|
require_relative '../shared/dtr_questionnaire_package_request_validation_test'
|
3
3
|
|
4
4
|
module DaVinciDTRTestKit
|
@@ -10,7 +10,7 @@ module DaVinciDTRTestKit
|
|
10
10
|
)
|
11
11
|
run_as_group
|
12
12
|
|
13
|
-
test from: :
|
13
|
+
test from: :dtr_resp_questionnaire_package_request
|
14
14
|
test from: :dtr_questionnaire_package_request_validation
|
15
15
|
end
|
16
16
|
end
|
@@ -10,21 +10,21 @@ module DaVinciDTRTestKit
|
|
10
10
|
Thist test provides the tester an opportunity to observe their client application following the receipt of the
|
11
11
|
questionnaire pacakage and attest that the application renders the questionnaire.
|
12
12
|
)
|
13
|
-
input :
|
13
|
+
input :client_id
|
14
14
|
|
15
15
|
run do
|
16
16
|
load_tagged_requests QUESTIONNAIRE_PACKAGE_TAG
|
17
17
|
skip_if request.blank?, 'A Questionnaire Package request must be made prior to running this test'
|
18
18
|
|
19
19
|
wait(
|
20
|
-
identifier:
|
20
|
+
identifier: client_id,
|
21
21
|
message: %(
|
22
22
|
I attest that the client application displays the questionnaire and respects the following rendering style:
|
23
23
|
- The "Signature" field label is rendered with green text
|
24
24
|
|
25
|
-
[Click here](#{resume_pass_url}?
|
25
|
+
[Click here](#{resume_pass_url}?client_id=#{client_id}) if the above statement is **true**.
|
26
26
|
|
27
|
-
[Click here](#{resume_fail_url}?
|
27
|
+
[Click here](#{resume_fail_url}?client_id=#{client_id}) if the above statement is **false**.
|
28
28
|
)
|
29
29
|
)
|
30
30
|
end
|
@@ -9,11 +9,11 @@ module DaVinciDTRTestKit
|
|
9
9
|
description %(
|
10
10
|
Inferno, acting as the EHR, will wait for a request to save the QuestionnaireResponse from the client.
|
11
11
|
)
|
12
|
-
input :
|
12
|
+
input :client_id
|
13
13
|
|
14
14
|
run do
|
15
15
|
wait(
|
16
|
-
identifier:
|
16
|
+
identifier: client_id,
|
17
17
|
message: %(
|
18
18
|
Complete the questionnaire, leaving the following items unmodified, because a subsequent test will expect
|
19
19
|
their pre-populated values:
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require_relative '../../urls'
|
2
|
+
|
3
|
+
module DaVinciDTRTestKit
|
4
|
+
class DTRRespQuestionnairePackageRequestTest < Inferno::Test
|
5
|
+
include URLs
|
6
|
+
|
7
|
+
id :dtr_resp_questionnaire_package_request
|
8
|
+
title 'Invoke the DTR Questionnaire Package operation'
|
9
|
+
description %(
|
10
|
+
Inferno will wait for a DTR questionnaire package request from the client. Upon receipt, Inferno will generate and
|
11
|
+
send a response.
|
12
|
+
)
|
13
|
+
input :smart_app_launch, type: 'radio', title: 'SMART App Launch',
|
14
|
+
description: 'How will the DTR SMART App launch?',
|
15
|
+
options: { list_options: [{ label: 'Launch from Inferno', value: 'inferno' },
|
16
|
+
{ label: 'Launch from EHR', value: 'ehr' }] }
|
17
|
+
input :client_id
|
18
|
+
input :launch_uri, optional: true, description: 'Required if "Launch from Inferno" is selected'
|
19
|
+
input :smart_patient_id, optional: true, title: 'SMART App Launch Patient ID (Respiratory Assist Device)',
|
20
|
+
type: 'text',
|
21
|
+
description: %(
|
22
|
+
Patient instance id to be provided by Inferno as the `patient` as a part of the SMART app
|
23
|
+
launch.
|
24
|
+
)
|
25
|
+
input :smart_fhir_context, optional: true, title: 'SMART App Launch fhirContext (Respiratory Assist Device)',
|
26
|
+
type: 'textarea',
|
27
|
+
description: %(
|
28
|
+
References to be provided by Inferno as the `fhirContext` as a part of the SMART app
|
29
|
+
launch. These references help determine the behavior of the app. Referenced instances
|
30
|
+
may be providedin the "EHR-available resources" input.
|
31
|
+
)
|
32
|
+
input :ehr_bundle, optional: true, title: 'EHR-available resources (Respiratory Assist Device)', type: 'textarea',
|
33
|
+
description: %(
|
34
|
+
Resources available from the EHR needed to drive the respiratory assist device
|
35
|
+
workflow. Formatted as a FHIR bundle that contains resources, each with an `id`
|
36
|
+
property populated. Each instance present will be available for retrieval from
|
37
|
+
Inferno at the endpoint `[fhir-base]/[resource type]/[instance id].`
|
38
|
+
)
|
39
|
+
|
40
|
+
def example_client_jwt_payload_part
|
41
|
+
Base64.strict_encode64({ inferno_client_id: client_id }.to_json).delete('=')
|
42
|
+
end
|
43
|
+
|
44
|
+
run do
|
45
|
+
launch_prompt = if smart_app_launch == 'inferno'
|
46
|
+
%(Launch the DTR SMART App from Inferno by right clicking
|
47
|
+
[this link](#{launch_uri}?iss=#{fhir_base_url}&launch=#{launch_uri})
|
48
|
+
and selecting or "Open in new window" or "Open in new tab".)
|
49
|
+
else
|
50
|
+
%(Launch the SMART App from your EHR.)
|
51
|
+
end
|
52
|
+
inferno_prompt_cont = %(As the DTR app steps through the launch steps, Inferno will wait and respond to the app's
|
53
|
+
requests for SMART configuration, authorization and access token.)
|
54
|
+
wait(
|
55
|
+
identifier: client_id,
|
56
|
+
message: %(
|
57
|
+
### SMART App Launch
|
58
|
+
|
59
|
+
#{launch_prompt}
|
60
|
+
|
61
|
+
#{inferno_prompt_cont if smart_app_launch == 'inferno'}
|
62
|
+
|
63
|
+
Then, Inferno will expect the SMART App to invoke the DTR Questionnaire Package operation by sending a POST
|
64
|
+
request to
|
65
|
+
|
66
|
+
`#{questionnaire_package_url}`
|
67
|
+
|
68
|
+
A questionnaire package generated by Inferno will be returned.
|
69
|
+
|
70
|
+
### Pre-population
|
71
|
+
|
72
|
+
Inferno will then wait for the client to complete Questionnaire pre-population. The client should make FHIR
|
73
|
+
GET requests using service base path:
|
74
|
+
|
75
|
+
`#{fhir_base_url}`
|
76
|
+
|
77
|
+
### Request Identification
|
78
|
+
|
79
|
+
In order to identify requests for this session, Inferno will look for
|
80
|
+
an `Authorization` header with value:
|
81
|
+
|
82
|
+
```
|
83
|
+
Bearer eyJhbGcmOiJub25lIn0.#{example_client_jwt_payload_part}.
|
84
|
+
```
|
85
|
+
|
86
|
+
### Continuing the Tests
|
87
|
+
|
88
|
+
When the DTR application has finished loading the Questionnaire,
|
89
|
+
including any clinical data requests to support pre-population,
|
90
|
+
[Click here](#{resume_pass_url}?client_id=#{client_id}) to continue.
|
91
|
+
)
|
92
|
+
)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -7,10 +7,11 @@ module DaVinciDTRTestKit
|
|
7
7
|
id :dtr_smart_app_questionnaire_workflow
|
8
8
|
title 'Respiratory Assist Device Questionnaire Workflow'
|
9
9
|
description %(
|
10
|
-
This workflow validates that a DTR SMART App can perform a full DTR
|
11
|
-
Questionnaire
|
10
|
+
This workflow validates that a DTR SMART App can perform a full DTR
|
11
|
+
Questionnaire workflow using a canned Questionnaire
|
12
|
+
for a respiratory assist device order:
|
12
13
|
|
13
|
-
1. Fetch the questionnaire package
|
14
|
+
1. Fetch the questionnaire package ([RespiratoryAssistDevices](https://github.com/inferno-framework/davinci-dtr-test-kit/blob/main/lib/davinci_dtr_test_kit/fixtures/questionnaire_package.json))
|
14
15
|
2. Render the questionnaire
|
15
16
|
3. Pre-populate the questionnaire response
|
16
17
|
)
|
@@ -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_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"
|
@@ -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,135 @@
|
|
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
|
+
smart_app_launch_input = test_input.find { |input| input['name'] == 'smart_app_launch' }
|
64
|
+
|
65
|
+
if smart_app_launch_input.present? && smart_app_launch_input['value'] == 'inferno'
|
66
|
+
fhir_context_input = test_input.find { |input| input['name'] == 'smart_fhir_context' }
|
67
|
+
fhir_context_input_value = fhir_context_input['value'] if fhir_context_input.present?
|
68
|
+
fhir_context = fhir_context_input_value || [
|
69
|
+
{ reference: 'Coverage/cov015' },
|
70
|
+
{ reference: 'DeviceRequest/devreqe0470' }
|
71
|
+
]
|
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
|
+
smart_patient = smart_patient_input_value || 'pat015'
|
76
|
+
|
77
|
+
response.merge!({ patient: smart_patient, fhirContext: fhir_context })
|
78
|
+
end
|
79
|
+
request.response_body = response.to_json
|
80
|
+
request.response_headers = { 'Access-Control-Allow-Origin' => '*' }
|
81
|
+
request.status = 200
|
82
|
+
end
|
83
|
+
|
84
|
+
def payer_token_response(request, _test = nil, _test_result = nil)
|
85
|
+
# Placeholder for a more complete mock token endpoint
|
86
|
+
request.response_body = { access_token: SecureRandom.hex, token_type: 'bearer', expires_in: 3600 }.to_json
|
87
|
+
request.status = 200
|
88
|
+
end
|
89
|
+
|
90
|
+
def extract_client_id_from_token_request(request)
|
91
|
+
# Public client || confidential client asymmetric || confidential client symmetric
|
92
|
+
extract_client_id_from_form_params(request) ||
|
93
|
+
extract_client_id_from_client_assertion(request) ||
|
94
|
+
extract_client_id_from_basic_auth(request)
|
95
|
+
end
|
96
|
+
|
97
|
+
def extract_client_id_from_form_params(request)
|
98
|
+
URI.decode_www_form(request.request_body).to_h['client_id']
|
99
|
+
end
|
100
|
+
|
101
|
+
def extract_client_id_from_client_assertion(request)
|
102
|
+
encoded_jwt = URI.decode_www_form(request.request_body).to_h['client_assertion']
|
103
|
+
return unless encoded_jwt.present?
|
104
|
+
|
105
|
+
jwt_payload = JWT.decode(encoded_jwt, nil, false)&.first # skip signature verification
|
106
|
+
jwt_payload['iss'] || jwt_payload['sub'] if jwt_payload.present?
|
107
|
+
end
|
108
|
+
|
109
|
+
def extract_client_id_from_basic_auth(request)
|
110
|
+
encoded_credentials = request.request_header('Authorization')&.value&.split&.last
|
111
|
+
return unless encoded_credentials.present?
|
112
|
+
|
113
|
+
decoded_credentials = Base64.decode64(encoded_credentials)
|
114
|
+
decoded_credentials&.split(':')&.first
|
115
|
+
end
|
116
|
+
|
117
|
+
def extract_client_id_from_query_params(request)
|
118
|
+
request.query_parameters['client_id']
|
119
|
+
end
|
120
|
+
|
121
|
+
def extract_client_id_from_bearer_token(request)
|
122
|
+
token = extract_bearer_token(request)
|
123
|
+
JWT.decode(token, nil, false)&.first&.dig('inferno_client_id')
|
124
|
+
end
|
125
|
+
|
126
|
+
# Header expected to be a bearer token of the form "Bearer: <token>"
|
127
|
+
def extract_bearer_token(request)
|
128
|
+
request.request_header('Authorization')&.value&.split&.last
|
129
|
+
end
|
130
|
+
|
131
|
+
def extract_token_from_query_params(request)
|
132
|
+
request.query_parameters['token']
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -1,11 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'urls'
|
4
|
+
|
3
5
|
module DaVinciDTRTestKit
|
4
6
|
module MockEHR
|
5
7
|
RESOURCE_SERVER_BASE = ENV.fetch('FHIR_REFERENCE_SERVER')
|
6
8
|
RESOURCE_SERVER_BEARER_TOKEN = 'SAMPLE_TOKEN'
|
7
9
|
|
8
|
-
RESPONSE_HEADERS = { 'Content-Type'
|
10
|
+
RESPONSE_HEADERS = { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }.freeze
|
9
11
|
|
10
12
|
def resource_server_client
|
11
13
|
return @resource_server_client if @resource_server_client
|
@@ -18,13 +20,13 @@ module DaVinciDTRTestKit
|
|
18
20
|
def metadata_handler(_env)
|
19
21
|
cs = resource_server_client.capability_statement
|
20
22
|
if cs.present?
|
21
|
-
[200,
|
23
|
+
[200, RESPONSE_HEADERS, [cs.to_json]]
|
22
24
|
else
|
23
25
|
[500, {}, ['Unexpected error occurred while fetching metadata']]
|
24
26
|
end
|
25
27
|
end
|
26
28
|
|
27
|
-
def get_fhir_resource(request, _test = nil,
|
29
|
+
def get_fhir_resource(request, _test = nil, test_result = nil)
|
28
30
|
resource_type, id = resource_type_and_id_from_url(request.url)
|
29
31
|
request.response_headers = RESPONSE_HEADERS
|
30
32
|
|
@@ -34,15 +36,7 @@ module DaVinciDTRTestKit
|
|
34
36
|
resource_type = nil
|
35
37
|
end
|
36
38
|
|
37
|
-
if resource_type.
|
38
|
-
response = if id.present?
|
39
|
-
resource_server_client.read(fhir_class, id)
|
40
|
-
else
|
41
|
-
resource_server_client.search(fhir_class, search: { parameters: request.query_parameters })
|
42
|
-
end
|
43
|
-
request.status = response.code
|
44
|
-
request.response_body = response.body
|
45
|
-
else
|
39
|
+
if resource_type.blank?
|
46
40
|
request.status = 400
|
47
41
|
request.response_headers = { 'Content-Type': 'application/json' }
|
48
42
|
request.response_body = FHIR::OperationOutcome.new(
|
@@ -51,7 +45,33 @@ module DaVinciDTRTestKit
|
|
51
45
|
text: 'No recognized resource type in URL'
|
52
46
|
))
|
53
47
|
).to_json
|
48
|
+
return
|
54
49
|
end
|
50
|
+
|
51
|
+
# Respond with user-inputted resource if there is one that matches the request
|
52
|
+
ehr_bundle_input = JSON.parse(test_result.input_json).find { |input| input['name'] == 'ehr_bundle' }
|
53
|
+
ehr_bundle_input_value = ehr_bundle_input_value = ehr_bundle_input['value'] if ehr_bundle_input.present?
|
54
|
+
ehr_bundle = FHIR.from_contents(ehr_bundle_input_value) if ehr_bundle_input_value.present?
|
55
|
+
if id.present? && ehr_bundle.present? && ehr_bundle.is_a?(FHIR::Bundle)
|
56
|
+
matching_resource = ehr_bundle.entry&.find do |entry|
|
57
|
+
entry.resource.is_a?(fhir_class) && entry.resource&.id == id
|
58
|
+
end&.resource
|
59
|
+
if matching_resource.present?
|
60
|
+
request.status = 200
|
61
|
+
request.response_headers = { 'Content-Type': 'application/json' }
|
62
|
+
request.response_body = matching_resource.to_json
|
63
|
+
return
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Proxy resource request to the reference server
|
68
|
+
response = if id.present?
|
69
|
+
resource_server_client.read(fhir_class, id)
|
70
|
+
else
|
71
|
+
resource_server_client.search(fhir_class, search: { parameters: request.query_parameters })
|
72
|
+
end
|
73
|
+
request.status = response.code
|
74
|
+
request.response_body = response.body
|
55
75
|
end
|
56
76
|
|
57
77
|
def questionnaire_response_response(request, _test = nil, _test_result = nil)
|
@@ -6,13 +6,7 @@ module DaVinciDTRTestKit
|
|
6
6
|
module MockPayer
|
7
7
|
include Fixtures
|
8
8
|
|
9
|
-
RESPONSE_HEADERS = { 'Content-Type'
|
10
|
-
|
11
|
-
def token_response(request, _test = nil, _test_result = nil)
|
12
|
-
# Placeholder for a more complete mock token endpoint
|
13
|
-
request.response_body = { access_token: SecureRandom.hex, token_type: 'bearer', expires_in: 300 }.to_json
|
14
|
-
request.status = 200
|
15
|
-
end
|
9
|
+
RESPONSE_HEADERS = { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }.freeze
|
16
10
|
|
17
11
|
def questionnaire_package_response(request, _test = nil, test_result = nil)
|
18
12
|
request.status = 200
|
@@ -47,19 +41,6 @@ module DaVinciDTRTestKit
|
|
47
41
|
request.response_body = payer_response.response[:body]
|
48
42
|
end
|
49
43
|
|
50
|
-
def extract_client_id(request)
|
51
|
-
URI.decode_www_form(request.request_body).to_h['client_id']
|
52
|
-
end
|
53
|
-
|
54
|
-
# Header expected to be a bearer token of the form "Bearer: <token>"
|
55
|
-
def extract_bearer_token(request)
|
56
|
-
request.request_header('Authorization')&.value&.split&.last
|
57
|
-
end
|
58
|
-
|
59
|
-
def extract_token_from_query_params(request)
|
60
|
-
request.query_parameters['token']
|
61
|
-
end
|
62
|
-
|
63
44
|
def test_resumes?(test)
|
64
45
|
!test.config.options[:accepts_multiple_requests]
|
65
46
|
end
|
@@ -1,7 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module DaVinciDTRTestKit
|
4
|
-
|
4
|
+
SMART_CONFIG_PATH = '/fhir/.well-known/smart-configuration'
|
5
|
+
EHR_AUTHORIZE_PATH = '/mock_ehr_auth/authorize'
|
6
|
+
EHR_TOKEN_PATH = '/mock_ehr_auth/token'
|
7
|
+
PAYER_TOKEN_PATH = '/mock_payer_auth/token'
|
5
8
|
QUESTIONNAIRE_PACKAGE_PATH = '/fhir/Questionnaire/$questionnaire-package'
|
6
9
|
NEXT_PATH = '/fhir/Questionnaire/$next-question'
|
7
10
|
QUESTIONNAIRE_RESPONSE_PATH = '/fhir/QuestionnaireResponse'
|
@@ -15,8 +18,16 @@ module DaVinciDTRTestKit
|
|
15
18
|
@base_url ||= "#{Inferno::Application['base_url']}/custom/#{suite_id}"
|
16
19
|
end
|
17
20
|
|
18
|
-
def
|
19
|
-
@
|
21
|
+
def ehr_authorize_url
|
22
|
+
@ehr_authorize_url ||= base_url + EHR_AUTHORIZE_PATH
|
23
|
+
end
|
24
|
+
|
25
|
+
def ehr_token_url
|
26
|
+
@ehr_token_url ||= base_url + EHR_TOKEN_PATH
|
27
|
+
end
|
28
|
+
|
29
|
+
def payer_token_url
|
30
|
+
@payer_token_url ||= base_url + PAYER_TOKEN_PATH
|
20
31
|
end
|
21
32
|
|
22
33
|
def questionnaire_package_url
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: davinci_dtr_test_kit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Karl Naden
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2024-07-
|
13
|
+
date: 2024-07-15 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: inferno_core
|
@@ -26,6 +26,20 @@ dependencies:
|
|
26
26
|
- - "~>"
|
27
27
|
- !ruby/object:Gem::Version
|
28
28
|
version: 0.4.37
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: jwt
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - "~>"
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '2.6'
|
36
|
+
type: :runtime
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - "~>"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '2.6'
|
29
43
|
description: Test Kit for the Da Vinci Documentation Templates and Rules (DTR) FHIR
|
30
44
|
Implementation Guide
|
31
45
|
email:
|
@@ -40,6 +54,7 @@ files:
|
|
40
54
|
- lib/davinci_dtr_test_kit/auth_groups/token_request_test.rb
|
41
55
|
- lib/davinci_dtr_test_kit/auth_groups/token_validation_test.rb
|
42
56
|
- lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_smart_app_questionnaire_workflow_group.rb
|
57
|
+
- lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_dinner_questionnaire_package_request_test.rb
|
43
58
|
- lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_questionnaire_response_save_test.rb
|
44
59
|
- lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_smart_app_questionnaire_workflow_group.rb
|
45
60
|
- lib/davinci_dtr_test_kit/client_groups/dinner_static/prepopulation_attestation_test.rb
|
@@ -51,8 +66,8 @@ files:
|
|
51
66
|
- lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_questionnaire_rendering_group.rb
|
52
67
|
- lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_questionnaire_response_group.rb
|
53
68
|
- lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_questionnaire_response_save_test.rb
|
69
|
+
- lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_resp_questionnaire_package_request_test.rb
|
54
70
|
- lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_smart_app_questionnaire_workflow_group.rb
|
55
|
-
- lib/davinci_dtr_test_kit/client_groups/shared/dtr_questionnaire_package_request_test.rb
|
56
71
|
- lib/davinci_dtr_test_kit/client_groups/shared/dtr_questionnaire_package_request_validation_test.rb
|
57
72
|
- lib/davinci_dtr_test_kit/client_groups/shared/dtr_questionnaire_response_basic_conformance_test.rb
|
58
73
|
- lib/davinci_dtr_test_kit/client_groups/shared/dtr_questionnaire_response_pre_population_test.rb
|
@@ -77,6 +92,7 @@ files:
|
|
77
92
|
- lib/davinci_dtr_test_kit/fixtures/dinner_static/questionnaire_response_dinner_order_static.json
|
78
93
|
- lib/davinci_dtr_test_kit/fixtures/pre_populated_questionnaire_response.json
|
79
94
|
- lib/davinci_dtr_test_kit/fixtures/questionnaire_package.json
|
95
|
+
- lib/davinci_dtr_test_kit/mock_auth_server.rb
|
80
96
|
- lib/davinci_dtr_test_kit/mock_ehr.rb
|
81
97
|
- lib/davinci_dtr_test_kit/mock_payer.rb
|
82
98
|
- lib/davinci_dtr_test_kit/payer_server_groups/adaptive_form_libraries_test.rb
|
data/lib/davinci_dtr_test_kit/client_groups/shared/dtr_questionnaire_package_request_test.rb
DELETED
@@ -1,36 +0,0 @@
|
|
1
|
-
require_relative '../../urls'
|
2
|
-
|
3
|
-
module DaVinciDTRTestKit
|
4
|
-
class DTRQuestionnairePackageRequestTest < Inferno::Test
|
5
|
-
include URLs
|
6
|
-
|
7
|
-
id :dtr_questionnaire_package_request
|
8
|
-
title 'Invoke the DTR Questionnaire Package operation'
|
9
|
-
description %(
|
10
|
-
Inferno will wait for a DTR questionnaire package request from the client. Upon receipt, Inferno will generate and
|
11
|
-
send a response.
|
12
|
-
)
|
13
|
-
input :access_token
|
14
|
-
|
15
|
-
run do
|
16
|
-
wait(
|
17
|
-
identifier: access_token,
|
18
|
-
message: %(
|
19
|
-
Invoke the DTR Questionnaire Package operation by sending a POST request to
|
20
|
-
|
21
|
-
`#{questionnaire_package_url}`
|
22
|
-
|
23
|
-
A questionnaire package generated by Inferno will be returned.
|
24
|
-
|
25
|
-
Inferno will wait for the client to complete Questionnaire pre-population. The client should make FHIR GET
|
26
|
-
requests using service base path:
|
27
|
-
|
28
|
-
`#{fhir_base_url}`
|
29
|
-
|
30
|
-
When the DTR application has finished loading the Questionnaire,
|
31
|
-
[Click here](#{resume_pass_url}?token=#{access_token}) to continue.
|
32
|
-
)
|
33
|
-
)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|