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: User Access Brands Fix Huge Bundle Error #85

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
107 changes: 80 additions & 27 deletions lib/smart_app_launch/smart_access_brands_validate_brands_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,27 @@ class SMARTAccessBrandsValidateBrands < Inferno::Test
This test does not currently validate availability or format of Brand or Portal logos.
)

input :user_access_brands_bundle,
optional: true
def regex_match?(resource_id, reference)
return false if resource_id.blank?

%r{#{resource_id}(?:/[^\/]*|\|[^\/]*)*/?$}.match?(reference)
end

def find_referenced_endpoint(bundle_resource, endpoint_id_ref)
bundle_resource
.entry
.map(&:resource)
.select { |resource| resource.resourceType == 'Endpoint' }
.map(&:id)
.select { |endpoint_id| endpoint_id_ref.include? endpoint_id }
.select { |endpoint_id| regex_match?(endpoint_id, endpoint_id_ref) }
end

def find_parent_organization(bundle_resource, org_reference)
bundle_resource
.entry
.map(&:resource)
.select { |resource| resource.resourceType == 'Organization' }
.find { |parent_org| regex_match?(parent_org.id, org_reference) }
end

def find_extension(extension_array, extension_name)
Expand All @@ -42,9 +53,12 @@ def check_portal_endpoints(portal_endpoints, organization_endpoints)
portal_endpoint_found = organization_endpoints.any? do |endpoint_reference|
portal_endpoint.valueReference.reference == endpoint_reference
end
assert(portal_endpoint_found, %(
next if portal_endpoint_found

add_message('error', %(
Portal endpoints must also appear at Organization.endpoint. The portal endpoint with reference
#{portal_endpoint.valueReference.reference} was not found at Organization.endpoint.))
#{portal_endpoint.valueReference.reference} was not found at Organization.endpoint.
))
end
end

Expand All @@ -56,34 +70,28 @@ def skip_message
)
end

run do
bundle_response = if user_access_brands_bundle.blank?
load_tagged_requests('smart_access_brands_bundle')
skip skip_message if requests.length != 1
requests.first.response_body
else
user_access_brands_bundle
end

skip_if bundle_response.blank?, 'No SMART Access Brands Bundle contained in the response'
def scratch_bundle_resource
scratch[:bundle_resource] ||= {}
end

assert_valid_json(bundle_response)
bundle_resource = FHIR.from_contents(bundle_response)
run do
bundle_resource = scratch_bundle_resource

skip_if bundle_resource.blank?, %(
No successful User Access Brands request was made in the previous test, or no User Access Brands Bundle 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: 'http://hl7.org/fhir/smart-app-launch/StructureDefinition/user-access-brand'
})

organization_resources = bundle_resource
.entry
.map(&:resource)
.select { |resource| resource.resourceType == 'Organization' }

organization_resources.each do |organization|
endpoint_references = organization.endpoint.map(&:reference)
resource_is_valid?(resource: organization)

endpoint_references = organization.endpoint.map(&:reference)
if organization.extension.present?
portal_extension = find_extension(organization.extension, '/organization-portal')
if portal_extension.present?
Expand All @@ -92,13 +100,58 @@ def skip_message
end
end

endpoint_references.each do |endpoint_id_ref|
organization_referenced_endpts = find_referenced_endpoint(bundle_resource, endpoint_id_ref)
assert !organization_referenced_endpts.empty?,
"Organization with id: #{organization.id} references an Endpoint that is not contained in this
bundle."
if organization.endpoint.empty?
if organization.partOf.blank?
add_message('error', %(
Organization with id: #{organization.id} does not have the endpoint or partOf field populated
))
next
end

parent_organization = find_parent_organization(bundle_resource, organization.partOf.reference)

if parent_organization.blank?
add_message('error', %(
Organization with id: #{organization.id} references parent Organization not found in the Bundle:
#{organization.partOf.reference}
))
next
end

if parent_organization.endpoint.empty?
add_message('error', %(
Organization with id: #{organization.id} has parent Organization with id: #{parent_organization.id} that
does not have the `endpoint` field populated.
))
end
else
endpoint_references.each do |endpoint_id_ref|
organization_referenced_endpts = find_referenced_endpoint(bundle_resource, endpoint_id_ref)
next unless organization_referenced_endpts.empty?

add_message('error', %(
Organization with id: #{organization.id} references an Endpoint endpoint_id_ref that is not contained in
this bundle.
))
end
end
end

error_messages = messages.select { |msg| msg[:type] == 'error' }
non_error_messages = messages.reject { |msg| msg[:type] == 'error' }

@messages = []
@messages += error_messages.first(20) unless error_messages.empty?
@messages += non_error_messages.first(50) unless non_error_messages.empty?

if error_messages.count > 20 || non_error_messages.count > 50
info_message = 'Inferno is only showing the first 20 error and 50 warning/information validation messages'
add_message('info', info_message)
end

assert messages.empty? || messages.none? { |msg| msg[:type] == 'error' }, %(
Some Organizations in the Service Base URL Bundle are invalid
)
end
end
end
150 changes: 148 additions & 2 deletions lib/smart_app_launch/smart_access_brands_validate_bundle_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ class SMARTAccessBrandsValidateBundle < Inferno::Test
This test also ensures the Bundle is the 'collection' type and that it is not empty.
)

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 :user_access_brands_bundle,
optional: true

