Skip to content

Commit

Permalink
FI-3300: Suite Endpoints (#37)
Browse files Browse the repository at this point in the history
* Rename mock_auth_server to token_endpoint

* Convert mock EHR auth to SuiteEndpoints

* Convert MockPayer to suite endpoints

* Rename and reorganize mock modules

* Convert MockEHR to SuiteEndpoints

* Reorganize Mock modules

* Convert Full EHR Suite to suite endpoints

* Convert Payer suite proxy endpoints to suite endpoints

* Remove record_respons_route monkey patch, MockPayer, MockEHR

* Fix a few remaining issues

* Fix rubocop issues

* Use content-type application/fhir+json

* Fix next question proxy tag

* Move allow_cors to a module instead of monkey patch

* Fix setting of response headers in token endpoint

* Add prefix to fhirContext input in token endpoint
  • Loading branch information
tstrass authored Dec 6, 2024
1 parent 99bb499 commit 8eb6c2a
Show file tree
Hide file tree
Showing 32 changed files with 822 additions and 887 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class DTRSmartAppDinnerQuestionnairePackageRequestTest < Inferno::Test
Inferno will wait for a DTR questionnaire package request from the client. Upon receipt, Inferno will generate and
send a response.
)
config options: { accepts_multiple_requests: true }
input :smart_app_launch,
type: 'radio',
title: 'SMART App Launch',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class DTRRespQuestionnairePackageRequestTest < Inferno::Test
Inferno will wait for a DTR questionnaire package request from the client. Upon receipt, Inferno will generate and
send a response.
)
config options: { accepts_multiple_requests: true }
input :smart_app_launch,
type: 'radio',
title: 'SMART App Launch',
Expand Down
39 changes: 10 additions & 29 deletions lib/davinci_dtr_test_kit/dtr_full_ehr_suite.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
require_relative 'ext/inferno_core/runnable'
require_relative 'ext/inferno_core/record_response_route'
require_relative 'ext/inferno_core/request'
require_relative 'client_groups/dinner_static/dtr_full_ehr_questionnaire_workflow_group'
require_relative 'client_groups/dinner_adaptive/dtr_full_ehr_adaptive_dinner_questionnaire_workflow_group'
require_relative 'auth_groups/oauth2_authentication_group'
require_relative 'mock_payer'
require_relative 'endpoints/cors'
require_relative 'endpoints/mock_authorization/simple_token_endpoint'
require_relative 'endpoints/mock_payer/full_ehr_questionnaire_package_endpoint'
require_relative 'endpoints/mock_payer/full_ehr_next_question_endpoint'
require_relative 'version'

module DaVinciDTRTestKit
class DTRFullEHRSuite < Inferno::TestSuite
extend MockPayer
extend MockAuthServer
extend CORS

id :dtr_full_ehr
title 'Da Vinci DTR Full EHR Test Suite'
Expand Down Expand Up @@ -48,35 +47,17 @@ class DTRFullEHRSuite < Inferno::TestSuite

allow_cors QUESTIONNAIRE_PACKAGE_PATH, NEXT_PATH

def self.test_resumes?(test)
!test.config.options[:accepts_multiple_requests]
end

def self.next_request_tag(test)
test.config.options[:next_tag]
end

record_response_route :post, PAYER_TOKEN_PATH, 'dtr_full_ehr_payer_token',
method(:payer_token_response) do |request|
DTRFullEHRSuite.extract_client_id_from_form_params(request)
end

record_response_route :post, QUESTIONNAIRE_PACKAGE_PATH, QUESTIONNAIRE_PACKAGE_TAG,
method(:questionnaire_package_response), resumes: method(:test_resumes?) do |request|
DTRFullEHRSuite.extract_bearer_token(request)
end
suite_endpoint :post, PAYER_TOKEN_PATH, MockAuthorization::SimpleTokenEndpoint

record_response_route :post, NEXT_PATH, method(:next_request_tag), method(:client_questionnaire_next_response),
resumes: method(:test_resumes?) do |request|
DTRFullEHRSuite.extract_bearer_token(request)
end
suite_endpoint :post, QUESTIONNAIRE_PACKAGE_PATH, MockPayer::FullEHRQuestionnairePackageEndpoint
suite_endpoint :post, NEXT_PATH, MockPayer::FullEHRNextQuestionEndpoint

resume_test_route :get, RESUME_PASS_PATH do |request|
DTRFullEHRSuite.extract_token_from_query_params(request)
request.query_parameters['token']
end

