davinci_dtr_test_kit 0.12.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_adaptive_questionnaire_completion_group.rb +23 -0
  3. data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_adaptive_questionnaire_followup_questions_group.rb +26 -0
  4. data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_adaptive_questionnaire_next_question_request_test.rb +93 -0
  5. data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_adaptive_questionnaire_next_question_request_validation_test.rb +62 -0
  6. data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_adaptive_questionnaire_next_question_retrieval_group.rb +23 -0
  7. data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_adaptive_questionnaire_response_validation_test.rb +66 -0
  8. data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_full_ehr_adaptive_dinner_questionnaire_workflow_group.rb +76 -0
  9. data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_full_ehr_adaptive_questionnaire_initial_retrieval_group.rb +27 -0
  10. data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_full_ehr_adaptive_questionnaire_request_test.rb +63 -0
  11. data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_smart_app_adaptive_questionnaire_initial_retrieval_group.rb +24 -0
  12. data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_smart_app_adaptive_questionnaire_request_test.rb +148 -0
  13. data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_smart_app_questionnaire_workflow_group.rb +75 -0
  14. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_full_ehr_questionnaire_workflow_group.rb +22 -28
  15. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_smart_app_dinner_questionnaire_package_request_test.rb +14 -17
  16. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_smart_app_questionnaire_workflow_group.rb +9 -31
  17. data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → full_ehr}/dtr_full_ehr_launch_attestation_test.rb +7 -6
  18. data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → full_ehr}/dtr_full_ehr_prepopulation_attestation_test.rb +7 -7
  19. data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → full_ehr}/dtr_full_ehr_prepopulation_override_attestation_test.rb +7 -7
  20. data/lib/davinci_dtr_test_kit/client_groups/{dinner_static/dtr_full_ehr_dinner_questionnaire_package_request_test.rb → full_ehr/dtr_full_ehr_questionnaire_package_request_test.rb} +2 -3
  21. data/lib/davinci_dtr_test_kit/client_groups/full_ehr/dtr_full_ehr_questionnaire_response_conformance_test.rb +19 -0
  22. data/lib/davinci_dtr_test_kit/client_groups/full_ehr/dtr_full_ehr_questionnaire_response_correctness_test.rb +37 -0
  23. data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → full_ehr}/dtr_full_ehr_rendering_enabled_questions_attestation_test.rb +7 -7
  24. data/lib/davinci_dtr_test_kit/client_groups/full_ehr/dtr_full_ehr_saving_questionnaire_response_group.rb +29 -0
  25. data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → full_ehr}/dtr_full_ehr_store_attestation_test.rb +7 -7
  26. data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_questionnaire_rendering_attestation_test.rb +7 -6
  27. data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_resp_questionnaire_package_request_test.rb +15 -18
  28. data/lib/davinci_dtr_test_kit/client_groups/shared/dtr_questionnaire_response_basic_conformance_test.rb +7 -7
  29. data/lib/davinci_dtr_test_kit/client_groups/shared/dtr_questionnaire_response_pre_population_test.rb +16 -5
  30. data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → smart_app}/dtr_smart_app_prepopulation_attestation_test.rb +7 -7
  31. data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → smart_app}/dtr_smart_app_prepopulation_override_attestation_test.rb +7 -6
  32. data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → smart_app}/dtr_smart_app_questionnaire_response_save_test.rb +17 -7
  33. data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → smart_app}/dtr_smart_app_rendering_enabled_questions_attestation_test.rb +7 -7
  34. data/lib/davinci_dtr_test_kit/client_groups/smart_app/dtr_smart_app_saving_questionnaire_response_group.rb +27 -0
  35. data/lib/davinci_dtr_test_kit/cql_test.rb +114 -172
  36. data/lib/davinci_dtr_test_kit/create_test.rb +25 -0
  37. data/lib/davinci_dtr_test_kit/docs/dtr_full_ehr_suite_description_v201.md +95 -37
  38. data/lib/davinci_dtr_test_kit/docs/dtr_light_ehr_suite_description_v201.md +34 -0
  39. data/lib/davinci_dtr_test_kit/docs/dtr_payer_server_suite_description_v201.md +32 -29
  40. data/lib/davinci_dtr_test_kit/docs/dtr_smart_app_suite_description_v201.md +48 -32
  41. data/lib/davinci_dtr_test_kit/dtr_full_ehr_suite.rb +13 -17
  42. data/lib/davinci_dtr_test_kit/dtr_light_ehr_suite.rb +101 -23
  43. data/lib/davinci_dtr_test_kit/dtr_options.rb +7 -0
  44. data/lib/davinci_dtr_test_kit/dtr_payer_server_suite.rb +9 -20
  45. data/lib/davinci_dtr_test_kit/dtr_questionnaire_response_validation.rb +126 -75
  46. data/lib/davinci_dtr_test_kit/dtr_smart_app_suite.rb +32 -56
  47. data/lib/davinci_dtr_test_kit/endpoints/cors.rb +20 -0
  48. data/lib/davinci_dtr_test_kit/endpoints/mock_authorization/authorize_endpoint.rb +32 -0
  49. data/lib/davinci_dtr_test_kit/endpoints/mock_authorization/simple_token_endpoint.rb +19 -0
  50. data/lib/davinci_dtr_test_kit/endpoints/mock_authorization/token_endpoint.rb +116 -0
  51. data/lib/davinci_dtr_test_kit/endpoints/mock_authorization.rb +83 -0
  52. data/lib/davinci_dtr_test_kit/endpoints/mock_ehr/fhir_get_endpoint.rb +95 -0
  53. data/lib/davinci_dtr_test_kit/endpoints/mock_ehr/questionnaire_response_endpoint.rb +22 -0
  54. data/lib/davinci_dtr_test_kit/endpoints/mock_ehr.rb +25 -0
  55. data/lib/davinci_dtr_test_kit/endpoints/mock_payer/full_ehr_next_question_endpoint.rb +11 -0
  56. data/lib/davinci_dtr_test_kit/endpoints/mock_payer/full_ehr_questionnaire_package_endpoint.rb +11 -0
  57. data/lib/davinci_dtr_test_kit/endpoints/mock_payer/next_question_endpoint.rb +162 -0
  58. data/lib/davinci_dtr_test_kit/endpoints/mock_payer/next_question_proxy_endpoint.rb +36 -0
  59. data/lib/davinci_dtr_test_kit/endpoints/mock_payer/questionnaire_package_endpoint.rb +62 -0
  60. data/lib/davinci_dtr_test_kit/endpoints/mock_payer/questionnaire_package_proxy_endpoint.rb +38 -0
  61. data/lib/davinci_dtr_test_kit/endpoints/mock_payer.rb +36 -0
  62. data/lib/davinci_dtr_test_kit/fixture_loader.rb +6 -84
  63. data/lib/davinci_dtr_test_kit/fixtures/dinner_adaptive/dinner_order_adaptive_next_question_burrito.json +10 -2
  64. data/lib/davinci_dtr_test_kit/fixtures/dinner_adaptive/dinner_order_adaptive_next_question_hamburger.json +10 -2
  65. data/lib/davinci_dtr_test_kit/fixtures/dinner_adaptive/dinner_order_adaptive_next_question_initial.json +10 -2
  66. data/lib/davinci_dtr_test_kit/fixtures/dinner_adaptive/questionnaire_dinner_order_adaptive.json +4 -3
  67. data/lib/davinci_dtr_test_kit/fixtures.rb +64 -46
  68. data/lib/davinci_dtr_test_kit/payer_server_groups/adaptive_form_libraries_test.rb +2 -2
  69. data/lib/davinci_dtr_test_kit/payer_server_groups/adaptive_form_questionnaire_expressions_test.rb +4 -3
  70. data/lib/davinci_dtr_test_kit/payer_server_groups/adaptive_form_questionnaire_extensions_test.rb +3 -2
  71. data/lib/davinci_dtr_test_kit/payer_server_groups/adaptive_next_questionnaire_expressions_test.rb +8 -8
  72. data/lib/davinci_dtr_test_kit/payer_server_groups/adaptive_next_questionnaire_extensions_test.rb +6 -5
  73. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_group.rb +2 -2
  74. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_request_validation_test.rb +2 -1
  75. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_response_bundles_validation_test.rb +6 -9
  76. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_response_search_validation_test.rb +15 -12
  77. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_response_validation_test.rb +33 -22
  78. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_next_request_validation_test.rb +1 -1
  79. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_next_response_complete_test.rb +4 -4
  80. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_next_response_validation_test.rb +16 -12
  81. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_libraries_test.rb +2 -2
  82. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_questionnaire_expressions_test.rb +5 -4
  83. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_questionnaire_extensions_test.rb +4 -3
  84. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_request_validation_test.rb +2 -1
  85. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_response_validation_test.rb +32 -23
  86. data/lib/davinci_dtr_test_kit/profiles/communication_request/communication_request_read.rb +29 -0
  87. data/lib/davinci_dtr_test_kit/profiles/communication_request/communication_request_validation.rb +35 -0
  88. data/lib/davinci_dtr_test_kit/profiles/communication_request_group.rb +39 -0
  89. data/lib/davinci_dtr_test_kit/profiles/coverage/coverage_read.rb +29 -0
  90. data/lib/davinci_dtr_test_kit/profiles/coverage/coverage_validation.rb +35 -0
  91. data/lib/davinci_dtr_test_kit/profiles/coverage_group.rb +38 -0
  92. data/lib/davinci_dtr_test_kit/profiles/device_request/device_request_read.rb +29 -0
  93. data/lib/davinci_dtr_test_kit/profiles/device_request/device_request_validation.rb +35 -0
  94. data/lib/davinci_dtr_test_kit/profiles/device_request_group.rb +39 -0
  95. data/lib/davinci_dtr_test_kit/profiles/encounter/encounter_read.rb +29 -0
  96. data/lib/davinci_dtr_test_kit/profiles/encounter/encounter_validation.rb +35 -0
  97. data/lib/davinci_dtr_test_kit/profiles/encounter_group.rb +39 -0
  98. data/lib/davinci_dtr_test_kit/profiles/medication_request/medication_request_read.rb +29 -0
  99. data/lib/davinci_dtr_test_kit/profiles/medication_request/medication_request_validation.rb +35 -0
  100. data/lib/davinci_dtr_test_kit/profiles/medication_request_group.rb +39 -0
  101. data/lib/davinci_dtr_test_kit/profiles/nutrition_order/nutrition_order_read.rb +29 -0
  102. data/lib/davinci_dtr_test_kit/profiles/nutrition_order/nutrition_order_validation.rb +35 -0
  103. data/lib/davinci_dtr_test_kit/profiles/nutrition_order_group.rb +39 -0
  104. data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_context_search.rb +35 -0
  105. data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_create.rb +26 -0
  106. data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_patient_search.rb +55 -0
  107. data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_read.rb +22 -0
  108. data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_update.rb +26 -0
  109. data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_validation.rb +37 -0
  110. data/lib/davinci_dtr_test_kit/profiles/questionnaire_response_group.rb +66 -0
  111. data/lib/davinci_dtr_test_kit/profiles/service_request/service_request_read.rb +29 -0
  112. data/lib/davinci_dtr_test_kit/profiles/service_request/service_request_validation.rb +35 -0
  113. data/lib/davinci_dtr_test_kit/profiles/service_request_group.rb +39 -0
  114. data/lib/davinci_dtr_test_kit/profiles/task/task_create.rb +26 -0
  115. data/lib/davinci_dtr_test_kit/profiles/task/task_read.rb +29 -0
  116. data/lib/davinci_dtr_test_kit/profiles/task/task_update.rb +26 -0
  117. data/lib/davinci_dtr_test_kit/profiles/task/task_validation.rb +35 -0
  118. data/lib/davinci_dtr_test_kit/profiles/task_group.rb +52 -0
  119. data/lib/davinci_dtr_test_kit/profiles/vision_prescription/vision_prescription_read.rb +29 -0
  120. data/lib/davinci_dtr_test_kit/profiles/vision_prescription/vision_prescription_validation.rb +35 -0
  121. data/lib/davinci_dtr_test_kit/profiles/vision_prescription_group.rb +39 -0
  122. data/lib/davinci_dtr_test_kit/read_test.rb +22 -0
  123. data/lib/davinci_dtr_test_kit/tags.rb +5 -6
  124. data/lib/davinci_dtr_test_kit/update_test.rb +25 -0
  125. data/lib/davinci_dtr_test_kit/urls.rb +13 -10
  126. data/lib/davinci_dtr_test_kit/validation_test.rb +21 -5
  127. data/lib/davinci_dtr_test_kit/version.rb +1 -1
  128. data/lib/davinci_dtr_test_kit.rb +1 -1
  129. metadata +129 -24
  130. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_full_ehr_prepopulation_representation_attestation_test.rb +0 -33
  131. data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_full_ehr_questionnaire_workflow_group.rb +0 -19
  132. data/lib/davinci_dtr_test_kit/ext/inferno_core/record_response_route.rb +0 -98
  133. data/lib/davinci_dtr_test_kit/ext/inferno_core/request.rb +0 -19
  134. data/lib/davinci_dtr_test_kit/ext/inferno_core/runnable.rb +0 -35
  135. data/lib/davinci_dtr_test_kit/mock_auth_server.rb +0 -145
  136. data/lib/davinci_dtr_test_kit/mock_ehr.rb +0 -97
  137. data/lib/davinci_dtr_test_kit/mock_payer.rb +0 -123
  138. /data/lib/davinci_dtr_test_kit/fixtures/{pre_populated_questionnaire_response.json → respiratory_assist_device/pre_populated_questionnaire_response.json} +0 -0
  139. /data/lib/davinci_dtr_test_kit/fixtures/{questionnaire_package.json → respiratory_assist_device/questionnaire_package.json} +0 -0