Expand All @@ -20,6 +29,93 @@ 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 parent_org.blank? || 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|
next if resource_already_exists?(new_entries, found_endpoint_entry, 'Endpoint')

new_entries.append(found_endpoint_entry)

endpoint_entries.delete_if do |entry|
entry.resource.resourceType == 'Endpoint' && entry.resource.id == found_endpoint_entry.resource.id
end

resource_validation_limit -= 1
end
end

endpoint_entries.each do |endpoint_entry|
break if resource_validation_limit <= 0

new_entries.append(endpoint_entry)
resource_validation_limit -= 1
end

new_entries
end

def regex_match?(resource_id, reference)
return false if resource_id.blank?

%r{#{resource_id}(?:/[^\/]*|\|[^\/]*)*/?$}.match?(reference)
end

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

def find_referenced_endpoints(organization_endpoints, endpoint_entries)
endpoints = []
organization_endpoints.each do |endpoint_ref|
found_endpoint = endpoint_entries.find do |endpoint_entry|
regex_match?(endpoint_entry.resource.id, endpoint_ref.reference)
end
endpoints.append(found_endpoint) if found_endpoint.present?
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

run do
bundle_response = if user_access_brands_bundle.blank?
load_tagged_requests('smart_access_brands_bundle')
Expand All @@ -29,17 +125,67 @@ def skip_message
user_access_brands_bundle
end

skip_if bundle_response.blank?, 'No SMART Access Brands Bundle contained in the response'
skip_if bundle_response.blank?, %(
No successful User Access Brands request was made in the previous test, or no User Access Brands Bundle was
provided
)

assert_valid_json(bundle_response)
bundle_resource = FHIR.from_contents(bundle_response)
assert_resource_type(:bundle, resource: bundle_resource)
assert_valid_resource(resource: bundle_resource, profile_url: 'http://hl7.org/fhir/smart-app-launch/StructureDefinition/user-access-brands-bundle')

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 SMART Access Brands Bundle is missing the required `type` field')
assert(bundle_resource.type == 'collection', 'The SMART Access Brands Bundle must be type `collection`')
assert(bundle_resource.timestamp.present?,
'Bundle.timestamp must be populated to advertise the timestamp of the last change to the contents')
assert !bundle_resource.entry.empty?, 'The given Bundle does not contain any brands or endpoints.'
assert(bundle_resource.total.blank?, 'The `total` field is not allowed in `collection` type Bundles')

entry_full_urls = []

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

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

assert(entry.fullUrl.exclude?('/_history/'), %(
Bundle entry #{entry_num} 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 SMART Access Brands 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
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ class SMARTAccessBrandsValidateEndpointURLs < Inferno::Test
and available.
)

input :user_access_brands_bundle,
optional: true

input :endpoint_availability_limit,
title: 'Endpoint Availability Limit',
description: %(
Expand Down Expand Up @@ -57,20 +54,17 @@ def skip_message
)
end

run do
bundle_response = if user_access_brands_bundle.blank?
load_tagged_requests('smart_access_brands_bundle')
skip skip_message if requests.length != 1
requests.first.response_body
else
user_access_brands_bundle
end

skip_if bundle_response.blank?, 'No SMART Access Brands Bundle contained in the response'
def scratch_bundle_resource
scratch[:bundle_resource] ||= {}
end

assert_valid_json(bundle_response)
bundle_resource = FHIR.from_contents(bundle_response)
run do
bundle_resource = scratch_bundle_resource

skip_if bundle_resource.blank?, %(
No successful User Access Brands request was made in the previous test, or no User Access Brands Bundle was
provided
)
skip_if bundle_resource.entry.empty?, 'The given Bundle does not contain any resources'

endpoint_list = bundle_resource
Expand Down
Loading
Loading