Skip to main content
Link Menu Expand (external link) Document Search Copy Copied

Waiting for Incoming Requests

Some testing workflows require Inferno to pause until it receives an incoming request. For example:

  • The OAuth2 workflow used by the SMART App Launch IG involves redirecting the user to an authorization server, which then redirects the user back to the application that requested authorization.
  • When testing a client system, Inferno must wait for the client to initiate the requests.

More generally, Inferno execution must pause and listen for requests whenever an external action is needed, such as when the tester is asked to

  • Perform an action in the system under test, e.g., authorize access in the workflow above,
  • Send one or more requests to Inferno, e.g., send a request to an Inferno-hosted FHIR API, or
  • Attest that a requirement has been met.

When waiting, Inferno needs to know how to identify the session the incoming request is for and when to restart its own execution.

Wait Method

A test is instructed to wait for an incoming request using the wait method. wait takes three arguments:

  • identifier - An identifier which can uniquely identify the current test session. It must be possible for this identifier to be reconstructed based on the incoming request.
  • message - A markdown string displayed to the user while the test is waiting.
  • timeout - The number of seconds the test will wait. If timeout is not specified, it defaults to 300 seconds.

Wait Modal

While a test is waiting, a “User Action Required” modal dialog is displayed to the user with the message defined in the wait call. The modal includes a CANCEL button, which can be used to end the test run if the tester does not want to or cannot complete the requested action.

The “User Action Required” modal displays a live countdown of the remaining seconds before the timeout is reached. When the countdown reaches zero, the test run expires and will no longer respond to incoming requests — the only option at that point is to click CANCEL to end the run.

Example of the Wait modal
The 'User Action Required' modal displayed while a test is in the wait state

A screenshot of the User Action Required modal dialog, showing a message to the user with options on how to progress the test, a live countdown timer, and a Cancel button

wait in the API docs

Simple Incoming Request Handling

The route for making a test resume execution is created with resume_test_route, which takes the following arguments:

  • method - A symbol for the HTTP verb for the incoming request (:get, :post, etc.)
  • path - A string for the route path. The route will be served at INFERNO_BASE/custom/SUITE_ID/CUSTOM_ROUTE_PATH.
  • tags - An array of tags to apply to the request. See Tagged Requests.
  • result - A string (default "pass") representing the result that the wait test should receive when the incoming request is received. See Results for possible values.
  • A block that extracts the identifier from the incoming request and returns it. In this block, request can be used to access a Request object which contains the details of the incoming request.

If it is necessary to inspect the incoming request in a test, the incoming request can be assigned a name using receives_request :my_request_name (see Reusing Requests).

To set a result_message along with the result, e.g., to indicate the resason for a failure, the message url parameter can be set when invoking the resume route.

resume_test_route in the API docs

receives_request in the API docs

Example

This example will show how to implement the redirect flow in the SMART App Launch Standalone Launch Sequence. The steps are as follows:

  • Redirect the user to the system under test’s authorize endpoint.
    • The client (Inferno) generates a random state value which the authorization server sends back, so state can be used as the identifier.
  • Wait for the user to be redirected back to Inferno.
    • Extract state from the incoming request to match the current test session.
  • Check that the incoming request contained a code parameter.
