davinci_crd_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 (341) hide show
  1. checksums.yaml +4 -4
  2. data/config/presets/inferno_crd_client_suite.json.erb +20 -14
  3. data/config/presets/inferno_crd_client_suite_prefetch_subset_v221.json.erb +125 -0
  4. data/config/presets/inferno_crd_client_suite_v221.json.erb +124 -0
  5. data/config/presets/inferno_crd_server_suite.json.erb +58 -1
  6. data/config/presets/inferno_crd_server_suite_v221.json.erb +94 -0
  7. data/config/presets/ri_crd_request_generator.json_v221.json.erb +13 -0
  8. data/config/presets/ri_crd_server.json.erb +19 -19
  9. data/lib/davinci_crd_test_kit/client/client_base_urls.rb +80 -0
  10. data/lib/davinci_crd_test_kit/{client_hook_request_validation.rb → client/client_hook_request_validation.rb} +1 -1
  11. data/lib/davinci_crd_test_kit/client/crd_client_options.rb +30 -0
  12. data/lib/davinci_crd_test_kit/client/endpoints/cds_services_discovery_handler.rb +34 -0
  13. data/lib/davinci_crd_test_kit/client/endpoints/custom_service_response.rb +342 -0
  14. data/lib/davinci_crd_test_kit/client/endpoints/gather_response_generation_data.rb +410 -0
  15. data/lib/davinci_crd_test_kit/client/endpoints/hook_request_endpoint.rb +233 -0
  16. data/lib/davinci_crd_test_kit/{mock_service_response.rb → client/endpoints/mock_service_response.rb} +165 -59
  17. data/lib/davinci_crd_test_kit/{card_responses → client/endpoints/mocked_card_responses}/companions_prerequisites.json +1 -0
  18. data/lib/davinci_crd_test_kit/{card_responses → client/endpoints/mocked_card_responses}/create_update_coverage_information.json +3 -2
  19. data/lib/davinci_crd_test_kit/{card_responses → client/endpoints/mocked_card_responses}/launch_smart_app.json +8 -1
  20. data/lib/davinci_crd_test_kit/{card_responses → client/endpoints/mocked_card_responses}/propose_alternate_request.json +1 -0
  21. data/lib/davinci_crd_test_kit/{card_responses → client/endpoints/mocked_card_responses}/request_form_completion.json +17 -16
  22. data/lib/davinci_crd_test_kit/client/multi_request_message_helper.rb +35 -0
  23. data/lib/davinci_crd_test_kit/client/tagged_request_load_helper.rb +38 -0
  24. data/lib/davinci_crd_test_kit/client/v2.0.1/api/client_fhir_api_create_test.rb +43 -0
  25. data/lib/davinci_crd_test_kit/client/v2.0.1/api/client_fhir_api_read_test.rb +43 -0
  26. data/lib/davinci_crd_test_kit/client/v2.0.1/api/client_fhir_api_search_test.rb +234 -0
  27. data/lib/davinci_crd_test_kit/client/v2.0.1/api/client_fhir_api_update_test.rb +43 -0
  28. data/lib/davinci_crd_test_kit/client/v2.0.1/api/client_fhir_api_validation_test.rb +63 -0
  29. data/lib/davinci_crd_test_kit/client/v2.0.1/auth/decode_auth_token_test.rb +65 -0
  30. data/lib/davinci_crd_test_kit/client/v2.0.1/auth/retrieve_jwks_test.rb +109 -0
  31. data/lib/davinci_crd_test_kit/client/v2.0.1/auth/token_header_test.rb +70 -0
  32. data/lib/davinci_crd_test_kit/client/v2.0.1/auth/token_payload_test.rb +85 -0
  33. data/lib/davinci_crd_test_kit/{routes/cds-services.json → client/v2.0.1/cds-services-v201.json} +1 -1
  34. data/lib/davinci_crd_test_kit/client/v2.0.1/client_appointment_book_group.rb +108 -0
  35. data/lib/davinci_crd_test_kit/client/v2.0.1/client_card_must_support_group.rb +31 -0
  36. data/lib/davinci_crd_test_kit/client/v2.0.1/client_encounter_discharge_group.rb +105 -0
  37. data/lib/davinci_crd_test_kit/client/v2.0.1/client_encounter_start_group.rb +105 -0
  38. data/lib/davinci_crd_test_kit/client/v2.0.1/client_fhir_api_group.rb +790 -0
  39. data/lib/davinci_crd_test_kit/client/v2.0.1/client_hooks_group.rb +74 -0
  40. data/lib/davinci_crd_test_kit/client/v2.0.1/client_order_dispatch_group.rb +111 -0
  41. data/lib/davinci_crd_test_kit/client/v2.0.1/client_order_select_group.rb +116 -0
  42. data/lib/davinci_crd_test_kit/client/v2.0.1/client_order_sign_group.rb +113 -0
  43. data/lib/davinci_crd_test_kit/{client_registration_group.rb → client/v2.0.1/client_registration_group.rb} +12 -8
  44. data/lib/davinci_crd_test_kit/client/v2.0.1/client_urls.rb +13 -0
  45. data/lib/davinci_crd_test_kit/client/v2.0.1/crd_client_suite.rb +134 -0
  46. data/lib/davinci_crd_test_kit/client/v2.0.1/invocation/appointment_book_receive_request_test.rb +129 -0
  47. data/lib/davinci_crd_test_kit/client/v2.0.1/invocation/encounter_discharge_receive_request_test.rb +126 -0
  48. data/lib/davinci_crd_test_kit/client/v2.0.1/invocation/encounter_start_receive_request_test.rb +126 -0
  49. data/lib/davinci_crd_test_kit/client/v2.0.1/invocation/order_dispatch_receive_request_test.rb +138 -0
  50. data/lib/davinci_crd_test_kit/client/v2.0.1/invocation/order_select_receive_request_test.rb +134 -0
  51. data/lib/davinci_crd_test_kit/client/v2.0.1/invocation/order_sign_receive_request_test.rb +136 -0
  52. data/lib/davinci_crd_test_kit/client/v2.0.1/must_support/client_card_must_support_coverage_information.rb +93 -0
  53. data/lib/davinci_crd_test_kit/client/v2.0.1/must_support/client_card_must_support_external_reference.rb +62 -0
  54. data/lib/davinci_crd_test_kit/client/v2.0.1/must_support/client_card_must_support_instructions.rb +62 -0
  55. data/lib/davinci_crd_test_kit/client/v2.0.1/registration/client_registration_verification_test.rb +94 -0
  56. data/lib/davinci_crd_test_kit/client/v2.0.1/registration/client_service_registration_attestation_test.rb +40 -0
  57. data/lib/davinci_crd_test_kit/client/v2.0.1/verify_request/hook_request_fetched_data_test.rb +86 -0
  58. data/lib/davinci_crd_test_kit/client/v2.0.1/verify_request/hook_request_optional_fields_test.rb +63 -0
  59. data/lib/davinci_crd_test_kit/client/v2.0.1/verify_request/hook_request_prefetch_equals_queried_test.rb +96 -0
  60. data/lib/davinci_crd_test_kit/client/v2.0.1/verify_request/hook_request_required_fields_test.rb +55 -0
  61. data/lib/davinci_crd_test_kit/client/v2.0.1/verify_request/hook_request_valid_context_test.rb +70 -0
  62. data/lib/davinci_crd_test_kit/client/v2.0.1/verify_request/hook_request_valid_prefetch_test.rb +62 -0
  63. data/lib/davinci_crd_test_kit/client/v2.0.1/verify_response/client_display_cards_attest.rb +83 -0
  64. data/lib/davinci_crd_test_kit/client/v2.0.1/verify_response/inferno_response_validation.rb +79 -0
  65. data/lib/davinci_crd_test_kit/client/v2.2.1/api/client_coverage_info_update_test.rb +212 -0
  66. data/lib/davinci_crd_test_kit/client/v2.2.1/api/client_crd_update_verification_group.rb +18 -0
  67. data/lib/davinci_crd_test_kit/client/v2.2.1/auth/decode_auth_token_test.rb +69 -0
  68. data/lib/davinci_crd_test_kit/client/v2.2.1/auth/retrieve_jwks_test.rb +120 -0
  69. data/lib/davinci_crd_test_kit/client/v2.2.1/auth/token_header_test.rb +92 -0
  70. data/lib/davinci_crd_test_kit/client/v2.2.1/auth/token_payload_test.rb +93 -0
  71. data/lib/davinci_crd_test_kit/client/v2.2.1/cds-services-prefetch-subset-v221.json +198 -0
  72. data/lib/davinci_crd_test_kit/client/v2.2.1/cds-services-v221.json +202 -0
  73. data/lib/davinci_crd_test_kit/client/v2.2.1/client_appointment_book_group.rb +102 -0
  74. data/lib/davinci_crd_test_kit/client/v2.2.1/client_cross_hook_group.rb +28 -0
  75. data/lib/davinci_crd_test_kit/client/v2.2.1/client_encounter_discharge_group.rb +96 -0
  76. data/lib/davinci_crd_test_kit/client/v2.2.1/client_encounter_start_group.rb +95 -0
  77. data/lib/davinci_crd_test_kit/client/v2.2.1/client_fhir_api_group.rb +88 -0
  78. data/lib/davinci_crd_test_kit/client/v2.2.1/client_hooks_group.rb +64 -0
  79. data/lib/davinci_crd_test_kit/client/v2.2.1/client_long_running_hook_group.rb +32 -0
  80. data/lib/davinci_crd_test_kit/client/v2.2.1/client_order_dispatch_group.rb +101 -0
  81. data/lib/davinci_crd_test_kit/client/v2.2.1/client_order_select_group.rb +102 -0
  82. data/lib/davinci_crd_test_kit/client/v2.2.1/client_order_sign_group.rb +107 -0
  83. data/lib/davinci_crd_test_kit/client/v2.2.1/client_registration_group.rb +27 -0
  84. data/lib/davinci_crd_test_kit/client/v2.2.1/client_urls.rb +27 -0
  85. data/lib/davinci_crd_test_kit/client/v2.2.1/crd_client_suite.rb +229 -0
  86. data/lib/davinci_crd_test_kit/client/v2.2.1/cross_hook/client_card_must_support_coverage_information_test.rb +63 -0
  87. data/lib/davinci_crd_test_kit/client/v2.2.1/cross_hook/client_fhirpath_collection_as_comma_delimited_string_test.rb +60 -0
  88. data/lib/davinci_crd_test_kit/client/v2.2.1/cross_hook/client_hook_instances_unique_test.rb +45 -0
  89. data/lib/davinci_crd_test_kit/client/v2.2.1/cross_hook/client_location_address_propagation_test.rb +135 -0
  90. data/lib/davinci_crd_test_kit/client/v2.2.1/cross_hook/client_prefetch_complete_and_subset_test.rb +103 -0
  91. data/lib/davinci_crd_test_kit/client/v2.2.1/invocation/appointment_book_receive_request_test.rb +156 -0
  92. data/lib/davinci_crd_test_kit/client/v2.2.1/invocation/encounter_discharge_receive_request_test.rb +157 -0
  93. data/lib/davinci_crd_test_kit/client/v2.2.1/invocation/encounter_start_receive_request_test.rb +157 -0
  94. data/lib/davinci_crd_test_kit/client/v2.2.1/invocation/order_dispatch_receive_request_test.rb +165 -0
  95. data/lib/davinci_crd_test_kit/client/v2.2.1/invocation/order_select_receive_request_test.rb +165 -0
  96. data/lib/davinci_crd_test_kit/client/v2.2.1/invocation/order_sign_receive_request_test.rb +165 -0
  97. data/lib/davinci_crd_test_kit/client/v2.2.1/long_running/client_long_running_receive_request_test.rb +64 -0
  98. data/lib/davinci_crd_test_kit/client/v2.2.1/long_running/client_skip_long_running_attestation_test.rb +49 -0
  99. data/lib/davinci_crd_test_kit/client/v2.2.1/registration/client_registration_verification_test.rb +161 -0
  100. data/lib/davinci_crd_test_kit/client/v2.2.1/registration/client_service_registration_attestation_test.rb +107 -0
  101. data/lib/davinci_crd_test_kit/client/v2.2.1/verify_request/hook_request_conformance_test.rb +47 -0
  102. data/lib/davinci_crd_test_kit/client/v2.2.1/verify_request/hook_request_coverage_verification_test.rb +152 -0
  103. data/lib/davinci_crd_test_kit/client/v2.2.1/verify_request/hook_request_data_fetch_verification_test.rb +55 -0
  104. data/lib/davinci_crd_test_kit/client/v2.2.1/verify_request/hook_request_granted_scopes_test.rb +123 -0
  105. data/lib/davinci_crd_test_kit/client/v2.2.1/verify_request/hook_request_prefetch_complete_test.rb +127 -0
  106. data/lib/davinci_crd_test_kit/client/v2.2.1/verify_request/hook_request_prefetch_profiles_test.rb +55 -0
  107. data/lib/davinci_crd_test_kit/client/v2.2.1/verify_request/hook_request_requested_version_test.rb +54 -0
  108. data/lib/davinci_crd_test_kit/client/v2.2.1/verify_request/hook_request_secured_transport_test.rb +48 -0
  109. data/lib/davinci_crd_test_kit/client/v2.2.1/verify_response/client_display_cards_attest.rb +74 -0
  110. data/lib/davinci_crd_test_kit/client/v2.2.1/verify_response/hook_response_support_coverage_information_test.rb +30 -0
  111. data/lib/davinci_crd_test_kit/client/v2.2.1/verify_response/inferno_response_validation.rb +77 -0
  112. data/lib/davinci_crd_test_kit/cross_suite/base_urls.rb +20 -0
  113. data/lib/davinci_crd_test_kit/cross_suite/cards_identification.rb +312 -0
  114. data/lib/davinci_crd_test_kit/{cards_validation.rb → cross_suite/cards_validation.rb} +104 -47
  115. data/lib/davinci_crd_test_kit/cross_suite/coverage-information_stu201_metadata.yml +27 -0
  116. data/lib/davinci_crd_test_kit/cross_suite/coverage-information_stu221_metadata.yml +60 -0
  117. data/lib/davinci_crd_test_kit/cross_suite/fhirpath_on_cds_request.rb +177 -0
  118. data/lib/davinci_crd_test_kit/{hook_request_field_validation.rb → cross_suite/hook_request_field_validation.rb} +282 -203
  119. data/lib/davinci_crd_test_kit/cross_suite/logical_models_override_helper.rb +220 -0
  120. data/lib/davinci_crd_test_kit/cross_suite/prefetch_completeness_checker.rb +462 -0
  121. data/lib/davinci_crd_test_kit/cross_suite/prefetch_contents_validation.rb +81 -0
  122. data/lib/davinci_crd_test_kit/cross_suite/prefetch_profile_validation.rb +48 -0
  123. data/lib/davinci_crd_test_kit/cross_suite/profiles_and_resource_types.rb +63 -0
  124. data/lib/davinci_crd_test_kit/cross_suite/replace_tokens.rb +38 -0
  125. data/lib/davinci_crd_test_kit/cross_suite/requests_logical_model_validation.rb +202 -0
  126. data/lib/davinci_crd_test_kit/cross_suite/response_logical_model_validation.rb +274 -0
  127. data/lib/davinci_crd_test_kit/{suggestion_actions_validation.rb → cross_suite/suggestion_actions_validation.rb} +70 -50
  128. data/lib/davinci_crd_test_kit/cross_suite/tags.rb +42 -0
  129. data/lib/davinci_crd_test_kit/metadata.rb +10 -44
  130. data/lib/davinci_crd_test_kit/requirements/cds-hooks-library_1.0.1_requirements.xlsx +0 -0
  131. data/lib/davinci_crd_test_kit/requirements/cds-hooks_2.0_requirements.xlsx +0 -0
  132. data/lib/davinci_crd_test_kit/requirements/cds-hooks_3.0.0-ballot_requirements.xlsx +0 -0
  133. data/lib/davinci_crd_test_kit/requirements/davinci_crd_test_kit_requirements.csv +742 -65
  134. data/lib/davinci_crd_test_kit/requirements/generated/crd_client_requirements_coverage.csv +279 -54
  135. data/lib/davinci_crd_test_kit/requirements/generated/crd_client_v221_requirements_coverage.csv +1430 -0
  136. data/lib/davinci_crd_test_kit/requirements/generated/crd_server_requirements_coverage.csv +36 -45
  137. data/lib/davinci_crd_test_kit/requirements/generated/crd_server_v221_requirements_coverage.csv +143 -0
  138. data/lib/davinci_crd_test_kit/requirements/hl7.fhir.us.davinci-crd_2.0.1_requirements.xlsx +0 -0
  139. data/lib/davinci_crd_test_kit/requirements/hl7.fhir.us.davinci-crd_2.2.1_requirements.xlsx +0 -0
  140. data/lib/davinci_crd_test_kit/server/endpoints/jwk_set_endpoint_handler.rb +13 -0
  141. data/lib/davinci_crd_test_kit/server/endpoints/mock_ehr/fhir_create_endpoint.rb +23 -0
  142. data/lib/davinci_crd_test_kit/server/endpoints/mock_ehr/fhir_delete_endpoint.rb +30 -0
  143. data/lib/davinci_crd_test_kit/server/endpoints/mock_ehr/fhir_metadata_endpoint.rb +112 -0
  144. data/lib/davinci_crd_test_kit/server/endpoints/mock_ehr/fhir_read_endpoint.rb +21 -0
  145. data/lib/davinci_crd_test_kit/server/endpoints/mock_ehr/fhir_request_handler.rb +261 -0
  146. data/lib/davinci_crd_test_kit/server/endpoints/mock_ehr/fhir_search_endpoint.rb +561 -0
  147. data/lib/davinci_crd_test_kit/server/endpoints/mock_ehr/fhir_update_endpoint.rb +24 -0
  148. data/lib/davinci_crd_test_kit/server/endpoints/mock_ehr/stress-test-Bundle.json +54687 -0
  149. data/lib/davinci_crd_test_kit/server/endpoints/mock_ehr_endpoints.rb +95 -0
  150. data/lib/davinci_crd_test_kit/server/jobs/invoke_hook.rb +225 -0
  151. data/lib/davinci_crd_test_kit/{jwt_helper.rb → server/jwt_helper.rb} +1 -12
  152. data/lib/davinci_crd_test_kit/server/resource_extractor.rb +68 -0
  153. data/lib/davinci_crd_test_kit/server/server_abstract_invoke_hook_test.rb +165 -0
  154. data/lib/davinci_crd_test_kit/server/server_base_urls.rb +30 -0
  155. data/lib/davinci_crd_test_kit/{server_hook_helper.rb → server/server_hook_helper.rb} +1 -1
  156. data/lib/davinci_crd_test_kit/{server_hook_request_validation.rb → server/server_hook_request_validation.rb} +1 -1
  157. data/lib/davinci_crd_test_kit/{test_helper.rb → server/server_test_helper.rb} +7 -3
  158. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_metadata/Appointment.yml +5 -0
  159. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_metadata/ClaimResponse.yml +5 -0
  160. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_metadata/CommunicationRequest.yml +5 -0
  161. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_metadata/Coverage.yml +21 -0
  162. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_metadata/Device.yml +5 -0
  163. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_metadata/DeviceRequest.yml +5 -0
  164. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_metadata/Encounter.yml +7 -0
  165. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_metadata/Location.yml +5 -0
  166. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_metadata/MedicationRequest.yml +5 -0
  167. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_metadata/NutritionOrder.yml +5 -0
  168. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_metadata/Organization.yml +5 -0
  169. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_metadata/Patient.yml +5 -0
  170. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_metadata/Practitioner.yml +5 -0
  171. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_metadata/PractitionerRole.yml +40 -0
  172. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_metadata/ServiceRequest.yml +5 -0
  173. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_metadata/Task.yml +5 -0
  174. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_metadata/VisionPrescription.yml +5 -0
  175. data/lib/davinci_crd_test_kit/server/v2.0.1/crd_server_suite.rb +99 -0
  176. data/lib/davinci_crd_test_kit/server/v2.0.1/discovery/discovery_endpoint_test.rb +90 -0
  177. data/lib/davinci_crd_test_kit/server/v2.0.1/discovery/discovery_services_validation_test.rb +67 -0
  178. data/lib/davinci_crd_test_kit/server/v2.0.1/interaction/server_invoke_hook_test.rb +12 -0
  179. data/lib/davinci_crd_test_kit/server/v2.0.1/must_support/coverage_information_system_action_across_hooks_validation_test.rb +34 -0
  180. data/lib/davinci_crd_test_kit/server/v2.0.1/must_support/external_reference_card_across_hooks_validation_test.rb +30 -0
  181. data/lib/davinci_crd_test_kit/server/v2.0.1/must_support/instructions_card_received_across_hooks_test.rb +27 -0
  182. data/lib/davinci_crd_test_kit/server/v2.0.1/server_appointment_book_group.rb +191 -0
  183. data/lib/davinci_crd_test_kit/server/v2.0.1/server_demonstrate_hook_response_group.rb +93 -0
  184. data/lib/davinci_crd_test_kit/server/v2.0.1/server_discovery_group.rb +62 -0
  185. data/lib/davinci_crd_test_kit/server/v2.0.1/server_encounter_discharge_group.rb +186 -0
  186. data/lib/davinci_crd_test_kit/server/v2.0.1/server_encounter_start_group.rb +186 -0
  187. data/lib/davinci_crd_test_kit/server/v2.0.1/server_hooks_group.rb +73 -0
  188. data/lib/davinci_crd_test_kit/server/v2.0.1/server_order_dispatch_group.rb +191 -0
  189. data/lib/davinci_crd_test_kit/server/v2.0.1/server_order_select_group.rb +211 -0
  190. data/lib/davinci_crd_test_kit/server/v2.0.1/server_order_sign_group.rb +216 -0
  191. data/lib/davinci_crd_test_kit/server/v2.0.1/server_required_card_response_validation_group.rb +28 -0
  192. data/lib/davinci_crd_test_kit/server/v2.0.1/server_urls.rb +13 -0
  193. data/lib/davinci_crd_test_kit/server/v2.0.1/verify_request/service_request_context_validation_test.rb +30 -0
  194. data/lib/davinci_crd_test_kit/server/v2.0.1/verify_request/service_request_optional_fields_validation_test.rb +39 -0
  195. data/lib/davinci_crd_test_kit/server/v2.0.1/verify_request/service_request_required_fields_validation_test.rb +40 -0
  196. data/lib/davinci_crd_test_kit/server/v2.0.1/verify_response/additional_orders_validation_test.rb +59 -0
  197. data/lib/davinci_crd_test_kit/server/v2.0.1/verify_response/card_optional_fields_validation_test.rb +51 -0
  198. data/lib/davinci_crd_test_kit/server/v2.0.1/verify_response/coverage_information_system_action_received_test.rb +65 -0
  199. data/lib/davinci_crd_test_kit/server/v2.0.1/verify_response/coverage_information_system_action_validation_test.rb +120 -0
  200. data/lib/davinci_crd_test_kit/server/v2.0.1/verify_response/create_or_update_coverage_info_response_validation_test.rb +70 -0
  201. data/lib/davinci_crd_test_kit/server/v2.0.1/verify_response/external_reference_card_validation_test.rb +37 -0
  202. data/lib/davinci_crd_test_kit/server/v2.0.1/verify_response/form_completion_response_validation_test.rb +67 -0
  203. data/lib/davinci_crd_test_kit/server/v2.0.1/verify_response/instructions_card_received_test.rb +30 -0
  204. data/lib/davinci_crd_test_kit/server/v2.0.1/verify_response/launch_smart_app_card_validation_test.rb +39 -0
  205. data/lib/davinci_crd_test_kit/server/v2.0.1/verify_response/propose_alternate_request_card_validation_test.rb +46 -0
  206. data/lib/davinci_crd_test_kit/server/v2.0.1/verify_response/service_response_validation_test.rb +83 -0
  207. data/lib/davinci_crd_test_kit/server/v2.2.1/crd_metadata/Appointment_withorder.yml +5 -0
  208. data/lib/davinci_crd_test_kit/server/v2.2.1/crd_metadata/Appointment_withoutorder.yml +5 -0
  209. data/lib/davinci_crd_test_kit/server/v2.2.1/crd_metadata/CommunicationRequest.yml +5 -0
  210. data/lib/davinci_crd_test_kit/server/v2.2.1/crd_metadata/Coverage.yml +5 -0
  211. data/lib/davinci_crd_test_kit/server/v2.2.1/crd_metadata/Device.yml +5 -0
  212. data/lib/davinci_crd_test_kit/server/v2.2.1/crd_metadata/DeviceRequest.yml +5 -0
  213. data/lib/davinci_crd_test_kit/server/v2.2.1/crd_metadata/Encounter.yml +5 -0
  214. data/lib/davinci_crd_test_kit/server/v2.2.1/crd_metadata/Location.yml +5 -0
  215. data/lib/davinci_crd_test_kit/server/v2.2.1/crd_metadata/MedicationRequest.yml +5 -0
  216. data/lib/davinci_crd_test_kit/server/v2.2.1/crd_metadata/NutritionOrder.yml +5 -0
  217. data/lib/davinci_crd_test_kit/server/v2.2.1/crd_metadata/Organization.yml +5 -0
  218. data/lib/davinci_crd_test_kit/server/v2.2.1/crd_metadata/Patient.yml +5 -0
  219. data/lib/davinci_crd_test_kit/server/v2.2.1/crd_metadata/PractitionerRole.yml +5 -0
  220. data/lib/davinci_crd_test_kit/server/v2.2.1/crd_metadata/ServiceRequest.yml +5 -0
  221. data/lib/davinci_crd_test_kit/server/v2.2.1/crd_metadata/VisionPrescription.yml +5 -0
  222. data/lib/davinci_crd_test_kit/server/v2.2.1/crd_server_suite.rb +115 -0
  223. data/lib/davinci_crd_test_kit/server/v2.2.1/discovery/discovery_configuration_test.rb +159 -0
  224. data/lib/davinci_crd_test_kit/server/v2.2.1/discovery/discovery_endpoint_test.rb +90 -0
  225. data/lib/davinci_crd_test_kit/server/v2.2.1/discovery/discovery_prefetch_support_test.rb +43 -0
  226. data/lib/davinci_crd_test_kit/server/v2.2.1/discovery/discovery_services_validation_test.rb +121 -0
  227. data/lib/davinci_crd_test_kit/server/v2.2.1/interaction/server_invoke_hook_test.rb +17 -0
  228. data/lib/davinci_crd_test_kit/server/v2.2.1/must_support/coverage_information_must_support_test.rb +71 -0
  229. data/lib/davinci_crd_test_kit/server/v2.2.1/must_support/coverage_information_system_action_across_hooks_validation_test.rb +36 -0
  230. data/lib/davinci_crd_test_kit/server/v2.2.1/must_support/supported_us_core_versions_test.rb +118 -0
  231. data/lib/davinci_crd_test_kit/server/v2.2.1/server_appointment_book_group.rb +213 -0
  232. data/lib/davinci_crd_test_kit/server/v2.2.1/server_demonstrate_hook_response_group.rb +93 -0
  233. data/lib/davinci_crd_test_kit/server/v2.2.1/server_discovery_group.rb +69 -0
  234. data/lib/davinci_crd_test_kit/server/v2.2.1/server_encounter_discharge_group.rb +194 -0
  235. data/lib/davinci_crd_test_kit/server/v2.2.1/server_encounter_start_group.rb +194 -0
  236. data/lib/davinci_crd_test_kit/server/v2.2.1/server_hooks_group.rb +73 -0
  237. data/lib/davinci_crd_test_kit/server/v2.2.1/server_order_dispatch_group.rb +214 -0
  238. data/lib/davinci_crd_test_kit/server/v2.2.1/server_order_select_group.rb +219 -0
  239. data/lib/davinci_crd_test_kit/server/v2.2.1/server_order_sign_group.rb +241 -0
  240. data/lib/davinci_crd_test_kit/server/v2.2.1/server_required_card_response_validation_group.rb +30 -0
  241. data/lib/davinci_crd_test_kit/server/v2.2.1/server_urls.rb +13 -0
  242. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_request/service_request_context_validation_test.rb +30 -0
  243. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_request/service_request_no_custom_extensions_test.rb +120 -0
  244. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_request/service_request_optional_fields_validation_test.rb +39 -0
  245. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_request/service_request_required_fields_validation_test.rb +40 -0
  246. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/additional_orders_validation_test.rb +66 -0
  247. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/all_responses_include_coverage_information_test.rb +123 -0
  248. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/card_optional_fields_validation_test.rb +57 -0
  249. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/coverage_info_configuration_test.rb +83 -0
  250. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/coverage_information_system_action_received_test.rb +65 -0
  251. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/coverage_information_system_action_validation_test.rb +184 -0
  252. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/create_or_update_coverage_info_response_validation_test.rb +75 -0
  253. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/external_reference_card_validation_test.rb +47 -0
  254. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/form_completion_response_validation_test.rb +91 -0
  255. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/hook_request_resource_resolution.rb +137 -0
  256. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/instructions_card_received_test.rb +32 -0
  257. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/launch_smart_app_card_validation_test.rb +49 -0
  258. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/order_dispatch_coverage_information_test.rb +38 -0
  259. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/propose_alternate_request_card_validation_test.rb +54 -0
  260. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/service_response_validation_test.rb +97 -0
  261. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/unknown_cds_hooks_elements_test.rb +78 -0
  262. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/unknown_configuration_test.rb +78 -0
  263. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/unknown_context_test.rb +78 -0
  264. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/verify_response_without_billing_options_test.rb +43 -0
  265. data/lib/davinci_crd_test_kit/server/v2.2.1/verify_response/verify_response_without_configuration_test.rb +44 -0
  266. data/lib/davinci_crd_test_kit/version.rb +2 -2
  267. data/lib/davinci_crd_test_kit.rb +4 -2
  268. metadata +297 -93
  269. data/lib/davinci_crd_test_kit/client_fhir_api_group.rb +0 -785
  270. data/lib/davinci_crd_test_kit/client_hooks_group.rb +0 -74
  271. data/lib/davinci_crd_test_kit/client_tests/appointment_book_receive_request_test.rb +0 -93
  272. data/lib/davinci_crd_test_kit/client_tests/client_appointment_book_group.rb +0 -75
  273. data/lib/davinci_crd_test_kit/client_tests/client_display_cards_attest.rb +0 -48
  274. data/lib/davinci_crd_test_kit/client_tests/client_encounter_discharge_group.rb +0 -73
  275. data/lib/davinci_crd_test_kit/client_tests/client_encounter_start_group.rb +0 -73
  276. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_create_test.rb +0 -42
  277. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_read_test.rb +0 -40
  278. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_search_test.rb +0 -232
  279. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_update_test.rb +0 -42
  280. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_validation_test.rb +0 -61
  281. data/lib/davinci_crd_test_kit/client_tests/client_order_dispatch_group.rb +0 -79
  282. data/lib/davinci_crd_test_kit/client_tests/client_order_select_group.rb +0 -82
  283. data/lib/davinci_crd_test_kit/client_tests/client_order_sign_group.rb +0 -81
  284. data/lib/davinci_crd_test_kit/client_tests/client_registration_verification_test.rb +0 -88
  285. data/lib/davinci_crd_test_kit/client_tests/decode_auth_token_test.rb +0 -60
  286. data/lib/davinci_crd_test_kit/client_tests/encounter_discharge_receive_request_test.rb +0 -90
  287. data/lib/davinci_crd_test_kit/client_tests/encounter_start_receive_request_test.rb +0 -90
  288. data/lib/davinci_crd_test_kit/client_tests/hook_request_optional_fields_test.rb +0 -57
  289. data/lib/davinci_crd_test_kit/client_tests/hook_request_required_fields_test.rb +0 -49
  290. data/lib/davinci_crd_test_kit/client_tests/hook_request_valid_context_test.rb +0 -68
  291. data/lib/davinci_crd_test_kit/client_tests/hook_request_valid_prefetch_test.rb +0 -69
  292. data/lib/davinci_crd_test_kit/client_tests/order_dispatch_receive_request_test.rb +0 -102
  293. data/lib/davinci_crd_test_kit/client_tests/order_select_receive_request_test.rb +0 -98
  294. data/lib/davinci_crd_test_kit/client_tests/order_sign_receive_request_test.rb +0 -101
  295. data/lib/davinci_crd_test_kit/client_tests/retrieve_jwks_test.rb +0 -105
  296. data/lib/davinci_crd_test_kit/client_tests/submitted_response_validation.rb +0 -48
  297. data/lib/davinci_crd_test_kit/client_tests/token_header_test.rb +0 -65
  298. data/lib/davinci_crd_test_kit/client_tests/token_payload_test.rb +0 -78
  299. data/lib/davinci_crd_test_kit/crd_client_suite.rb +0 -193
  300. data/lib/davinci_crd_test_kit/crd_options.rb +0 -9
  301. data/lib/davinci_crd_test_kit/crd_server_suite.rb +0 -125
  302. data/lib/davinci_crd_test_kit/igs/davinci-crd-2.0.1.tgz +0 -0
  303. data/lib/davinci_crd_test_kit/routes/cds_services_discovery_handler.rb +0 -18
  304. data/lib/davinci_crd_test_kit/routes/hook_request_endpoint.rb +0 -77
  305. data/lib/davinci_crd_test_kit/routes/jwk_set_endpoint_handler.rb +0 -15
  306. data/lib/davinci_crd_test_kit/server_appointment_book_group.rb +0 -176
  307. data/lib/davinci_crd_test_kit/server_demonstrate_hook_response_group.rb +0 -77
  308. data/lib/davinci_crd_test_kit/server_discovery_group.rb +0 -60
  309. data/lib/davinci_crd_test_kit/server_encounter_discharge_group.rb +0 -170
  310. data/lib/davinci_crd_test_kit/server_encounter_start_group.rb +0 -170
  311. data/lib/davinci_crd_test_kit/server_hooks_group.rb +0 -71
  312. data/lib/davinci_crd_test_kit/server_order_dispatch_group.rb +0 -176
  313. data/lib/davinci_crd_test_kit/server_order_select_group.rb +0 -195
  314. data/lib/davinci_crd_test_kit/server_order_sign_group.rb +0 -201
  315. data/lib/davinci_crd_test_kit/server_required_card_response_validation_group.rb +0 -26
  316. data/lib/davinci_crd_test_kit/server_tests/additional_orders_validation_test.rb +0 -68
  317. data/lib/davinci_crd_test_kit/server_tests/card_optional_fields_validation_test.rb +0 -47
  318. data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_across_hooks_validation_test.rb +0 -32
  319. data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_received_test.rb +0 -63
  320. data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_validation_test.rb +0 -118
  321. data/lib/davinci_crd_test_kit/server_tests/create_or_update_coverage_info_response_validation_test.rb +0 -71
  322. data/lib/davinci_crd_test_kit/server_tests/discovery_endpoint_test.rb +0 -88
  323. data/lib/davinci_crd_test_kit/server_tests/discovery_services_validation_test.rb +0 -65
  324. data/lib/davinci_crd_test_kit/server_tests/external_reference_card_across_hooks_validation_test.rb +0 -28
  325. data/lib/davinci_crd_test_kit/server_tests/external_reference_card_validation_test.rb +0 -36
  326. data/lib/davinci_crd_test_kit/server_tests/form_completion_response_validation_test.rb +0 -78
  327. data/lib/davinci_crd_test_kit/server_tests/instructions_card_received_across_hooks_test.rb +0 -25
  328. data/lib/davinci_crd_test_kit/server_tests/instructions_card_received_test.rb +0 -26
  329. data/lib/davinci_crd_test_kit/server_tests/launch_smart_app_card_validation_test.rb +0 -38
  330. data/lib/davinci_crd_test_kit/server_tests/propose_alternate_request_card_validation_test.rb +0 -63
  331. data/lib/davinci_crd_test_kit/server_tests/service_call_test.rb +0 -101
  332. data/lib/davinci_crd_test_kit/server_tests/service_request_context_validation_test.rb +0 -28
  333. data/lib/davinci_crd_test_kit/server_tests/service_request_optional_fields_validation_test.rb +0 -37
  334. data/lib/davinci_crd_test_kit/server_tests/service_request_required_fields_validation_test.rb +0 -38
  335. data/lib/davinci_crd_test_kit/server_tests/service_response_validation_test.rb +0 -81
  336. data/lib/davinci_crd_test_kit/tags.rb +0 -10
  337. data/lib/davinci_crd_test_kit/urls.rb +0 -52
  338. /data/lib/davinci_crd_test_kit/{card_responses → client/endpoints/mocked_card_responses}/external_reference.json +0 -0
  339. /data/lib/davinci_crd_test_kit/{card_responses → client/endpoints/mocked_card_responses}/instructions.json +0 -0
  340. /data/lib/davinci_crd_test_kit/{crd_jwks.json → server/endpoints/crd_jwks.json} +0 -0
  341. /data/lib/davinci_crd_test_kit/{jwks.rb → server/endpoints/jwks.rb} +0 -0