@@ -1,30 +1,86 @@
1
- require_relative 'fixtures'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module DaVinciDTRTestKit
4
4
  module DTRQuestionnaireResponseValidation
5
- include Fixtures
6
-
7
5
  CQL_EXPRESSION_EXTENSIONS = [
8
6
  'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression',
9
7
  'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression',
10
8
  'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-candidateExpression'
11
9
  ].freeze
12
10
 
13
- def validate_questionnaire_pre_population(questionnaire_response, test_id)
14
- # Requirements:
15
- # - Prior to exposing the draft QuestionnaireResponse to the user for completion and/or review, the DTR client
16
- # SHALL execute all CQL necessary to resolve the initialExpression, candidateExpression and
17
- # calculatedExpression extensions found in the Questionnaire for any enabled elements.
18
- # - All items that are pre-populated (whether by the payer in the initial QuestionnaireResponse provided in the
19
- # questionnaire package, or from data retrieved from the EHR) SHALL have their origin.source set to ‘auto’.
20
- #
21
- # Note that in the questionnaire fixture, all cql expression elements are enabled, so we don't filter
22
- template_questionnaire_response = find_questionnaire_response_for_test_id(test_id)
23
- raise "missing QuestionnaireResponse template for test #{test_id}" unless template_questionnaire_response.present?
24
-
25
- questionnaire = find_questionnaire_instance_for_test_id(test_id)
26
- raise "missing Questionnaire for test #{test_id}" unless questionnaire.present?
11
+ def check_is_questionnaire_response(questionnaire_response_json)
12
+ assert_valid_json(questionnaire_response_json)
13
+ questionnaire_response = begin
14
+ FHIR.from_contents(questionnaire_response_json)
15
+ rescue StandardError
16
+ nil
17
+ end
18
+
19
+ assert questionnaire_response.present?, 'The QuestionnaireResponse is not a recognized FHIR object'
20
+ assert_resource_type(:questionnaire_response, resource: questionnaire_response)
21
+ end
22
+
23
+ def verify_basic_conformance(questionnaire_response_json, profile_url = nil)
24
+ profile_url ||= 'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/dtr-questionnaireresponse|2.0.1'
25
+ check_is_questionnaire_response(questionnaire_response_json)
26
+ assert_valid_resource(resource: FHIR.from_contents(questionnaire_response_json), profile_url:)
27
+ end
28
+
29
+ # This only checks answers in the questionnaire response, meaning it does not catch missing answers
30
+ def check_origin_sources(questionnaire_items, response_items, expected_overrides: [])
31
+ response_items&.each do |response_item|
32
+ check_origin_sources(questionnaire_items, response_item.item, expected_overrides:)
33
+ next unless response_item.answer&.any?
34
+
35
+ link_id = response_item.linkId
36
+ origin_source = find_origin_source(response_item)
37
+ questionnaire_item = find_item_by_link_id(questionnaire_items, link_id)
38
+ is_cql_expression = item_is_cql_expression?(questionnaire_item)
39
+
40
+ if origin_source.nil?
41
+ add_message('error', "Required `origin.source` extension not present on answer to item `#{link_id}`")
42
+ else
43
+ check_origin_source(origin_source, link_id, is_cql_expression, override: expected_overrides.include?(link_id))
44
+ end
45
+ end
46
+ end
47
+
48
+ def check_origin_source(origin_source, link_id, is_cql_expression, override: false)
49
+ if override
50
+ origin_source_error(link_id, ['override'], origin_source) unless origin_source == 'override'
51
+ elsif is_cql_expression && !['auto', 'override'].include?(origin_source)
52
+ origin_source_error(link_id, 'auto or override', origin_source)
53
+ elsif !is_cql_expression && origin_source != 'manual'
54
+ origin_source_error(link_id, 'manual', origin_source)
55
+ end
56
+ end
57
+
58
+ # Ensures that all required questions have been answered.
59
+ # If required_link_ids not provided, all questions are treated as optional.
60
+ def check_answer_presence(response_items, required_link_ids = [])
61
+ required_link_ids.each do |link_id|
62
+ item = find_item_by_link_id(response_items, link_id)
63
+ unless item&.answer&.any? { |answer| answer.value.present? }
64
+ add_message('error', "No answer for item #{link_id}")
65
+ end
66
+ end
67
+ end
27
68
 
