Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FI-3263: Huge Bundle Fix #39

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
31 changes: 29 additions & 2 deletions lib/service_base_url_test_kit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,36 @@ class ServiceBaseURLTestSuite < Inferno::TestSuite
].freeze

# All FHIR validation requests will use this FHIR validator
fhir_resource_validator :default do
fhir_resource_validator do
$num_messages = 0
$capped_message = false
$num_errors = 0
$capped_errors = false

exclude_message do |message|
VALIDATION_MESSAGE_FILTERS.any? { |filter| filter.match? message.message }
matches_filter = VALIDATION_MESSAGE_FILTERS.any? { |filter| filter.match? message.message }

unless matches_filter
if message.type == 'error'
$num_errors += 1
else
$num_messages += 1
end
end

matches_filter ||
(message.type != 'error' && $num_messages > 50 && !message.message.include?('Inferno is only showing the first')) ||
(message.type == 'error' && $num_errors > 20 && !message.message.include?('Inferno is only showing the first'))
end

perform_additional_validation do
if $num_messages > 50 && !$capped_message
$capped_message = true
{ type: 'info', message: 'Inferno is only showing the first 50 validation info and warning messages.' }
elsif $num_errors > 20 && !$capped_errors
$capped_errors = true
{ type: 'error', message: 'Inferno is only showing the first 20 validation error messages.' }
end
end
end

Expand Down
190 changes: 146 additions & 44 deletions lib/service_base_url_test_kit/service_base_url_validate_group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ class ServiceBaseURLBundleTestGroup < Inferno::TestGroup
input :service_base_url_bundle,
optional: true

input :resource_validation_limit,
title: 'Limit Validation to a Maximum Resource Count',
description: %(
Input a number to limit the number of Bundle entries that are validated. For very large bundles, it is
recommended to limit the number of Bundle entries to avoid long test run times.
To validate all, leave blank.
),
optional: true

input :endpoint_availability_success_rate,
title: 'Endpoint Availability Success Rate',
description: %(
Expand Down Expand Up @@ -92,6 +101,74 @@ def skip_message
)
end

def get_resource_entries(bundle_resource, resource_type)
bundle_resource
.entry
.select { |entry| entry.resource.resourceType == resource_type }
.uniq
end

def limit_bundle_entries(resource_validation_limit, bundle_resource)
new_entries = []

organization_entries = get_resource_entries(bundle_resource, 'Organization')
endpoint_entries = get_resource_entries(bundle_resource, 'Endpoint')

organization_entries.each do |organization_entry|
break if resource_validation_limit <= 0

new_entries.append(organization_entry)
resource_validation_limit -= 1

found_endpoint_entries = []
organization_resource = organization_entry.resource

if organization_resource.endpoint.present?
found_endpoint_entries = find_referenced_endpoints(organization_resource.endpoint, endpoint_entries)
elsif organization_resource.partOf.present?
parent_org = find_parent_organization_entry(organization_entries, organization_resource.partOf.reference)

unless resource_already_exists?(new_entries, parent_org, 'Organization')
new_entries.append(parent_org)
resource_validation_limit -= 1

parent_org_resource = parent_org.resource
found_endpoint_entries = find_referenced_endpoints(parent_org_resource.endpoint, endpoint_entries)
end
end

found_endpoint_entries.each do |found_endpoint_entry|
unless resource_already_exists?(new_entries, found_endpoint_entry, 'Endpoint')
new_entries.append(found_endpoint_entry)
resource_validation_limit -= 1
end
end
end
new_entries
end

def find_parent_organization_entry(organization_entries, org_reference)
organization_entries
.find { |parent_org_entry| org_reference.include? parent_org_entry.resource.id }
end

def find_referenced_endpoints(organization_endpoints, endpoint_entries)
endpoints = []
organization_endpoints.each do |endpoint_ref|
found_endpoint = endpoint_entries.find do |endpoint_entry|
endpoint_ref.reference.include?(endpoint_entry.resource.id)
end
endpoints.append(found_endpoint)
end
endpoints
end

def resource_already_exists?(new_entries, found_resource_entry, resource_type)
new_entries.any? do |entry|
entry.resource.resourceType == resource_type && (entry.resource.id == found_resource_entry.resource.id)
end
end

# Valid BUNDLE TESTS
test do
id :service_base_url_valid_bundle
Expand All @@ -112,24 +189,73 @@ def skip_message

assert_valid_json(bundle_response)
bundle_resource = FHIR.from_contents(bundle_response)
assert_valid_resource(resource: bundle_resource)

assert_resource_type(:bundle, resource: bundle_resource)

info do
assert !bundle_resource.entry.empty?, %(
The given Bundle does not contain any resources
)
end

additional_resources = bundle_resource.entry
.map { |entry| entry.resource.resourceType }
.reject { |resource_type| ['Organization', 'Endpoint'].include?(resource_type) }
.uniq
if resource_validation_limit.present?
limited_entries = limit_bundle_entries(resource_validation_limit.to_i,
bundle_resource)
bundle_resource.entry = limited_entries
end

