davinci_dtr_test_kit 0.13.0 → 0.14.1

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