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.
- checksums.yaml +4 -4
- data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_adaptive_questionnaire_completion_group.rb +23 -0
- data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_adaptive_questionnaire_followup_questions_group.rb +26 -0
- data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_adaptive_questionnaire_next_question_request_test.rb +93 -0
- data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_adaptive_questionnaire_next_question_request_validation_test.rb +62 -0
- data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_adaptive_questionnaire_next_question_retrieval_group.rb +23 -0
- data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_adaptive_questionnaire_response_validation_test.rb +66 -0
- data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_full_ehr_adaptive_dinner_questionnaire_workflow_group.rb +76 -0
- data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_full_ehr_adaptive_questionnaire_initial_retrieval_group.rb +27 -0
- data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_full_ehr_adaptive_questionnaire_request_test.rb +63 -0
- data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_smart_app_adaptive_questionnaire_initial_retrieval_group.rb +24 -0
- data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_smart_app_adaptive_questionnaire_request_test.rb +148 -0
- data/lib/davinci_dtr_test_kit/client_groups/dinner_adaptive/dtr_smart_app_questionnaire_workflow_group.rb +75 -0
- data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_full_ehr_questionnaire_workflow_group.rb +22 -28
- data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_smart_app_dinner_questionnaire_package_request_test.rb +14 -17
- data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_smart_app_questionnaire_workflow_group.rb +9 -31
- data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → full_ehr}/dtr_full_ehr_launch_attestation_test.rb +7 -6
- data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → full_ehr}/dtr_full_ehr_prepopulation_attestation_test.rb +7 -7
- data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → full_ehr}/dtr_full_ehr_prepopulation_override_attestation_test.rb +7 -7
- 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
- data/lib/davinci_dtr_test_kit/client_groups/full_ehr/dtr_full_ehr_questionnaire_response_conformance_test.rb +19 -0
- data/lib/davinci_dtr_test_kit/client_groups/full_ehr/dtr_full_ehr_questionnaire_response_correctness_test.rb +37 -0
- data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → full_ehr}/dtr_full_ehr_rendering_enabled_questions_attestation_test.rb +7 -7
- data/lib/davinci_dtr_test_kit/client_groups/full_ehr/dtr_full_ehr_saving_questionnaire_response_group.rb +29 -0
- data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → full_ehr}/dtr_full_ehr_store_attestation_test.rb +7 -7
- data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_questionnaire_rendering_attestation_test.rb +7 -6
- data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_resp_questionnaire_package_request_test.rb +15 -18
- data/lib/davinci_dtr_test_kit/client_groups/shared/dtr_questionnaire_response_basic_conformance_test.rb +7 -7
- data/lib/davinci_dtr_test_kit/client_groups/shared/dtr_questionnaire_response_pre_population_test.rb +16 -5
- data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → smart_app}/dtr_smart_app_prepopulation_attestation_test.rb +7 -7
- data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → smart_app}/dtr_smart_app_prepopulation_override_attestation_test.rb +7 -6
- data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → smart_app}/dtr_smart_app_questionnaire_response_save_test.rb +17 -7
- data/lib/davinci_dtr_test_kit/client_groups/{dinner_static → smart_app}/dtr_smart_app_rendering_enabled_questions_attestation_test.rb +7 -7
- data/lib/davinci_dtr_test_kit/client_groups/smart_app/dtr_smart_app_saving_questionnaire_response_group.rb +27 -0
- data/lib/davinci_dtr_test_kit/cql_test.rb +114 -172
- data/lib/davinci_dtr_test_kit/create_test.rb +25 -0
- data/lib/davinci_dtr_test_kit/docs/dtr_full_ehr_suite_description_v201.md +95 -37
- data/lib/davinci_dtr_test_kit/docs/dtr_light_ehr_suite_description_v201.md +34 -0
- data/lib/davinci_dtr_test_kit/docs/dtr_payer_server_suite_description_v201.md +32 -29
- data/lib/davinci_dtr_test_kit/docs/dtr_smart_app_suite_description_v201.md +48 -32
- data/lib/davinci_dtr_test_kit/dtr_full_ehr_suite.rb +13 -17
- data/lib/davinci_dtr_test_kit/dtr_light_ehr_suite.rb +101 -23
- data/lib/davinci_dtr_test_kit/dtr_options.rb +7 -0
- data/lib/davinci_dtr_test_kit/dtr_payer_server_suite.rb +9 -20
- data/lib/davinci_dtr_test_kit/dtr_questionnaire_response_validation.rb +126 -75
- data/lib/davinci_dtr_test_kit/dtr_smart_app_suite.rb +32 -56
- data/lib/davinci_dtr_test_kit/endpoints/cors.rb +20 -0
- data/lib/davinci_dtr_test_kit/endpoints/mock_authorization/authorize_endpoint.rb +32 -0
- data/lib/davinci_dtr_test_kit/endpoints/mock_authorization/simple_token_endpoint.rb +19 -0
- data/lib/davinci_dtr_test_kit/endpoints/mock_authorization/token_endpoint.rb +116 -0
- data/lib/davinci_dtr_test_kit/endpoints/mock_authorization.rb +83 -0
- data/lib/davinci_dtr_test_kit/endpoints/mock_ehr/fhir_get_endpoint.rb +95 -0
- data/lib/davinci_dtr_test_kit/endpoints/mock_ehr/questionnaire_response_endpoint.rb +22 -0
- data/lib/davinci_dtr_test_kit/endpoints/mock_ehr.rb +25 -0
- data/lib/davinci_dtr_test_kit/endpoints/mock_payer/full_ehr_next_question_endpoint.rb +11 -0
- data/lib/davinci_dtr_test_kit/endpoints/mock_payer/full_ehr_questionnaire_package_endpoint.rb +11 -0
- data/lib/davinci_dtr_test_kit/endpoints/mock_payer/next_question_endpoint.rb +162 -0
- data/lib/davinci_dtr_test_kit/endpoints/mock_payer/next_question_proxy_endpoint.rb +36 -0
- data/lib/davinci_dtr_test_kit/endpoints/mock_payer/questionnaire_package_endpoint.rb +62 -0
- data/lib/davinci_dtr_test_kit/endpoints/mock_payer/questionnaire_package_proxy_endpoint.rb +38 -0
- data/lib/davinci_dtr_test_kit/endpoints/mock_payer.rb +36 -0
- data/lib/davinci_dtr_test_kit/fixture_loader.rb +6 -84
- data/lib/davinci_dtr_test_kit/fixtures/dinner_adaptive/dinner_order_adaptive_next_question_burrito.json +10 -2
- data/lib/davinci_dtr_test_kit/fixtures/dinner_adaptive/dinner_order_adaptive_next_question_hamburger.json +10 -2
- data/lib/davinci_dtr_test_kit/fixtures/dinner_adaptive/dinner_order_adaptive_next_question_initial.json +10 -2
- data/lib/davinci_dtr_test_kit/fixtures/dinner_adaptive/questionnaire_dinner_order_adaptive.json +4 -3
- data/lib/davinci_dtr_test_kit/fixtures.rb +64 -46
- data/lib/davinci_dtr_test_kit/payer_server_groups/adaptive_form_libraries_test.rb +2 -2
- data/lib/davinci_dtr_test_kit/payer_server_groups/adaptive_form_questionnaire_expressions_test.rb +4 -3
- data/lib/davinci_dtr_test_kit/payer_server_groups/adaptive_form_questionnaire_extensions_test.rb +3 -2
- data/lib/davinci_dtr_test_kit/payer_server_groups/adaptive_next_questionnaire_expressions_test.rb +8 -8
- data/lib/davinci_dtr_test_kit/payer_server_groups/adaptive_next_questionnaire_extensions_test.rb +6 -5
- data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_group.rb +2 -2
- data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_request_validation_test.rb +2 -1
- data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_response_bundles_validation_test.rb +6 -9
- data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_response_search_validation_test.rb +15 -12
- data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_response_validation_test.rb +33 -22
- data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_next_request_validation_test.rb +1 -1
- data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_next_response_complete_test.rb +4 -4
- data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_next_response_validation_test.rb +16 -12
- data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_libraries_test.rb +2 -2
- data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_questionnaire_expressions_test.rb +5 -4
- data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_questionnaire_extensions_test.rb +4 -3
- data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_request_validation_test.rb +2 -1
- data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_response_validation_test.rb +32 -23
- data/lib/davinci_dtr_test_kit/profiles/communication_request/communication_request_read.rb +29 -0
- data/lib/davinci_dtr_test_kit/profiles/communication_request/communication_request_validation.rb +35 -0
- data/lib/davinci_dtr_test_kit/profiles/communication_request_group.rb +39 -0
- data/lib/davinci_dtr_test_kit/profiles/coverage/coverage_read.rb +29 -0
- data/lib/davinci_dtr_test_kit/profiles/coverage/coverage_validation.rb +35 -0
- data/lib/davinci_dtr_test_kit/profiles/coverage_group.rb +38 -0
- data/lib/davinci_dtr_test_kit/profiles/device_request/device_request_read.rb +29 -0
- data/lib/davinci_dtr_test_kit/profiles/device_request/device_request_validation.rb +35 -0
- data/lib/davinci_dtr_test_kit/profiles/device_request_group.rb +39 -0
- data/lib/davinci_dtr_test_kit/profiles/encounter/encounter_read.rb +29 -0
- data/lib/davinci_dtr_test_kit/profiles/encounter/encounter_validation.rb +35 -0
- data/lib/davinci_dtr_test_kit/profiles/encounter_group.rb +39 -0
- data/lib/davinci_dtr_test_kit/profiles/medication_request/medication_request_read.rb +29 -0
- data/lib/davinci_dtr_test_kit/profiles/medication_request/medication_request_validation.rb +35 -0
- data/lib/davinci_dtr_test_kit/profiles/medication_request_group.rb +39 -0
- data/lib/davinci_dtr_test_kit/profiles/nutrition_order/nutrition_order_read.rb +29 -0
- data/lib/davinci_dtr_test_kit/profiles/nutrition_order/nutrition_order_validation.rb +35 -0
- data/lib/davinci_dtr_test_kit/profiles/nutrition_order_group.rb +39 -0
- data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_context_search.rb +35 -0
- data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_create.rb +26 -0
- data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_patient_search.rb +55 -0
- data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_read.rb +22 -0
- data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_update.rb +26 -0
- data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_validation.rb +37 -0
- data/lib/davinci_dtr_test_kit/profiles/questionnaire_response_group.rb +66 -0
- data/lib/davinci_dtr_test_kit/profiles/service_request/service_request_read.rb +29 -0
- data/lib/davinci_dtr_test_kit/profiles/service_request/service_request_validation.rb +35 -0
- data/lib/davinci_dtr_test_kit/profiles/service_request_group.rb +39 -0
- data/lib/davinci_dtr_test_kit/profiles/task/task_create.rb +26 -0
- data/lib/davinci_dtr_test_kit/profiles/task/task_read.rb +29 -0
- data/lib/davinci_dtr_test_kit/profiles/task/task_update.rb +26 -0
- data/lib/davinci_dtr_test_kit/profiles/task/task_validation.rb +35 -0
- data/lib/davinci_dtr_test_kit/profiles/task_group.rb +52 -0
- data/lib/davinci_dtr_test_kit/profiles/vision_prescription/vision_prescription_read.rb +29 -0
- data/lib/davinci_dtr_test_kit/profiles/vision_prescription/vision_prescription_validation.rb +35 -0
- data/lib/davinci_dtr_test_kit/profiles/vision_prescription_group.rb +39 -0
- data/lib/davinci_dtr_test_kit/read_test.rb +22 -0
- data/lib/davinci_dtr_test_kit/tags.rb +5 -6
- data/lib/davinci_dtr_test_kit/update_test.rb +25 -0
- data/lib/davinci_dtr_test_kit/urls.rb +13 -10
- data/lib/davinci_dtr_test_kit/validation_test.rb +21 -5
- data/lib/davinci_dtr_test_kit/version.rb +1 -1
- data/lib/davinci_dtr_test_kit.rb +1 -1
- metadata +129 -24
- data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_full_ehr_prepopulation_representation_attestation_test.rb +0 -33
- data/lib/davinci_dtr_test_kit/client_groups/resp_assist_device/dtr_full_ehr_questionnaire_workflow_group.rb +0 -19
- data/lib/davinci_dtr_test_kit/ext/inferno_core/record_response_route.rb +0 -98
- data/lib/davinci_dtr_test_kit/ext/inferno_core/request.rb +0 -19
- data/lib/davinci_dtr_test_kit/ext/inferno_core/runnable.rb +0 -35
- data/lib/davinci_dtr_test_kit/mock_auth_server.rb +0 -145
- data/lib/davinci_dtr_test_kit/mock_ehr.rb +0 -97
- data/lib/davinci_dtr_test_kit/mock_payer.rb +0 -123
- /data/lib/davinci_dtr_test_kit/fixtures/{pre_populated_questionnaire_response.json → respiratory_assist_device/pre_populated_questionnaire_response.json} +0 -0
- /data/lib/davinci_dtr_test_kit/fixtures/{questionnaire_package.json → respiratory_assist_device/questionnaire_package.json} +0 -0
@@ -1,30 +1,86 @@
|
|
1
|
-
|
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
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
91
|
+
|
36
92
|
validate_cql_executed(questionnaire_response.item, questionnaire_cql_expression_link_ids,
|
37
|
-
template_prepopulation_expectations, template_override_expectations
|
93
|
+
template_prepopulation_expectations, template_override_expectations)
|
38
94
|
|
39
95
|
if template_prepopulation_expectations.size.positive?
|
40
|
-
|
41
|
-
|
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
|
-
|
46
|
-
|
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
|
-
|
50
|
-
|
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
|
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),
|
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
|
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
|
-
|
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
|
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
|
149
|
+
if origin_source == 'auto'
|
99
150
|
expected_prepopulated[target_link_id] = target_item_answer
|
100
|
-
elsif
|
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: #{
|
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
|
109
|
-
|
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
|
-
|
117
|
-
|
118
|
-
|
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
|
-
|
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
|
-
|
136
|
-
|
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
|
-
|
141
|
-
|
142
|
-
|
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
|
-
|
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 '
|
9
|
-
require_relative '
|
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
|
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
|
-
|
56
|
-
|
57
|
-
route(:get,
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|