davinci_dtr_test_kit 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) 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 -38
  15. data/lib/davinci_dtr_test_kit/client_groups/dinner_static/dtr_smart_app_dinner_questionnaire_package_request_test.rb +13 -16
  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/{dinner_static/dtr_full_ehr_dinner_static_questionnaire_response_conformance_test.rb → full_ehr/dtr_full_ehr_questionnaire_response_conformance_test.rb} +7 -3
  22. data/lib/davinci_dtr_test_kit/client_groups/{dinner_static/dtr_full_ehr_dinner_static_questionnaire_response_correctness_test.rb → full_ehr/dtr_full_ehr_questionnaire_response_correctness_test.rb} +14 -7
  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 +5 -1
  29. data/lib/davinci_dtr_test_kit/client_groups/shared/dtr_questionnaire_response_pre_population_test.rb +12 -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 +37 -140
  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 +11 -6
  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 +67 -2
  43. data/lib/davinci_dtr_test_kit/dtr_payer_server_suite.rb +9 -20
  44. data/lib/davinci_dtr_test_kit/dtr_questionnaire_response_validation.rb +18 -10
  45. data/lib/davinci_dtr_test_kit/dtr_smart_app_suite.rb +32 -59
  46. data/lib/davinci_dtr_test_kit/endpoints/cors.rb +20 -0
  47. data/lib/davinci_dtr_test_kit/endpoints/mock_authorization/authorize_endpoint.rb +32 -0
  48. data/lib/davinci_dtr_test_kit/endpoints/mock_authorization/simple_token_endpoint.rb +19 -0
  49. data/lib/davinci_dtr_test_kit/endpoints/mock_authorization/token_endpoint.rb +116 -0
  50. data/lib/davinci_dtr_test_kit/endpoints/mock_authorization.rb +83 -0
  51. data/lib/davinci_dtr_test_kit/endpoints/mock_ehr/fhir_get_endpoint.rb +95 -0
  52. data/lib/davinci_dtr_test_kit/endpoints/mock_ehr/questionnaire_response_endpoint.rb +22 -0
  53. data/lib/davinci_dtr_test_kit/endpoints/mock_ehr.rb +25 -0
  54. data/lib/davinci_dtr_test_kit/endpoints/mock_payer/full_ehr_next_question_endpoint.rb +11 -0
  55. data/lib/davinci_dtr_test_kit/endpoints/mock_payer/full_ehr_questionnaire_package_endpoint.rb +11 -0
  56. data/lib/davinci_dtr_test_kit/endpoints/mock_payer/next_question_endpoint.rb +162 -0
  57. data/lib/davinci_dtr_test_kit/endpoints/mock_payer/next_question_proxy_endpoint.rb +36 -0
  58. data/lib/davinci_dtr_test_kit/endpoints/mock_payer/questionnaire_package_endpoint.rb +62 -0
  59. data/lib/davinci_dtr_test_kit/endpoints/mock_payer/questionnaire_package_proxy_endpoint.rb +38 -0
  60. data/lib/davinci_dtr_test_kit/endpoints/mock_payer.rb +36 -0
  61. data/lib/davinci_dtr_test_kit/fixtures/dinner_adaptive/dinner_order_adaptive_next_question_burrito.json +10 -2
  62. data/lib/davinci_dtr_test_kit/fixtures/dinner_adaptive/dinner_order_adaptive_next_question_hamburger.json +10 -2
  63. data/lib/davinci_dtr_test_kit/fixtures/dinner_adaptive/dinner_order_adaptive_next_question_initial.json +10 -2
  64. data/lib/davinci_dtr_test_kit/fixtures/dinner_adaptive/questionnaire_dinner_order_adaptive.json +4 -3
  65. data/lib/davinci_dtr_test_kit/fixtures.rb +24 -1
  66. data/lib/davinci_dtr_test_kit/payer_server_groups/adaptive_form_libraries_test.rb +2 -2
  67. data/lib/davinci_dtr_test_kit/payer_server_groups/adaptive_form_questionnaire_expressions_test.rb +4 -3
  68. data/lib/davinci_dtr_test_kit/payer_server_groups/adaptive_form_questionnaire_extensions_test.rb +3 -2
  69. data/lib/davinci_dtr_test_kit/payer_server_groups/adaptive_next_questionnaire_expressions_test.rb +6 -6
  70. data/lib/davinci_dtr_test_kit/payer_server_groups/adaptive_next_questionnaire_extensions_test.rb +6 -5
  71. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_group.rb +2 -2
  72. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_response_bundles_validation_test.rb +6 -9
  73. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_response_search_validation_test.rb +15 -12
  74. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_adaptive_response_validation_test.rb +33 -23
  75. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_next_response_complete_test.rb +4 -4
  76. data/lib/davinci_dtr_test_kit/payer_server_groups/payer_server_next_response_validation_test.rb +16 -12
  77. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_libraries_test.rb +2 -2
  78. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_questionnaire_expressions_test.rb +5 -4
  79. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_questionnaire_extensions_test.rb +4 -3
  80. data/lib/davinci_dtr_test_kit/payer_server_groups/static_form_response_validation_test.rb +32 -25
  81. data/lib/davinci_dtr_test_kit/profiles/communication_request/communication_request_read.rb +29 -0
  82. data/lib/davinci_dtr_test_kit/profiles/communication_request/communication_request_validation.rb +35 -0
  83. data/lib/davinci_dtr_test_kit/profiles/communication_request_group.rb +39 -0
  84. data/lib/davinci_dtr_test_kit/profiles/coverage/coverage_read.rb +29 -0
  85. data/lib/davinci_dtr_test_kit/profiles/coverage/coverage_validation.rb +35 -0
  86. data/lib/davinci_dtr_test_kit/profiles/coverage_group.rb +38 -0
  87. data/lib/davinci_dtr_test_kit/profiles/device_request/device_request_read.rb +29 -0
  88. data/lib/davinci_dtr_test_kit/profiles/device_request/device_request_validation.rb +35 -0
  89. data/lib/davinci_dtr_test_kit/profiles/device_request_group.rb +39 -0
  90. data/lib/davinci_dtr_test_kit/profiles/encounter/encounter_read.rb +29 -0
  91. data/lib/davinci_dtr_test_kit/profiles/encounter/encounter_validation.rb +35 -0
  92. data/lib/davinci_dtr_test_kit/profiles/encounter_group.rb +39 -0
  93. data/lib/davinci_dtr_test_kit/profiles/medication_request/medication_request_read.rb +29 -0
  94. data/lib/davinci_dtr_test_kit/profiles/medication_request/medication_request_validation.rb +35 -0
  95. data/lib/davinci_dtr_test_kit/profiles/medication_request_group.rb +39 -0
  96. data/lib/davinci_dtr_test_kit/profiles/nutrition_order/nutrition_order_read.rb +29 -0
  97. data/lib/davinci_dtr_test_kit/profiles/nutrition_order/nutrition_order_validation.rb +35 -0
  98. data/lib/davinci_dtr_test_kit/profiles/nutrition_order_group.rb +39 -0
  99. data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_context_search.rb +35 -0
  100. data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_create.rb +26 -0
  101. data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_patient_search.rb +55 -0
  102. data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_read.rb +22 -0
  103. data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_update.rb +26 -0
  104. data/lib/davinci_dtr_test_kit/profiles/questionnaire_response/questionnaire_response_validation.rb +37 -0
  105. data/lib/davinci_dtr_test_kit/profiles/questionnaire_response_group.rb +66 -0
  106. data/lib/davinci_dtr_test_kit/profiles/service_request/service_request_read.rb +29 -0
  107. data/lib/davinci_dtr_test_kit/profiles/service_request/service_request_validation.rb +35 -0
  108. data/lib/davinci_dtr_test_kit/profiles/service_request_group.rb +39 -0
  109. data/lib/davinci_dtr_test_kit/profiles/task/task_create.rb +26 -0
  110. data/lib/davinci_dtr_test_kit/profiles/task/task_read.rb +29 -0
  111. data/lib/davinci_dtr_test_kit/profiles/task/task_update.rb +26 -0
  112. data/lib/davinci_dtr_test_kit/profiles/task/task_validation.rb +35 -0
  113. data/lib/davinci_dtr_test_kit/profiles/task_group.rb +52 -0
  114. data/lib/davinci_dtr_test_kit/profiles/vision_prescription/vision_prescription_read.rb +29 -0
  115. data/lib/davinci_dtr_test_kit/profiles/vision_prescription/vision_prescription_validation.rb +35 -0
  116. data/lib/davinci_dtr_test_kit/profiles/vision_prescription_group.rb +39 -0
  117. data/lib/davinci_dtr_test_kit/read_test.rb +22 -0
  118. data/lib/davinci_dtr_test_kit/tags.rb +5 -7
  119. data/lib/davinci_dtr_test_kit/update_test.rb +25 -0
  120. data/lib/davinci_dtr_test_kit/validation_test.rb +19 -4
  121. data/lib/davinci_dtr_test_kit/version.rb +1 -1
  122. metadata +109 -20
  123. data/lib/davinci_dtr_test_kit/ext/inferno_core/record_response_route.rb +0 -98
  124. data/lib/davinci_dtr_test_kit/ext/inferno_core/request.rb +0 -19
  125. data/lib/davinci_dtr_test_kit/ext/inferno_core/runnable.rb +0 -35
  126. data/lib/davinci_dtr_test_kit/mock_auth_server.rb +0 -228
  127. data/lib/davinci_dtr_test_kit/mock_ehr.rb +0 -105
  128. data/lib/davinci_dtr_test_kit/mock_payer.rb +0 -100
