davinci_dtr_test_kit 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) 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 +107 -43
  14. data/lib/davinci_dtr_test_kit/dtr_full_ehr_suite.rb +30 -6
  15. data/lib/davinci_dtr_test_kit/dtr_light_ehr_suite.rb +26 -3
  16. data/lib/davinci_dtr_test_kit/dtr_payer_server_suite.rb +33 -7
  17. data/lib/davinci_dtr_test_kit/dtr_questionnaire_response_validation.rb +26 -10
  18. data/lib/davinci_dtr_test_kit/dtr_smart_app_suite.rb +59 -14
  19. data/lib/davinci_dtr_test_kit/fixtures/dinner_static/questionnaire_dinner_order_static.json +1 -1
  20. data/lib/davinci_dtr_test_kit/fixtures/questionnaire_package.json +1 -1
  21. data/lib/davinci_dtr_test_kit/mock_auth_server.rb +135 -0
  22. data/lib/davinci_dtr_test_kit/mock_ehr.rb +32 -12
  23. data/lib/davinci_dtr_test_kit/mock_payer.rb +2 -21
  24. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_static_group.rb +4 -2
  25. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_libraries_test.rb +2 -2
  26. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_questionnaire_expressions_test.rb +3 -3
  27. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_questionnaire_extensions_test.rb +2 -2
  28. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_request_test.rb +1 -0
  29. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_request_validation_test.rb +5 -3
  30. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_response_validation_test.rb +15 -11
  31. data/lib/davinci_dtr_test_kit/urls.rb +14 -3
  32. data/lib/davinci_dtr_test_kit/version.rb +1 -1
  33. metadata +19 -3
  34. data/lib/davinci_dtr_test_kit/client_groups/shared/dtr_questionnaire_package_request_test.rb +0 -36
@@ -16,6 +16,25 @@ module DaVinciDTRTestKit
16
16
 
17
17
  version VERSION
18
18
 
19
+ links [
20
+ {
21
+ label: 'Report Issue',
22
+ url: 'https://github.com/inferno-framework/davinci-dtr-test-kit/issues'
23
+ },
24
+ {
25
+ label: 'Open Source',
26
+ url: 'https://github.com/inferno-framework/davinci-dtr-test-kit'
27
+ },
28
+ {
29
+ label: 'Download',
30
+ url: 'https://github.com/inferno-framework/davinci-dtr-test-kit/releases'
31
+ },
32
+ {
33
+ label: 'Implementation Guide',
34
+ url: 'https://hl7.org/fhir/us/davinci-dtr/STU2/'
35
+ }
36
+ ]
37
+
19
38
  # These inputs will be available to all tests in this suite
20
39
  input :url,
21
40
  title: 'FHIR Server Base Url'
@@ -31,9 +50,13 @@ module DaVinciDTRTestKit
31
50
  oauth_credentials :credentials
32
51
  end
33
52
 
34
- # All FHIR validation requsets will use this FHIR validator
35
- validator do
36
- url ENV.fetch('VALIDATOR_URL')
53
+ # Hl7 Validator Wrapper:
54
+ fhir_resource_validator do
55
+ igs 'hl7.fhir.us.davinci-dtr#2.0.1'
56
+
57
+ exclude_message do |message|
58
+ message.message.match?(/\A\S+: \S+: URL value '.*' does not resolve/)
59
+ end
37
60
  end
38
61
  end
39
62
  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
@@ -16,6 +18,26 @@ module DaVinciDTRTestKit
16
18
  description File.read(File.join(__dir__, 'docs', 'dtr_payer_server_suite_description_v201.md'))
17
19
 
18
20
  version VERSION
21
+
22
+ links [
23
+ {
24
+ label: 'Report Issue',
25
+ url: 'https://github.com/inferno-framework/davinci-dtr-test-kit/issues'
26
+ },
27
+ {
28
+ label: 'Open Source',
29
+ url: 'https://github.com/inferno-framework/davinci-dtr-test-kit'
30
+ },
31
+ {
32
+ label: 'Download',
33
+ url: 'https://github.com/inferno-framework/davinci-dtr-test-kit/releases'
34
+ },
35
+ {
36
+ label: 'Implementation Guide',
37
+ url: 'https://hl7.org/fhir/us/davinci-dtr/STU2/'
38
+ }
39
+ ]
40
+
19
41
  # These inputs will be available to all tests in this suite
20
42
 
21
43
  input :retrieval_method,
@@ -73,23 +95,27 @@ module DaVinciDTRTestKit
73
95
  oauth_credentials :credentials
74
96
  end
75
97
 
