davinci_crd_test_kit 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/davinci_crd_test_kit/card_responses/companions_prerequisites.json +58 -0
  4. data/lib/davinci_crd_test_kit/card_responses/create_update_coverage_information.json +20 -0
  5. data/lib/davinci_crd_test_kit/card_responses/external_reference.json +21 -0
  6. data/lib/davinci_crd_test_kit/card_responses/instructions.json +14 -0
  7. data/lib/davinci_crd_test_kit/card_responses/launch_smart_app.json +21 -0
  8. data/lib/davinci_crd_test_kit/card_responses/propose_alternate_request.json +71 -0
  9. data/lib/davinci_crd_test_kit/card_responses/request_form_completion.json +227 -0
  10. data/lib/davinci_crd_test_kit/cards_validation.rb +234 -0
  11. data/lib/davinci_crd_test_kit/client_fhir_api_group.rb +762 -0
  12. data/lib/davinci_crd_test_kit/client_hook_request_validation.rb +15 -0
  13. data/lib/davinci_crd_test_kit/client_hooks_group.rb +706 -0
  14. data/lib/davinci_crd_test_kit/client_tests/appointment_book_receive_request_test.rb +71 -0
  15. data/lib/davinci_crd_test_kit/client_tests/client_display_cards_attest.rb +48 -0
  16. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_create_test.rb +40 -0
  17. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_read_test.rb +39 -0
  18. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_search_test.rb +232 -0
  19. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_update_test.rb +40 -0
  20. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_validation_test.rb +60 -0
  21. data/lib/davinci_crd_test_kit/client_tests/decode_auth_token_test.rb +40 -0
  22. data/lib/davinci_crd_test_kit/client_tests/encounter_discharge_receive_request_test.rb +68 -0
  23. data/lib/davinci_crd_test_kit/client_tests/encounter_start_receive_request_test.rb +68 -0
  24. data/lib/davinci_crd_test_kit/client_tests/hook_request_optional_fields_test.rb +41 -0
  25. data/lib/davinci_crd_test_kit/client_tests/hook_request_required_fields_test.rb +40 -0
  26. data/lib/davinci_crd_test_kit/client_tests/hook_request_valid_context_test.rb +63 -0
  27. data/lib/davinci_crd_test_kit/client_tests/hook_request_valid_prefetch_test.rb +151 -0
  28. data/lib/davinci_crd_test_kit/client_tests/order_dispatch_receive_request_test.rb +79 -0
  29. data/lib/davinci_crd_test_kit/client_tests/order_select_receive_request_test.rb +76 -0
  30. data/lib/davinci_crd_test_kit/client_tests/order_sign_receive_request_test.rb +79 -0
  31. data/lib/davinci_crd_test_kit/client_tests/retrieve_jwks_test.rb +65 -0
  32. data/lib/davinci_crd_test_kit/client_tests/token_header_test.rb +34 -0
  33. data/lib/davinci_crd_test_kit/client_tests/token_payload_test.rb +61 -0
  34. data/lib/davinci_crd_test_kit/crd_client_suite.rb +156 -0
  35. data/lib/davinci_crd_test_kit/crd_jwks.json +59 -0
  36. data/lib/davinci_crd_test_kit/crd_options.rb +9 -0
  37. data/lib/davinci_crd_test_kit/crd_server_suite.rb +115 -0
  38. data/lib/davinci_crd_test_kit/ext/inferno_core/runnable.rb +22 -0
  39. data/lib/davinci_crd_test_kit/hook_request_field_validation.rb +410 -0
  40. data/lib/davinci_crd_test_kit/jwks.rb +25 -0
  41. data/lib/davinci_crd_test_kit/jwt_helper.rb +74 -0
  42. data/lib/davinci_crd_test_kit/mock_service_response.rb +421 -0
  43. data/lib/davinci_crd_test_kit/routes/cds-services.json +74 -0
  44. data/lib/davinci_crd_test_kit/routes/cds_services_discovery_handler.rb +18 -0
  45. data/lib/davinci_crd_test_kit/routes/hook_request_endpoint.rb +93 -0
  46. data/lib/davinci_crd_test_kit/routes/jwk_set_endpoint_handler.rb +15 -0
  47. data/lib/davinci_crd_test_kit/server_appointment_book_group.rb +173 -0
  48. data/lib/davinci_crd_test_kit/server_discovery_group.rb +59 -0
  49. data/lib/davinci_crd_test_kit/server_encounter_discharge_group.rb +144 -0
  50. data/lib/davinci_crd_test_kit/server_encounter_start_group.rb +144 -0
  51. data/lib/davinci_crd_test_kit/server_hook_request_validation.rb +15 -0
  52. data/lib/davinci_crd_test_kit/server_hooks_group.rb +69 -0
  53. data/lib/davinci_crd_test_kit/server_order_dispatch_group.rb +173 -0
  54. data/lib/davinci_crd_test_kit/server_order_select_group.rb +169 -0
  55. data/lib/davinci_crd_test_kit/server_order_sign_group.rb +198 -0
  56. data/lib/davinci_crd_test_kit/server_required_card_response_validation_group.rb +23 -0
  57. data/lib/davinci_crd_test_kit/server_tests/additional_orders_validation_test.rb +70 -0
  58. data/lib/davinci_crd_test_kit/server_tests/card_optional_fields_validation_test.rb +47 -0
  59. data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_across_hooks_validation_test.rb +32 -0
  60. data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_received_test.rb +58 -0
  61. data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_validation_test.rb +121 -0
  62. data/lib/davinci_crd_test_kit/server_tests/create_or_update_coverage_info_response_validation_test.rb +72 -0
  63. data/lib/davinci_crd_test_kit/server_tests/discovery_endpoint_test.rb +88 -0
  64. data/lib/davinci_crd_test_kit/server_tests/discovery_services_validation_test.rb +65 -0
  65. data/lib/davinci_crd_test_kit/server_tests/external_reference_card_across_hooks_validation_test.rb +28 -0
  66. data/lib/davinci_crd_test_kit/server_tests/external_reference_card_validation_test.rb +36 -0
  67. data/lib/davinci_crd_test_kit/server_tests/form_completion_response_validation_test.rb +79 -0
  68. data/lib/davinci_crd_test_kit/server_tests/instructions_card_received_across_hooks_test.rb +25 -0
  69. data/lib/davinci_crd_test_kit/server_tests/instructions_card_received_test.rb +28 -0
  70. data/lib/davinci_crd_test_kit/server_tests/launch_smart_app_card_validation_test.rb +38 -0
  71. data/lib/davinci_crd_test_kit/server_tests/propose_alternate_request_card_validation_test.rb +65 -0
  72. data/lib/davinci_crd_test_kit/server_tests/service_call_test.rb +86 -0
  73. data/lib/davinci_crd_test_kit/server_tests/service_request_context_validation_test.rb +30 -0
  74. data/lib/davinci_crd_test_kit/server_tests/service_request_optional_fields_validation_test.rb +41 -0
  75. data/lib/davinci_crd_test_kit/server_tests/service_request_required_fields_validation_test.rb +43 -0
  76. data/lib/davinci_crd_test_kit/server_tests/service_response_validation_test.rb +82 -0
  77. data/lib/davinci_crd_test_kit/suggestion_actions_validation.rb +123 -0
  78. data/lib/davinci_crd_test_kit/tags.rb +8 -0
  79. data/lib/davinci_crd_test_kit/test_helper.rb +23 -0
  80. data/lib/davinci_crd_test_kit/urls.rb +52 -0
  81. data/lib/davinci_crd_test_kit/version.rb +3 -0
  82. data/lib/davinci_crd_test_kit.rb +2 -0
  83. metadata +170 -0
