davinci_dtr_test_kit 0.11.1 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/lib/davinci_dtr_test_kit/auth_groups/token_request_test.rb +1 -1
  3. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_full_ehr_dinner_questionnaire_package_request_test.rb +52 -0
  4. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_full_ehr_dinner_static_questionnaire_response_conformance_test.rb +15 -0
  5. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_full_ehr_dinner_static_questionnaire_response_correctness_test.rb +30 -0
  6. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_full_ehr_launch_attestation_test.rb +28 -0
  7. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_full_ehr_prepopulation_attestation_test.rb +30 -0
  8. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_full_ehr_prepopulation_override_attestation_test.rb +27 -0
  9. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_full_ehr_questionnaire_workflow_group.rb +91 -0
  10. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_full_ehr_rendering_enabled_questions_attestation_test.rb +30 -0
  11. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_full_ehr_store_attestation_test.rb +29 -0
  12. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/{dtr_dinner_questionnaire_package_request_test.rb → dtr_smart_app_dinner_questionnaire_package_request_test.rb} +5 -5
  13. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/{prepopulation_attestation_test.rb → dtr_smart_app_prepopulation_attestation_test.rb} +2 -2
  14. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/{prepopulation_override_attestation_test.rb → dtr_smart_app_prepopulation_override_attestation_test.rb} +2 -2
  15. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/{dtr_questionnaire_response_save_test.rb → dtr_smart_app_questionnaire_response_save_test.rb} +2 -2
  16. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_smart_app_questionnaire_workflow_group.rb +13 -13
  17. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/{rendering_enabled_questions_attestation_test.rb → dtr_smart_app_rendering_enabled_questions_attestation_test.rb} +2 -2
  18. data/lib/davinci_dtr_test_kit/client_groups/shared/dtr_questionnaire_package_request_validation_test.rb +1 -1
  19. data/lib/davinci_dtr_test_kit/client_groups/shared/dtr_questionnaire_response_basic_conformance_test.rb +3 -7
  20. data/lib/davinci_dtr_test_kit/client_groups/shared/dtr_questionnaire_response_pre_population_test.rb +9 -5
  21. data/lib/davinci_dtr_test_kit/cql_test.rb +182 -137
  22. data/lib/davinci_dtr_test_kit/docs/dtr_full_ehr_suite_description_v201.md +127 -0
  23. data/lib/davinci_dtr_test_kit/docs/dtr_light_ehr_suite_description_v201.md +29 -0
  24. data/lib/davinci_dtr_test_kit/dtr_full_ehr_suite.rb +4 -12
  25. data/lib/davinci_dtr_test_kit/dtr_light_ehr_suite.rb +38 -25
  26. data/lib/davinci_dtr_test_kit/dtr_options.rb +7 -0
  27. data/lib/davinci_dtr_test_kit/dtr_questionnaire_response_validation.rb +118 -75
  28. data/lib/davinci_dtr_test_kit/dtr_smart_app_suite.rb +6 -3
  29. data/lib/davinci_dtr_test_kit/fixture_loader.rb +6 -84
  30. data/lib/davinci_dtr_test_kit/fixtures.rb +43 -48
  31. data/lib/davinci_dtr_test_kit/mock_auth_server.rb +101 -18
  32. data/lib/davinci_dtr_test_kit/mock_ehr.rb +32 -24
  33. data/lib/davinci_dtr_test_kit/mock_payer.rb +41 -64
  34. data/lib/davinci_dtr_test_kit/payer_server_groups/adaptive_next_questionnaire_expressions_test.rb +2 -2
  35. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_request_validation_test.rb +11 -19
  36. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_response_bundles_validation_test.rb +6 -6
  37. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_response_search_validation_test.rb +6 -6
  38. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_response_validation_test.rb +17 -18
  39. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_next_request_validation_test.rb +6 -7
  40. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_next_response_validation_test.rb +3 -1
  41. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_request_validation_test.rb +10 -21
  42. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_response_validation_test.rb +7 -13
  43. data/lib/davinci_dtr_test_kit/tags.rb +1 -0
  44. data/lib/davinci_dtr_test_kit/urls.rb +13 -10
  45. data/lib/davinci_dtr_test_kit/validation_test.rb +8 -9
  46. data/lib/davinci_dtr_test_kit/version.rb +1 -1
  47. data/lib/davinci_dtr_test_kit.rb +2 -2
  48. metadata +37 -12
  49. data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_full_ehr_questionnaire_workflow_group.rb +0 -19
  50. /data/lib/davinci_dtr_test_kit/fixtures/{pre_populated_questionnaire_response.json → respiratory_assist_device/pre_populated_questionnaire_response.json} +0 -0
  51. /data/lib/davinci_dtr_test_kit/fixtures/{questionnaire_package.json → respiratory_assist_device/questionnaire_package.json} +0 -0