class SMARTAppLaunchSuite < Inferno::TestSuite
  id :smart_app_launch
  
  # This route will be served at INFERNO_BASE/custom/smart_app_launch/redirect
  # Since the `state` query parameter is what uniquely links the incoming request
  # to the current test session, return that from the block.
  resume_test_route :get, '/redirect' do |request|
    request.query_parameters['state']
  end
  
  group do
    id :standalone_launch
    
    test do
      id :smart_redirect
      
      # Assign a name to the incoming request so that it can be inspected by
      # other tests.
      receives_request :redirect
      
      run do
        # Generate a random unique state value which uniquely identifies this
        # authorization request.
        state = SecureRandom.uuid
        
        # Build authorization url based on information from discovery, app
        # registration, and state.
        authorization_url = ...
        
        # Make this test wait.
        wait(
          identifier: state,
          message: %(
            [Follow this link to authorize with the SMART server](#{authorization_url}).

            Tests will resume once Inferno receives a request at
            `#{Inferno::Application['base_url']}/custom/smart_app_launch/redirect`
            with a state of `#{state}`.
          )
        )
      end
    end
    
    # Execution will resume with this test once the incoming request is
    # received.
    test do
      id :redirect_contains_code
      
      # Make the incoming request from the previous test available here.
      uses_request :redirect
      
      run do
        code = request.query_parameters['code']
        
        assert code.present?, 'No `code` parameter received'
      end
    end
  end
end

Advanced Incoming Request Handling

The suite_endpoint method can be used for more advanced handling of incoming requests, such as:

  • handling multiple incoming requests with customized logic to determine when to resume the test, or
  • returning particular responses to incoming requests rather than redirecting the user to the UI. Like resume_test_route, suite_endpoint takes a method (such as :get or :post), and a path for the final url fragment in the route. All of the other configuration, however, is handled by an Inferno::DSL::SuiteEndpoint class.

A SuiteEndpoint is based on a Hanami endpoint, so much of the functionality is the same. In a SuiteEndpoint, request and response return Hanami request/response objects. There are several methods that can be overridden to control the behavior of a SuiteEndpoint:

  • test_run_identifier (Required) - This method needs to return the identifier value specified by wait in the waiting test based on information in the incoming request
  • make_response (Required) - This method constructs the response.
  • persist_result? (Optional) - When this method returns true (which is the default) the incoming request will be persisted, automatically associated with the waiting test and visible in its Requests tab, and accessible to other tests when loaded using tags or by name.
  • tags (Optional) - This method defines which tags, if any, should be applied to the request so that it can be retrieved for analysis later (see the Tagged Requests section for details). If neither tags nor a name are associated with a request, it will not be retrievable.
  • name (Optional) - This method defines a name for the request for use in associating the request with an Inferno test using uses_request. If neither tags nor a name are associated with a request, it will not be retrievable.
  • update_result (Optional) - Override this method to (conditionally) update the waiting test’s result after processing a request. Updating the result to anything other than waiting will cause Inferno to resume the test run. See the example below for how to update the result.
  • error_response_format (Optional) - Not a method, but can be set to :operation_outcome to cause error responses to be returned as FHIR OperationOutcome resources. See the syntax in the example below.
  • test_run_identifier_location_description (Optional) - provide a short narrative string describing where this endpoint looks for the test_run_identifier. If provided, the string will be used in creating error messages when the target session cannot be found. When used in errors, it will follow the text “found in”.
  • no_session_response (Optional) - provide a custom error response, including the response body, content type, and http response code, for Inferno to use when no session was found, either because of a missing test_run_identifier or one not associated with a session. Use when the standard text and OperationOutcome options are not correct, such as for OAuth endpoints.

suite_endpoint in the API docs

Inferno::DSL::SuiteEndpoint in the API docs

“No Session” Error responses

When Inferno is unable to identify an active waiting session for a request made to a suite endpoint, a 500 error is returned. By default, the body of the response is plain text indicating that no session was found. Developers have the option of adding details to that error message and changing its format. These details are important to help testers debug connectivity issues:

  • Override method test_run_identifier_location_description: provide a short narrative description of where the test run identifier is expected to be found in requests made to this endpoint. Inferno adds this detail to the errors returned by the endpoint when no session is found to help testers debug the issue.
  • Set error_response_format :operation_outcome: including this line in the suite endpoint definition will cause the error response to be returned as a FHIR OperationOutcome resource. This option should be used for suite endpoints that normally return FHIR resources.
  • Override method no_session_response: if neither text nor FHIR OperationOutcome is appropriate for these errors when returned from this endpoint, developers can override this method to provide custom error response details. Overrides should set the response.status, response.format, and response.body properties to populate the response.

test_run_identifier_location_description in the API docs

error_response_format in the API docs

no_session_response in the API docs

Example

In the example below, an endpoint is defined which waits for an incoming request with a particular bearer token and returns a FHIR Patient resource. After a request is received at this endpoint with the specified bearer token during the “wait” test, Inferno will continue execution and verify that Inferno’s simulated response contains a conformant Patient resource.

class AuthorizedEndpoint < Inferno::DSL::SuiteEndpoint
  # Identify the incoming request based on a bearer token
  def test_run_identifier
    request.headers['authorization']&.delete_prefix('Bearer ')
  end

  # Describe where the run identifier will be pulled from to
  # help with debugging issues.
  def test_run_identifier_location_description
    'the Bearer token sent in the HTTP Authorization header'
  end

  # Return a json FHIR Patient resource
  def make_response
    response.status = 200
    response.body = FHIR::Patient.new(id: request.params[:id]).to_json
    response.format = :json
  end

  # Return no-session errors as an OperationOutcome
  error_response_format :operation_outcome

  # Update the waiting test to pass when the incoming request is received.
  # This will resume the test run.
  def update_result
    results_repo.update(result.id, result: 'pass')
  end

  # Apply the 'authorized' tag to the incoming request so that it may be
  # used by later tests.
  def tags
    ['authorized']
  end
end

class AuthorizedRequestSuite < Inferno::TestSuite
  id :authorized_suite
  suite_endpoint :get, '/Patient/:id', AuthorizedEndpoint

  group do
    title 'Authorized Request Group'
    run_as_group

    test do
      title 'Wait for authorized request'
      flag_as_tester_initiated_exchange

      input :bearer_token

      run do
        wait(
          identifier: bearer_token,
          message: "Waiting to receive a request with bearer_token: #{bearer_token}" \
                    "at `#{Inferno::Application['base_url']}/custom/authorized_suite/Patient/<id>`"
        )
      end
    end

    test do
      title 'Check that the response contains a valid Patient resource'
      flag_as_simulation_verification

      run do
        load_tagged_requests('authorized')
        resource = FHIR.from_contents(request.response_body)
        assert_resource_type(:patient, resource:)
        assert_valid_resource(resource:)
      end
    end
  end
end

Suggest an improvement

Want to make an change? Contribute an edit for this page on the Inferno Framework GitHub repository.