scratch[:bundle_resource] = bundle_resource

assert(bundle_resource.type.present?, 'The Service Base URL Bundle Bundle is missing the required `type` field')
assert(bundle_resource.type == 'collection', 'The Service Base URL Bundle must be type `collection`')
assert(bundle_resource.total.blank?, 'The `total` field is not allowed in `collection` type Bundles')

entry_full_urls = []
additional_resources = []

bundle_resource.entry.each_with_index do |entry, index|
assert(entry.resource.present?, %(
Bundle entry #{index} missing the `resource` field. For Bundles of type collection, all entries must contain
resources.
))

unless ['Organization', 'Endpoint'].include?(entry.resource.resourceType)
additional_resources.append(entry.resource.resourceType)
end

assert(entry.request.blank?, %(
Bundle entry #{index} contains the `request` field. For Bundles of type collection, all entries must not
have request or response elements
))
assert(entry.response.blank?, %(
Bundle entry #{index} contains the `response` field. For Bundles of type collection, all entries must not
have request or response elements
))
assert(entry.search.blank?, %(
Bundle entry #{index} contains the `search` field. Entry.search is allowed only for `search` type Bundles.
))

assert(entry.fullUrl.exclude?('/_history/'), %(
Bundle entry #{index} contains a version specific reference in the `fullUrl` field
))

full_url_exists = entry_full_urls.any? do |hash|
hash['fullUrl'] == entry.fullUrl && hash['versionId'] == entry.resource&.meta&.versionId
end

assert(!full_url_exists, %(
The Service Base URL Bundle contains entries with duplicate fullUrls (#{entry.fullUrl}) and versionIds
(#{entry.resource&.meta&.versionId}). FullUrl must be unique in a bundle, or else entries with the same
fullUrl must have different meta.versionId
))

entry_full_urls.append({ 'fullUrl' => entry.fullUrl, 'versionId' => entry.resource&.meta&.versionId })
end

warning do
assert(additional_resources.empty?, %(
The Service Base URL List contained the following additional resources other than Endpoint and
Organization resources: #{additional_resources.join(', ')}))
unique_additional_resources = additional_resources.uniq
assert(unique_additional_resources.empty?, %(
The Service Base URL List contained the following additional resources other than Endpoint and
Organization resources: #{unique_additional_resources.join(', ')}))
end
end
end
Expand All @@ -152,17 +278,9 @@ def skip_message
)

run do
bundle_response = if service_base_url_bundle.blank?
load_tagged_requests('service_base_url_bundle')
skip skip_message if requests.length != 1
requests.first.response_body
else
service_base_url_bundle
end
skip_if bundle_response.blank?, 'No Bundle response was provided'
bundle_resource = scratch[:bundle_resource]

assert_valid_json(bundle_response)
bundle_resource = FHIR.from_contents(bundle_response)
skip_if bundle_resource.blank?, 'No Bundle response was provided'

skip_if bundle_resource.entry.empty?, 'The given Bundle does not contain any resources'

Expand Down Expand Up @@ -196,17 +314,9 @@ def skip_message
)

run do
bundle_response = if service_base_url_bundle.blank?
load_tagged_requests('service_base_url_bundle')
skip skip_message if requests.length != 1
requests.first.response_body
else
service_base_url_bundle
end
skip_if bundle_response.blank?, 'No Bundle response was provided'
bundle_resource = scratch[:bundle_resource]

assert_valid_json(bundle_response)
bundle_resource = FHIR.from_contents(bundle_response)
skip_if bundle_resource.blank?, 'No Bundle response was provided'

skip_if bundle_resource.entry.empty?, 'The given Bundle does not contain any resources'

Expand Down Expand Up @@ -292,25 +402,12 @@ def skip_message
)

run do
bundle_response = if service_base_url_bundle.blank?
load_tagged_requests('service_base_url_bundle')
skip skip_message if requests.length != 1
requests.first.response_body
else
service_base_url_bundle
end
skip_if bundle_response.blank?, 'No Bundle response was provided'
bundle_resource = scratch[:bundle_resource]

assert_valid_json(bundle_response)
bundle_resource = FHIR.from_contents(bundle_response)
skip_if bundle_resource.blank?, 'No Bundle response was provided'

skip_if bundle_resource.entry.empty?, 'The given Bundle does not contain any resources'

assert_valid_bundle_entries(bundle: bundle_resource,
resource_types: {
Organization: nil
})

organization_resources = bundle_resource
.entry
.map(&:resource)
Expand All @@ -323,6 +420,11 @@ def skip_message
'The provided Service Base URL List contains only 1 Organization resource')
end

assert_valid_bundle_entries(bundle: bundle_resource,
resource_types: {
Organization: nil
})

organization_resources.each do |organization|
assert !organization.address.empty?,
"Organization with id: #{organization.id} does not have the address field populated"
Expand Down
Loading
Loading