76
- # All FHIR validation requsets will use this FHIR validator
77
- validator do
78
- url ENV.fetch('VALIDATOR_URL')
98
+ # Hl7 Validator Wrapper:
99
+ fhir_resource_validator do
100
+ igs 'hl7.fhir.us.davinci-dtr#2.0.1'
101
+
102
+ exclude_message do |message|
103
+ message.message.match?(/\A\S+: \S+: URL value '.*' does not resolve/)
104
+ end
79
105
  end
80
106
 
81
107
  allow_cors QUESTIONNAIRE_PACKAGE_PATH, NEXT_PATH
82
108
 
83
- record_response_route :post, TOKEN_PATH, 'dtr_auth', method(:token_response) do |request|
84
- 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)
85
111
  end
86
112
 
87
- record_response_route :post, '/fhir/Questionnaire/$questionnaire-package', QUESTIONNAIRE_TAG,
113
+ record_response_route :post, QUESTIONNAIRE_PACKAGE_PATH, QUESTIONNAIRE_TAG,
88
114
  method(:payer_questionnaire_response), resumes: method(:test_resumes?) do |request|
89
115
  DTRPayerServerSuite.extract_bearer_token(request)
90
116
  end
91
117
 
92
- record_response_route :post, '/fhir/Questionnaire/$next-question', NEXT_TAG,
118
+ record_response_route :post, NEXT_PATH, NEXT_TAG,
93
119
  method(:questionnaire_next_response), resumes: method(:test_resumes?) do |request|
94
120
  DTRPayerServerSuite.extract_bearer_token(request)
95
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'
@@ -20,48 +21,92 @@ module DaVinciDTRTestKit
20
21
 
21
22
  version VERSION
22
23
 
23
- # All FHIR validation requsets will use this FHIR validator
24
- validator do
25
- url ENV.fetch('VALIDATOR_URL')
24
+ links [
25
+ {
26
+ label: 'Report Issue',
27
+ url: 'https://github.com/inferno-framework/davinci-dtr-test-kit/issues'
28
+ },
29
+ {
30
+ label: 'Open Source',
31
+ url: 'https://github.com/inferno-framework/davinci-dtr-test-kit'
32
+ },
33
+ {
34
+ label: 'Download',
35
+ url: 'https://github.com/inferno-framework/davinci-dtr-test-kit/releases'
36
+ },
37
+ {
38
+ label: 'Implementation Guide',
39
+ url: 'https://hl7.org/fhir/us/davinci-dtr/STU2/'
40
+ }
41
+ ]
42
+
43
+ # Hl7 Validator Wrapper:
44
+ fhir_resource_validator do
45
+ igs 'hl7.fhir.us.davinci-dtr#2.0.1'
46
+
47
+ exclude_message do |message|
48
+ message.message.match?(/\A\S+: \S+: URL value '.*' does not resolve/)
49
+ end
26
50
  end
27
51
 
28
- 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
29
54
 
30
55
  route(:get, '/fhir/metadata', method(:metadata_handler))
31
56
 
32
- record_response_route :post, TOKEN_PATH, 'dtr_auth', method(:token_response) do |request|
33
- 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)
34
77
  end
35
78
 
36
79
  record_response_route :post, QUESTIONNAIRE_PACKAGE_PATH, QUESTIONNAIRE_PACKAGE_TAG,
37
80
  method(:questionnaire_package_response), resumes: ->(_) { false } do |request|
38
- DTRSmartAppSuite.extract_bearer_token(request)
81
+ DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
39
82
  end
40
83
 
41
84
  record_response_route :post, QUESTIONNAIRE_RESPONSE_PATH, 'dtr_smart_app_questionnaire_response',
42
85
  method(:questionnaire_response_response) do |request|
43
- DTRSmartAppSuite.extract_bearer_token(request)
86
+ DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
44
87
  end
45
88
 
46
89
  record_response_route :get, FHIR_RESOURCE_PATH, SMART_APP_EHR_REQUEST_TAG, method(:get_fhir_resource),
47
90
  resumes: ->(_) { false } do |request|
48
- DTRSmartAppSuite.extract_bearer_token(request)
91
+ DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
49
92
  end
50
93
 
51
94
  record_response_route :get, FHIR_SEARCH_PATH, SMART_APP_EHR_REQUEST_TAG, method(:get_fhir_resource),
52
95
  resumes: ->(_) { false } do |request|
53
- DTRSmartAppSuite.extract_bearer_token(request)
96
+ DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
54
97
  end
55
98
 
56
99
  resume_test_route :get, RESUME_PASS_PATH do |request|