@@ -1,6 +1,17 @@
1
+ require 'us_core_test_kit'
1
2
  require 'tls_test_kit'
2
3
  require_relative 'version'
3
4
  require_relative 'dtr_options'
5
+ require_relative 'profiles/questionnaire_response_group'
6
+ require_relative 'profiles/coverage_group'
7
+ require_relative 'profiles/communication_request_group'
8
+ require_relative 'profiles/device_request_group'
9
+ require_relative 'profiles/encounter_group'
10
+ require_relative 'profiles/medication_request_group'
11
+ require_relative 'profiles/nutrition_order_group'
12
+ require_relative 'profiles/service_request_group'
13
+ require_relative 'profiles/task_group'
14
+ require_relative 'profiles/vision_prescription_group'
4
15
  require 'smart_app_launch/smart_stu1_suite'
5
16
  require 'smart_app_launch/smart_stu2_suite'
6
17
 
@@ -32,8 +43,17 @@ module DaVinciDTRTestKit
32
43
  ]
33
44
 
34
45
  input :url,
35
- title: 'FHIR Endpoint',
36
- description: 'URL of the DTR FHIR server'
46
+ title: 'FHIR Server Base Url',
47
+ description: 'URL of the target DTR Light EHR'
48
+
49
+ # Hl7 Validator Wrapper:
50
+ fhir_resource_validator do
51
+ igs('hl7.fhir.us.davinci-dtr#2.0.1', 'hl7.fhir.us.davinci-pas#2.0.1', 'hl7.fhir.us.davinci-crd#2.0.1')
52
+
53
+ exclude_message do |message|
54
+ message.message.match?(/\A\S+: \S+: URL value '.*' does not resolve/)
55
+ end
56
+ end
37
57
 
