davinci_dtr_test_kit 0.9.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
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.'