davinci_crd_test_kit 0.9.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 (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'