38
58
  group do
39
59
  title 'Authorization'
@@ -71,5 +91,50 @@ module DaVinciDTRTestKit
71
91
  required_suite_options: DTROptions::SMART_2_REQUIREMENT,
72
92
  run_as_group: true
73
93
  end
94
+
95
+ group do
96
+ title 'FHIR API'
97
+ description %(This test group tests systems for their conformance to
98
+ the US Core v3.1.1 Capability Statement as defined by the DaVinci Documentation
99
+ Templates and Rules (DTR) v2.0.1 Implementation Guide Light DTR EHR
100
+ Capability Statement.
101
+
102
+ )
103
+
104
+ group from: :'us_core_v311-us_core_v311_fhir_api',
105
+ run_as_group: true
106
+ end
107
+
108
+ group do
109
+ title 'DTR Light EHR Profiles'
110
+ description %(This test group tests system for their conformance to
111
+ the RESTful capabilities by specified Resources/Profiles as defined by
112
+ the DaVinci Documentation Templates and Rules (DTR) v2.0,1 Implementation
113
+ Guide Light DTR EHR Capability Statement.
114
+
115
+ )
116
+
117
+ input :credentials,
118
+ title: 'OAuth Credentials',
119
+ type: :oauth_credentials,
120
+ optional: true
121
+
122
+ # All FHIR requests in this suite will use this FHIR client
123
+ fhir_client do
124
+ url :url
125
+ oauth_credentials :credentials
126
+ end
127
+
128
+ group from: :questionnaire_response_group
129
+ group from: :coverage_group
130
+ group from: :communication_request_group
131
+ group from: :device_request_group
132
+ group from: :encounter_group
133
+ group from: :medication_request_group
134
+ group from: :nutrition_order_group
135
+ group from: :service_request_group
136
+ group from: :task_group
137
+ group from: :vision_prescription_group
138
+ end
74
139
  end