69
+ def extract_required_link_ids(questionnaire_items)
70
+ questionnaire_items.each_with_object([]) do |item, required_link_ids|
71
+ required_link_ids << item.linkId if item.required
72
+
73
+ required_link_ids.concat(extract_required_link_ids(item.item)) if item.item.present?
74
+ end
75
+ end
76
+
77
+ # Requirements:
78
+ # - Prior to exposing the draft QuestionnaireResponse to the user for completion and/or review, the DTR client
79
+ # SHALL execute all CQL necessary to resolve the initialExpression, candidateExpression and
80
+ # calculatedExpression extensions found in the Questionnaire for any enabled elements.
81
+ # - All items that are pre-populated (whether by the payer in the initial QuestionnaireResponse provided in the
82
+ # questionnaire package, or from data retrieved from the EHR) SHALL have their origin.source set to ‘auto’.
83
+ def validate_questionnaire_pre_population(questionnaire, template_questionnaire_response, questionnaire_response)
28
84
  questionnaire_cql_expression_link_ids = collect_questionnaire_cql_expression_link_ids(questionnaire.item)
29
85
  template_prepopulation_expectations = {}
30
86
  template_override_expectations = {}
@@ -32,45 +88,42 @@ module DaVinciDTRTestKit
32
88
  questionnaire_cql_expression_link_ids,
