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-2035: Improve error handling for validator errors #379

Merged
merged 3 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 52 additions & 11 deletions lib/inferno/dsl/fhir_validation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,24 +116,44 @@ def exclude_message(&block)
def resource_is_valid?(resource, profile_url, runnable)
profile_url ||= FHIR::Definitions.resource_definition(resource.resourceType).url

outcome = FHIR::OperationOutcome.new(JSON.parse(validate(resource, profile_url)))
outcome, http_status = validate(resource, profile_url, runnable)

message_hashes = outcome.issue&.map { |issue| message_hash_from_issue(issue, resource) } || []
message_hashes = message_hashes_from_outcome(outcome, resource, profile_url)

message_hashes.concat(additional_validation_messages(resource, profile_url))
message_hashes
.each { |message_hash| runnable.add_message(message_hash[:type], message_hash[:message]) }

filter_messages(message_hashes)
unless http_status == 200
raise Inferno::Exceptions::ErrorInValidatorException,
'Error occurred in the validator. Review Messages tab or validator service logs for more information.'
end

message_hashes
.each { |message_hash| runnable.add_message(message_hash[:type], message_hash[:message]) }
.none? { |message_hash| message_hash[:type] == 'error' }
rescue Inferno::Exceptions::ErrorInValidatorException
raise
rescue StandardError => e
runnable.add_message('error', e.message)
raise Inferno::Exceptions::ErrorInValidatorException,
'Error occurred in the validator. Review Messages tab or validator service logs for more information.'
end

# @private
def filter_messages(message_hashes)
message_hashes.reject! { |message| exclude_message.call(Entities::Message.new(message)) } if exclude_message
end

# @private
def message_hashes_from_outcome(outcome, resource, profile_url)
message_hashes = outcome.issue&.map { |issue| message_hash_from_issue(issue, resource) } || []

message_hashes.concat(additional_validation_messages(resource, profile_url))

filter_messages(message_hashes)

message_hashes
end

# @private
def message_hash_from_issue(issue, resource)
{
Expand Down Expand Up @@ -171,12 +191,33 @@ def issue_message(issue, resource)
#
# @param resource [FHIR::Model]
# @param profile_url [String]
# @return [String] the body of the validation response
def validate(resource, profile_url)
Faraday.new(
url,
params: { profile: profile_url }
).post('validate', resource.source_contents).body
# @param runnable [Inferno::Entities::Test]
# @return [[Array(FHIR::OperationOutcome, Number)] the validation response and HTTP status code
def validate(resource, profile_url, runnable)
begin
response = Faraday.new(
url,
params: { profile: profile_url }
).post('validate', resource.source_contents)
rescue StandardError => e
runnable.add_message('error', e.message)
raise Inferno::Exceptions::ErrorInValidatorException, "Unable to connect to validator at #{url}."
end
outcome = operation_outcome_from_validator_response(response.body, runnable)

[outcome, response.status]
end

# @private
def operation_outcome_from_validator_response(response, runnable)
if response.start_with? '{'
FHIR::OperationOutcome.new(JSON.parse(response))
else
runnable.add_message('error', "Validator Response: #{response}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use a newline here:

Screenshot 2023-08-07 at 8 44 30 AM

raise Inferno::Exceptions::ErrorInValidatorException,
'Validator response was an unexpected format. '\
'Review Messages tab or validator service logs for more information.'
end
end
end

Expand Down
10 changes: 10 additions & 0 deletions lib/inferno/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ def result
end
end

class ErrorInValidatorException < TestResultException
# This extends TestResultException instead of RuntimeError
# to bypass printing the stack trace in the UI.
# (The stack trace of this exception may not be useful,
# instead the message should point to where in the validator an error occurred)
def result
'error'
end
end

class ParentNotLoadedException < RuntimeError
def initialize(klass, id)
super("No #{klass.name.demodulize} found with id '#{id}'")
Expand Down
39 changes: 39 additions & 0 deletions spec/inferno/dsl/fhir_validation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,45 @@
end
end

context 'with error from validator' do
let(:error_outcome) do
{
resourceType: 'OperationOutcome',
issue: [
{
severity: 'fatal',
code: 'structure',
diagnostics: 'Validator still warming up... Please wait',
details: {
text: 'Validator still warming up... Please wait'
}
}
]
}.to_json
end

it 'throws ErrorInValidatorException when validator not ready yet' do
stub_request(:post, "#{validation_url}/validate?profile=#{profile_url}")
.with(body: resource_string)
.to_return(status: 503, body: error_outcome)

expect do
validator.resource_is_valid?(resource, profile_url, runnable)
end.to raise_error(Inferno::Exceptions::ErrorInValidatorException)
expect(runnable.messages.first[:message]).to include('Validator still warming up... Please wait')
end

it 'throws ErrorInValidatorException for non-JSON response' do
stub_request(:post, "#{validation_url}/validate?profile=#{profile_url}")
.with(body: resource_string)
.to_return(status: 500, body: '<html><body>Internal Server Error</body></html>')

expect do
validator.resource_is_valid?(resource, profile_url, runnable)
end.to raise_error(Inferno::Exceptions::ErrorInValidatorException)
end
end

it 'posts the resource with primitive extensions intact' do
stub_request(:post, "#{validation_url}/validate?profile=#{profile_url}")
.with(body: resource_string)
Expand Down