75
140
  end
@@ -1,17 +1,15 @@
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 'payer_server_groups/payer_server_static_group'
5
2
  require_relative 'payer_server_groups/payer_server_adaptive_group'
6
3
  require_relative 'tags'
7
- require_relative 'mock_payer'
8
- require_relative 'mock_auth_server'
4
+ require_relative 'endpoints/cors'
5
+ require_relative 'endpoints/mock_authorization/simple_token_endpoint'
6
+ require_relative 'endpoints/mock_payer/questionnaire_package_proxy_endpoint'
7
+ require_relative 'endpoints/mock_payer/next_question_proxy_endpoint'
9
8
  require_relative 'version'
10
9
 
11
10
  module DaVinciDTRTestKit
12
11
  class DTRPayerServerSuite < Inferno::TestSuite
13
- extend MockAuthServer
14
- extend MockPayer
12
+ extend CORS
15
13
 
16
14
  id :dtr_payer_server
17
15
  title 'Da Vinci DTR Payer Server Test Suite'
@@ -106,22 +104,13 @@ module DaVinciDTRTestKit
106
104
 
107
105
  allow_cors QUESTIONNAIRE_PACKAGE_PATH, NEXT_PATH
108
106
 
109
- record_response_route :post, PAYER_TOKEN_PATH, 'dtr_payer_auth', method(:payer_token_response) do |request|
110
- DTRPayerServerSuite.extract_client_id_from_form_params(request)
111
- end
112
-
113
- record_response_route :post, QUESTIONNAIRE_PACKAGE_PATH, QUESTIONNAIRE_TAG,
114
- method(:payer_questionnaire_response), resumes: method(:test_resumes?) do |request|
115
- DTRPayerServerSuite.extract_bearer_token(request)
116
- end
107
+ suite_endpoint :post, PAYER_TOKEN_PATH, MockAuthorization::SimpleTokenEndpoint
117
108
 
118
- record_response_route :post, NEXT_PATH, NEXT_TAG,
119
- method(:questionnaire_next_response), resumes: method(:test_resumes?) do |request|
120
- DTRPayerServerSuite.extract_bearer_token(request)
121
- end
109
+ suite_endpoint :post, QUESTIONNAIRE_PACKAGE_PATH, MockPayer::QuestionnairePackageProxyEndpoint
110
+ suite_endpoint :post, NEXT_PATH, MockPayer::NextQuestionProxyEndpoint
122
111
 
123
112
  resume_test_route :get, RESUME_PASS_PATH do |request|
124
- DTRPayerServerSuite.extract_token_from_query_params(request)
113
+ request.query_parameters['token']
125
114
  end
126
115
 
127
116
  group from: :payer_server_static_package
@@ -20,10 +20,10 @@ module DaVinciDTRTestKit
20
20
  assert_resource_type(:questionnaire_response, resource: questionnaire_response)
