Skip to content

Commit

Permalink
Merge pull request #22 from inferno-framework/fi-3154-full-ehr-adapti…
Browse files Browse the repository at this point in the history
…ve-questionnaire-tests

FI-3154: Full EHR Adaptive Questionnaire Tests
  • Loading branch information
vanessuniq authored Nov 27, 2024
2 parents 2327258 + 3b17b82 commit 4ee804d
Show file tree
Hide file tree
Showing 58 changed files with 2,731 additions and 237 deletions.
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
FHIR_RESOURCE_VALIDATOR_URL=http://localhost/hl7validatorapi
REDIS_URL=redis://localhost:6379/0
FHIR_REFERENCE_SERVER=http://localhost:8080/reference-server/r4
FHIRPATH_URL=http://localhost/fhirpath
1 change: 1 addition & 0 deletions .env.production
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
REDIS_URL=redis://redis:6379/0
FHIR_RESOURCE_VALIDATOR_URL=http://hl7_validator_service:3500
FHIR_REFERENCE_SERVER=https://inferno.healthit.gov/reference-server/r4
FHIRPATH_URL=http://fhirpath:6789
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
FHIR_RESOURCE_VALIDATOR_URL=https://example.com/validatorapi
ASYNC_JOBS=false
FHIR_REFERENCE_SERVER=http://example.com/reference-server/r4
FHIRPATH_URL=https://example.com/fhirpath
334 changes: 330 additions & 4 deletions config/DTR Full EHR Tests Postman Demo.postman_collection.json

Large diffs are not rendered by default.

416 changes: 361 additions & 55 deletions config/DTR SMART App Tests Postman Demo.postman_collection.json

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions config/nginx.background.conf
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,20 @@ http {

proxy_pass http://hl7_validator_service:3500/;
}

location /fhirpath/ {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_redirect off;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;

proxy_pass http://fhirpath:6789/;
}
}
}
5 changes: 5 additions & 0 deletions docker-compose.background.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ services:
# To let the service share your local FHIR package cache,
# uncomment the below line
# - ~/.fhir:/home/ktor/.fhir
fhirpath:
image: infernocommunity/fhirpath-service
ports:
- "6789:6789"
nginx:
image: nginx
volumes:
Expand All @@ -32,6 +36,7 @@ services:
command: [nginx, '-g', 'daemon off;']
depends_on:
- hl7_validator_service
- fhirpath
# - fhir_validator_app
redis:
image: redis
Expand Down
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ services:
extends:
file: docker-compose.background.yml
service: hl7_validator_service
fhirpath:
extends:
file: docker-compose.background.yml
service: fhirpath
nginx:
extends:
file: docker-compose.background.yml
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require_relative 'dtr_adaptive_questionnaire_next_question_retrieval_group'