@@ -1,16 +1,37 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'urls'
2
- require_relative 'fixtures'
3
4
 
4
5
  module DaVinciDTRTestKit
5
6
  module MockAuthServer
6
- include Fixtures
7
+ AUTHORIZED_PRACTITIONER_ID = 'pra1234' # Must exist on the FHIR_REFERENCE_SERVER (env var)
8
+
9
+ RSA_PRIVATE_KEY = OpenSSL::PKey::RSA.generate(2048)
10
+ RSA_PUBLIC_KEY = RSA_PRIVATE_KEY.public_key
11
+ SUPPORTED_SCOPES = ['launch', 'patient/*.rs', 'user/*.rs', 'offline_access', 'openid', 'fhirUser'].freeze
12
+
13
+ def requests_repo
14
+ @requests_repo ||= Inferno::Repositories::Requests.new
15
+ end
16
+
17
+ def auth_server_jwks(_env)
18
+ response_body = {
19
+ keys: [
20
+ {
21
+ kty: 'RSA',
22
+ alg: 'RS256',
23
+ n: Base64.urlsafe_encode64(RSA_PUBLIC_KEY.n.to_s(2), padding: false),
24
+ e: Base64.urlsafe_encode64(RSA_PUBLIC_KEY.e.to_s(2), padding: false),
25
+ use: 'sig'
26
+ }
27
+ ]
28
+ }.to_json
29
+
30
+ [200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]]
31
+ end
7
32
 
8
33
  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}"
34
+ base_url = env_base_url(env, SMART_CONFIG_PATH)
14
35
  response_body =
15
36
  {
16
37
  authorization_endpoint: base_url + EHR_AUTHORIZE_PATH,
@@ -18,7 +39,7 @@ module DaVinciDTRTestKit
18
39
  token_endpoint_auth_methods_supported: ['private_key_jwt'],
19
40
  token_endpoint_auth_signing_alg_values_supported: ['RS256'],
20
41
  grant_types_supported: ['authorization_code'],
21
- scopes_supported: ['launch', 'patient/*.rs', 'user/*.rs', 'offline_access'],
42
+ scopes_supported: SUPPORTED_SCOPES,
22
43
  response_types_supported: ['code'],
23
44
  code_challenge_methods_supported: ['S256'],
24
45
  capabilities: [
@@ -34,9 +55,23 @@ module DaVinciDTRTestKit
34
55
  [200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]]
35
56
  end
36
57
 
58
+ def ehr_openid_config(env)
59
+ base_url = env_base_url(env, OPENID_CONFIG_PATH)
60
+ response_body = {
61
+ issuer: base_url + FHIR_BASE_PATH,
62
+ authorization_endpoint: base_url + EHR_AUTHORIZE_PATH,
63
+ token_endpoint: base_url + EHR_TOKEN_PATH,
64
+ jwks_uri: base_url + JKWS_PATH,
65
+ response_types_supported: ['id_token'],
66
+ subject_types_supported: ['public'],
67
+ id_token_signing_alg_values_supported: ['RS256']
68
+ }.to_json
69
+ [200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]]
70
+ end
71
+
37
72
  def ehr_authorize(request, _test = nil, _test_result = nil)
38
73
  # Authorization requests can bet GET or POST
39
- params = request.verb == 'get' ? request.query_parameters : URI.decode_www_form(request.request_body)&.to_h
74
+ params = params_hash(request)
40
75
  if params['redirect_uri'].present?
41
76
  redirect_uri = "#{params['redirect_uri']}?" \
42
77
  "code=#{SecureRandom.hex}&" \
@@ -57,25 +92,34 @@ module DaVinciDTRTestKit
57
92
 
58
93
  def ehr_token_response(request, _test = nil, test_result = nil)
59
94
  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)
95
+ access_token = JWT.encode({ inferno_client_id: client_id }, nil, 'none')
96
+ granted_scopes = SUPPORTED_SCOPES & requested_scopes(test_result.test_session_id)
63
97
 
