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.
- 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,71 @@
|
|
|
1
|
+
require_relative '../urls'
|
|
2
|
+
|
|
3
|
+
module DaVinciCRDTestKit
|
|
4
|
+
class AppointmentBookReceiveRequestTest < Inferno::Test
|
|
5
|
+
include URLs
|
|
6
|
+
|
|
7
|
+
id :crd_appointment_book_request
|
|
8
|
+
title 'Request received for appointment-book hook'
|
|
9
|
+
description %(
|
|
10
|
+
This test waits for an incoming [appointment-book](https://hl7.org/fhir/us/davinci-crd/STU2/hooks.html#appointment-book)
|
|
11
|
+
hook request and responds to the client with the response types selected as an input. This hook is a 'primary'
|
|
12
|
+
hook, meaning that CRD Servers SHALL, at minimum, return a [Coverage Information](https://hl7.org/fhir/us/davinci-crd/STU2/StructureDefinition-ext-coverage-information.html)
|
|
13
|
+
system action for these hooks, even if the response indicates that further information is needed or that the
|
|
14
|
+
level of detail provided is insufficient to determine coverage.
|
|
15
|
+
)
|
|
16
|
+
receives_request :appointment_book
|
|
17
|
+
|
|
18
|
+
input :iss
|
|
19
|
+
input :appointment_book_selected_response_types,
|
|
20
|
+
title: 'Response types to return from appointment-book hook requests',
|
|
21
|
+
description: %(
|
|
22
|
+
Select the cards/action response types that the Inferno hook request endpoints will return. The default
|
|
23
|
+
response type that will be returned for this hook is the `Coverage Information` card type.
|
|
24
|
+
),
|
|
25
|
+
type: 'checkbox',
|
|
26
|
+
default: ['coverage_information'],
|
|
27
|
+
options: {
|
|
28
|
+
list_options: [
|
|
29
|
+
{
|
|
30
|
+
label: 'External Reference',
|
|
31
|
+
value: 'external_reference'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: 'Instructions',
|
|
35
|
+
value: 'instructions'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
label: 'Coverage Information',
|
|
39
|
+
value: 'coverage_information'
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: 'Request Form Completion',
|
|
43
|
+
value: 'request_form_completion'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: 'Create/Update Coverage Information',
|
|
47
|
+
value: 'create_update_coverage_info'
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
label: 'Launch SMART Application',
|
|
51
|
+
value: 'launch_smart_app'
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
run do
|
|
57
|
+
wait(
|
|
58
|
+
identifier: "appointment-book #{iss}",
|
|
59
|
+
message: %(
|
|
60
|
+
**Appointment Book CDS Service Test**:
|
|
61
|
+
|
|
62
|
+
Invoke the appointment-book hook and send a request to:
|
|
63
|
+
|
|
64
|
+
`#{appointment_book_url}`
|
|
65
|
+
|
|
66
|
+
Inferno will process the request and return CDS cards if successful.
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require_relative '../urls'
|
|
2
|
+
|
|
3
|
+
module DaVinciCRDTestKit
|
|
4
|
+
class ClientCardDisplayAttest < Inferno::Test
|
|
5
|
+
include URLs
|
|
6
|
+
|
|
7
|
+
id :crd_card_display_attest_test
|
|
8
|
+
title 'Check that Cards returned are displayed to the user'
|
|
9
|
+
|
|
10
|
+
input :selected_response_types,
|
|
11
|
+
type: 'checkbox'
|
|
12
|
+
|
|
13
|
+
def format_selected_response_types
|
|
14
|
+
selected_response_types
|
|
15
|
+
.map do |response_type|
|
|
16
|
+
response_type_string =
|
|
17
|
+
response_type.split('_')
|
|
18
|
+
.map(&:capitalize)
|
|
19
|
+
.join(' ')
|
|
20
|
+
.prepend('- ')
|
|
21
|
+
.sub('Smart', 'SMART')
|
|
22
|
+
.sub('Create Update', 'Create/Update')
|
|
23
|
+
.sub('Companions Prerequisites', 'Companions/Prerequisites')
|
|
24
|
+
response_type_string
|
|
25
|
+
end
|
|
26
|
+
.join("\n")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
run do
|
|
30
|
+
identifier = SecureRandom.hex(32)
|
|
31
|
+
wait(
|
|
32
|
+
identifier:,
|
|
33
|
+
message: <<~MESSAGE
|
|
34
|
+
**Approval Workflow Test**:
|
|
35
|
+
|
|
36
|
+
I attest that the following CDS response types were returned and that the client system displays
|
|
37
|
+
each of the CDS Service Cards to the user:
|
|
38
|
+
|
|
39
|
+
#{format_selected_response_types}
|
|
40
|
+
|
|
41
|
+
[Click here](#{resume_pass_url}?token=#{identifier}) if the above statement is **true**.
|
|
42
|
+
|
|
43
|
+
[Click here](#{resume_fail_url}?token=#{identifier}) if the above statement is **false**.
|
|
44
|
+
MESSAGE
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module DaVinciCRDTestKit
|
|
2
|
+
class ClientFHIRApiCreateTest < Inferno::Test
|
|
3
|
+
id :crd_client_fhir_api_create_test
|
|
4
|
+
title 'Create Interaction'
|
|
5
|
+
description %(
|
|
6
|
+
Verify that the CRD client supports the create interaction for the given resource. The capabilities required
|
|
7
|
+
by each resource can be found here: https://hl7.org/fhir/us/davinci-crd/CapabilityStatement-crd-client.html#resourcesSummary1
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
input :create_resources,
|
|
11
|
+
type: 'textarea',
|
|
12
|
+
description:
|
|
13
|
+
'Provide a list of resources to create. e.g., [json_resource_1, json_resource_2]'
|
|
14
|
+
|
|
15
|
+
def resource_type
|
|
16
|
+
config.options[:resource_type]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
run do
|
|
20
|
+
assert_valid_json(create_resources)
|
|
21
|
+
create_resources_list = JSON.parse(create_resources)
|
|
22
|
+
skip_if(!create_resources_list.is_a?(Array), 'Resources to create not inputted in list format, skipping test.')
|
|
23
|
+
|
|
24
|
+
valid_create_resources =
|
|
25
|
+
create_resources_list
|
|
26
|
+
.compact_blank
|
|
27
|
+
.map { |resource| FHIR.from_contents(resource.to_json) }
|
|
28
|
+
.select { |resource| resource.resourceType == resource_type }
|
|
29
|
+
.select { |resource| resource_is_valid?(resource:) }
|
|
30
|
+
|
|
31
|
+
skip_if(valid_create_resources.blank?,
|
|
32
|
+
%(No valid #{resource_type} resources were provided to send in Create requests, skipping test.))
|
|
33
|
+
|
|
34
|
+
valid_create_resources.each do |create_resource|
|
|
35
|
+
fhir_create(create_resource)
|
|
36
|
+
assert_response_status(201)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module DaVinciCRDTestKit
|
|
2
|
+
class ClientFHIRApiReadTest < Inferno::Test
|
|
3
|
+
id :crd_client_fhir_api_read_test
|
|
4
|
+
title 'Read Interaction'
|
|
5
|
+
description %(
|
|
6
|
+
Verify that the CRD client supports the read interaction for the given resource. The capabilities required by
|
|
7
|
+
each resource can be found here: https://hl7.org/fhir/us/davinci-crd/CapabilityStatement-crd-client.html#resourcesSummary1
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
input :resource_ids
|
|
11
|
+
|
|
12
|
+
def resource_type
|
|
13
|
+
config.options[:resource_type]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def no_resources_skip_message
|
|
17
|
+
"No #{resource_type} resource ids were provided, skipping test. "
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def bad_resource_id_message(expected_id)
|
|
21
|
+
"Expected resource to have id: `#{expected_id}`, but found `#{resource.id}`"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
run do
|
|
25
|
+
skip_if resource_ids.blank?, no_resources_skip_message
|
|
26
|
+
|
|
27
|
+
resource_id_list = resource_ids.split(',').map(&:strip)
|
|
28
|
+
assert resource_id_list.present?, "No #{resource_type} id provided."
|
|
29
|
+
|
|
30
|
+
resource_id_list.each do |resource_id_to_read|
|
|
31
|
+
fhir_read resource_type, resource_id_to_read, tags: [resource_type, 'read']
|
|
32
|
+
|
|
33
|
+
assert_response_status(200)
|
|
34
|
+
assert_resource_type(resource_type)
|
|
35
|
+
assert resource.id.present? && resource.id == resource_id_to_read, bad_resource_id_message(resource_id_to_read)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
module DaVinciCRDTestKit
|
|
2
|
+
class ClientFHIRApiSearchTest < Inferno::Test
|
|
3
|
+
id :crd_client_fhir_api_search_test
|
|
4
|
+
title 'Search Interaction'
|
|
5
|
+
description %(
|
|
6
|
+
Verify that the CRD client supports the specified search interaction for the given resource. The capabilities
|
|
7
|
+
required by each resource can be found here: https://hl7.org/fhir/us/davinci-crd/CapabilityStatement-crd-client.html#resourcesSummary1
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
input :search_param_values,
|
|
11
|
+
optional: true
|
|
12
|
+
|
|
13
|
+
attr_accessor :successful_search
|
|
14
|
+
|
|
15
|
+
def resource_type
|
|
16
|
+
config.options[:resource_type]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def search_type
|
|
20
|
+
config.options[:search_type]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def include_searches
|
|
24
|
+
['organization_include', 'practitioner_include', 'location_include']
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reference_search_paramaters
|
|
28
|
+
['organization', 'practitioner', 'patient']
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def bad_resource_id_message(expected_id, actual_id)
|
|
32
|
+
"Expected resource to have id: `#{expected_id}`, but found `#{actual_id}`"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def perform_fhir_search(search_params, tags)
|
|
36
|
+
fhir_search(resource_type, params: search_params, tags:)
|
|
37
|
+
assert_response_status(200)
|
|
38
|
+
assert_resource_type(:bundle)
|
|
39
|
+
resource
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def status_search_result_check(bundle, status)
|
|
43
|
+
return if bundle.entry.empty?
|
|
44
|
+
|
|
45
|
+
self.successful_search = true
|
|
46
|
+
|
|
47
|
+
bundle.entry
|
|
48
|
+
.reject { |entry| entry&.resource&.resourceType == 'OperationOutcome' }
|
|
49
|
+
.map(&:resource)
|
|
50
|
+
.each do |resource|
|
|
51
|
+
assert_resource_type(resource_type, resource:)
|
|
52
|
+
assert(resource.status == status, %(
|
|
53
|
+
Each #{resource_type} resource in search result bundle should have a status of `#{status}`, instead got:
|
|
54
|
+
`#{resource.status}` for resource with id: `#{resource.id}`
|
|
55
|
+
))
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def check_id_search_result_entry(bundle_entry, search_id, entry_resource_type)
|
|
60
|
+
assert_resource_type(entry_resource_type, resource: bundle_entry)
|
|
61
|
+
|
|
62
|
+
assert bundle_entry.id.present?, "Expected id field in returned #{entry_resource_type} resource"
|
|
63
|
+
|
|
64
|
+
assert bundle_entry.id == search_id,
|
|
65
|
+
bad_resource_id_message(search_id, bundle_entry.id)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def id_search_result_check(bundle, search_id)
|
|
69
|
+
warning do
|
|
70
|
+
assert bundle.entry.any?,
|
|
71
|
+
"Search result bundle is empty for #{resource_type} _id search with an id of `#{search_id}`"
|
|
72
|
+
end
|
|
73
|
+
return if bundle.entry.empty?
|
|
74
|
+
|
|
75
|
+
self.successful_search = true
|
|
76
|
+
|
|
77
|
+
bundle.entry
|
|
78
|
+
.reject { |entry| entry&.resource&.resourceType == 'OperationOutcome' }
|
|
79
|
+
.map(&:resource)
|
|
80
|
+
.each do |resource|
|
|
81
|
+
check_id_search_result_entry(resource, search_id, resource_type)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def check_include_reference(base_resource_entry, include_resource_id, include_resource_type)
|
|
86
|
+
base_resource_references = Array.wrap(get_reference_field(include_resource_type, base_resource_entry)).compact
|
|
87
|
+
|
|
88
|
+
assert(base_resource_references.present?, %(
|
|
89
|
+
#{resource_type} resource with id #{base_resource_entry.id} did not include the field that references a
|
|
90
|
+
#{include_resource_type} resource}
|
|
91
|
+
))
|
|
92
|
+
|
|
93
|
+
base_resource_reference_match_found = base_resource_references.any? do |base_resource_reference|
|
|
94
|
+
base_resource_reference.reference_id == include_resource_id
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
assert(base_resource_reference_match_found, %(
|
|
98
|
+
The #{resource_type} resource in search result bundle with id #{base_resource_entry.id} did not have a
|
|
99
|
+
#{include_resource_type} reference with an id of `#{include_resource_id}`.`
|
|
100
|
+
))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def include_search_result_check(bundle, search_id, included_resource_type) # rubocop:disable Metrics/CyclomaticComplexity
|
|
104
|
+
warning do
|
|
105
|
+
assert bundle.entry.any?,
|
|
106
|
+
"Search result bundle is empty for #{resource_type} _include #{search_type} search with an id
|
|
107
|
+
of `#{search_id}`"
|
|
108
|
+
end
|
|
109
|
+
return if bundle.entry.empty?
|
|
110
|
+
|
|
111
|
+
self.successful_search = true
|
|
112
|
+
|
|
113
|
+
base_resource_entry_list = bundle.entry.select do |entry|
|
|
114
|
+
entry.resource&.resourceType == resource_type
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
assert(base_resource_entry_list.length == 1, %(
|
|
118
|
+
The #{included_resource_type} _include search for #{resource_type} resource with id #{search_id}
|
|
119
|
+
should include exactly 1 #{resource_type} resource, instead got #{base_resource_entry_list.length}.
|
|
120
|
+
))
|
|
121
|
+
|
|
122
|
+
base_resource_entry = base_resource_entry_list.first.resource
|
|
123
|
+
|
|
124
|
+
bundle.entry
|
|
125
|
+
.map(&:resource)
|
|
126
|
+
.each do |resource|
|
|
127
|
+
entry_resource_type = resource.resourceType
|
|
128
|
+
|
|
129
|
+
if entry_resource_type == resource_type
|
|
130
|
+
check_id_search_result_entry(resource, search_id, entry_resource_type)
|
|
131
|
+
elsif entry_resource_type != 'OperationOutcome'
|
|
132
|
+
entry_resource_type = included_resource_type.capitalize
|
|
133
|
+
assert_resource_type(entry_resource_type, resource:)
|
|
134
|
+
|
|
135
|
+
included_resource_id = resource.id
|
|
136
|
+
assert included_resource_id.present?, "Expected id field in returned #{entry_resource_type} resource"
|
|
137
|
+
check_include_reference(base_resource_entry, included_resource_id, included_resource_type)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def get_reference_field(reference_type, entry)
|
|
143
|
+
case reference_type
|
|
144
|
+
when 'patient'
|
|
145
|
+
entry.beneficiary
|
|
146
|
+
when 'practitioner'
|
|
147
|
+
entry.practitioner
|
|
148
|
+
when 'organization'
|
|
149
|
+
if resource_type == 'Encounter'
|
|
150
|
+
entry.serviceProvider
|
|
151
|
+
else
|
|
152
|
+
entry.organization
|
|
153
|
+
end
|
|
154
|
+
when 'location'
|
|
155
|
+
locations = entry.location
|
|
156
|
+
locations.map(&:location)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def reference_search_result_check(bundle, reference_id, reference_type)
|
|
161
|
+
warning do
|
|
162
|
+
assert bundle.entry.any?, %(
|
|
163
|
+
Search result bundle is empty for #{resource_type} #{reference_type} search with a #{reference_type} id
|
|
164
|
+
`#{reference_id}`
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
return if bundle.entry.empty?
|
|
168
|
+
|
|
169
|
+
self.successful_search = true
|
|
170
|
+
|
|
171
|
+
bundle.entry
|
|
172
|
+
.reject { |entry| entry&.resource&.resourceType == 'OperationOutcome' }
|
|
173
|
+
.map(&:resource)
|
|
174
|
+
.each do |resource|
|
|
175
|
+
assert_resource_type(resource_type, resource:)
|
|
176
|
+
|
|
177
|
+
entry_reference_field = get_reference_field(reference_type, resource)
|
|
178
|
+
assert(
|
|
179
|
+
entry_reference_field.present?,
|
|
180
|
+
%(
|
|
181
|
+
#{resource_type} resource with id #{resource.id} did not include the field that references
|
|
182
|
+
a #{reference_type} resource
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
entry_reference_id = entry_reference_field.reference_id
|
|
187
|
+
assert(
|
|
188
|
+
entry_reference_id == reference_id,
|
|
189
|
+
%(
|
|
190
|
+
The #{resource_type} resource in search result bundle with id #{resource.id} should have a
|
|
191
|
+
#{reference_type} reference with an id of `#{reference_id}`, instead got: `#{entry_reference_id}`
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
run do
|
|
198
|
+
if search_type == 'status'
|
|
199
|
+
coverage_status = ['active', 'cancelled', 'draft', 'entered-in-error']
|
|
200
|
+
coverage_status.each do |status|
|
|
201
|
+
bundle = perform_fhir_search({ status: }, [resource_type, 'status_search'])
|
|
202
|
+
status_search_result_check(bundle, status)
|
|
203
|
+
end
|
|
204
|
+
else
|
|
205
|
+
skip_if search_param_values.blank?, 'No search parameters passed in, skipping test.'
|
|
206
|
+
|
|
207
|
+
search_id_list = search_param_values.split(',').map(&:strip)
|
|
208
|
+
search_id_list.each do |search_id|
|
|
209
|
+
if search_type == '_id'
|
|
210
|
+
bundle = perform_fhir_search({ _id: search_id }, [resource_type, 'id_search'])
|
|
211
|
+
id_search_result_check(bundle, search_id)
|
|
212
|
+
elsif reference_search_paramaters.include?(search_type)
|
|
213
|
+
search_params = {}
|
|
214
|
+
search_params[search_type] = search_id
|
|
215
|
+
bundle = perform_fhir_search(search_params, [resource_type, "#{search_type}_search"])
|
|
216
|
+
reference_search_result_check(bundle, search_id, search_type)
|
|
217
|
+
elsif include_searches.include?(search_type)
|
|
218
|
+
include_resource_type = search_type.gsub('_include', '')
|
|
219
|
+
bundle = perform_fhir_search({ _id: search_id, _include: "#{resource_type}:#{include_resource_type}" },
|
|
220
|
+
[resource_type, "include_#{include_resource_type}_search"])
|
|
221
|
+
include_search_result_check(bundle, search_id, include_resource_type)
|
|
222
|
+
else
|
|
223
|
+
raise StandardError,
|
|
224
|
+
'Passed in search_type does not match to any of the search types handled by this search test.'
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
skip_if !successful_search,
|
|
229
|
+
'No resources returned in any of the search result bundles.'
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module DaVinciCRDTestKit
|
|
2
|
+
class ClientFHIRApiUpdateTest < Inferno::Test
|
|
3
|
+
id :crd_client_fhir_api_update_test
|
|
4
|
+
title 'Update Interaction'
|
|
5
|
+
description %(
|
|
6
|
+
Verify that the CRD client supports the update interaction for the given resource. The capabilities required by
|
|
7
|
+
each resource can be found here: https://hl7.org/fhir/us/davinci-crd/CapabilityStatement-crd-client.html#resourcesSummary1
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
input :update_resources,
|
|
11
|
+
type: 'textarea',
|
|
12
|
+
description:
|
|
13
|
+
'Provide a list of resources to update. e.g., [json_resource_1, json_resource_2]'
|
|
14
|
+
|
|
15
|
+
def resource_type
|
|
16
|
+
config.options[:resource_type]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
run do
|
|
20
|
+
assert_valid_json(update_resources)
|
|
21
|
+
update_resources_list = JSON.parse(update_resources)
|
|
22
|
+
skip_if(!update_resources_list.is_a?(Array), 'Resources to update not inputted in list format, skipping test.')
|
|
23
|
+
|
|
24
|
+
valid_update_resources =
|
|
25
|
+
update_resources_list
|
|
26
|
+
.compact_blank
|
|
27
|
+
.map { |resource| FHIR.from_contents(resource.to_json) }
|
|
28
|
+
.select { |resource| resource.resourceType == resource_type }
|
|
29
|
+
.select { |resource| resource_is_valid?(resource:) }
|
|
30
|
+
|
|
31
|
+
skip_if(valid_update_resources.blank?,
|
|
32
|
+
%(No valid #{resource_type} resources were provided to send in Update requests, skipping test.))
|
|
33
|
+
|
|
34
|
+
valid_update_resources.each do |update_resource|
|
|
35
|
+
fhir_update(update_resource, update_resource.id)
|
|
36
|
+
assert_response_status([200, 201])
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module DaVinciCRDTestKit
|
|
2
|
+
class ClientFHIRApiValidationTest < Inferno::Test
|
|
3
|
+
id :crd_client_fhir_api_validation_test
|
|
4
|
+
title 'FHIR Resource Validation'
|
|
5
|
+
description %(
|
|
6
|
+
Verify that the given resources returned from the previous client API interactions are valid resources. Each
|
|
7
|
+
resource is validated against its corresponding [CRD resorce profile](https://hl7.org/fhir/us/davinci-crd/STU2/artifacts.html).
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
def resource_type
|
|
11
|
+
config.options[:resource_type]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def structure_definition_map
|
|
15
|
+
{
|
|
16
|
+
'Practitioner' => 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-practitioner',
|
|
17
|
+
'PractitionerRole' => 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-practitionerrole',
|
|
18
|
+
'Patient' => 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-patient',
|
|
19
|
+
'Encounter' => 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-encounter',
|
|
20
|
+
'Coverage' => 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-coverage',
|
|
21
|
+
'Device' => 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-device',
|
|
22
|
+
'Location' => 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-location',
|
|
23
|
+
'Organization' => 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-organization'
|
|
24
|
+
}.freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def profile_url
|
|
28
|
+
structure_definition_map[resource_type]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
run do
|
|
32
|
+
load_tagged_requests(resource_type)
|
|
33
|
+
skip_if requests.empty?, 'No FHIR api requests were made'
|
|
34
|
+
|
|
35
|
+
requests.keep_if { |req| req.status == 200 }
|
|
36
|
+
skip_if(requests.blank?,
|
|
37
|
+
'There were no successful FHIR API requests made in previous tests to use in validation.')
|
|
38
|
+
|
|
39
|
+
validated_resources =
|
|
40
|
+
requests
|
|
41
|
+
.map(&:resource)
|
|
42
|
+
.compact
|
|
43
|
+
.flat_map { |resource| resource.is_a?(FHIR::Bundle) ? resource.entry.map(&:resource) : resource }
|
|
44
|
+
.select { |resource| resource.resourceType == resource_type }
|
|
45
|
+
.uniq { |resource| resource.to_reference.reference }
|
|
46
|
+
.each { |resource| resource_is_valid?(resource:, profile_url:) }
|
|
47
|
+
|
|
48
|
+
skip_if(validated_resources.blank?,
|
|
49
|
+
%(No #{resource_type} resources were returned from any of the FHIR API requests made in previous tests
|
|
50
|
+
that could be validated.))
|
|
51
|
+
|
|
52
|
+
validation_error_count = messages.count { |msg| msg[:type] == 'error' }
|
|
53
|
+
assert(validation_error_count.zero?,
|
|
54
|
+
%(#{validation_error_count}/#{validated_resources.length} #{resource_type} resources returned from previous
|
|
55
|
+
test's FHIR API requests failed validation.))
|
|
56
|
+
|
|
57
|
+
skip_if validated_resources.blank?, 'No FHIR resources were made in previous tests that could be validated.'
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module DaVinciCRDTestKit
|
|
2
|
+
class DecodeAuthTokenTest < Inferno::Test
|
|
3
|
+
id :crd_decode_auth_token
|
|
4
|
+
title 'Bearer token can be decoded'
|
|
5
|
+
description %(
|
|
6
|
+
Verify that the Bearer token is a properly constructed JWT. As per the [CDS hooks specification](https://cds-hooks.hl7.org/2.0#trusting-cds-clients),
|
|
7
|
+
each time a CDS Client transmits a request to a CDS Service which requires authentication, the request MUST
|
|
8
|
+
include an Authorization header presenting the JWT as a "Bearer" token.
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
output :auth_token, :auth_token_payload_json, :auth_token_header_json
|
|
12
|
+
|
|
13
|
+
uses_request :hook_request
|
|
14
|
+
|
|
15
|
+
run do
|
|
16
|
+
authorization_header = request.request_header('Authorization')&.value
|
|
17
|
+
skip_if authorization_header.blank?, 'Request does not include an Authorization header'
|
|
18
|
+
|
|
19
|
+
assert(authorization_header.start_with?('Bearer '),
|
|
20
|
+
'Authorization token must be a JWT presented as a `Bearer` token')
|
|
21
|
+
|
|
22
|
+
auth_token = authorization_header.delete_prefix('Bearer ')
|
|
23
|
+
output(auth_token:)
|
|
24
|
+
|
|
25
|
+
begin
|
|
26
|
+
payload, header =
|
|
27
|
+
JWT.decode(
|
|
28
|
+
auth_token,
|
|
29
|
+
nil,
|
|
30
|
+
false
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
output auth_token_payload_json: payload.to_json,
|
|
34
|
+
auth_token_header_json: header.to_json
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
assert false, "Token is not a properly constructed JWT: #{e.message}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require_relative '../urls'
|
|
2
|
+
|
|
3
|
+
module DaVinciCRDTestKit
|
|
4
|
+
class EncounterDischargeReceiveRequestTest < Inferno::Test
|
|
5
|
+
include URLs
|
|
6
|
+
|
|
7
|
+
id :crd_encounter_discharge_request
|
|
8
|
+
title 'Request received for encounter-discharge hook'
|
|
9
|
+
description %(
|
|
10
|
+
This test waits for an incoming [encounter-discharge](https://hl7.org/fhir/us/davinci-crd/STU2/hooks.html#encounter-discharge)
|
|
11
|
+
hook request and responds to the client with the response types selected as an input.
|
|
12
|
+
)
|
|
13
|
+
receives_request :encounter_discharge
|
|
14
|
+
|
|
15
|
+
input :iss
|
|
16
|
+
input :encounter_discharge_selected_response_types,
|
|
17
|
+
title: 'Response types to return from encounter-discharge hook requests',
|
|
18
|
+
description: %(
|
|
19
|
+
Select the cards/action response types that the Inferno hook request endpoints will return. The default
|
|
20
|
+
response type that will be returned for this hook is the `Instructions` card type.
|
|
21
|
+
),
|
|
22
|
+
type: 'checkbox',
|
|
23
|
+
default: ['instructions'],
|
|
24
|
+
options: {
|
|
25
|
+
list_options: [
|
|
26
|
+
{
|
|
27
|
+
label: 'External Reference',
|
|
28
|
+
value: 'external_reference'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
label: 'Instructions',
|
|
32
|
+
value: 'instructions'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: 'Coverage Information',
|
|
36
|
+
value: 'coverage_information'
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: 'Request Form Completion',
|
|
40
|
+
value: 'request_form_completion'
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: 'Create/Update Coverage Information',
|
|
44
|
+
value: 'create_update_coverage_info'
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
label: 'Launch SMART Application',
|
|
48
|
+
value: 'launch_smart_app'
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
run do
|
|
54
|
+
wait(
|
|
55
|
+
identifier: "encounter-discharge #{iss}",
|
|
56
|
+
message: %(
|
|
57
|
+
**Encounter Discharge CDS Service Test**:
|
|
58
|
+
|
|
59
|
+
Invoke the encounter-discharge hook and send a request to:
|
|
60
|
+
|
|
61
|
+
`#{encounter_discharge_url}`
|
|
62
|
+
|
|
63
|
+
Inferno will process the request and return CDS cards if successful.
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|