davinci_crd_test_kit 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/lib/davinci_crd_test_kit/card_responses/companions_prerequisites.json +58 -0
- data/lib/davinci_crd_test_kit/card_responses/create_update_coverage_information.json +20 -0
- data/lib/davinci_crd_test_kit/card_responses/external_reference.json +21 -0
- data/lib/davinci_crd_test_kit/card_responses/instructions.json +14 -0
- data/lib/davinci_crd_test_kit/card_responses/launch_smart_app.json +21 -0
- data/lib/davinci_crd_test_kit/card_responses/propose_alternate_request.json +71 -0
- data/lib/davinci_crd_test_kit/card_responses/request_form_completion.json +227 -0
- data/lib/davinci_crd_test_kit/cards_validation.rb +234 -0
- data/lib/davinci_crd_test_kit/client_fhir_api_group.rb +762 -0
- data/lib/davinci_crd_test_kit/client_hook_request_validation.rb +15 -0
- data/lib/davinci_crd_test_kit/client_hooks_group.rb +706 -0
- data/lib/davinci_crd_test_kit/client_tests/appointment_book_receive_request_test.rb +71 -0
- data/lib/davinci_crd_test_kit/client_tests/client_display_cards_attest.rb +48 -0
- data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_create_test.rb +40 -0
- data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_read_test.rb +39 -0
- data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_search_test.rb +232 -0
- data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_update_test.rb +40 -0
- data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_validation_test.rb +60 -0
- data/lib/davinci_crd_test_kit/client_tests/decode_auth_token_test.rb +40 -0
- data/lib/davinci_crd_test_kit/client_tests/encounter_discharge_receive_request_test.rb +68 -0
- data/lib/davinci_crd_test_kit/client_tests/encounter_start_receive_request_test.rb +68 -0
- data/lib/davinci_crd_test_kit/client_tests/hook_request_optional_fields_test.rb +41 -0
- data/lib/davinci_crd_test_kit/client_tests/hook_request_required_fields_test.rb +40 -0
- data/lib/davinci_crd_test_kit/client_tests/hook_request_valid_context_test.rb +63 -0
- data/lib/davinci_crd_test_kit/client_tests/hook_request_valid_prefetch_test.rb +151 -0
- data/lib/davinci_crd_test_kit/client_tests/order_dispatch_receive_request_test.rb +79 -0
- data/lib/davinci_crd_test_kit/client_tests/order_select_receive_request_test.rb +76 -0
- data/lib/davinci_crd_test_kit/client_tests/order_sign_receive_request_test.rb +79 -0
- data/lib/davinci_crd_test_kit/client_tests/retrieve_jwks_test.rb +65 -0
- data/lib/davinci_crd_test_kit/client_tests/token_header_test.rb +34 -0
- data/lib/davinci_crd_test_kit/client_tests/token_payload_test.rb +61 -0
- data/lib/davinci_crd_test_kit/crd_client_suite.rb +156 -0
- data/lib/davinci_crd_test_kit/crd_jwks.json +59 -0
- data/lib/davinci_crd_test_kit/crd_options.rb +9 -0
- data/lib/davinci_crd_test_kit/crd_server_suite.rb +115 -0
- data/lib/davinci_crd_test_kit/ext/inferno_core/runnable.rb +22 -0
- data/lib/davinci_crd_test_kit/hook_request_field_validation.rb +410 -0
- data/lib/davinci_crd_test_kit/jwks.rb +25 -0
- data/lib/davinci_crd_test_kit/jwt_helper.rb +74 -0
- data/lib/davinci_crd_test_kit/mock_service_response.rb +421 -0
- data/lib/davinci_crd_test_kit/routes/cds-services.json +74 -0
- data/lib/davinci_crd_test_kit/routes/cds_services_discovery_handler.rb +18 -0
- data/lib/davinci_crd_test_kit/routes/hook_request_endpoint.rb +93 -0
- data/lib/davinci_crd_test_kit/routes/jwk_set_endpoint_handler.rb +15 -0
- data/lib/davinci_crd_test_kit/server_appointment_book_group.rb +173 -0
- data/lib/davinci_crd_test_kit/server_discovery_group.rb +59 -0
- data/lib/davinci_crd_test_kit/server_encounter_discharge_group.rb +144 -0
- data/lib/davinci_crd_test_kit/server_encounter_start_group.rb +144 -0
- data/lib/davinci_crd_test_kit/server_hook_request_validation.rb +15 -0
- data/lib/davinci_crd_test_kit/server_hooks_group.rb +69 -0
- data/lib/davinci_crd_test_kit/server_order_dispatch_group.rb +173 -0
- data/lib/davinci_crd_test_kit/server_order_select_group.rb +169 -0
- data/lib/davinci_crd_test_kit/server_order_sign_group.rb +198 -0
- data/lib/davinci_crd_test_kit/server_required_card_response_validation_group.rb +23 -0
- data/lib/davinci_crd_test_kit/server_tests/additional_orders_validation_test.rb +70 -0
- data/lib/davinci_crd_test_kit/server_tests/card_optional_fields_validation_test.rb +47 -0
- data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_across_hooks_validation_test.rb +32 -0
- data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_received_test.rb +58 -0
- data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_validation_test.rb +121 -0
- data/lib/davinci_crd_test_kit/server_tests/create_or_update_coverage_info_response_validation_test.rb +72 -0
- data/lib/davinci_crd_test_kit/server_tests/discovery_endpoint_test.rb +88 -0
- data/lib/davinci_crd_test_kit/server_tests/discovery_services_validation_test.rb +65 -0
- data/lib/davinci_crd_test_kit/server_tests/external_reference_card_across_hooks_validation_test.rb +28 -0
- data/lib/davinci_crd_test_kit/server_tests/external_reference_card_validation_test.rb +36 -0
- data/lib/davinci_crd_test_kit/server_tests/form_completion_response_validation_test.rb +79 -0
- data/lib/davinci_crd_test_kit/server_tests/instructions_card_received_across_hooks_test.rb +25 -0
- data/lib/davinci_crd_test_kit/server_tests/instructions_card_received_test.rb +28 -0
- data/lib/davinci_crd_test_kit/server_tests/launch_smart_app_card_validation_test.rb +38 -0
- data/lib/davinci_crd_test_kit/server_tests/propose_alternate_request_card_validation_test.rb +65 -0
- data/lib/davinci_crd_test_kit/server_tests/service_call_test.rb +86 -0
- data/lib/davinci_crd_test_kit/server_tests/service_request_context_validation_test.rb +30 -0
- data/lib/davinci_crd_test_kit/server_tests/service_request_optional_fields_validation_test.rb +41 -0
- data/lib/davinci_crd_test_kit/server_tests/service_request_required_fields_validation_test.rb +43 -0
- data/lib/davinci_crd_test_kit/server_tests/service_response_validation_test.rb +82 -0
- data/lib/davinci_crd_test_kit/suggestion_actions_validation.rb +123 -0
- data/lib/davinci_crd_test_kit/tags.rb +8 -0
- data/lib/davinci_crd_test_kit/test_helper.rb +23 -0
- data/lib/davinci_crd_test_kit/urls.rb +52 -0
- data/lib/davinci_crd_test_kit/version.rb +3 -0
- data/lib/davinci_crd_test_kit.rb +2 -0
- 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
|