64
- fhir_context_input = test_input.find { |input| input['name'] == 'smart_fhir_context' }
65
- fhir_context_input_value = fhir_context_input['value'] if fhir_context_input.present?
98
+ response = { access_token:, scope: granted_scopes.join(' '), token_type: 'bearer', expires_in: 3600 }
99
+
100
+ if granted_scopes.include?('openid')
101
+ response.merge!(id_token: create_id_token(request, client_id, fhir_user: granted_scopes.include?('fhirUser')))
102
+ end
103
+
104
+ fhir_context_input = find_test_input(test_result, 'smart_fhir_context')
105
+ fhir_context_input_value = fhir_context_input['value'] if fhir_context_input
66
106
  begin
67
107
  fhir_context = JSON.parse(fhir_context_input_value)
68
108
  rescue StandardError
69
109
  fhir_context = nil
70
110
  end
71
- response.merge!({ fhirContext: fhir_context }) if fhir_context
111
+ response.merge!(fhirContext: fhir_context) if fhir_context
72
112
 
73
- smart_patient_input = test_input.find { |input| input['name'] == 'smart_patient_id' }
113
+ smart_patient_input = find_test_input(test_result, 'smart_patient_id')
74
114
  smart_patient_input_value = smart_patient_input['value'] if smart_patient_input.present?
75
- response.merge!({ patient: smart_patient_input_value }) if smart_patient_input_value
115
+ response.merge!(patient: smart_patient_input_value) if smart_patient_input_value
76
116
 
77
117
  request.response_body = response.to_json
78
- request.response_headers = { 'Access-Control-Allow-Origin' => '*' }
118
+ request.response_headers = {
119
+ 'Cache-Control' => 'no-store',
120
+ 'Pragma' => 'no-cache',
121
+ 'Access-Control-Allow-Origin' => '*'
122
+ }
79
123
  request.status = 200
80
124
  end
81
125
 
@@ -141,5 +185,44 @@ module DaVinciDTRTestKit
141
185
  def extract_token_from_query_params(request)
142
186
  request.query_parameters['token']
143
187
  end