module DaVinciDTRTestKit
class DTRAdaptiveQuestionnaireCompletionGroup < Inferno::TestGroup
id :dtr_adaptive_questionnaire_completion
title 'Completing the Adaptive Questionnaire'
description %(
The client makes a final $next-question call, including the answers to all required questions asked so far.
Inferno will validate that the request conforms to the [next question operation input parameters profile](http://hl7.org/fhir/uv/sdc/StructureDefinition/parameters-questionnaire-next-question-in)
and will update the status of the QuestionnaireResponse resource parameter to `complete`.
Inferno will also validate the completed QuestionnaireResponse conformance.
)

config(
options: {
next_question_prompt_title: 'Last Next Question Request'
}
)
run_as_group

group from: :dtr_adaptive_questionnaire_next_question_retrieval
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require_relative 'dtr_adaptive_questionnaire_next_question_retrieval_group'

module DaVinciDTRTestKit
class DTRAdaptiveQuestionnaireFollowupQuestionsGroup < Inferno::TestGroup
id :dtr_adaptive_questionnaire_followup_questions
title 'Retrieving the Next Question'
description %(
The client makes a subsequent call to request the next question or set of questions
using the $next-question operation, and including the answers to all required questions
in the questionnaire to this point.
Inferno will validate that the request conforms to the [next question operation input parameters profile](http://hl7.org/fhir/uv/sdc/StructureDefinition/parameters-questionnaire-next-question-in)
and will provide the next questions accordingly for the tester to complete and attest to pre-population
and questionnaire rendering.
)

config(
options: {
next_question_prompt_title: 'Follow-up Next Question Request'
}
)

run_as_group

group from: :dtr_adaptive_questionnaire_next_question_retrieval
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
require_relative '../../urls'

module DaVinciDTRTestKit
class DTRAdaptiveQuestionnaireNextQuestionRequestTest < Inferno::Test
include URLs

id :dtr_adaptive_questionnaire_next_question_request
title 'Invoke the $next-question operation'
description %(
Inferno will wait for the client to invoke the $next-question operation to retrieve the next question
or set of questions.
Inferno will validate the request body and update the contained Questionnaire to include
the next question or set of questions.
)

input :access_token

def example_client_jwt_payload_part
Base64.strict_encode64({ inferno_client_id: access_token }.to_json).delete('=')
end

def request_identification
if config.options[:smart_app]
"eyJhbGciOiJub25lIn0.#{example_client_jwt_payload_part}"
else
access_token
end
end

def cont_test_description
<<~DESCRIPTION
### Continuing the Tests
When the DTR application has finished loading the Questionnaire,
including any clinical data requests to support pre-population,
[Click here](#{resume_pass_url}?token=#{access_token}) to continue.
DESCRIPTION
end

def next_question_prompt_title
config.options[:next_question_prompt_title]
end

def next_question_prompt
if next_question_prompt_title&.include?('Initial')
'Invoke the $next-question operation by sending a POST request to'
elsif next_question_prompt_title&.include?('Last')
'Answer the remaining questions and then make a final next-question request by sending a POST request to'
else
"Answer the 'What do you want for dinner' question and then make a next-question request by sending a POST " \
'request to'
end
end

def prompt_cont
if next_question_prompt_title&.include?('Initial')
%(Upon receipt, Inferno will provide the first set of questions to complete.)
elsif next_question_prompt_title&.include?('Last')
%(Upon receipt, Inferno will update the status of the QuestionnaireResponse
resource parameter to `complete`.)
else
%(Upon receipt, Inferno will provide the next set of questions to complete
based on previous answers.)
end
end

run do
wait(
identifier: access_token,
message: <<~MESSAGE
### #{next_question_prompt_title}
#{next_question_prompt}
`#{next_url}`
#{prompt_cont}
### Request Identification
In order to identify requests for this session, Inferno will look for
an `Authorization` header with value:
```
Bearer #{request_identification}
```
#{cont_test_description if config.options[:accepts_multiple_requests]}
MESSAGE
)
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
require_relative '../../urls'

module DaVinciDTRTestKit
class DTRAdaptiveQuestionnaireNextQuestionRequestValidationTest < Inferno::Test
include URLs

id :dtr_next_question_request_validation
title '$next-question request is valid'
description %(
Per the [OperationDefinition: Adaptive questionnaire next question](https://build.fhir.org/ig/HL7/sdc/OperationDefinition-Questionnaire-next-question.html#root)
section in the [Structured Data Capture IG](http://hl7.org/fhir/uv/sdc/ImplementationGuide/hl7.fhir.uv.sdc),
the request body for the `$next-question` operation should be a FHIR Parameters resource containing a
single parameter with:
- name: `questionnaire-response`
- resource: A `QuestionnaireResponse` resource
As outlined in the [FHIR Operation Request](https://hl7.org/fhir/r4/operations.html#request) section of the
FHIR specification, if an operation has exactly one input parameter of type Resource, it can also be invoked via
a POST request using that resource as the body (with no additional URL parameters).
This test validates the structure of the `$next-question` request body. It confirms that the body is either a
Parameters resource or a QuestionnaireResponse resource.
If it is a Parameters resource, it must contain one parameter named `questionnaire-response`
with a `resource` attribute set to a FHIR QuestionnaireResponse instance, as specified above.
The QuestionnaireResponse resource's structure and conformance will be validated
in the following test ('Adaptive QuestionnaireResponse is valid').
)

def assert_valid_resource_type(resource)
type = resource.resourceType
valid = ['Parameters', 'QuestionnaireResponse'].include?(type)
assert valid, "Request body not valid. Expected Parameters or QuestionnaireResponse, got #{type}"
end

def next_request_tag
config.options[:next_tag]
end

run do
load_tagged_requests next_request_tag
skip_if request.blank?, 'A $next-question request must be made prior to running this test'

assert request.url == next_url, "Request made to wrong URL: #{request.url}. Should instead be to #{next_url}"
assert_valid_json(request.request_body)
input_params = FHIR.from_contents(request.request_body)
assert input_params.present?, 'Request does not contain a recognized FHIR object'
assert_valid_resource_type(input_params)

if input_params.is_a?(FHIR::Parameters)
questionnaire_response_params = input_params.parameter.select { |param| param.name == 'questionnaire-response' }
qr_params_count = questionnaire_response_params.length
assert qr_params_count == 1,
"Input parameter must contain one `parameter:questionnaire-response` slice. Found #{qr_params_count}"

questionnaire_response = questionnaire_response_params.first.resource
assert_resource_type(:questionnaire_response, resource: questionnaire_response)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require_relative 'dtr_adaptive_questionnaire_next_question_request_test'
require_relative 'dtr_adaptive_questionnaire_next_question_request_validation_test'
require_relative 'dtr_adaptive_questionnaire_response_validation_test'

module DaVinciDTRTestKit
class DTRAdaptiveQuestionnaireNextQuestionRetrievalGroup < Inferno::TestGroup
id :dtr_adaptive_questionnaire_next_question_retrieval
title 'Next Question Request and Validation'
description %(
Inferno will wait for the client system to request the next question (or set of questions) using the
$next-question operation and will return an updated QuestionnaireResponse with a contained Questionnaire
that includes the next question (or set of questions) for the tester to complete.
Inferno will then validate the conformance of the request.
)

# Test 1: wait for the $next-question request
test from: :dtr_adaptive_questionnaire_next_question_request
# Test 2: validate the $next-question request
test from: :dtr_next_question_request_validation
# Test 3: validate the QuestionnaireResponse in the input parameter
test from: :dtr_adaptive_questionnaire_response_validation
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
require_relative '../../urls'
require_relative '../../dtr_questionnaire_response_validation'

module DaVinciDTRTestKit
class DTRAdaptiveQuestionnaireResponseValidationTest < Inferno::Test
include URLs
include DTRQuestionnaireResponseValidation

id :dtr_adaptive_questionnaire_response_validation
title 'Adaptive QuestionnaireResponse is valid'
description %(
This test validates the conformance of the Adative QuestionnaireResponse to the
[SDCQuestionnaireResponseAdapt](http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaireresponse-adapt)
structure. It verifies the presence of mandatory elements and that elements
with required bindings contain appropriate values.
It also ensures that all required questions are answered, and that the `origin.source`
extension is correct for each answer:
- `PBD.1` (Last Name) and `LOC.1` (Location): `auto`
- `PBD.2` (First Name): `override`
- `3` (all nested dinner questions): `manual`
Note: For the initial next-question request, only the conformance to the profile is checked
since neither the QuestionnaireResponse nor the contained Questionnaire will have any items,
as no questions are yet known.
)

def profile_url
'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaireresponse-adapt'
end

def next_request_tag
config.options[:next_tag]
end

run do
load_tagged_requests next_request_tag
skip_if request.blank?, 'A $next-question request must be made prior to running this test'

assert request.url == next_url, "Request made to wrong URL: #{request.url}. Should instead be to #{next_url}"
assert_valid_json(request.request_body)
input_params = FHIR.from_contents(request.request_body)
skip_if input_params.blank?, 'Request does not contain a recognized FHIR object'

questionnaire_response = nil
if input_params.is_a?(FHIR::QuestionnaireResponse)
questionnaire_response = input_params
elsif input_params.is_a?(FHIR::Parameters)
questionnaire_response = input_params.parameter&.find do |param|
param.name == 'questionnaire-response'
end&.resource
end

skip_if questionnaire_response.nil?, 'QuestionnaireResponse resource not provided.'
verify_basic_conformance(questionnaire_response.to_json, profile_url)

questionnaire = questionnaire_response.contained.find { |res| res.resourceType == 'Questionnaire' }
check_origin_sources(questionnaire.item, questionnaire_response.item, expected_overrides: ['PBD.2'])

required_link_ids = extract_required_link_ids(questionnaire.item)
check_answer_presence(questionnaire_response.item, required_link_ids)

assert(messages.none? { |m| m[:type] == 'error' }, 'QuestionnaireResponse is not correct, see error message(s)')
end
end
end
Loading

0 comments on commit 4ee804d

Please sign in to comment.