resume_test_route :get, RESUME_FAIL_PATH, result: 'fail' do |request|
DTRFullEHRSuite.extract_token_from_query_params(request)
request.query_parameters['token']
end

group from: :oauth2_authentication
Expand Down
3 changes: 0 additions & 3 deletions lib/davinci_dtr_test_kit/dtr_light_ehr_suite.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
require_relative 'ext/inferno_core/runnable'
require_relative 'ext/inferno_core/record_response_route'
require_relative 'ext/inferno_core/request'
require 'us_core_test_kit'
require 'tls_test_kit'
require_relative 'version'
Expand Down
29 changes: 9 additions & 20 deletions lib/davinci_dtr_test_kit/dtr_payer_server_suite.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
require_relative 'ext/inferno_core/runnable'
require_relative 'ext/inferno_core/record_response_route'
require_relative 'ext/inferno_core/request'
require_relative 'payer_server_groups/payer_server_static_group'
require_relative 'payer_server_groups/payer_server_adaptive_group'
require_relative 'tags'
require_relative 'mock_payer'
require_relative 'mock_auth_server'
require_relative 'endpoints/cors'
require_relative 'endpoints/mock_authorization/simple_token_endpoint'
require_relative 'endpoints/mock_payer/questionnaire_package_proxy_endpoint'
require_relative 'endpoints/mock_payer/next_question_proxy_endpoint'
require_relative 'version'

module DaVinciDTRTestKit
class DTRPayerServerSuite < Inferno::TestSuite
extend MockAuthServer
extend MockPayer
extend CORS

id :dtr_payer_server
title 'Da Vinci DTR Payer Server Test Suite'
Expand Down Expand Up @@ -106,22 +104,13 @@ class DTRPayerServerSuite < Inferno::TestSuite

allow_cors QUESTIONNAIRE_PACKAGE_PATH, NEXT_PATH

record_response_route :post, PAYER_TOKEN_PATH, 'dtr_payer_auth', method(:payer_token_response) do |request|
DTRPayerServerSuite.extract_client_id_from_form_params(request)
end

record_response_route :post, QUESTIONNAIRE_PACKAGE_PATH, QUESTIONNAIRE_TAG,
method(:payer_questionnaire_response), resumes: method(:test_resumes?) do |request|
DTRPayerServerSuite.extract_bearer_token(request)
end
suite_endpoint :post, PAYER_TOKEN_PATH, MockAuthorization::SimpleTokenEndpoint

record_response_route :post, NEXT_PATH, NEXT_TAG,
method(:questionnaire_next_response), resumes: method(:test_resumes?) do |request|
DTRPayerServerSuite.extract_bearer_token(request)
end
suite_endpoint :post, QUESTIONNAIRE_PACKAGE_PATH, MockPayer::QuestionnairePackageProxyEndpoint
suite_endpoint :post, NEXT_PATH, MockPayer::NextQuestionProxyEndpoint

resume_test_route :get, RESUME_PASS_PATH do |request|
DTRPayerServerSuite.extract_token_from_query_params(request)
request.query_parameters['token']
end

group from: :payer_server_static_package
Expand Down
94 changes: 27 additions & 67 deletions lib/davinci_dtr_test_kit/dtr_smart_app_suite.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
require_relative 'ext/inferno_core/runnable'
require_relative 'ext/inferno_core/record_response_route'
require_relative 'ext/inferno_core/request'
require_relative 'auth_groups/oauth2_authentication_group'
require_relative 'client_groups/resp_assist_device/dtr_smart_app_questionnaire_workflow_group'
require_relative 'client_groups/dinner_static/dtr_smart_app_questionnaire_workflow_group'
require_relative 'client_groups/dinner_adaptive/dtr_smart_app_questionnaire_workflow_group'
require_relative 'mock_payer'
require_relative 'mock_ehr'
require_relative 'endpoints/cors'
require_relative 'endpoints/mock_authorization'
require_relative 'endpoints/mock_authorization/authorize_endpoint'
require_relative 'endpoints/mock_authorization/token_endpoint'
require_relative 'endpoints/mock_payer/questionnaire_package_endpoint'
require_relative 'endpoints/mock_payer/next_question_endpoint'
require_relative 'endpoints/mock_ehr'
require_relative 'endpoints/mock_ehr/questionnaire_response_endpoint'
require_relative 'endpoints/mock_ehr/fhir_get_endpoint'
require_relative 'version'

module DaVinciDTRTestKit
class DTRSmartAppSuite < Inferno::TestSuite
extend MockAuthServer
extend MockEHR
extend MockPayer
extend CORS