@@ -0,0 +1,38 @@
1
+ require_relative '../test_helper'
2
+
3
+ module DaVinciCRDTestKit
4
+ class LaunchSmartAppCardValidationTest < Inferno::Test
5
+ include DaVinciCRDTestKit::TestHelper
6
+
7
+ title 'Valid Launch SMART Application cards received'
8
+ id :crd_launch_smart_app_card_validation
9
+ description %(
10
+ This test verifies the presence of valid Launch SMART Application cards within the list of valid cards
11
+ returned by the CRD service.
12
+ As per the [Da Vinci CRD Implementation Guide](https://hl7.org/fhir/us/davinci-crd/STU2/cards.html#launch-smart-application),
13
+ Launch SMART Application cards must contain links with the type set to `smart`.
14
+ This test checks for the presence of any Launch SMART Application cards by verifying:
15
+ - The existence of a `links` array within each card.
16
+ - That every link in the `links` array of a card is of type `smart`.
17
+
18
+ The test will be skipped if no Launch SMART Application cards are found within the returned valid cards.
19
+ )
20
+
21
+ optional
22
+ input :valid_cards_with_links
23
+
24
+ def hook_name
25
+ config.options[:hook_name]
26
+ end
27
+
28
+ run do
29
+ parsed_cards = parse_json(valid_cards_with_links)
30
+ external_reference_cards = parsed_cards.select do |card|
31
+ links = card['links']
32
+ links.present? && links.all? { |link| link['type'] == 'smart' }
33
+ end
34
+
35
+ skip_if external_reference_cards.blank?, "#{hook_name} hook response does not contain any Launch SMART App cards."
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,65 @@
1
+ require_relative '../test_helper'
2
+ require_relative '../suggestion_actions_validation'
3
+
4
+ module DaVinciCRDTestKit
5
+ class ProposeAlternateRequestCardValidationTest < Inferno::Test
6
+ include DaVinciCRDTestKit::TestHelper
7
+ include DaVinciCRDTestKit::SuggestionActionsValidation
8
+
9
+ title 'Valid Propose Alternate Request cards received'
10
+ id :crd_propose_alternate_request_card_validation
11
+ description %(
12
+ This test validates that all [Propose Alternate Request](https://hl7.org/fhir/us/davinci-crd/STU2/cards.html#propose-alternate-request)
13
+ cards received are valid. It checks for the presence of a card's suggestion
14
+ with a single action with `Action.type` of `update` or a card with at least
15
+ two actions, one with `Action.type` of `delete` and the other with
16
+ `Action.type` of `create`.
17
+ )
18
+ optional
19
+ input :valid_cards_with_suggestions, :contexts
20
+
21
+ EXPECTED_RESOURCE_TYPES = %w[
22
+ Device DeviceRequest Encounter Medication
23
+ MedicationRequest NutritionOrder ServiceRequest
24
+ VisionPrescription
25
+ ].freeze
26
+
27
+ def hook_name
28
+ config.options[:hook_name]
29
+ end
30
+
31
+ def check_action_type(actions, action_type)
32
+ actions&.any? do |action|
33
+ action['type'] == action_type && action_resource_type_check(action, EXPECTED_RESOURCE_TYPES)
34
+ end
35
+ end
36
+
37
+ def propose_alternate_request_card?(card)
38
+ card['suggestions'].any? do |suggestion|
39
+ actions = suggestion['actions']
40
+ has_update = check_action_type(actions, 'update')
41
+ has_delete = check_action_type(actions, 'delete')
42
+ has_create = check_action_type(actions, 'create')
43
+
44
+ has_update || (has_delete && has_create)
45
+ end
46
+ end
47
+
48
+ run do
49
+ parsed_cards = parse_json(valid_cards_with_suggestions)
50
+ parsed_contexts = parse_json(contexts)
51
+ proposed_alternate_cards = parsed_cards.filter { |card| propose_alternate_request_card?(card) }
52
+
53
+ skip_if proposed_alternate_cards.blank?,
54
+ "#{hook_name} hook response does not contain a Propose Alternate Request card."
55
+
56
+ proposed_alternate_cards.each do |card|
57
+ card['suggestions'].each do |suggestion|
58
+ actions_check(suggestion['actions'], parsed_contexts)
59
+ end
60
+ end
61
+
62
+ no_error_validation('Some Proposed Alternate Request cards are not valid.')
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,86 @@
1
+ module DaVinciCRDTestKit
2
+ class ServiceCallTest < Inferno::Test
3
+ title 'Submit user-defined service requests'
4
+ id :crd_service_call_test
5
+ description %(
6
+ This test initiates POST request(s) to a specified CDS Service using the JSON body list provided by the user.
7
+ As indicated in the [CDS Hooks specification section on Calling a CDS Service](https://cds-hooks.hl7.org/2.0/#calling-a-cds-service),
8
+ the service endpoint is constructed by appending the individual service id to the CDS Service base URL,
9
+ following the format `{baseUrl}/cds-services/{service.id}`.
10
+
11
+ If running this group only, the user will need to provide the `service.id` to call the specified service.
12
+ Otherwise, the `service.id` is derived from the CDS Services that are retrieved through a query to the
13
+ discovery endpoint.
14
+
15
+ The test will be skipped if the CRD server does not host a CDS Service corresponding to the hook that
16
+ is being tested.
17
+
18
+ The test is deemed successful if the CRD server returns a 200 HTTP response for all requests.
19
+ )
20
+ input_order :base_url, :encryption_method, :jwks_kid
21
+ input :base_url, :service_ids
22
+ input :service_request_bodies,
23
+ optional: true,
24
+ type: 'textarea',
25
+ description: 'Provide a list of request bodies send multiple requests. e.g. [json_body_1, json_body_2]'
26
+ input :encryption_method,
27
+ title: 'JWT Signing Algorithm',
28
+ description: <<~DESCRIPTION,
29
+ CDS Hooks recommends ES384 and RS384 for JWT signature verification.
30
+ Select which method to use.
31
+ DESCRIPTION
32
+ type: 'radio',
33
+ options: {
34
+ list_options: [
35
+ {
36
+ label: 'ES384',
37
+ value: 'ES384'
38
+ },
39
+ {
40
+ label: 'RS384',
41
+ value: 'RS384'
42
+ }
43
+ ]
44
+ }
45
+ input :jwks_kid,
46
+ title: 'CDS Services JWKS kid',
47
+ description: <<~DESCRIPTION,
48
+ The key ID of the JWKS private key to use for signing the JWTs when invoking a CDS service endpoint
49
+ requiring authentication.
50
+ Defaults to the first JWK in the list if no kid is supplied.
51
+ DESCRIPTION
52
+ optional: true
53
+
54
+ def hook_name
55
+ config.options[:hook_name]
56
+ end
57
+
58
+ run do
59
+ discovery_url = "#{base_url.chomp('/')}/cds-services"
60
+ skip_if service_request_bodies.blank?,
61
+ 'Request body not provided, skipping test.'
62
+ assert_valid_json(service_request_bodies)
63
+
64
+ payloads = [JSON.parse(service_request_bodies)].flatten
65
+ service_id = service_ids.split(', ').first.strip
66
+ service_endpoint = "#{discovery_url}/#{service_id}"
67
+ token = JwtHelper.build(
68
+ aud: service_endpoint,
69
+ iss: inferno_base_url,
70
+ jku: "#{inferno_base_url}/jwks.json",
71
+ kid: jwks_kid,
72
+ encryption_method:
73
+ )
74
+ headers = { 'Content-type' => 'application/json', 'Authorization' => "Bearer #{token}" }
75
+
76
+ payloads.each do |payload|
77
+ post(service_endpoint, body: payload.to_json, headers:, tags: [hook_name])
78
+ end
79
+
80
+ requests.each do |request|
81
+ assert_response_status(200, request:)
82
+ assert_valid_json(request.response_body)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,30 @@
1
+ require_relative '../server_hook_request_validation'
2
+ require_relative '../test_helper'
3
+
4
+ module DaVinciCRDTestKit
5
+ class ServiceRequestContextValidationTest < Inferno::Test
6
+ include DaVinciCRDTestKit::ServerHookRequestValidation
7
+ include DaVinciCRDTestKit::TestHelper
8
+
9
+ title 'All service requests contain valid context'
10
+ id :crd_service_request_context_validation
11
+ description %(
12
+ This test verifies that all service requests `context` field is valid and contains all the
13
+ required fields.
14
+ )
15
+ input :contexts
16
+
17
+ def hook_name
18
+ config.options[:hook_name]
19
+ end
20
+
21
+ run do
22
+ parsed_contexts = parse_json(contexts)
23
+ parsed_contexts.each do |context|
24
+ hook_request_context_check(context, hook_name)
25
+ end
26
+
27
+ no_error_validation('Some contexts are not valid.')
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,41 @@
1
+ require_relative '../server_hook_request_validation'
2
+
3
+ module DaVinciCRDTestKit
4
+ class ServiceRequestOptionalFieldsValidationTest < Inferno::Test
5
+ include DaVinciCRDTestKit::ServerHookRequestValidation
6
+
7
+ title 'All service requests contain optional fields'
8
+ id :crd_service_request_optional_fields_validation
9
+ description %(
10
+ This optional test reviews the user-submitted CRD service requests for the presence of optional fields:
11
+ `fhirAuthorization` and `prefetch`.
12
+
13
+ The test will not fail if these optional fields are missing from a service request; instead, it generates an
14
+ informational message.
15
+ )
16
+ optional
17
+
18
+ def hook_name
19
+ config.options[:hook_name]
20
+ end
21
+
22
+ run do
23
+ load_tagged_requests(hook_name)
24
+ skip_if requests.empty?, "No #{hook_name} request was made in a previous test as expected."
25
+
26
+ error_messages = []
27
+ requests.each_with_index do |request, index|
28
+ assert_valid_json(request.request_body)
29
+ request_body = JSON.parse(request.request_body)
30
+ hook_request_optional_fields_check(request_body)
31
+ rescue Inferno::Exceptions::AssertionException => e
32
+ error_messages << "Request #{index + 1}: #{e.message}"
33
+ end
34
+
35
+ error_messages.each do |msg|
36
+ messages << { type: 'error', message: msg }
37
+ end
38
+ assert error_messages.empty?, 'Some service requests have invalid optional fields.'
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,43 @@
1
+ require_relative '../server_hook_request_validation'
2
+ module DaVinciCRDTestKit
3
+ class ServiceRequestRequiredFieldsValidationTest < Inferno::Test
4
+ include DaVinciCRDTestKit::ServerHookRequestValidation
5
+
6
+ title 'All service requests contain required fields'
7
+ id :crd_service_request_required_fields_validation
8
+ description %(
9
+ This test validates all CRD service requests provided by the user, ensuring each includes all required fields
10
+ specified in the [CDS Hooks spec section on Calling a CDS Service](https://cds-hooks.hl7.org/2.0/#calling-a-cds-service):
11
+ `hook`, `hookInstance`, and `context`. Furthermore, the test checks for the conditional presence of the
12
+ `fhirServer` field if `fhirAuthorization` is included.
13
+ )
14
+ output :contexts
15
+
16
+ def hook_name
17
+ config.options[:hook_name]
18
+ end
19
+
20
+ run do
21
+ load_tagged_requests(hook_name)
22
+ skip_if requests.empty?, "No #{hook_name} request was made in a previous test as expected."
23
+
24
+ error_messages = []
25
+ contexts = []
26
+ requests.each_with_index do |request, index|
27
+ assert_valid_json(request.request_body)
28
+ request_body = JSON.parse(request.request_body)
29
+ contexts << request_body['context'] if request_body['context'].is_a?(Hash)
30
+ hook_request_required_fields_check(request_body, hook_name)
31
+ rescue Inferno::Exceptions::AssertionException => e
32
+ error_messages << "Request #{index + 1}: #{e.message}"
33
+ end
34
+
35
+ output contexts: contexts.to_json
36
+
37
+ error_messages.each do |msg|
38
+ messages << { type: 'error', message: msg }
39
+ end
40
+ assert error_messages.empty?, 'Some service requests made are not valid.'
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,82 @@
1
+ require_relative '../cards_validation'
2
+
3
+ module DaVinciCRDTestKit
4
+ class ServiceResponseValidationTest < Inferno::Test
5
+ include DaVinciCRDTestKit::CardsValidation
6
+
7
+ title 'All service responses contain valid cards and optional systemActions'
8
+ id :crd_service_response_validation
9
+ description %(
10
+ As per the [CDS Hooks spec section on CDS Service Response](https://cds-hooks.hl7.org/2.0/#cds-service-response),
11
+ a successful server's response to a service request must be a JSON object containing a `cards` array.
12
+ It must also contain a `systemActions` array for `appointment-book` and `order-sign` hook.
13
+
14
+ Each card must contain the following required fields: `summary`, `indicator`, and `source`.
15
+ The required fields must have a valid data structure.
16
+ )
17
+ output :valid_cards, :valid_system_actions
18
+
19
+ SYSTEM_ACTIONS_HOOK_NAMES = ['appointment-book', 'order-sign'].freeze
20
+
21
+ def hook_name
22
+ config.options[:hook_name]
23
+ end
24
+
25
+ def valid_cards
26
+ @valid_cards ||= []
27
+ end
28
+
29
+ def valid_system_actions
30
+ @valid_system_actions ||= []
31
+ end
32
+
33
+ def system_actions_check(system_actions)
34
+ system_actions.each do |action|
35
+ current_error_count = messages.count { |msg| msg[:type] == 'error' }
36
+ action_fields_validation(action)
37
+ valid_system_actions << action if current_error_count == messages.count { |msg| msg[:type] == 'error' }
38
+ end
39
+ end
40
+
41
+ def perform_system_actions_validation(system_actions, response_index)
42
+ if SYSTEM_ACTIONS_HOOK_NAMES.include?(hook_name) && system_actions.nil?
43
+ msg = "Server response #{response_index + 1} did not have `systemActions` field." \
44
+ "Must be present for #{hook_name}."
45
+ add_message('error', msg)
46
+ end
47
+ return if system_actions.nil?
48
+
49
+ unless system_actions.is_a?(Array)
50
+ add_message('error', "`systemActions` of server response #{response_index + 1} is not an array.")
51
+ return
52
+ end
53
+ system_actions_check(system_actions)
54
+ end
55
+
56
+ run do
57
+ load_tagged_requests(hook_name)
58
+ skip_if requests.blank?, "No #{hook_name} request was made in a previous test as expected."
59
+ successful_requests = requests.select { |request| request.status == 200 }
60
+ skip_if successful_requests.empty?, 'All service requests were unsuccessful.'
61
+
62
+ info do
63
+ unsuncessful_count = (requests - successful_requests).length
64
+ assert unsuncessful_count.zero?, "#{unsuncessful_count} out of #{requests.length} requests were unsuccessful"
65
+ end
66
+
67
+ successful_requests.each_with_index do |request, index|
68
+ service_response = JSON.parse(request.response_body)
69
+ perform_cards_validation(service_response['cards'], index)
70
+
71
+ perform_system_actions_validation(service_response['systemActions'], index)
72
+ rescue JSON::ParserError
73
+ add_message('error', "Invalid JSON: server response #{index + 1} is not a valid JSON.")
74
+ end
75
+
76
+ output valid_system_actions: valid_system_actions.to_json
77
+ output valid_cards: valid_cards.to_json
78
+
79
+ no_error_validation('Some service responses are not valid. Check messages for issues found.')
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,123 @@
1
+ require_relative 'server_hook_request_validation'
2
+ module DaVinciCRDTestKit
3
+ module SuggestionActionsValidation
4
+ include DaVinciCRDTestKit::ServerHookRequestValidation
5
+
6
+ def action_required_fields
7
+ { 'type' => String, 'description' => String }
8
+ end
9
+
10
+ def action_fields_validation(action)
11
+ action_required_fields.each do |field, type|
12
+ validate_presence_and_type(action, field, type, 'Action')
13
+ end
14
+
15
+ action_type_field_validation(action)
16
+ end
17
+
18
+ def action_type_field_validation(action)
19
+ return unless action['type']
20
+
21
+ allowed_types = ['create', 'update', 'delete']
22
+ type = action['type']
23
+ unless allowed_types.include?(type)
24
+ error_msg = "Action type value `#{type}` is not allowed. Allowed values: #{allowed_types.to_sentence}. " \
25
+ "In Action `#{action}`"
26
+ add_message('error', error_msg)
27
+ return
28
+ end
29
+
30
+ if ['create', 'update'].include?(type)
31
+ action_resource_field_validation(action, type)
32
+ else
33
+ action_resource_id_field_validation(action)
34
+ end
35
+ end
36
+
37
+ def action_resource_field_validation(action, type)
38
+ unless action['resource']
39
+ add_message('error', "`Action.resource` must be present for `#{type}` actions: `#{action}`.")
40
+ return
41
+ end
42
+
43
+ resource = FHIR.from_contents(action['resource'].to_json)
44
+ return if resource
45
+
46
+ add_message('error', "`Action.resource` must be a FHIR resource: `#{action}`.")
47
+ end
48
+
49
+ def action_resource_id_field_validation(action)
50
+ validate_presence_and_type(action, 'resourceId', Array, '`delete` Action')
51
+ return unless action['resourceId'].is_a?(Array)
52
+
53
+ action['resourceId'].each do |ref|
54
+ resource_reference_check(ref, 'Action.resourceId item')
55
+ end
56
+ end
57
+
58
+ def draft_orders_bundle_entry_refs(contexts)
59
+ @draft_orders_bundle_entry_refs ||= contexts.flat_map do |context|
60
+ draft_orders_bundle = parse_fhir_bundle_from_context('draftOrders', context)
61
+ draft_orders_bundle.entry.map { |entry| "#{entry.resource.resourceType}/#{entry.resource.id}" }
62
+ end
63
+ end
64
+
65
+ def action_resource_type_check(action, expected_resource_types)
66
+ resource_types = if ['create', 'update'].include?(action['type'])
67
+ [FHIR.from_contents(action['resource'].to_json).resourceType]
68
+ else
69
+ action['resourceId'].map { |ref| ref.split('/').first }
70
+ end
71
+ resource_types.all? { |resource_type| expected_resource_types.include?(resource_type) }
72
+ end
73
+
74
+ def extract_resource_types_by_action(actions, action_type)
75
+ actions.each_with_object([]) do |act, resource_types|
76
+ resource_types << act['resource']['resourceType'] if act['type'] == action_type
77
+ end
78
+ end
79
+
80
+ def actions_check(actions, contexts = nil)
81
+ create_actions_resource_types = extract_resource_types_by_action(actions, 'create')
82
+
83
+ actions.each do |action|
84
+ case action['type']
85
+ when 'create', 'update'
86
+ create_or_update_action_check(action, contexts)
87
+ when 'delete'
88
+ delete_action_check(action, create_actions_resource_types, contexts)
89
+ end
90
+ end
91
+ end
92
+
93
+ def create_or_update_action_check(action, contexts)
94
+ resource = FHIR.from_contents(action['resource'].to_json)
95
+ resource_is_valid?(resource:, profile_url: structure_definition_map[resource.resourceType])
96
+ return unless action['type'] == 'update' && contexts
97
+
98
+ ref = "#{resource.resourceType}/#{resource.id}"
99
+ return if draft_orders_bundle_entry_refs(contexts).include?(ref)
100
+
101
+ error_msg = "Resource being updated must be from the `draftOrders` entry. #{ref} is not in the " \
102
+ "`context.drafOrders` of the submitted requests. In Action `#{action}`"
103
+ add_message('error', error_msg)
104
+ end
105
+
106
+ def delete_action_check(action, create_actions_resource_types, contexts)
107
+ action['resourceId'].each do |ref|
108
+ unless draft_orders_bundle_entry_refs(contexts).include?(ref)
109
+ error_msg = '`Action.resourceId` must reference FHIR resource from the `draftOrders` entry. ' \
110
+ "#{ref} is not in `draftOrders`. In Action `#{action}`"
111
+ add_message('error', error_msg)
112
+ next
113
+ end
114
+
115
+ resource_type = ref.split('/').first
116
+ next if create_actions_resource_types.include?(resource_type)
117
+
118
+ error_msg = "There's no `create` action for the proposed order being deleted: `#{ref}`. In Action `#{action}`"
119
+ add_message('error', error_msg)
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,8 @@
1
+ module DaVinciCRDTestKit
2
+ APPOINTMENT_BOOK_TAG = 'crd_appointment_book'.freeze
3
+ ENCOUNTER_START_TAG = 'crd_encounter_start'.freeze
4
+ ENCOUNTER_DISCHARGE_TAG = 'crd_encounter_discharge'.freeze
5
+ ORDER_DISPATCH_TAG = 'crd_order_dispatch'.freeze
6
+ ORDER_SELECT_TAG = 'crd_order_select'.freeze
7
+ ORDER_SIGN_TAG = 'crd_order_sign'.freeze
8
+ end
@@ -0,0 +1,23 @@
1
+ module DaVinciCRDTestKit
2
+ module TestHelper
3
+ def parse_json(input)
4
+ assert_valid_json(input)
5
+ JSON.parse(input)
6
+ end
7
+
8
+ def verify_at_least_one_test_passes(test_groups, id_pattern, error_message, id_exclude_pattern = nil)
9
+ runnables = test_groups.map do |group|
10
+ group.tests.find do |test|
11
+ test.id.include?(id_pattern) && (!id_exclude_pattern || !test.id.include?(id_exclude_pattern))
12
+ end
13
+ end.compact
14
+
15
+ results_repo = Inferno::Repositories::Results.new
16
+ results = results_repo.current_results_for_test_session_and_runnables(test_session_id, runnables)
17
+
18
+ pass_if(results.any? { |result| result.result == 'pass' })
19
+
20
+ skip error_message
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,52 @@
1
+ module DaVinciCRDTestKit
2
+ APPOINTMENT_BOOK_PATH = '/cds-services/appointment-book-service'.freeze
3
+ ENCOUNTER_START_PATH = '/cds-services/encounter-start-service'.freeze
4
+ ENCOUNTER_DISCHARGE_PATH = '/cds-services/encounter-discharge-service'.freeze
5
+ ORDER_DISPATCH_PATH = '/cds-services/order-dispatch-service'.freeze
6
+ ORDER_SELECT_PATH = '/cds-services/order-select-service'.freeze
7
+ ORDER_SIGN_PATH = '/cds-services/order-sign-service'.freeze
8
+ RESUME_PASS_PATH = '/resume_pass'.freeze
9
+ RESUME_FAIL_PATH = '/resume_fail'.freeze
10
+
11
+ module URLs
12
+ def base_url
13
+ @base_url ||= "#{Inferno::Application['base_url']}/custom/#{suite_id}"
14
+ end
15
+
16
+ def appointment_book_url
17
+ @appointment_book_url ||= base_url + APPOINTMENT_BOOK_PATH
18
+ end
19
+
20
+ def encounter_start_url
21
+ @encounter_start_url ||= base_url + ENCOUNTER_START_PATH
22
+ end
23
+
24
+ def encounter_discharge_url
25
+ @encounter_discharge_url ||= base_url + ENCOUNTER_DISCHARGE_PATH
26
+ end
27
+
28
+ def order_dispatch_url
29
+ @order_dispatch_url ||= base_url + ORDER_DISPATCH_PATH
30
+ end
31
+
32
+ def order_select_url
33
+ @order_select_url ||= base_url + ORDER_SELECT_PATH
34
+ end
35
+
36
+ def order_sign_url
37
+ @order_sign_url ||= base_url + ORDER_SIGN_PATH
38
+ end
39
+
40
+ def resume_pass_url
41
+ @resume_pass_url ||= base_url + RESUME_PASS_PATH
42
+ end
43
+
44
+ def resume_fail_url
45
+ @resume_fail_url ||= base_url + RESUME_FAIL_PATH
46
+ end
47
+
48
+ def suite_id
49
+ self.class.suite.id
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,3 @@
1
+ module DaVinciCRDTestKit
2
+ VERSION = '0.9.0'.freeze
3
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'davinci_crd_test_kit/crd_server_suite'
2
+ require_relative 'davinci_crd_test_kit/crd_client_suite'