33
89
  template_prepopulation_expectations,
34
90
  template_override_expectations)
35
- validation_errors = []
91
+
36
92
  validate_cql_executed(questionnaire_response.item, questionnaire_cql_expression_link_ids,
37
- template_prepopulation_expectations, template_override_expectations, validation_errors)
93
+ template_prepopulation_expectations, template_override_expectations)
38
94
 
39
95
  if template_prepopulation_expectations.size.positive?
40
- validation_errors << 'Items expected to be pre-populated not found: ' \
41
- "#{template_prepopulation_expectations.keys.join(', ')}"
96
+ add_message('error', %(Items expected to be pre-populated not found:
97
+ #{template_prepopulation_expectations.keys.join(', ')}))
42
98
  end
43
99
 
44
100
  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(', ')}"
101
+ add_message('error', %(Items expected to be pre-poplated and overridden not found:
102
+ #{template_override_expectations.keys.join(', ')}))
47
103
  end
48
104
 
49
- validation_errors.each { |msg| messages << { type: 'error', message: msg } }
50
- assert validation_errors.blank?, 'QuestionnaireResponse is not conformant. Check messages for issues found.'
105
+ assert(messages.none? { |m| m[:type] == 'error' },
106
+ 'QuestionnaireResponse is not conformant. Check messages for issues found.')
51
107
  end
52
108
 
53
109
  def validate_cql_executed(actual_items, questionnaire_cql_expression_link_ids, template_prepopulation_expectations,
54
- template_override_expectations, error_messages)
110
+ template_override_expectations)
55
111
 
56
112
  actual_items&.each do |item_to_validate|
57
113
  link_id = item_to_validate.linkId
58
114
  if questionnaire_cql_expression_link_ids.include?(link_id)
59
115
  if template_prepopulation_expectations.key?(link_id)
60
- check_item_prepopulation(item_to_validate, template_prepopulation_expectations.delete(link_id),
61
- error_messages, false)
116
+ check_item_prepopulation(item_to_validate, template_prepopulation_expectations.delete(link_id), false)
62
117
  elsif template_override_expectations.include?(link_id)
63
- check_item_prepopulation(item_to_validate, template_override_expectations.delete(link_id), error_messages,
64
- true)
118
+ check_item_prepopulation(item_to_validate, template_override_expectations.delete(link_id), true)
65
119
  else
66
120
  raise "template missing expectation for question `#{link_id}`"
67
121
  end
68
122
  end
69
123
 
70
124
  validate_cql_executed(item_to_validate.item, questionnaire_cql_expression_link_ids,
71
- template_prepopulation_expectations, template_override_expectations, error_messages)
125
+ template_prepopulation_expectations, template_override_expectations)
72
126
  end
73
- error_messages
74
127
  end
75
128
 
76
129
  def extract_expected_answers_from_template(template_questionnaire_response,
@@ -87,70 +140,60 @@ module DaVinciDTRTestKit
87
140
  raise "Template QuestionnaireResponse missing an answer for item with link id `#{target_link_id}`"
88
141
  end
89
142
 
90
- origin_extension = find_extension(target_item_answer,
91
- 'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/information-origin|2.0.1')
92
- source_extension = find_extension(origin_extension, 'source')
143
+ origin_source = find_origin_source(target_item)
93
144
 
94
- unless source_extension.present?
145
+ unless origin_source.present?
95
146
  raise "Template QuestionnaireResponse item `#{target_link_id}` missing the `origin.source` extension"
96
147
  end
97
148
 
98
- if source_extension.value == 'auto'
149
+ if origin_source == 'auto'
99
150
  expected_prepopulated[target_link_id] = target_item_answer
100
- elsif source_extension.value == 'override'
151
+ elsif origin_source == 'override'
101
152
  expected_overrides[target_link_id] = target_item_answer
102
153
  else
103
- raise "`origin.source` extension for item `#{target_link_id}` has unexpected value: #{source_extension.value}"
154
+ raise "`origin.source` extension for item `#{target_link_id}` has unexpected value: #{origin_source}"
104
155
  end
105
156
  end
106
157
  end
107
158
 
108
- def validate_data_requirements_retrieved(expected_questionnaire_response, questionnaire_response)
109
- error_messages = []
110
-
111
- DATA_REQUIREMENT_ANSWERS.each do |library_name, link_id|
112
- expected = find_item_by_link_id(expected_questionnaire_response.item, link_id).answer.first.value
113
- actual = find_item_by_link_id(questionnaire_response.item, link_id)&.answer&.first&.value
114
- next if coding_equal?(expected, actual)
159
+ def check_item_prepopulation(item, expected_answer, override)
160
+ answer = item.answer.first
161
+ link_id = item.linkId
115
162
 
116
- error_messages << "dataRequirement not satisfied for Library '#{library_name}'. Expected answer to " \
117
- "question with linkId `#{link_id}` to have coding with system: '`#{expected.system}`' " \
118
- "and value: '`#{expected.code}`'"
163
+ unless answer&.value&.present?
164
+ add_message('error', "No answer for item `#{link_id}`")
165
+ return
119
166
  end
120
- error_messages
121
- end
122
167
 
123
- def check_item_prepopulation(item, expected_answer, error_list, override)
124
- answer = item.answer.first
125
- if answer&.value&.present?
126
- # check answer
127
- if override && answer_value_equal?(expected_answer, answer)
128
- error_list << "Answer to item `#{item.linkId}` was not overriden from the pre-populated value. " \
129
- "Found #{expected_answer}, but should be different"
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)}"
133
- end
168
+ check_answer(link_id, override, expected_answer, answer)
134
169
 