@@ -0,0 +1,220 @@
1
+ require_relative 'profiles_and_resource_types'
2
+
3
+ module DaVinciCRDTestKit
4
+ module LogicalModelsOverrideHelper
5
+ # -------------------------------------------------------------------------
6
+ # Clean up messages returned from logical model validation
7
+ # -------------------------------------------------------------------------
8
+
9
+ def reject_resource_issues(issues)
10
+ issues.reject do |issue|
11
+ issue.location&.match?(%r{/\*[A-Za-z]+/}) || # looking for /*<resourceType>/
12
+ issue.message&.match(/.resource: Unable to find a match for the specified profile among choices/)
13
+ end
14
+ end
15
+
16
+ # -------------------------------------------------------------------------
17
+ # Check resource conformance outside the logical models
18
+ # -------------------------------------------------------------------------
19
+
20
+ def check_resource_conformance_to_coverage_profile(resource_hash, error_prefix, ig_semver)
21
+ check_resource_type_and_validate(resource_hash, error_prefix, ig_semver, FHIR::Coverage)
22
+ end
23
+
24
+ def check_resource_conformance_to_questionnaire_task_profile(resource_hash, error_prefix, ig_semver)
25
+ check_resource_type_and_validate(resource_hash, error_prefix, ig_semver, FHIR::Task)
26
+ end
27
+
28
+ def check_resource_conformance_to_order_or_encounter_profile(resource_hash, request_body, error_prefix, ig_semver)
29
+ check_order_like_resource_conformance(resource_hash, request_body, error_prefix, ig_semver,
30
+ allowed_types: ProfilesAndResourceTypes::ORDER_OR_ENCOUNTER_RESOURCE_CLASSES,
31
+ disallowed_message: 'is not allowed as a target ' \
32
+ 'for a coverage-information action')
33
+ end
34
+
35
+ def check_resource_conformance_to_order_profile(resource_hash, request_body, error_prefix, ig_semver)
36
+ check_order_like_resource_conformance(resource_hash, request_body, error_prefix, ig_semver,
37
+ allowed_types: ProfilesAndResourceTypes::ORDER_RESOURCE_CLASSES,
38
+ disallowed_message: 'is not allowed for CRD orders')
39
+ end
40
+
41
+ # -------------------------------------------------------------------------
42
+ # Resource conformance helpers
43
+ # -------------------------------------------------------------------------
44
+
45
+ def parse_action_resource(resource_hash, error_prefix)
46
+ resource = FHIR.from_contents(resource_hash.to_json)
47
+ unless resource.present?
48
+ add_message('error', "#{error_prefix}resource is not FHIR.")
49
+ return
50
+ end
51
+ yield resource
52
+ end
53
+
54
+ def check_resource_type_and_validate(resource_hash, error_prefix, ig_semver, expected_class)
55
+ parse_action_resource(resource_hash, error_prefix) do |resource|
56
+ if resource.is_a?(expected_class)
57
+ resource_is_valid?(resource:, profile_url: structure_definition_map(ig_semver)[resource.resourceType],
58
+ message_prefix: error_prefix)
59
+ else
60
+ add_message('error', "#{error_prefix}found resource type '#{resource.resourceType}' " \
61
+ "expected '#{expected_class.name.split('::').last}'.")
62
+ end
63
+ end
64
+ end
65
+
66
+ def check_order_like_resource_conformance(resource_hash, request_body, error_prefix, ig_semver,
67
+ allowed_types:, disallowed_message:)
68
+ parse_action_resource(resource_hash, error_prefix) do |resource|
69
+ case resource
70
+ when FHIR::Appointment
71
+ check_appointment_conformance(resource, request_body, error_prefix, ig_semver)
72
+ when *allowed_types
73
+ resource_is_valid?(resource:, profile_url: structure_definition_map(ig_semver)[resource.resourceType],
74
+ message_prefix: error_prefix)
75
+ else
76
+ add_message('error', "#{error_prefix}resource type '#{resource.resourceType}' #{disallowed_message}.")
77
+ end
78
+ end
79
+ end
80
+
81
+ # -------------------------------------------------------------------------
82
+ # Appointment conformance (requires extra help to decide profile and check profile-based slicing)
83
+ # -------------------------------------------------------------------------
84
+
85
+ def check_appointment_conformance(appointment, request_body, error_prefix, ig_semver)
86
+ target_appointment_profile =
87
+ if appointment.basedOn.present?
88
+ 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-appointment-with-order'
89
+ else
90
+ 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-appointment-no-order'
91
+ end
92
+
93
+ validation_issues = []
94
+ resource_is_valid?(resource: appointment, profile_url: "#{target_appointment_profile}|#{ig_semver}",
95
+ add_messages_to_runnable: false, validator_response_details: validation_issues)
96
+
97
+ manually_check_appointment_validation_errors(validation_issues, appointment, request_body)
98
+ .each do |issue|
99
+ add_message(issue.severity, "#{error_prefix}#{issue.message}")
100
+ end
101
+ end
102
+
103
+ def manually_check_appointment_validation_errors(validation_issues, appointment, request_body)
104
+ @matched_participant_slice_indexes = []
105
+ validation_issues.reverse.reject do |issue|
106
+ issue.filtered ||
107
+ resolved_participant_primary_performer_slice_issue?(issue, appointment) ||
108
+ resolved_participant_patient_slice_issue?(issue, appointment, request_body) ||
109
+ (
110
+ # list reversed to hit these issues after the slice matching
111
+ @matched_participant_slice_indexes.present? &&
112
+ issue.message.match(/Appointment\.participant\[#{Regexp.union(@matched_participant_slice_indexes.map(&:to_s))}\]: This element does not match any known slice defined in the profile/) # rubocop:disable Layout/LineLength
113
+ )
114
+ end.reverse
115
+ end
116
+
117
+ def resolved_participant_primary_performer_slice_issue?(issue, appointment)
118
+ return false unless issue.message.match(/Slice 'Appointment.participant:PrimaryPerformer': a matching slice is required, but not found/) # rubocop:disable Layout/LineLength
119
+
120
+ appointment.participant.each_with_index.any? do |participant, index|
121
+ # type + profile of the reference checked during prefetch checking
122
+ match = participant.actor.present? &&
123
+ participant.actor.reference.present? &&
124
+ primary_performer_type?(participant.type)
125
+ @matched_participant_slice_indexes << index if match
126
+ match
127
+ end
128
+ end
129
+
130
+ def primary_performer_type?(types)
131
+ types&.any? do |type|
132
+ type.coding&.any? do |coding|
133
+ coding.code == 'PPRF' &&
134
+ coding.system == 'http://terminology.hl7.org/CodeSystem/v3-ParticipationType'
135
+ end
136
+ end
137
+ end
138
+
139
+ # NOTE: for simplicity and to avoid duplication of checks, this looks for
140
+ # a particular patient reference from the context.patientId,
141
+ # the profile of which will be verified during prefetch profile checking
142
+ def resolved_participant_patient_slice_issue?(issue, appointment, request_body)
143
+ return false unless issue.message.match(/Slice 'Appointment.participant:Patient': a matching slice is required, but not found/) # rubocop:disable Layout/LineLength
144
+
145
+ local_patient_ref = "Patient/#{request_body.dig('context', 'patientId')}"
146
+ absolute_patient_ref = "#{request_body['fhirServer'].chomp('/')}/#{local_patient_ref}"
147
+ appointment.participant.each_with_index.any? do |participant, index|
148
+ match = [local_patient_ref, absolute_patient_ref].include?(participant.actor&.reference)
149
+ @matched_participant_slice_indexes << index if match
150
+ match
151
+ end
152
+ end
153
+
154
+ # -------------------------------------------------------------------------
155
+ # Local/Relative reference helpers
156
+ # -------------------------------------------------------------------------
157
+
158
+ def local_reference?(value, error_prefix, allowed_resource_types: nil)
159
+ is_local_reference = true
160
+ local_reference_match = value.match(%r{^([A-Za-z]+)/(.+)$})
161
+ if local_reference_match.present?
162
+ resource_type = local_reference_match[1]
163
+ id = local_reference_match[2]
164
+
165
+ unless allowed_resource_type?(resource_type, allowed_resource_types)
166
+ allowed_types_error_suffix =
167
+ if allowed_resource_types.nil?
168
+ 'a valid FHIR resource type.'
169
+ else
170
+ "one of the allowed resource types (#{allowed_resource_types.join(', ')})"
171
+ end
172
+
173
+ add_message('error',
174
+ "#{error_prefix} local reference resourceType '#{resource_type}' " \
175
+ "is not #{allowed_types_error_suffix}.")
176
+ is_local_reference = false
177
+ end
178
+ unless id.match(/\A[A-Za-z0-9\-.]{1,64}\z/)
179
+ add_message('error',
180
+ "#{error_prefix} local reference id '#{id}' does not meet " \
181
+ '[FHIR id data type](https://hl7.org/fhir/R4/datatypes.html#id) requirements.')
182
+ is_local_reference = false
183
+ end
184
+ else
185
+ add_message('error', "#{error_prefix} expected a local reference, got '#{value}'.")
186
+ is_local_reference = false
187
+ end
188
+
189
+ is_local_reference
190
+ end
191
+
192
+ def allowed_resource_type?(resource_type, allowed_resource_types)
193
+ if allowed_resource_types.nil?
194
+ FHIR::RESOURCES.include?(resource_type)
195
+ else
196
+ allowed_resource_types.include?(resource_type)
197
+ end
198
+ end
199
+
200
+ def referenced_resource_present_in_bundle?(bundle, local_reference, error_prefix, bundle_location)
201
+ unless bundle.present? && bundle['entry'].present?
202
+ add_message('error',
203
+ "#{error_prefix} referenced resource '#{local_reference}' " \
204
+ "not found in the #{bundle_location} Bundle.")
205
+ return false
206
+ end
207
+
208
+ target_resource_type, target_id = local_reference.split('/')
209
+ found = bundle['entry'].any? do |entry|
210
+ entry.dig('resource', 'resourceType') == target_resource_type && entry.dig('resource', 'id') == target_id
211
+ end
212
+ return true if found
213
+
214
+ add_message('error',
215
+ "#{error_prefix} referenced resource '#{local_reference}' " \
216
+ "not found in the #{bundle_location} Bundle.")
217
+ false
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,462 @@
1
+ require_relative 'fhirpath_on_cds_request'
2
+ require_relative 'replace_tokens'
3
+ require 'uri'
4
+
5
+ module DaVinciCRDTestKit
6
+ # -----------------------------------------------------------------------
7
+ # Prefetch Check Helper Class
8
+ # -----------------------------------------------------------------------
9
+ class PrefetchCompletenessChecker
10
+ include FhirpathOnCDSRequest
11
+ include ReplaceTokens
12
+
13
+ attr_accessor :hook_request, :request_index, :services_file_path,
14
+ :instantiated_prefetch_templates,
15
+ :observed_fhirpath_collection_as_comma_delimited_string
16
+
17
+ def initialize(hook_request, request_index, services_file_path)
18
+ @hook_request = hook_request
19
+ @request_index = request_index
20
+ @services_file_path = services_file_path
21
+ @observed_fhirpath_collection_as_comma_delimited_string = false
22
+ @instantiated_prefetch_templates = {}
23
+ extract_prefetched_resources
24
+ end
25
+
26
+ def check_prefetched_data
27
+ return ["#{request_error_prefix} No prefetch data provided."] unless hook_request.key?('prefetch')
28
+
29
+ hook_prefetch_templates.each do |prefetch_key, prefetch_request|
30
+ check_prefetch_template(prefetch_key, prefetch_request)
31
+ end
32
+
33
+ hook_request['prefetch'].each_key do |prefetch_template|
34
+ next if hook_prefetch_templates.key?(prefetch_template)
35
+
36
+ errors << "#{request_error_prefix} Extra prefetch data " \
37
+ "provided in unrequested template '#{prefetch_template}'."
38
+ end
39
+
40
+ errors.uniq
41
+ end
42
+
43
+ # -----------------------------------------------------------------------
44
+ # Complete vs Subset Prefetch Difference Checking
45
+ # -----------------------------------------------------------------------
46
+
47
+ def data_set_different_with_alternate_service?(alternate_services_file_path, compare_key_map)
48
+ PrefetchCompletenessChecker.new(hook_request, request_index, alternate_services_file_path)
49
+ .data_sets_different?(self, compare_key_map)
50
+ end
51
+
52
+ # Precondition: the original checker passed without errors meaning the expected set was present
53
+ # handles both subset < complete and complete > subset checks via two checks.
54
+ # - differences in the calculated id set for `_id` searches signal differences in the data sets.
55
+ # - errors during instantiation signal that a resource in the set wasn't present. Since the original
56
+ # set was complete, this will only happen if the original set was a subset and the complete set
57
+ # includes more data, meaning that the sets are different. (NOTE: triggers an error rather than a different
58
+ # set of ids because fhirpath like `...resolve().id` requires the resource be present to evaluate it)
59
+ def data_sets_different?(original_service_checker, compare_key_map)
60
+ template_key_map = compare_key_map.transform_keys { |key| "%#{key}." }.transform_values { |value| "%#{value}." }
61
+ hook_prefetch_templates.each do |prefetch_key, prefetch_request|
62
+ data_set_different = data_set_different?(original_service_checker,
63
+ compare_key_map,
64
+ template_key_map,
65
+ prefetch_key,
66
+ prefetch_request)
67
+
68
+ return true if data_set_different
69
+ end
70
+
71
+ false
72
+ end
73
+
74
+ private
75
+
76
+ def data_set_different?(original_service_checker, compare_key_map, template_key_map, prefetch_key, prefetch_request)
77
+ return false if ['patient', 'pat', 'encounter', 'enc', 'coverage', 'cov'].include?(prefetch_key)
78
+
79
+ mapped_prefetch_request = prefetch_request.gsub(/%[a-zA-Z]+\./, template_key_map)
80
+ instantiated_request = instantiate_template(prefetch_key, mapped_prefetch_request)
81
+ return true if errors.present? # fetch errors - there was more data to include for complete (complete > subset)
82
+
83
+ compare_prefetch_key = compare_key_map.key?(prefetch_key) ? compare_key_map[prefetch_key] : prefetch_key
84
+ original_instantiated_request = original_service_checker.instantiated_prefetch_templates[compare_prefetch_key]
85
+ # don't try to compare when the original didn't have this key
86
+ return false unless original_instantiated_request.present?
87
+
88
+ _, alternate_ids = resource_type_and_ids_from_search(instantiated_request)
89
+ _, original_ids = resource_type_and_ids_from_search(original_instantiated_request)
90
+
91
+ # missing ids - the subset requested strictly less data (subset < complete)
92
+ alternate_ids != original_ids
93
+ end
94
+
95
+ # -----------------------------------------------------------------------
96
+ # Errors to return
97
+ # -----------------------------------------------------------------------
98
+ def errors
99
+ @errors ||= []
100
+ end
101
+
102
+ def request_error_prefix
103
+ "(Request #{request_index + 1})"
104
+ end
105
+
106
+ def error_prefix
107
+ "#{request_error_prefix} Prefetch Template #{@current_prefetch_key} -"
108
+ end
109
+
110
+ # -----------------------------------------------------------------------
111
+ # Requested Prefetch Templates
112
+ # -----------------------------------------------------------------------
113
+ def hook_prefetch_templates
114
+ @hook_prefetch_templates ||=
115
+ JSON.parse(File.read(services_file_path))['services'].find do |service|
116
+ service['hook'] == hook_request['hook']
117
+ end['prefetch']
118
+ end
119
+
120
+ # -----------------------------------------------------------------------
121
+ # Instantiated Prefetch Templates
122
+ # -----------------------------------------------------------------------
123
+ def instantiate_template(prefetch_key, prefetch_request)
124
+ instantiated_request = replace_tokens_in_string(prefetch_request.dup, hook_request)
125
+ instantiated_prefetch_templates[prefetch_key] = instantiated_request
126
+ instantiated_request
127
+ end
128
+
129
+ # -----------------------------------------------------------------------
130
+ # Check of actual prefetch against an instantiated request
131
+ # -----------------------------------------------------------------------
132
+ def check_prefetch_template(prefetch_key, prefetch_request)
133
+ @current_prefetch_key = prefetch_key
134
+ instantiated_request = instantiate_template(prefetch_key, prefetch_request)
135
+ if demonstrates_collection_as_comma_delimited_string?(prefetch_request, instantiated_request)
136
+ @observed_fhirpath_collection_as_comma_delimited_string = true
137
+ end
138
+ unless hook_request['prefetch'].key?(prefetch_key)
139
+ errors << "#{error_prefix} No prefetch data provided."
140
+ return
141
+ end
142
+ check_provided_against_request(hook_request['prefetch'][prefetch_key], instantiated_request)
143
+ rescue FhirpathServiceError => e
144
+ raise "#{error_prefix} FHIRPath service error while evaluating prefetch template. " \
145
+ "This indicates an implementation problem in Inferno - please log a ticket. Details: #{e.message}"
146
+ end
147
+
148
+ def check_provided_against_request(prefetched_value, instantiated_request)
149
+ if instantiated_request.include?('?')
150
+ if id_search?(instantiated_request)
151
+ check_id_search(prefetched_value, instantiated_request)
152
+ elsif instantiated_request.starts_with?('Coverage')
153
+ check_coverage_search(prefetched_value, instantiated_request)
154
+ else
155
+ raise "#{error_prefix} Unexpected search template '#{instantiated_request}'. " \
156
+ 'This indicates an implementation problem in the test kit — please log a ticket.'
157
+ end
158
+ else
159
+ check_read(prefetched_value, instantiated_request)
160
+ end
161
+ end
162
+
163
+ def id_search?(request_string)
164
+ resource_type = request_string.split('?').first
165
+ request_string.starts_with?("#{resource_type}?_id=")
166
+ end
167
+
168
+ def check_coverage_search(prefetched_value, _instantiated_request)
169
+ unless prefetched_value.present?
170
+ errors << "#{error_prefix} requested Coverage not provided."
171
+ return
172
+ end
173
+
174
+ check_is_fhir_resource(prefetched_value, target_resource_type: 'Bundle')
175
+ unless prefetched_value['entry'].size == 1
176
+ errors << "#{error_prefix} exactly one Coverage must be provided."
177
+ return
178
+ end
179
+
180
+ check_coverage(prefetched_value.dig('entry', 0, 'resource'))
181
+ end
182
+
183
+ def check_coverage(prefetched_coverage)
184
+ unless prefetched_coverage['resourceType'].present?
185
+ errors << "#{error_prefix} entry in prefetched Coverage Bundle is not a FHIR resource (no resourceType)."
186
+ return
187
+ end
188
+
189
+ unless prefetched_coverage['resourceType'] == 'Coverage'
190
+ errors << "#{error_prefix} entry in prefetched Coverage Bundle has an unexpected type: " \
191
+ "expected Coverage, got #{prefetched_coverage['resourceType']}."
192
+ return
193
+ end
194
+
195
+ unless prefetched_coverage['status'] == 'active'
196
+ errors << "#{error_prefix} prefetched Coverage has an unexpected status: " \
197
+ "expected active, got #{prefetched_coverage['status']}."
198
+ end
199
+
200
+ target_patient_id = hook_request.dig('context', 'patientId')
201
+ unless prefetched_coverage.dig('beneficiary', 'reference') == "Patient/#{target_patient_id}"
202
+ errors << "#{error_prefix} prefetched Coverage has an unexpected beneficiary reference: " \
203
+ "expected Patient/#{target_patient_id}, got #{prefetched_coverage.dig('beneficiary',
204
+ 'reference')}."
205
+ end
206
+
207
+ nil
208
+ end
209
+
210
+ def check_read(prefetched_value, instantiated_request)
211
+ resource_type, resource_id = instantiated_request.split('/')
212
+
213
+ resource_requested = resource_id.present?
214
+
215
+ unless prefetched_value.present?
216
+ if resource_requested
217
+ errors << "#{error_prefix} requested resource '#{full_url_for_target_id(instantiated_request)}' not provided."
218
+ end
219
+ return
220
+ end
221
+
222
+ check_is_fhir_resource(prefetched_value, target_resource_type: resource_type)
223
+ unless prefetched_value.key?('id')
224
+ errors << "#{error_prefix} prefetched #{resource_type} is missing an id."
225
+ return
226
+ end
227
+ unless prefetched_value['id'] == resource_id
228
+ errors << "#{error_prefix} prefetched #{resource_type} has unexpected id: " \
229
+ "expected #{resource_id}, got #{prefetched_value['id']}."
230
+ end
231
+
232
+ nil
233
+ end
234
+
235
+ def resource_type_and_ids_from_search(instantiated_search)
236
+ resource_type, id_list = instantiated_search.split('?_id=')
237
+ target_ids = id_list.present? ? id_list.split(',').map { |id| "#{resource_type}/#{id}" }.uniq.sort : []
238
+
239
+ [resource_type, target_ids]
240
+ end
241
+
242
+ def check_id_search(prefetched_value, instantiated_request)
243
+ resource_type, target_ids = resource_type_and_ids_from_search(instantiated_request)
244
+ resources_requested = target_ids.present?
245
+
246
+ unless prefetched_value.present?
247
+ if resources_requested
248
+ errors << "#{error_prefix} requested resources not provided: " \
249
+ "#{target_ids.map { |id| full_url_for_target_id(id) }.join(', ')}."
250
+ end
251
+ return
252
+ end
253
+
254
+ check_is_fhir_resource(prefetched_value, target_resource_type: 'Bundle')
255
+ check_bundle_entry_resource_type(prefetched_value, resource_type)
256
+ check_ids(target_ids, actual_ids(prefetched_value))
257
+ nil
258
+ end
259
+
260
+ def actual_ids(prefetched_value)
261
+ return [] unless prefetched_value['entry'].present?
262
+
263
+ prefetched_value['entry'].map do |entry|
264
+ type = entry.dig('resource', 'resourceType')
265
+ id = entry.dig('resource', 'id')
266
+ next unless type.present? && id.present?
267
+
268
+ type_and_id = "#{type}/#{id}"
269
+ [type_and_id, entry['fullUrl'].presence || type_and_id]
270
+ end.compact
271
+ end
272
+
273
+ def check_ids(target_ids, actual_id_pairs)
274
+ actual_type_ids = actual_id_pairs.map(&:first)
275
+ unless actual_type_ids.size == actual_type_ids.uniq.size
276
+ errors << "#{error_prefix} prefetched Bundle has multiple entries with the same resource id."
277
+ end
278
+
279
+ actual_ids_map = actual_id_pairs.to_h
280
+
281
+ missing_ids = target_ids - actual_type_ids
282
+ if missing_ids.present?
283
+ errors << "#{error_prefix} prefetched Bundle missing expected entries: " \
284
+ "#{missing_ids.map { |id| full_url_for_target_id(id) }.join('\', \'')}."
285
+ end
286
+
287
+ extra_ids = actual_type_ids - target_ids
288
+ return unless extra_ids.present?
289
+
290
+ errors << "#{error_prefix} prefetched Bundle includes unrequested entries: " \
291
+ "#{extra_ids.map { |id| actual_ids_map[id] }.join('\', \'')}."
292
+ end
293
+
294
+ # NOTE: would possibly fail in the case of duplicate <resource>/<id> with different
295
+ # base urls but this will cause other problems since in the _id search form those
296
+ # would be the same. So not worth trying to work around it.
297
+ def full_url_for_target_id(type_and_id)
298
+ prefetched_resources.keys.find { |url| url.end_with?("/#{type_and_id}") } ||
299
+ fhir_server_url_for(type_and_id)
300
+ end
301
+
302
+ def fhir_server_url_for(type_and_id)
303
+ return type_and_id unless hook_request['fhirServer'].present?
304
+
305
+ "#{hook_request['fhirServer'].chomp('/')}/#{type_and_id}"
306
+ end
307
+
308
+ def check_bundle_entry_resource_type(bundle, target_resource_type)
309
+ bundle['entry']&.each_with_index do |entry, index|
310
+ entry_resource_type = entry.dig('resource', 'resourceType')
311
+ next if entry_resource_type == target_resource_type
312
+
313
+ errors << if entry_resource_type.present?
314
+ "#{error_prefix} prefetched Bundle entry #{index + 1} has an unexpected resourceType: " \
315
+ "expected #{target_resource_type}, got #{entry_resource_type}."
316
+ else
317
+ "#{error_prefix} prefetched Bundle entry #{index + 1} is not a FHIR resource (no resourceType)."
318
+ end
319
+ end
320
+ end
321
+
322
+ def check_is_fhir_resource(prefetched_value, target_resource_type: nil)
323
+ unless prefetched_value.key?('resourceType')
324
+ errors << "#{error_prefix} prefetched value is not a FHIR resource (no resourceType)."
325
+ return
326
+ end
327
+
328
+ return if target_resource_type.blank? || prefetched_value['resourceType'] == target_resource_type
329
+
330
+ errors << "#{error_prefix} prefetched value has unexpected resourceType: " \
331
+ "expected #{target_resource_type}, got #{prefetched_value['resourceType']}."
332
+ end
333
+
334
+ # -----------------------------------------------------------------------
335
+ # Map of prefetched resources by fullUrl
336
+ # -----------------------------------------------------------------------
337
+ def prefetched_resources
338
+ @prefetched_resources ||= {}
339
+ end
340
+
341
+ def extract_prefetched_resources
342
+ hook_request['prefetch']&.each_value do |prefetch_resource|
343
+ next unless prefetch_resource&.dig('resourceType').present?
344
+
345
+ if prefetch_resource['resourceType'] == 'Bundle'
346
+ extract_resources_from_prefetched_bundle(prefetch_resource)
347
+ else
348
+ extract_prefetched_resource_instance(prefetch_resource)
349
+ end
350
+ end
351
+ end
352
+
353
+ def extract_resources_from_prefetched_bundle(bundle)
354
+ bundle['entry']&.each do |entry|
355
+ next unless entry['resource'].present?
356
+
357
+ if entry['fullUrl'].present?
358
+ prefetched_resources[entry['fullUrl']] = entry['resource'] unless prefetched_resources.key?(entry['fullUrl'])
359
+ else
360
+ extract_prefetched_resource_instance(entry['resource'])
361
+ end
362
+ end
363
+ end
364
+
365
+ # no fullUrl available, so assume that it is a restful FHIR url
366
+ # relative to the fhirServer of the hook request
367
+ def extract_prefetched_resource_instance(resource_instance)
368
+ return unless resource_instance['id'].present? &&
369
+ resource_instance['resourceType'].present? &&
370
+ hook_request['fhirServer'].present?
371
+
372
+ fhir_server =
373
+ if hook_request['fhirServer'].ends_with?('/')
374
+ hook_request['fhirServer']
375
+ else
376
+ "#{hook_request['fhirServer']}/"
377
+ end
378
+ key = "#{fhir_server}#{resource_instance['resourceType']}/#{resource_instance['id']}"
379
+ return if prefetched_resources.key?(key)
380
+
381
+ prefetched_resources[key] = resource_instance
382
+ end
383
+
384
+ # -------------------------------------------------------------------------
385
+ # fhirpath resolve() handling
386
+ # -------------------------------------------------------------------------
387
+
388
+ def resolve(reference)
389
+ return nil unless reference.present?
390
+
391
+ key = absolute_reference(reference)
392
+ return nil unless key.present?
393
+
394
+ unless prefetched_resources[key].present?
395
+ errors << "#{error_prefix} resource '#{key}' needed to instantiate the query " \
396
+ 'was not provided in the prefetched values.'
397
+
398
+ return nil
399
+ end
400
+
401
+ @current_base_fhir_server = base_fhir_server_for_identity(key)
402
+ prefetched_resources[key]
403
+ end
404
+
405
+ def absolute_reference(reference)
406
+ reference = reference['reference'] if reference.is_a?(Hash)
407
+ return nil unless reference.present?
408
+
409
+ if URI.parse(reference).absolute?
410
+ reference
411
+ else
412
+ relative_to_absolute_reference(reference)
413
+ end
414
+ rescue URI::InvalidURIError => e
415
+ errors << "#{error_prefix} '#{reference}' needed to instantiate the query " \
416
+ "is invalid: #{e.message}."
417
+ nil
418
+ end
419
+
420
+ def relative_to_absolute_reference(relative_reference)
421
+ return nil unless relative_reference_valid?(relative_reference)
422
+
423
+ if @current_base_fhir_server.nil?
424
+ errors << "#{error_prefix} '#{relative_reference}' needed to instantiate the query " \
425
+ 'is a relative reference, but the base FHIR Server is not known.'
426
+ return nil
427
+ end
428
+
429
+ "#{@current_base_fhir_server}/#{relative_reference}"
430
+ end
431
+
432
+ def relative_reference_valid?(relative_reference)
433
+ if relative_reference.split('/').length > 2
434
+ errors << "#{error_prefix} '#{relative_reference}' needed to instantiate the query " \
435
+ 'is not an absolute reference but has too many segments to be a relative reference.'
436
+ return false
437
+ end
438
+
439
+ resource_type, id = relative_reference.split('/')
440
+ if resource_type.blank? || id.blank?
441
+ errors << "#{error_prefix} '#{relative_reference}' needed to instantiate the query " \
442
+ 'is not a valid relative reference of the form <resource type>/<id>.'
443
+ end
444
+
445
+ true
446
+ end
447
+
448
+ # -------------------------------------------------------------------------
449
+ # Observe a Collection represented as a comma-delimited string in instantiated search
450
+ # -------------------------------------------------------------------------
451
+
452
+ # client will have demonstrated turning a collection into a comma-delimited string if both
453
+ # - the prefetch request follows the RESOURCE?_id={{TOKEN}} form when TOKEN has a | indicating 'and', and
454
+ # - the instantiated query actually has multiple unique ids
455
+ def demonstrates_collection_as_comma_delimited_string?(prefetch_request, instantiated_request)
456
+ token = prefetch_request.match(/[a-zA-Z]*\?_id=\{\{(.+?)\}\}/)&.[](1)
457
+ return false unless token.present? && token.include?('|')
458
+
459
+ instantiated_request.split('?_id=').last.split(',').uniq.size > 1
460
+ end
461
+ end
462
+ end