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,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
|