135
- # check origin.source extension
136
- origin_extension = find_extension(answer,
137
- 'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/information-origin|2.0.1')
138
- source_extension = find_extension(origin_extension, 'source')
170
+ origin_source = find_origin_source(item)
171
+ expected_origin_source = override ? 'override' : 'auto'
139
172
 
140
- if source_extension.present?
141
- expected_source_value = override ? 'override' : 'auto'
142
- if source_extension.value != expected_source_value
143
- error_list << "`origin.source` extension on item `#{item.linkId}` contains unexpected value. Expected: " \
144
- "#{expected_source_value}. Found #{source_extension.value}"
145
- end
146
- else
147
- error_list << "Required `origin.source` extension not present on answer to item `#{item.linkId}`"
173
+ if origin_source.present?
174
+ unless origin_source == expected_origin_source
175
+ origin_source_error(link_id, expected_origin_source, origin_source)
148
176
  end
149
177
  else
150
- error_list << "No answer for item `#{item.linkId}`"
178
+ add_message('error', "Required `origin.source` extension not present on answer to item `#{item.linkId}`")
179
+ end
180
+ end
181
+
182
+ def check_answer(link_id, override, expected_answer, answer)
183
+ if override && answer_value_equal?(expected_answer, answer)
184
+ add_message('error', %(Answer to item `#{link_id}` was not overriden from the pre-populated value.
185
+ Found #{expected_answer}, but should be different))
186
+ elsif !override && !answer_value_equal?(expected_answer, answer)
187
+ add_message('error', %(Answer to item `#{link_id}` contains unexpected value. Expected:
188
+ #{value_for_display(expected_answer)}. Found #{value_for_display(answer)}))
151
189
  end
152
190
  end
153
191
 
192
+ def origin_source_error(link_id, expected, actual)
193
+ add_message('error', %(`origin.source` extension on item `#{link_id}` contains unexpected value.
194
+ Expected: #{expected}. Found: #{actual}))
195
+ end
196
+
154
197
  def find_item_by_link_id(items, link_id)
155
198
  items.each do |item|
156
199
  return item if item.linkId == link_id
@@ -161,6 +204,14 @@ module DaVinciDTRTestKit
161
204
  nil
162
205
  end
163
206
 
207
+ def find_origin_source(item)
208
+ origin_extension = find_extension(
209
+ item&.answer&.first,
210
+ 'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/information-origin'
211
+ )
212
+ find_extension(origin_extension, 'source')&.value
213
+ end
214
+
164
215
  def find_extension(element, url)
165
216
  element&.extension&.find { |e| e.url == url }
166
217
  end
@@ -1,19 +1,21 @@
1
- require_relative 'ext/inferno_core/runnable'
2
- require_relative 'ext/inferno_core/record_response_route'
3
- require_relative 'ext/inferno_core/request'
4
1
  require_relative 'auth_groups/oauth2_authentication_group'
5
2
  require_relative 'client_groups/resp_assist_device/dtr_smart_app_questionnaire_workflow_group'
6
3
  require_relative 'client_groups/dinner_static/dtr_smart_app_questionnaire_workflow_group'
7
4
  require_relative 'client_groups/dinner_adaptive/dtr_smart_app_questionnaire_workflow_group'
8
- require_relative 'mock_payer'
9
- require_relative 'mock_ehr'
5
+ require_relative 'endpoints/cors'
6
+ require_relative 'endpoints/mock_authorization'
7
+ require_relative 'endpoints/mock_authorization/authorize_endpoint'
8
+ require_relative 'endpoints/mock_authorization/token_endpoint'
9
+ require_relative 'endpoints/mock_payer/questionnaire_package_endpoint'
10
+ require_relative 'endpoints/mock_payer/next_question_endpoint'
11
+ require_relative 'endpoints/mock_ehr'
12
+ require_relative 'endpoints/mock_ehr/questionnaire_response_endpoint'
13
+ require_relative 'endpoints/mock_ehr/fhir_get_endpoint'
10
14
  require_relative 'version'
11
15
 
12
16
  module DaVinciDTRTestKit
13
17
  class DTRSmartAppSuite < Inferno::TestSuite
14
- extend MockAuthServer
15
- extend MockEHR
16
- extend MockPayer
18
+ extend CORS
17
19
 
18
20
  id :dtr_smart_app
19
21
  title 'Da Vinci DTR SMART App Test Suite'
@@ -50,58 +52,32 @@ module DaVinciDTRTestKit
50
52
  end
51
53
 
52
54
  allow_cors QUESTIONNAIRE_PACKAGE_PATH, QUESTIONNAIRE_RESPONSE_PATH, FHIR_RESOURCE_PATH, FHIR_SEARCH_PATH,
53
- EHR_AUTHORIZE_PATH, EHR_TOKEN_PATH
54
-
55
- route(:get, '/fhir/metadata', method(:metadata_handler))
56
-
57
- route(:get, SMART_CONFIG_PATH, method(:ehr_smart_config))
58
-
59
- record_response_route :get, EHR_AUTHORIZE_PATH, 'dtr_smart_app_ehr_authorize', method(:ehr_authorize),
60
- resumes: ->(_) { false } do |request|
61
- DTRSmartAppSuite.extract_client_id_from_query_params(request)
62
- end
63
-
64
- record_response_route :post, EHR_AUTHORIZE_PATH, 'dtr_smart_app_ehr_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)
77
- end
78
-
79
- record_response_route :post, QUESTIONNAIRE_PACKAGE_PATH, QUESTIONNAIRE_PACKAGE_TAG,
80
- method(:questionnaire_package_response), resumes: ->(_) { false } do |request|
81
- DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
82
- end
83
-
84
- record_response_route :post, QUESTIONNAIRE_RESPONSE_PATH, 'dtr_smart_app_questionnaire_response',
85
- method(:questionnaire_response_response) do |request|
86
- DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
87
- end
88
-
89
- record_response_route :get, FHIR_RESOURCE_PATH, SMART_APP_EHR_REQUEST_TAG, method(:get_fhir_resource),
90
- resumes: ->(_) { false } do |request|
91
- DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
92
- end
93
-
94
- record_response_route :get, FHIR_SEARCH_PATH, SMART_APP_EHR_REQUEST_TAG, method(:get_fhir_resource),
95
- resumes: ->(_) { false } do |request|
96
- DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
97
- end
55
+ EHR_AUTHORIZE_PATH, EHR_TOKEN_PATH, JKWS_PATH, OPENID_CONFIG_PATH, NEXT_PATH
56
+
57
+ # Authorization server
58
+ route(:get, SMART_CONFIG_PATH, MockAuthorization.method(:ehr_smart_config))
59
+ route(:get, OPENID_CONFIG_PATH, MockAuthorization.method(:ehr_openid_config))
60
+ route(:get, JKWS_PATH, MockAuthorization.method(:jwks))
61
+ suite_endpoint :get, EHR_AUTHORIZE_PATH, MockAuthorization::AuthorizeEndpoint
62
+ suite_endpoint :post, EHR_AUTHORIZE_PATH, MockAuthorization::AuthorizeEndpoint
63
+ suite_endpoint :post, EHR_TOKEN_PATH, MockAuthorization::TokenEndpoint
64
+
65
+ # Payer
66
+ suite_endpoint :post, QUESTIONNAIRE_PACKAGE_PATH, MockPayer::QuestionnairePackageEndpoint
67
+ suite_endpoint :post, NEXT_PATH, MockPayer::NextQuestionEndpoint
68
+
69
+ # EHR
70
+ route(:get, '/fhir/metadata', MockEHR.method(:metadata))
71
+ suite_endpoint :post, QUESTIONNAIRE_RESPONSE_PATH, MockEHR::QuestionnaireResponseEndpoint
72
+ suite_endpoint :get, FHIR_RESOURCE_PATH, MockEHR::FHIRGetEndpoint
73
+ suite_endpoint :get, FHIR_SEARCH_PATH, MockEHR::FHIRGetEndpoint
98
74
 
99
75
  resume_test_route :get, RESUME_PASS_PATH do |request|
100
- DTRSmartAppSuite.extract_client_id_from_query_params(request)
76
+ request.query_parameters['client_id'] || request.query_parameters['token']
101
77
  end
102
78
 
103
79
  resume_test_route :get, RESUME_FAIL_PATH, result: 'fail' do |request|
104
- DTRSmartAppSuite.extract_client_id_from_query_params(request)
80
+ request.query_parameters['client_id'] || request.query_parameters['token']
105
81
  end
106
82
 
107
83
  # TODO: Update based on SMART Launch changes. Do we even want to have this group now?
@@ -115,7 +91,7 @@ module DaVinciDTRTestKit
115
91
  )
116
92
 
117
93
  group from: :dtr_smart_app_static_dinner_questionnaire_workflow
118
- # group from: :dtr_smart_app_adaptive_dinner_questionnaire_workflow - TODO
94
+ group from: :dtr_smart_app_adaptive_dinner_questionnaire_workflow
119
95
  end
120
96
  group do
121
97
  id :dtr_smart_app_questionnaire_functionality
@@ -0,0 +1,20 @@
1
+ module DaVinciDTRTestKit
2
+ module CORS
3
+ PRE_FLIGHT_HANDLER = proc do
4
+ [
5
+ 200,
6
+ {
7
+ 'Access-Control-Allow-Origin' => '*',
8
+ 'Access-Control-Allow-Headers' => 'Content-Type, Authorization'
9
+ },
10
+ ['']
11
+ ]
12
+ end
13
+
14
+ def allow_cors(*paths)
15
+ paths.each do |path|
16
+ route(:options, path, PRE_FLIGHT_HANDLER)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,32 @@
1
+ module DaVinciDTRTestKit
2
+ module MockAuthorization
3
+ class AuthorizeEndpoint < Inferno::DSL::SuiteEndpoint
4
+ def test_run_identifier
5
+ request.params[:client_id]
6
+ end
7
+
8
+ def tags
9
+ [EHR_AUTHORIZE_TAG]
10
+ end
11
+
12
+ def make_response
13
+ if request.params[:redirect_uri].present?
14
+ redirect_uri = "#{request.params[:redirect_uri]}?" \
15
+ "code=#{SecureRandom.hex}&" \
16
+ "state=#{request.params[:state]}"
17
+ response.status = 302
18
+ response.headers['Location'] = redirect_uri
19
+ else
20
+ response.status = 400
21
+ response.format = 'application/fhir+json'
22
+ response.body = FHIR::OperationOutcome.new(
23
+ issue: FHIR::OperationOutcome::Issue.new(severity: 'fatal', code: 'required',
24
+ details: FHIR::CodeableConcept.new(
25
+ text: 'No redirect_uri provided'
26
+ ))
27
+ ).to_json
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,19 @@
1
+ module DaVinciDTRTestKit
2
+ module MockAuthorization
3
+ class SimpleTokenEndpoint < Inferno::DSL::SuiteEndpoint
4
+ def test_run_identifier
5
+ request.params[:client_id]
6
+ end
7
+
8
+ # Placeholder for a more complete mock token endpoint
9
+ def make_response
10
+ response.body = { access_token: SecureRandom.hex, token_type: 'bearer', expires_in: 3600 }.to_json
11
+ response.status = 200
12
+ end
13
+
14
+ def update_result
15
+ results_repo.update_result(result.id, 'pass')
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../urls'
4
+ require_relative '../mock_authorization'
5
+
6
+ module DaVinciDTRTestKit
7
+ module MockAuthorization
8
+ AUTHORIZED_PRACTITIONER_ID = 'pra1234' # Must exist on the FHIR_REFERENCE_SERVER (env var)
9
+
10
+ class TokenEndpoint < Inferno::DSL::SuiteEndpoint
11
+ def test_run_identifier
12
+ extract_client_id
13
+ end
14
+
15
+ def make_response
16
+ client_id = extract_client_id
17
+ access_token = JWT.encode({ inferno_client_id: client_id }, nil, 'none')
18
+ granted_scopes = SUPPORTED_SCOPES & requested_scopes
19
+
20
+ response_hash = { access_token:, scope: granted_scopes.join(' '), token_type: 'bearer', expires_in: 3600 }
21
+
22
+ if granted_scopes.include?('openid')
23
+ response_hash.merge!(id_token: create_id_token(client_id, fhir_user: granted_scopes.include?('fhirUser')))
24
+ end
25
+
26
+ fhir_context_input = find_test_input("#{input_group_prefix}_smart_fhir_context")
27
+ begin
28
+ fhir_context = JSON.parse(fhir_context_input)
29
+ rescue StandardError
30
+ fhir_context = nil
31
+ end
32
+ response_hash.merge!(fhirContext: fhir_context) if fhir_context
33
+
34
+ smart_patient_input = find_test_input("#{input_group_prefix}_smart_patient_id")
35
+ response_hash.merge!(patient: smart_patient_input) if smart_patient_input
36
+
37
+ response.body = response_hash.to_json
38
+ response.headers['Cache-Control'] = 'no-store'
39
+ response.headers['Pragma'] = 'no-cache'
40
+ response.headers['Access-Control-Allow-Origin'] = '*'
41
+ response.status = 200
42
+ end
43
+
44
+ private
45
+
46
+ def extract_client_id
47
+ # Public client || confidential client asymmetric || confidential client symmetric
48
+ request.params[:client_id] || extract_client_id_from_client_assertion || extract_client_id_from_basic_auth
49
+ end
50
+
51
+ def extract_client_id_from_client_assertion
52
+ encoded_jwt = request.params[:client_assertion]
53
+ return unless encoded_jwt.present?
54
+
55
+ jwt_payload =
56
+ begin
57
+ JWT.decode(encoded_jwt, nil, false)&.first # skip signature verification
58
+ rescue StandardError
59
+ nil
60
+ end
61
+
62
+ jwt_payload['iss'] || jwt_payload['sub'] if jwt_payload.present?
63
+ end
64
+
65
+ def input_group_prefix
66
+ if test.id.include?('static')
67
+ 'static'
68
+ elsif test.id.include?('adaptive')
69
+ 'adaptive'
70
+ else
71
+ 'resp'
72
+ end
73
+ end
74
+
75
+ def find_test_input(input_name)
76
+ JSON.parse(result.input_json)&.find { |input| input['name'] == input_name }&.dig('value')
77
+ end
78
+
79
+ def extract_client_id_from_basic_auth
80
+ encoded_credentials = request.headers['authorization']&.delete_prefix('Basic ')
81
+ return unless encoded_credentials.present?
82
+
83
+ decoded_credentials = Base64.decode64(encoded_credentials)
84
+ decoded_credentials&.split(':')&.first
85
+ end
86
+
87
+ def requested_scopes
88
+ auth_request = requests_repo.tagged_requests(result.test_session_id, [EHR_AUTHORIZE_TAG]).last
89
+ return [] unless auth_request
90
+
91
+ auth_params = if auth_request.verb.downcase == 'get'
92
+ auth_request.query_parameters
93
+ else
94
+ URI.decode_www_form(auth_request.request_body)&.to_h
95
+ end
96
+ scope_str = auth_params&.dig('scope')
97
+ scope_str ? URI.decode_www_form_component(scope_str).split : []
98
+ end
99
+
100
+ def create_id_token(client_id, fhir_user: false)
101
+ # No point in mocking an identity provider, just always use known Practitioner as the authorized user
102
+ suite_fhir_base_url = request.url.split(EHR_TOKEN_PATH).first + FHIR_BASE_PATH
103
+ id_token_hash = {
104
+ iss: suite_fhir_base_url,
105
+ sub: AUTHORIZED_PRACTITIONER_ID,
106
+ aud: client_id,
107
+ exp: Time.now.to_i + (24 * 60 * 60), # 24 hrs
108
+ iat: Time.now.to_i
109
+ }
110
+ id_token_hash.merge!(fhirUser: "#{suite_fhir_base_url}/Practitioner/#{AUTHORIZED_PRACTITIONER_ID}") if fhir_user
111
+
112
+ JWT.encode(id_token_hash, RSA_PRIVATE_KEY, 'RS256')
113
+ end
114
+ end
115
+ end
116
+ end