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.
Files changed (26) 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 +97 -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 +95 -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/docs/dtr_smart_app_suite_description_v201.md +106 -42
  14. data/lib/davinci_dtr_test_kit/dtr_full_ehr_suite.rb +4 -3
  15. data/lib/davinci_dtr_test_kit/dtr_payer_server_suite.rb +6 -4
  16. data/lib/davinci_dtr_test_kit/dtr_questionnaire_response_validation.rb +26 -10
  17. data/lib/davinci_dtr_test_kit/dtr_smart_app_suite.rb +33 -11
  18. data/lib/davinci_dtr_test_kit/fixtures/dinner_static/questionnaire_dinner_order_static.json +1 -1
  19. data/lib/davinci_dtr_test_kit/fixtures/questionnaire_package.json +1 -1
  20. data/lib/davinci_dtr_test_kit/mock_auth_server.rb +135 -0
  21. data/lib/davinci_dtr_test_kit/mock_ehr.rb +32 -12
  22. data/lib/davinci_dtr_test_kit/mock_payer.rb +1 -20
  23. data/lib/davinci_dtr_test_kit/urls.rb +14 -3
  24. data/lib/davinci_dtr_test_kit/version.rb +1 -1
  25. metadata +19 -3
  26. 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: 940914e499d108663302b9df9fac473315dff874b8e6d1f7afa354a0fb02b412
4
- data.tar.gz: 878e70bfea50b8458ad08cbe7ad4834ae05c1c34c21a883dd1cdce883d1c3500
3
+ metadata.gz: e95ac1a81ec266133aa196823f9c4af450b6ff0c628b2ca9dccbb0fcd3b32863
4
+ data.tar.gz: d4ac1d46eadaa7f1067d07b4811e00803308f8d1c91cf9165f6e17534a280449
5
5
  SHA512:
6
- metadata.gz: 4b6888ef1ce9334ced832a2816c614ae4fd846c376181fcb7ff698e4069afe197db2f7305ff0100de94518ef22cd6d043ddd6516e965839e4acc7d836b9d98e2
7
- data.tar.gz: 5a4003c46adfd4f682979ef22bdf7d97dbff6c7be12416e744851eada2ece766f4dbceaa77a01468d7a5f2a4567f65e1d5885dcabfca1d42eddbc66ef575dafe
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
@@ -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 :access_token
12
+ input :client_id
13
13
 
14
14
  run do
15
15
  wait(
16
- identifier: access_token,
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 '../shared/dtr_questionnaire_package_request_test'
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: :dtr_questionnaire_package_request
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 :access_token
12
+ input :client_id
13
13
 
14
14
  run do
15
15
  wait(
16
- identifier: access_token,
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}?token=#{access_token}) if the above statement is **true**.
22
+ [Click here](#{resume_pass_url}?client_id=#{client_id}) if the above statement is **true**.
23
23
 
24
- [Click here](#{resume_fail_url}?token=#{access_token}) if the above statement is **false**.
24
+ [Click here](#{resume_fail_url}?client_id=#{client_id}) if the above statement is **false**.
25
25
  )
26
26
  )
27
27
  end
@@ -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 :access_token
12
+ input :client_id
13
13
 
14
14
  run do
15
15
  wait(
16
- identifier: access_token,
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}?token=#{access_token}) if the above statement is **true**.
23
+ [Click here](#{resume_pass_url}?client_id=#{client_id}) if the above statement is **true**.
24
24
 
25
- [Click here](#{resume_fail_url}?token=#{access_token}) if the above statement is **false**.
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 :access_token
13
+ input :client_id
14
14
 
15
15
  run do
16
16
  wait(
17
- identifier: access_token,
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}?token=#{access_token}) if the above statement is **true**.
23
+ [Click here](#{resume_pass_url}?client_id=#{client_id}) if the above statement is **true**.
24
24
 
25
- [Click here](#{resume_fail_url}?token=#{access_token}) if the above statement is **false**.
25
+ [Click here](#{resume_fail_url}?client_id=#{client_id}) if the above statement is **false**.
26
26
  )
27
27
  )
28
28
  end
@@ -1,4 +1,4 @@
1
- require_relative '../shared/dtr_questionnaire_package_request_test'
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: :dtr_questionnaire_package_request
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 :access_token
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: access_token,
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}?token=#{access_token}) if the above statement is **true**.
25
+ [Click here](#{resume_pass_url}?client_id=#{client_id}) if the above statement is **true**.
26
26
 
27
- [Click here](#{resume_fail_url}?token=#{access_token}) if the above statement is **false**.
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 :access_token
12
+ input :client_id
13
13
 
14
14
  run do
15
15
  wait(
16
- identifier: access_token,
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 Questionnaire workflow using a canned
11
- Questionnaire for a respiratory assist device order:
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
- 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_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'
@@ -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': 'application/json', 'Access-Control-Allow-Origin': '*' }.freeze
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, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [cs.to_json]]
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, _test_result = 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.present?
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': 'application/json', 'Access-Control-Allow-Origin': '*' }.freeze
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
- TOKEN_PATH = '/mock_auth/token'
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 token_url
19
- @token_url ||= base_url + TOKEN_PATH
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DaVinciDTRTestKit
4
- VERSION = '0.10.0'
4
+ VERSION = '0.11.0'
5
5
  end
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.10.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-08 00:00:00.000000000 Z
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
@@ -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