21
21
  end
22
22
 
23
- def verify_basic_conformance(questionnaire_response_json)
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'
24
25
  check_is_questionnaire_response(questionnaire_response_json)
25
- assert_valid_resource(resource: FHIR.from_contents(questionnaire_response_json),
26
- profile_url: 'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/dtr-questionnaireresponse|2.0.1')
26
+ assert_valid_resource(resource: FHIR.from_contents(questionnaire_response_json), profile_url:)
27
27
  end
28
28
 
29
29
  # This only checks answers in the questionnaire response, meaning it does not catch missing answers
@@ -55,17 +55,25 @@ module DaVinciDTRTestKit
55
55
  end
56
56
  end
57
57
 
58
- # This checks presence of all answers if link_ids is nil
59
- def check_answer_presence(items, link_ids: nil)
60
- items.each do |item|
61
- check_answer_presence(item.item, link_ids:)
62
-
63
- if !item.answer&.first&.value.present? && (link_ids.nil? || link_ids.include?(item.linkId))
64
- add_message('error', "No answer for item #{item.linkId}")
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
65
  end
66
66
  end
67
67
  end
68
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
+
69
77
  # Requirements:
70
78
  # - Prior to exposing the draft QuestionnaireResponse to the user for completion and/or review, the DTR client
71
79
  # SHALL execute all CQL necessary to resolve the initialExpression, candidateExpression and
@@ -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,61 +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, JKWS_PATH, OPENID_CONFIG_PATH
54
-
55
- route(:get, '/fhir/metadata', method(:metadata_handler))
56
-
57
- route(:get, SMART_CONFIG_PATH, method(:ehr_smart_config))
58
- route(:get, OPENID_CONFIG_PATH, method(:ehr_openid_config))
59
-
60
- route(:get, JKWS_PATH, method(:auth_server_jwks))
61
-
62
- record_response_route :get, EHR_AUTHORIZE_PATH, EHR_AUTHORIZE_TAG, method(:ehr_authorize),
63
- resumes: ->(_) { false } do |request|
64
- DTRSmartAppSuite.extract_client_id_from_query_params(request)
65
- end
66
-
67
- record_response_route :post, EHR_AUTHORIZE_PATH, EHR_AUTHORIZE_TAG, method(:ehr_authorize),
68
- resumes: ->(_) { false } do |request|
69
- DTRSmartAppSuite.extract_client_id_from_form_params(request)
70
- end
71
-
72
- record_response_route :post, EHR_TOKEN_PATH, 'dtr_smart_app_ehr_token', method(:ehr_token_response),
73
- resumes: ->(_) { false } do |request|
74
- DTRSmartAppSuite.extract_client_id_from_token_request(request)
75
- end
76
-
77
- record_response_route :post, PAYER_TOKEN_PATH, 'dtr_smart_app_payer_token',
78
- method(:payer_token_response) do |request|
79
- DTRSmartAppSuite.extract_client_id_from_client_assertion(request)
80
- end
81
-
82
- record_response_route :post, QUESTIONNAIRE_PACKAGE_PATH, QUESTIONNAIRE_PACKAGE_TAG,
83
- method(:questionnaire_package_response), resumes: ->(_) { false } do |request|
84
- DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
85
- end
86
-
87
- record_response_route :post, QUESTIONNAIRE_RESPONSE_PATH, 'dtr_smart_app_questionnaire_response',
88
- method(:questionnaire_response_response) do |request|
89
- DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
90
- end
91
-
92
- record_response_route :get, FHIR_RESOURCE_PATH, SMART_APP_EHR_REQUEST_TAG, method(:get_fhir_resource),
93
- resumes: ->(_) { false } do |request|
94
- DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
95
- end
96
-
97
- record_response_route :get, FHIR_SEARCH_PATH, SMART_APP_EHR_REQUEST_TAG, method(:get_fhir_resource),
98
- resumes: ->(_) { false } do |request|
99
- DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
100
- 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
101
74
 
102
75
  resume_test_route :get, RESUME_PASS_PATH do |request|
