Testing with VCR and Token Authentication

- mbialas - Element 84, Rails, Testing

If you are using VCR to record/playback HTTP requests for your Rails tests, you may run into problems if your cassettes use tokens to authenticate with those services.

The error

After recording a cassette, you may see this familiar error upon running your tests:

# Error
An HTTP request has been made that VCR does not know how to handle

The cause

VCR uses a RequestMatcher to determine if an outgoing request (and response) already exist in a cassette. Upon finding a match, the test will use the response pre-recorded in the cassette.

Two common ways to Authenticate on an external request are:

# Token Param
https://example.com/service/data?user=123&account=johnsmith&token=qW413vsj4Sv4
# HTTP Basic Header Auth
https://johnsmith:qW413vsj4Sv4@example.com/service/data?user=123

Because tokens typically expire, cassettes recorded at different times may contain different authentication tokens. This becomes a problem when running the whole test suite. Because the request/response to get a token will also be pre-recorded, VCR will return the same response (and auth token) to all of the tests. The first test will pass, but those using cassettes recorded at a different time will experience the error mentioned above. Which test(s) pass may vary on each run because VCR uses a Seed to (randomly) order & execute the tests.

Breaking down a request

The next step is to understand which part of the request is failing to match, and what can be done about it.

# Sample Request
https://example.com/service/data?user=123&account=johnsmith#time=789

# Components
* Scheme (http://, https://, etc)
* Host (example.com)
* Path (/service/data)
* Query (?user=123&account=johnsmith)
* Fragment (#time=789)

The culprit

Depending on which authentication method is used, the culprit is in one of two places:

# Token is in the Query
https://example.com/service/data?user=123&account=johnsmith&token=qW413vsj4Sv4
# Token is in the Header
https://johnsmith:qW413vsj4Sv4@example.com/service/data?user=123

The Solution

VCR allows you to create a Custom RequestMatcher. With this, we can create a matcher that ignores the auth token (or other problematic/mutable param).

If you supply a Token Param…

VCR.configure do |config|

  ...

  config.default_cassette_options = {
    ...
    :match_requests_on => [:method, VCR.request_matchers.uri_without_param(:token)]
  }
end

If you use a Basic Auth Header…

VCR.configure do |config|

  ...

  config.default_cassette_options = {
    ...
    :match_requests_on => [:method, :no_basic_auth]
  }

  config.register_request_matcher :no_basic_auth do |req1, req2|
    (URI(req1.uri).scheme == URI(req2.uri).scheme) &&
    (URI(req1.uri).host == URI(req2.uri).host) &&
    (URI(req1.uri).path == URI(req2.uri).path) &&
    (URI(req1.uri).query == URI(req2.uri).query)
  end
end

Wrap Up

While the ability to use uri_without_param is easy enough, creating a custom matcher is definitely a workaround at best. Unfortunately, requests for a cleaner solution to this problem seem to have been closed. Until that changes, we’ll have to keep using the custom matcher.