57
- DTRSmartAppSuite.extract_token_from_query_params(request)
100
+ DTRSmartAppSuite.extract_client_id_from_query_params(request)
58
101
  end
59
102
 
60
103
  resume_test_route :get, RESUME_FAIL_PATH, result: 'fail' do |request|
61
- DTRSmartAppSuite.extract_token_from_query_params(request)
104
+ DTRSmartAppSuite.extract_client_id_from_query_params(request)
62
105
  end
63
106
 
64
- 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
+
65
110
  group do
66
111
  id :dtr_smart_app_basic_workflows
67
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
@@ -25,7 +19,7 @@ module DaVinciDTRTestKit
25
19
  url_input = JSON.parse(test_result.input_json).find { |input| input['name'] == 'url' }
26
20
  client = FHIR::Client.new(url_input['value'])
27
21
  client.default_json
28
- endpoint = endpoint_input['value'].nil? ? '/Questionnaire/$questionnaire-package' : endpoint_input['value']
22
+ endpoint = endpoint_input.to_h['value'].nil? ? '/Questionnaire/$questionnaire-package' : endpoint_input['value']
29
23
  payer_response = client.send(:post, endpoint, JSON.parse(request.request_body),
30
24
  { 'Content-Type' => 'application/json' })
31
25
 
@@ -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
@@ -37,11 +37,13 @@ module DaVinciDTRTestKit
37
37
  description: 'Manual Flow',
38
38
  type: 'textarea'
39
39
 
40
+ input :access_token, :retrieval_method
41
+
40
42
  input_order :retrieval_method,
41
- :url,
43
+ :access_token,
42
44
  :initial_static_questionnaire_request
43
45
 
44
- test from: :dtr_v201_payer_static_questionnaire_request_test, receives_request: :statice_questionnaire_request
46
+ test from: :dtr_v201_payer_static_questionnaire_request_test, receives_request: :static_questionnaire_request
45
47
  test from: :dtr_v201_payer_static_form_request_validation_test
46
48
  test from: :dtr_v201_payer_static_form_response_test
47
49
  test from: :dtr_v201_payer_static_form_libraries_test
@@ -12,8 +12,8 @@ module DaVinciDTRTestKit
12
12
 
13
13
  run do
14
14
  skip_if retrieval_method == 'Adaptive', 'Performing only adaptive flow tests - only one flow is required.'
15
- skip_if scratch[:questionnaire_bundle].nil?, 'No questionnaire bundle returned.'
16
- check_libraries(scratch[:questionnaire_bundle])
15
+ skip_if scratch[:output_parameters].nil?, 'No questionnaire bundle returned.'
16
+ check_libraries(scratch[:output_parameters])
17
17
  end
18
18
  end
19
19
  end
@@ -12,9 +12,9 @@ module DaVinciDTRTestKit
12
12
 
13
13
  run do
14
14
  skip_if retrieval_method == 'Adaptive', 'Performing only adaptive flow tests - only one flow is required.'
15
- skip_if scratch[:questionnaire_bundle].nil?, 'No questionnaire bundle returned.'
16
- questionnaire_items_test(scratch[:questionnaire_bundle], final_cql_test: true)
17
- scratch[:questionnaire_bundle] = nil
15
+ skip_if scratch[:output_parameters].nil?, 'No questionnaire bundle returned.'
16
+ questionnaire_items_test(scratch[:output_parameters], final_cql_test: true)
17
+ scratch[:output_parameters] = nil
18
18
  end
19
19
  end
20
20
  end
@@ -12,8 +12,8 @@ module DaVinciDTRTestKit
12
12
 
13
13
  run do
14
14
  skip_if retrieval_method == 'Adaptive', 'Performing only adaptive flow tests - only one flow is required.'
15
- skip_if scratch[:questionnaire_bundle].nil?, 'No questionnaire bundle returned.'
16
- questionnaire_extensions_test(scratch[:questionnaire_bundle])
15
+ skip_if scratch[:output_parameters].nil?, 'No questionnaire bundle returned.'
16
+ questionnaire_extensions_test(scratch[:output_parameters])
17
17
  end
18
18
  end
19
19
  end
@@ -9,6 +9,7 @@ module DaVinciDTRTestKit
9
9
  )
10
10
  id :dtr_v201_payer_static_questionnaire_request_test
11
11
  config options: { accepts_multiple_requests: false }
12
+ input :initial_static_questionnaire_request, :access_token, :retrieval_method, :url
12
13
 
13
14
  run do
14
15
  skip_if retrieval_method == 'Adaptive', 'Performing only adaptive flow tests - only one flow is required.'