id :dtr_smart_app
title 'Da Vinci DTR SMART App Test Suite'
Expand Down Expand Up @@ -52,72 +54,30 @@ class DTRSmartAppSuite < Inferno::TestSuite
allow_cors QUESTIONNAIRE_PACKAGE_PATH, QUESTIONNAIRE_RESPONSE_PATH, FHIR_RESOURCE_PATH, FHIR_SEARCH_PATH,
EHR_AUTHORIZE_PATH, EHR_TOKEN_PATH, JKWS_PATH, OPENID_CONFIG_PATH, NEXT_PATH

def self.test_resumes?(test)
!test.config.options[:accepts_multiple_requests]
end

def self.next_request_tag(test)
test.config.options[:next_tag]
end

route(:get, '/fhir/metadata', method(:metadata_handler))

route(:get, SMART_CONFIG_PATH, method(:ehr_smart_config))
route(:get, OPENID_CONFIG_PATH, method(:ehr_openid_config))

route(:get, JKWS_PATH, method(:auth_server_jwks))

record_response_route :get, EHR_AUTHORIZE_PATH, EHR_AUTHORIZE_TAG, method(:ehr_authorize),
resumes: ->(_) { false } do |request|
DTRSmartAppSuite.extract_client_id_from_query_params(request)
end

record_response_route :post, EHR_AUTHORIZE_PATH, EHR_AUTHORIZE_TAG, method(:ehr_authorize),
resumes: ->(_) { false } do |request|
DTRSmartAppSuite.extract_client_id_from_form_params(request)
end

record_response_route :post, EHR_TOKEN_PATH, 'dtr_smart_app_ehr_token', method(:ehr_token_response),
resumes: ->(_) { false } do |request|
DTRSmartAppSuite.extract_client_id_from_token_request(request)
end

record_response_route :post, PAYER_TOKEN_PATH, 'dtr_smart_app_payer_token',
method(:payer_token_response) do |request|
DTRSmartAppSuite.extract_client_id_from_client_assertion(request)
end
# Authorization server
route(:get, SMART_CONFIG_PATH, MockAuthorization.method(:ehr_smart_config))
route(:get, OPENID_CONFIG_PATH, MockAuthorization.method(:ehr_openid_config))
route(:get, JKWS_PATH, MockAuthorization.method(:jwks))
suite_endpoint :get, EHR_AUTHORIZE_PATH, MockAuthorization::AuthorizeEndpoint
suite_endpoint :post, EHR_AUTHORIZE_PATH, MockAuthorization::AuthorizeEndpoint
suite_endpoint :post, EHR_TOKEN_PATH, MockAuthorization::TokenEndpoint

record_response_route :post, QUESTIONNAIRE_PACKAGE_PATH, QUESTIONNAIRE_PACKAGE_TAG,
method(:questionnaire_package_response), resumes: ->(_) { false } do |request|
DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
end

record_response_route :post, NEXT_PATH, method(:next_request_tag), method(:client_questionnaire_next_response),
resumes: method(:test_resumes?) do |request|
DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
end
# Payer
suite_endpoint :post, QUESTIONNAIRE_PACKAGE_PATH, MockPayer::QuestionnairePackageEndpoint
suite_endpoint :post, NEXT_PATH, MockPayer::NextQuestionEndpoint

record_response_route :post, QUESTIONNAIRE_RESPONSE_PATH, 'dtr_smart_app_questionnaire_response',
method(:questionnaire_response_response) do |request|
DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
end

record_response_route :get, FHIR_RESOURCE_PATH, SMART_APP_EHR_REQUEST_TAG, method(:get_fhir_resource),
resumes: ->(_) { false } do |request|
DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
end

record_response_route :get, FHIR_SEARCH_PATH, SMART_APP_EHR_REQUEST_TAG, method(:get_fhir_resource),
resumes: ->(_) { false } do |request|
DTRSmartAppSuite.extract_client_id_from_bearer_token(request)
end
# EHR
route(:get, '/fhir/metadata', MockEHR.method(:metadata))
suite_endpoint :post, QUESTIONNAIRE_RESPONSE_PATH, MockEHR::QuestionnaireResponseEndpoint
suite_endpoint :get, FHIR_RESOURCE_PATH, MockEHR::FHIRGetEndpoint
suite_endpoint :get, FHIR_SEARCH_PATH, MockEHR::FHIRGetEndpoint

resume_test_route :get, RESUME_PASS_PATH do |request|
DTRSmartAppSuite.extract_query_param_value(request)
request.query_parameters['client_id'] || request.query_parameters['token']
end