188
+
189
+ def create_id_token(request, client_id, fhir_user: false)
190
+ # No point in mocking an identity provider, just always use known Practitioner as the authorized user
191
+ suite_base_url = request.url.split(EHR_TOKEN_PATH).first
192
+ id_token_hash = {
193
+ iss: suite_base_url + FHIR_BASE_PATH,
194
+ sub: AUTHORIZED_PRACTITIONER_ID,
195
+ aud: client_id,
196
+ exp: Time.now.to_i + (24 * 60 * 60), # 24 hrs
197
+ iat: Time.now.to_i
198
+ }
199
+ id_token_hash.merge!(fhirUser: "#{suite_base_url}/fhir/Practitioner/#{AUTHORIZED_PRACTITIONER_ID}") if fhir_user
200
+
201
+ JWT.encode(id_token_hash, RSA_PRIVATE_KEY, 'RS256')
202
+ end
203
+
204
+ def requested_scopes(test_session_id)
205
+ auth_request = requests_repo.tagged_requests(test_session_id, [EHR_AUTHORIZE_TAG]).last
206
+ return [] unless auth_request
207
+
208
+ scope_str = params_hash(auth_request)&.dig('scope')
209
+ scope_str ? URI.decode_www_form_component(scope_str).split : []
210
+ end
211
+
212
+ def find_test_input(test_result, input_name)
213
+ JSON.parse(test_result.input_json)&.find { |input| input['name'] == input_name }
214
+ end
215
+
216
+ def params_hash(request)
217
+ request.verb == 'get' ? request.query_parameters : URI.decode_www_form(request.request_body)&.to_h
218
+ end
219
+
220
+ def env_base_url(env, endpoint_path)
221
+ protocol = env['rack.url_scheme']
222
+ host = env['HTTP_HOST']
223
+ path = env['REQUEST_PATH'] || env['PATH_INFO']
224
+ path.gsub!(%r{#{endpoint_path}(/)?}, '')
225
+ "#{protocol}://#{host + path}"
226
+ end
144
227
  end
145
228
  end
@@ -27,16 +27,10 @@ module DaVinciDTRTestKit
27
27
  end
28
28
 
29
29
  def get_fhir_resource(request, _test = nil, test_result = nil)
30
- resource_type, id = resource_type_and_id_from_url(request.url)
30
+ fhir_class, id = fhir_class_and_id_from_url(request.url)
31
31
  request.response_headers = RESPONSE_HEADERS
32
32
 
33
- begin
34
- fhir_class = FHIR.const_get(resource_type)
35
- rescue NameError
36
- resource_type = nil
37
- end
38
-
39
- if resource_type.blank?
33
+ if fhir_class.nil?
40
34
  request.status = 400
41
35
  request.response_headers = { 'Content-Type': 'application/json' }
42
36
  request.response_body = FHIR::OperationOutcome.new(
@@ -49,18 +43,9 @@ module DaVinciDTRTestKit
49
43
  end
50
44
 
51
45
  # Respond with user-inputted resource if there is one that matches the request
52
- begin
53
- ehr_bundle_input = JSON.parse(test_result.input_json).find { |input| input['name'] == 'ehr_bundle' }
54
- ehr_bundle_input_value = ehr_bundle_input_value = ehr_bundle_input['value'] if ehr_bundle_input.present?
55
- ehr_bundle = FHIR.from_contents(ehr_bundle_input_value) if ehr_bundle_input_value.present?
56
- rescue StandardError
57
- ehr_bundle = nil
58
- end
59
-
60
- if id.present? && ehr_bundle.present? && ehr_bundle.is_a?(FHIR::Bundle)
61
- matching_resource = ehr_bundle.entry&.find do |entry|
62
- entry.resource.is_a?(fhir_class) && entry.resource&.id == id
63
- end&.resource
46
+ ehr_bundle = ehr_input_bundle(test_result)
47
+ if id.present? && ehr_bundle.present?
48
+ matching_resource = find_resource_in_bundle(ehr_bundle, fhir_class, id)
64
49
  if matching_resource.present?
65
50
  request.status = 200
66
51
  request.response_headers = { 'Content-Type': 'application/json' }
@@ -85,13 +70,36 @@ module DaVinciDTRTestKit
85
70
  request.response_body = request.request_body
86
71
  end
87
72
 
88
- # Pull resource type and ID from url
89
- # e.g. http://example.org/fhir/Patient/123 -> ['Patient', '123']
73
+ def ehr_input_bundle(test_result)
74
+ ehr_bundle_input = JSON.parse(test_result.input_json).find { |input| input['name'] == 'ehr_bundle' }
75
+ ehr_bundle_input_value = ehr_bundle_input_value = ehr_bundle_input['value'] if ehr_bundle_input.present?
76
+ ehr_bundle = FHIR.from_contents(ehr_bundle_input_value) if ehr_bundle_input_value.present?
77
+ ehr_bundle if ehr_bundle.is_a?(FHIR::Bundle)
78
+ rescue StandardError
79
+ nil
80
+ end
81
+
82
+ def find_resource_in_bundle(bundle, fhir_class, id)
83
+ bundle.entry&.find do |entry|
84
+ entry.resource.is_a?(fhir_class) && entry.resource&.id == id
85
+ end&.resource
86
+ end
87
+
88
+ # Pull resource type class and ID from url
89
+ # e.g. http://example.org/fhir/Patient/123 -> [FHIR::Patient, '123']
90
90
  # @private
91
- def resource_type_and_id_from_url(url)
91
+ def fhir_class_and_id_from_url(url)
92
92
  path = url.split('?').first.split('/fhir/').second
93
93
  path.sub!(%r{/$}, '')
94
- path.split('/')
94
+ resource_type, id = path.split('/')
95
+
96
+ begin
97
+ fhir_class = FHIR.const_get(resource_type)
98
+ rescue NameError
99
+ fhir_class = nil
100
+ end
101
+
102
+ [fhir_class, id]
95
103
  end
96
104
  end
97
105
  end
@@ -4,14 +4,12 @@ require_relative 'fixtures'
4
4
 
5
5
  module DaVinciDTRTestKit
6
6
  module MockPayer
7
- include Fixtures
8
-
9
7
  RESPONSE_HEADERS = { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }.freeze
10
8
 
11
9
  def questionnaire_package_response(request, _test = nil, test_result = nil)
12
10
  request.status = 200
13
11
  request.response_headers = RESPONSE_HEADERS
14
- request.response_body = build_package_questionnaire_response(request, test_result.test_id).to_json
12
+ request.response_body = build_questionnaire_package_response(request, test_result.test_id).to_json
15
13
  end
16
14
 
17
15
  def payer_questionnaire_response(request, _test = nil, test_result = nil)
@@ -45,79 +43,58 @@ module DaVinciDTRTestKit
45
43
  !test.config.options[:accepts_multiple_requests]
46
44
  end
47
45
 
48
- def build_package_questionnaire_response(request, test_id)
49
- test_questionnaire_canonical = find_questionnaire_canonical_for_test_id(test_id)
50
- test_questionnaire_loaded = false
51
-
52
- bundles = []
53
- issues = []
54
-
55
- # first try the parameters - load the questionnaire specified by the questionnaire parameter
56
- input_parameters = FHIR.from_contents(request.request_body)
57
- input_parameters.parameter.each do |one_parameter|
58
- next unless one_parameter.name == 'questionnaire'
59
- next unless one_parameter.valueCanonical
46
+ private
60
47
 
61
- # don't load test questionnaire if it is also specified explicitly
62
- test_questionnaire_loaded = true if one_parameter.valueCanonical == test_questionnaire_canonical
48
+ def build_questionnaire_package_response(request, test_id)
49
+ begin
50
+ input_parameters = FHIR.from_contents(request.request_body)
51
+ rescue StandardError
52
+ return operation_outcome('error', 'invalid', 'No valid input parameters')
53
+ end
63
54
 
64
- add_questionnaire_canonical_to_response(one_parameter.valueCanonical, bundles, issues)
55
+ questionnaire_package = Fixtures.questionnaire_package_for_test(test_id)
56
+ unless questionnaire_package
57
+ return operation_outcome('error', 'business-rule', "No Questionnaire found for Inferno test #{test_id}")
65
58
  end
66
59
 
67
- unless test_questionnaire_loaded
68
- if test_questionnaire_canonical
69
- add_questionnaire_canonical_to_response(test_questionnaire_canonical, bundles, issues)
70
-
71
- elsif bundles.empty?
72
- # no questionnaire for this test ...
73
- operation_outcome_issue = FHIR::OperationOutcome::Issue.new
74
- operation_outcome_issue.severity = 'error'
75
- operation_outcome_issue.code = 'business-rule'
76
- details = FHIR::CodeableConcept.new
77
- details.text = "no questionnaire found for test #{test_id}"
78
- operation_outcome_issue.details = details
79
- issues << operation_outcome_issue
80
- end
60
+ questionnaire_canonical = find_questionnaire_canonical(questionnaire_package)
61
+
62
+ other_questionnaire_params = input_parameters.parameter.filter do |param|
63
+ param.name == 'questionnaire' && param.valueCanonical != questionnaire_canonical
81
64
  end
82
65
 
83
- build_package_questionnaire_response_from_lists(bundles, issues)
66
+ return questionnaire_package unless other_questionnaire_params.any?
67
+
68
+ FHIR::Parameters.new(
69
+ parameter: [
70
+ FHIR::Parameters::Parameter.new(
71
+ name: 'PackageBundle',
72
+ resource: questionnaire_package
73
+ ),
74
+ FHIR::Parameters::Parameter.new(
75
+ name: 'Outcome',
76
+ resource: FHIR::OperationOutcome.new(
77
+ issue: other_questionnaire_params.map do |param|
78
+ outcome_issue('warning', 'not-found', "Questionnaire #{param.valueCanonical} does not exist")
79
+ end
80
+ )
81
+ )
82
+ ]
83
+ )
84
84
  end
85
85
 
86
- def add_questionnaire_canonical_to_response(questionnaire_canonical, bundles, issues)
87
- questionnaire_bundle = get_questionnaire_packcage_for_canonical(questionnaire_canonical)
88
-
89
- if questionnaire_bundle
90
- bundles << questionnaire_bundle
91
- else
92
- operation_outcome_issue = FHIR::OperationOutcome::Issue.new
93
- operation_outcome_issue.severity = 'warning'
94
- operation_outcome_issue.code = 'value'
95
- details = FHIR::CodeableConcept.new
96
- details.text = "Questionnaire Canonical #{questionnaire_canonical} does not exist"
97
- operation_outcome_issue.details = details
98
- issues << operation_outcome_issue
99
- end
86
+ def find_questionnaire_canonical(questionnaire_package)
87
+ questionnaire_package&.entry&.find { |e| e.resource.is_a?(FHIR::Questionnaire) }&.resource&.url
100
88
  end
101
89
 
102
- def build_package_questionnaire_response_from_lists(bundles, issues)
103
- response = FHIR::Parameters.new
104
- bundles.each do |one_bundle|
105
- return_param = FHIR::Parameters::Parameter.new
106
- return_param.name = 'return'
107
- return_param.resource = one_bundle
108
- response.parameter << return_param
109
- end
90
+ def operation_outcome(severity, code, text = nil)
91
+ FHIR::OperationOutcome.new(issue: outcome_issue(severity, code, text))
92
+ end
110
93
 
111
- unless issues.empty?
112
- outcome = FHIR::OperationOutcome.new
113
- outcome.issue = issues
114
- outcome_param = FHIR::Parameters::Parameter.new
115
- outcome_param.name = 'outcome'
116
- outcome_param.resource = outcome
117
- response.parameter << outcome_param
94
+ def outcome_issue(severity, code, text = nil)
95
+ FHIR::OperationOutcome::Issue.new(severity:, code:).tap do |issue|
96
+ issue.details = FHIR::CodeableConcept.new(text:) if text.present?
118
97
  end
119
-
120
- response
121
98
  end
122
99
  end
123
100
  end
@@ -6,8 +6,8 @@ module DaVinciDTRTestKit
6
6
  id :dtr_v201_payer_adaptive_next_form_expressions_test
7
7
  title 'Questionnaire(s) contains items with expressions necessary for pre-population'
8
8
  description %(
9
- Inferno checks that the payer server response has appropriate expressions and that expressions are
10
- written in cql.
9
+ Inferno checks that the payer server response to $next-question operation has appropriate expressions and that
10
+ expressions are written in cql.
11
11
  )
12
12
 
13
13
  run do
@@ -3,7 +3,7 @@ module DaVinciDTRTestKit
3
3
  class PayerAdaptiveFormRequestTest < Inferno::Test
4
4
  include URLs
5
5
  include DaVinciDTRTestKit::ValidationTest
6
- title '[USER INPUT VALIDATION] Questionnaire Package request is valid'
6
+ title 'User Input Validation: Questionnaire Package request is valid'
7
7
  description %(
8
8
  This test validates the conformance of the client's request to the
9
9
  [DTR Questionnaire Package Input Parameters](http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/dtr-qpackage-input-parameters)
@@ -13,32 +13,24 @@ module DaVinciDTRTestKit
13
13
  values. CodeableConcept element bindings will fail if none of their codings have a code/system belonging
14
14
  to the bound ValueSet. Quantity, Coding, and code element bindings will fail if their code/system are not found in
15
15
  the valueset.
16
-
17
- This test may process multiple resources, labeling messages with the corresponding tested resources
18
- in the order that they were received.
19
16
  )
20
17
  id :payer_server_adaptive_questionnaire_request_validation
21
18
 
22
19
  run do
23
20
  skip_if retrieval_method == 'Static', 'Performing only static flow tests - only one flow is required.'
21
+ profile_with_version = 'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/dtr-qpackage-input-parameters|2.0.1'
24
22
  if initial_adaptive_questionnaire_request.nil?
25
- resources = load_tagged_requests(QUESTIONNAIRE_TAG)
26
- using_manual_entry = false
23
+ requests = load_tagged_requests(QUESTIONNAIRE_TAG)
24
+ skip_if requests.blank?, 'No request resource received from the client.'
25
+ # making the assumption that only one request was made here - if there were multiple, we are only validating the
26
+ # first
27
+ resource_is_valid?(resource: FHIR.from_contents(requests[0].request[:body]), profile_url: profile_with_version)
27
28
  else
28
- resources = initial_adaptive_questionnaire_request
29
- using_manual_entry = true
29
+ request = FHIR.from_contents(initial_adaptive_questionnaire_request)
30
+ resource_is_valid?(resource: request, profile_url: profile_with_version)
30
31
  end
31
- skip_if resources.nil?, 'No request resources to validate.'
32
- perform_request_validation_test(
33
- resources,
34
- :parameters,
35
- 'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/dtr-qpackage-input-parameters',
36
- questionnaire_package_url,
37
- using_manual_entry
38
- )
39
- rescue Inferno::Exceptions::AssertionException => e
40
- msg = e.message.to_s.strip
41
- skip msg
32
+ errors_found = messages.any? { |message| message[:type] == 'error' }
33
+ skip_if errors_found, "Resource does not conform to the profile #{profile_with_version}"
42
34
  end
43
35
  end
44
36
  end
@@ -20,21 +20,21 @@ module DaVinciDTRTestKit
20
20
 
21
21
  run do
22
22
  skip_if retrieval_method == 'Static', 'Performing only static flow tests - only one flow is required.'
23
- test_passed = false
24
- profile_url = 'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/DTR-QPackageBundle'
23
+ test_passed = true
24
+ profile_url = 'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/DTR-QPackageBundle|2.0.1'
25
25
  assert !scratch[:adaptive_responses].nil?, 'No resources to validate.'
26
26
  scratch[:adaptive_responses].each_with_index do |resource, index|
27
27
  fhir_resource = FHIR.from_contents(resource.response[:body])
28
28
  fhir_resource.parameter.each do |param|
29
29
  resource_is_valid = validate_resource(param.resource, :bundle, profile_url, index)
30
- test_passed = true if resource_is_valid
30
+ test_passed = false unless resource_is_valid
31
31
  rescue StandardError
32
32
  next
33
33
  end
34
34
  end
35
- raise tests_failed[profile_url][0] if !test_passed && !tests_failed[profile_url].blank?
36
-
37
- messages.clear if test_passed
35
+ if !test_passed && !tests_failed[profile_url].blank?
36
+ assert test_passed, "Not all returned resources conform to the profile: #{profile_url}"
37
+ end
38
38
  end
39
39
  end
40
40
  end
@@ -20,23 +20,23 @@ module DaVinciDTRTestKit
20
20
 
21
21
  run do
22
22
  skip_if retrieval_method == 'Static', 'Performing only static flow tests - only one flow is required.'
23
- test_passed = false
24
- profile_url = 'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/dtr-questionnaire-adapt-search'
23
+ test_passed = true
24
+ profile_url = 'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/dtr-questionnaire-adapt-search|2.0.1'
25
25
  assert !scratch[:adaptive_responses].nil?, 'No resources to validate.'
26
26
  scratch[:adaptive_responses].each_with_index do |resource, index|
27
27
  fhir_resource = FHIR.from_contents(resource.response[:body])
28
28
  fhir_resource.parameter.each do |param|
29
29
  param.resource.entry.each do |entry|
30
30
  resource_is_valid = validate_resource(entry.resource, :questionnaire, profile_url, index)
31
- test_passed = true if resource_is_valid
31
+ test_passed = false unless resource_is_valid
32
32
  end
33
33
  rescue StandardError
34
34
  next
35
35
  end
36
36
  end
37
- raise tests_failed[profile_url][0] if !test_passed && !tests_failed[profile_url].blank?
38
-
39
- messages.clear if test_passed
37
+ if !test_passed && !tests_failed[profile_url].blank?
38
+ assert test_passed, "Not all returned resources conform to the profile: #{profile_url}"
39
+ end
40
40
  end
41
41
  end
42
42
  end
@@ -15,35 +15,34 @@ module DaVinciDTRTestKit
15
15
  to the bound ValueSet. Quantity, Coding, and code element bindings will fail if their code/system are not found in
16
16
  the valueset.
17
17
 
18
- This test may process multiple resources, labeling messages with the corresponding tested resources
19
18
  This test may process multiple resources, labeling messages with the corresponding tested resources
20
19
  in the order that they were received.
21
20
  )
22
21
 
23
22
  run do
24
23
  skip_if retrieval_method == 'Static', 'Performing only static flow tests - only one flow is required.'
24
+ profile_with_version = 'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/dtr-qpackage-output-parameters|2.0.1'
25
25
  endpoint = custom_endpoint.blank? ? '/Questionnaire/$questionnaire-package' : custom_endpoint
26
26
  if initial_adaptive_questionnaire_request.nil?
27
- resources = load_tagged_requests(QUESTIONNAIRE_TAG)
27
+ # making the assumption that only one response was received - if there were multiple, we are only validating the
28
+ # first
29
+ response = load_tagged_requests(QUESTIONNAIRE_TAG)[0]
30
+ scratch[:adaptive_responses] = [response]
31
+ resource = FHIR.from_contents(response.response[:body])
28
32
  else
29
- resources = []
30
- if initial_adaptive_questionnaire_request.is_a?(Array)
31
- initial_adaptive_questionnaire_request.each do |_resource|
32
- resources.push(fhir_operation("#{url}#{endpoint}", body: JSON.parse(initial_adaptive_questionnaire_request),
33
- headers: { 'Content-Type': 'application/json' }))
34
- end
35
- else
36
- resources.push(fhir_operation("#{url}#{endpoint}", body: JSON.parse(initial_adaptive_questionnaire_request),
37
- headers: { 'Content-Type': 'application/json' }))
38
- end
33
+ response = fhir_operation("#{url}#{endpoint}", body: JSON.parse(initial_adaptive_questionnaire_request),
34
+ headers: { 'Content-Type': 'application/json' })
35
+ resource = FHIR.from_contents(response.response[:body])
36
+ scratch[:adaptive_responses] = [response]
39
37
  end
40
- scratch[:adaptive_responses] = resources
38
+
41
39
  assert !scratch[:adaptive_responses].nil?, 'No resources to validate.'
42
- perform_response_validation_test(
43
- resources,
44
- :parameters,
45
- 'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/dtr-qpackage-output-parameters'
46
- )
40
+ assert_response_status([200, 201], response: response.response)
41
+ assert_resource_type(:parameters, resource:)
42
+ assert_valid_resource(resource:, profile_url: profile_with_version)
43
+ questionnaire_bundle = resource.parameter.find { |param| param.resource.resourceType == 'Bundle' }&.resource
44
+ assert questionnaire_bundle, 'No questionnaire bundle found in the response'
45
+ assert_valid_resource(resource: questionnaire_bundle, profile_url: 'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/DTR-QPackageBundle|2.0.1')
47
46
  end
48
47
  end
49
48
  end
@@ -3,7 +3,7 @@ module DaVinciDTRTestKit
3
3
  class PayerAdaptiveFormNextRequestTest < Inferno::Test
4
4
  include URLs
5
5
  include DaVinciDTRTestKit::ValidationTest
6
- title '[USER INPUT VALIDATION] Next Question request is valid'
6
+ title 'User Input Validation: Next Question request is valid'
7
7
  description %(
8
8
  This test validates the conformance of the client's request to the
9
9
  [SDC Parameters Next Question In](http://hl7.org/fhir/uv/sdc/StructureDefinition/parameters-questionnaire-next-question-in)
@@ -49,13 +49,12 @@ module DaVinciDTRTestKit
49
49
  using_manual_entry
50
50
  )
51
51
  else
52
- raise new Inferno::Exceptions::AssertionException.new "Resource does not conform to the either
53
- accepted profiles: http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaireresponse-adapt
54
- or http://hl7.org/fhir/uv/sdc/StructureDefinition/parameters-questionnaire-next-question-in"
52
+ messages << { type: 'error',
53
+ message: format_markdown("No resources were of type 'Parameters' or 'QuestionnaireResponse'") }
55
54
  end
56
- rescue Inferno::Exceptions::AssertionException => e
57
- msg = e.message.to_s.strip
58
- skip msg
55
+ errors_found = messages.any? { |message| message[:type] == 'error' }
56
+ skip_if errors_found, "No resources conform to the profiles http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaireresponse-adapt
57
+ or http://hl7.org/fhir/uv/sdc/StructureDefinition/parameters-questionnaire-next-question-in"
59
58
  end
60
59
  end
61
60
  end
@@ -14,7 +14,6 @@ module DaVinciDTRTestKit
14
14
  to the bound ValueSet. Quantity, Coding, and code element bindings will fail if their code/system are not found in
15
15
  the valueset.
16
16
 
17
- This test may process multiple resources, labeling messages with the corresponding tested resources
18
17
  This test may process multiple resources, labeling messages with the corresponding tested resources
19
18
  in the order that they were received.
20
19
  )
@@ -38,6 +37,9 @@ module DaVinciDTRTestKit
38
37
  :questionnaireResponse,
39
38
  'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaireresponse'
40
39
  )
40
+ errors_found = messages.any? { |message| message[:type] == 'error' }
41
+ skip_if errors_found, "No resources conform to the profiles http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaireresponse-adapt
42
+ or http://hl7.org/fhir/uv/sdc/StructureDefinition/parameters-questionnaire-next-question-in"
41
43
  end
42
44
  end
43
45
  end