103
- DTRSmartAppSuite.extract_client_id_from_query_params(request)
76
+ request.query_parameters['client_id'] || request.query_parameters['token']
104
77
  end
105
78
 
106
79
  resume_test_route :get, RESUME_FAIL_PATH, result: 'fail' do |request|
107
- DTRSmartAppSuite.extract_client_id_from_query_params(request)
80
+ request.query_parameters['client_id'] || request.query_parameters['token']
108
81
  end
109
82
 
110
83
  # TODO: Update based on SMART Launch changes. Do we even want to have this group now?
@@ -118,7 +91,7 @@ module DaVinciDTRTestKit
118
91
  )
119
92
 
120
93
  group from: :dtr_smart_app_static_dinner_questionnaire_workflow
121
- # group from: :dtr_smart_app_adaptive_dinner_questionnaire_workflow - TODO
94
+ group from: :dtr_smart_app_adaptive_dinner_questionnaire_workflow
122
95
  end
123
96
  group do
124
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
@@ -0,0 +1,83 @@
1
+ module DaVinciDTRTestKit
2
+ module MockAuthorization
3
+ RSA_PRIVATE_KEY = OpenSSL::PKey::RSA.generate(2048)
4
+ RSA_PUBLIC_KEY = RSA_PRIVATE_KEY.public_key
5
+ SUPPORTED_SCOPES = ['launch', 'patient/*.rs', 'user/*.rs', 'offline_access', 'openid', 'fhirUser'].freeze
6
+
7
+ module_function
8
+
9
+ def extract_client_id_from_bearer_token(request)
10
+ token = request.headers['authorization']&.delete_prefix('Bearer ')
11
+ jwt =
12
+ begin
13
+ JWT.decode(token, nil, false)
14
+ rescue StandardError
15
+ nil
16
+ end
17
+ jwt&.first&.dig('inferno_client_id')
18
+ end
19
+
20
+ def jwks(_env)
21
+ response_body = {
22
+ keys: [
23
+ {
24
+ kty: 'RSA',
25
+ alg: 'RS256',
26
+ n: Base64.urlsafe_encode64(RSA_PUBLIC_KEY.n.to_s(2), padding: false),
27
+ e: Base64.urlsafe_encode64(RSA_PUBLIC_KEY.e.to_s(2), padding: false),
28
+ use: 'sig'
29
+ }
30
+ ]
31
+ }.to_json
32
+
33
+ [200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]]
34
+ end
35
+
36
+ def ehr_smart_config(env)
37
+ base_url = env_base_url(env, SMART_CONFIG_PATH)
38
+ response_body =
39
+ {
40
+ authorization_endpoint: base_url + EHR_AUTHORIZE_PATH,
41
+ token_endpoint: base_url + EHR_TOKEN_PATH,
42
+ token_endpoint_auth_methods_supported: ['private_key_jwt'],
43
+ token_endpoint_auth_signing_alg_values_supported: ['RS256'],
44
+ grant_types_supported: ['authorization_code'],
45
+ scopes_supported: SUPPORTED_SCOPES,
46
+ response_types_supported: ['code'],
47
+ code_challenge_methods_supported: ['S256'],
48
+ capabilities: [
49
+ 'launch-ehr',
50
+ 'permission-patient',
51
+ 'permission-user',
52
+ 'client-public',
53
+ 'client-confidential-symmetric',
54
+ 'client-confidential-asymmetric'
55
+ ]
56
+ }.to_json
57
+
58
+ [200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]]
59
+ end
60
+
61
+ def ehr_openid_config(env)
62
+ base_url = env_base_url(env, OPENID_CONFIG_PATH)
63
+ response_body = {
64
+ issuer: base_url + FHIR_BASE_PATH,
65
+ authorization_endpoint: base_url + EHR_AUTHORIZE_PATH,
66
+ token_endpoint: base_url + EHR_TOKEN_PATH,
67
+ jwks_uri: base_url + JKWS_PATH,
68
+ response_types_supported: ['id_token'],
69
+ subject_types_supported: ['public'],
70
+ id_token_signing_alg_values_supported: ['RS256']
71
+ }.to_json
72
+ [200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]]
73
+ end
74
+
75
+ def env_base_url(env, endpoint_path)
76
+ protocol = env['rack.url_scheme']
77
+ host = env['HTTP_HOST']
78
+ path = env['REQUEST_PATH'] || env['PATH_INFO']
79
+ path.gsub!(%r{#{endpoint_path}(/)?}, '')
80
+ "#{protocol}://#{host + path}"
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,95 @@
1
+ require_relative '../mock_authorization'
2
+ require_relative '../mock_ehr'
3
+
4
+ module DaVinciDTRTestKit
5
+ module MockEHR
6
+ class FHIRGetEndpoint < Inferno::DSL::SuiteEndpoint
7
+ include MockEHR
8
+
9
+ def test_run_identifier
10
+ MockAuthorization.extract_client_id_from_bearer_token(request)
11
+ end
12
+
13
+ def make_response
14
+ fhir_class, id = fhir_class_and_id_from_url(request.url)
15
+ response.format = 'application/fhir+json'
16
+ response.headers['Access-Control-Allow-Origin'] = '*'
17
+
18
+ if fhir_class.nil?
19
+ response.status = 400
20
+ response.body = FHIR::OperationOutcome.new(
21
+ issue: FHIR::OperationOutcome::Issue.new(severity: 'warning', code: 'not-supported',
22
+ details: FHIR::CodeableConcept.new(
23
+ text: 'No recognized resource type in URL'
24
+ ))
25
+ ).to_json
26
+ return
27
+ end
28
+
29
+ # Respond with user-inputted resource if there is one that matches the request
30
+ ehr_bundle = ehr_input_bundle(test, result)
31
+ if id.present? && ehr_bundle.present?
32
+ matching_resource = find_resource_in_bundle(ehr_bundle, fhir_class, id)
33
+ if matching_resource.present?
34
+ response.status = 200
35
+ response.body = matching_resource.to_json
36
+ return
37
+ end
38
+ end
39
+
40
+ # Proxy resource request to the reference server
41
+ proxy_response = if id.present?
42
+ resource_server_client.read(fhir_class, id)
43
+ else
44
+ resource_server_client.search(fhir_class,
45
+ search: { parameters: request.env['rack.request.query_hash'] })
46
+ end
47
+ response.status = proxy_response.code
48
+ response.body = proxy_response.body
49
+ end
50
+
51
+ def ehr_input_bundle(test, test_result)
52
+ input_name = "#{input_group_prefix(test)}_ehr_bundle"
53
+ ehr_bundle_input = JSON.parse(test_result.input_json).find { |input| input['name'] == input_name }
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
+ ehr_bundle if ehr_bundle.is_a?(FHIR::Bundle)
57
+ rescue StandardError
58
+ nil
59
+ end
60
+
61
+ def input_group_prefix(test)
62
+ if test.id.include?('static')
63
+ 'static'
64
+ elsif test.id.include?('adaptive')
65
+ 'adaptive'
66
+ else
67
+ 'resp'
68
+ end
69
+ end
70
+
71
+ def find_resource_in_bundle(bundle, fhir_class, id)
72
+ bundle.entry&.find do |entry|
73
+ entry.resource.is_a?(fhir_class) && entry.resource&.id == id
74
+ end&.resource
75
+ end
76
+
77
+ # Pull resource type class and ID from url
78
+ # e.g. http://example.org/fhir/Patient/123 -> [FHIR::Patient, '123']
79
+ # @private
80
+ def fhir_class_and_id_from_url(url)
81
+ path = url.split('?').first.split('/fhir/').second
82
+ path.sub!(%r{/$}, '')
83
+ resource_type, id = path.split('/')
84
+
85
+ begin
86
+ fhir_class = FHIR.const_get(resource_type)
87
+ rescue NameError
88
+ fhir_class = nil
89
+ end
90
+
91
+ [fhir_class, id]
92
+ end
93
+ end
94
+ end
95
+ end