resume_test_route :get, RESUME_FAIL_PATH, result: 'fail' do |request|
DTRSmartAppSuite.extract_query_param_value(request)
request.query_parameters['client_id'] || request.query_parameters['token']
end

# TODO: Update based on SMART Launch changes. Do we even want to have this group now?
Expand Down
20 changes: 20 additions & 0 deletions lib/davinci_dtr_test_kit/endpoints/cors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module DaVinciDTRTestKit
module CORS
PRE_FLIGHT_HANDLER = proc do
[
200,
{
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Headers' => 'Content-Type, Authorization'
},
['']
]
end

def allow_cors(*paths)
paths.each do |path|
route(:options, path, PRE_FLIGHT_HANDLER)
end
end
end
end
83 changes: 83 additions & 0 deletions lib/davinci_dtr_test_kit/endpoints/mock_authorization.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
module DaVinciDTRTestKit
module MockAuthorization
RSA_PRIVATE_KEY = OpenSSL::PKey::RSA.generate(2048)
RSA_PUBLIC_KEY = RSA_PRIVATE_KEY.public_key
SUPPORTED_SCOPES = ['launch', 'patient/*.rs', 'user/*.rs', 'offline_access', 'openid', 'fhirUser'].freeze

module_function

def extract_client_id_from_bearer_token(request)
token = request.headers['authorization']&.delete_prefix('Bearer ')
jwt =
begin
JWT.decode(token, nil, false)
rescue StandardError
nil
end
jwt&.first&.dig('inferno_client_id')
end

def jwks(_env)
response_body = {
keys: [
{
kty: 'RSA',
alg: 'RS256',
n: Base64.urlsafe_encode64(RSA_PUBLIC_KEY.n.to_s(2), padding: false),
e: Base64.urlsafe_encode64(RSA_PUBLIC_KEY.e.to_s(2), padding: false),
use: 'sig'
}
]
}.to_json

[200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]]
end

def ehr_smart_config(env)
base_url = env_base_url(env, SMART_CONFIG_PATH)
response_body =
{
authorization_endpoint: base_url + EHR_AUTHORIZE_PATH,
token_endpoint: base_url + EHR_TOKEN_PATH,
token_endpoint_auth_methods_supported: ['private_key_jwt'],
token_endpoint_auth_signing_alg_values_supported: ['RS256'],
grant_types_supported: ['authorization_code'],
scopes_supported: SUPPORTED_SCOPES,
response_types_supported: ['code'],
code_challenge_methods_supported: ['S256'],
capabilities: [
'launch-ehr',
'permission-patient',
'permission-user',
'client-public',
'client-confidential-symmetric',
'client-confidential-asymmetric'
]
}.to_json

[200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]]
end

def ehr_openid_config(env)
base_url = env_base_url(env, OPENID_CONFIG_PATH)
response_body = {
issuer: base_url + FHIR_BASE_PATH,
authorization_endpoint: base_url + EHR_AUTHORIZE_PATH,
token_endpoint: base_url + EHR_TOKEN_PATH,
jwks_uri: base_url + JKWS_PATH,
response_types_supported: ['id_token'],
subject_types_supported: ['public'],
id_token_signing_alg_values_supported: ['RS256']
}.to_json
[200, { 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' }, [response_body]]
end

def env_base_url(env, endpoint_path)
protocol = env['rack.url_scheme']
host = env['HTTP_HOST']
path = env['REQUEST_PATH'] || env['PATH_INFO']
path.gsub!(%r{#{endpoint_path}(/)?}, '')
"#{protocol}://#{host + path}"
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module DaVinciDTRTestKit
module MockAuthorization
class AuthorizeEndpoint < Inferno::DSL::SuiteEndpoint
def test_run_identifier
request.params[:client_id]
end

def tags
[EHR_AUTHORIZE_TAG]
end

def make_response
if request.params[:redirect_uri].present?
redirect_uri = "#{request.params[:redirect_uri]}?" \
"code=#{SecureRandom.hex}&" \
"state=#{request.params[:state]}"
response.status = 302
response.headers['Location'] = redirect_uri
else
response.status = 400
response.format = 'application/fhir+json'
response.body = FHIR::OperationOutcome.new(
issue: FHIR::OperationOutcome::Issue.new(severity: 'fatal', code: 'required',
details: FHIR::CodeableConcept.new(
text: 'No redirect_uri provided'
))
).to_json
end
end
end
end
end
Loading

0 comments on commit 8eb6c2a

Please sign in to comment.