httptest
makes it easy for you to write tests that don’t require a network connection. With capture_requests()
, you can record responses from real requests so that you can use them later in tests. A further benefit of testing with mocks is that you don’t have to deal with authentication and authorization on the server in your tests—you don’t need to supply real login credentials for your test suite to run. You can have full test coverage of your code, both on public continuous-integration services like Travis-CI and when you submit packages to CRAN, all without having to publish secret tokens or passwords.
It is important to ensure that the mocks you include in your test suite do not inadvertently reveal private information as well. For many requests and responses, the default behavior of capture_requests
is to write out only the response body, which makes for clean, easy-to-read test fixtures. For other responses, however—those returning non-JSON content or an error status—it writes a .R
file containing a httr
“response” object. This response contains all of the headers and cookies that the server returns, and it also has a copy of your “request” object, with the headers, tokens, and other configuration you sent to the server. If not addressed, this would mean that you might be exposing your personal credentials publicly.
httptest
provides a framework for sanitizing the responses that capture_requests
records. By default, it redacts the standard ways that auth credentials are passed in requests: cookies, authorization headers, basic HTTP auth, and OAuth. The framework is extensible and allows you to specify custom redaction policies that match how your API accepts and returns sensitive information. Common redacting functions are configurable and natural to adapt to your needs, while the workflow also supports custom redacting functions that can alter the recorded requests however you want, including altering the response content and URL.
By default, the capture_requests
context evaluates the redact_auth()
function on a response object before writing it to disk. redact_auth
wraps a few smaller redacting functions that (1) sanitize any cookies in the request and response; (2) redact common headers including “Authorization”, if present; and (3) if using basic HTTP authentication with username and password, removes those credentials.
What does “redacting” entail? We aren’t the CIA working with classified reports, taking a heavy black marker over certain details. In our case, redacting means replacing the sensitive content with the string “REDACTED”. Your recorded responses will be as “real” as possible: if, for example, you have an “Authorization” header in your request, the header will remain in your test fixture, but real token value will be replaced with “REDACTED”. And only the recorded responses will be affected—the actual response you’re capturing in your active R session is not modified, only the mock that is written out.
To illustrate, if you make a request that includes a cookie, that cookie will also be included in the response
object that is returned.
capture_requests(simplify=FALSE, {
real_resp <- GET("http://httpbin.org/cookies", set_cookies(token="12345"))
})
real_resp$request$options$cookie
## [1] "token=12345"
But when we load that recorded response in tests later, the cookie won’t appear because it was redacted:
mockfile <- "httpbin.org/cookies.R"
mock <- source(mockfile)$value
mock$request$options$cookie
## [1] "REDACTED"
(Side note: the example uses the simplify=FALSE
option to capture_requests
for illustration purposes. With the default simplify=TRUE
, only the response body would be written to a mock file because this particular GET request returns JSON content. Thus, there would be no cookie present anyway. simplify=FALSE
forces capture_requests
to write the verbose .R response object file for every request, not just those that don’t return JSON content.)
Some APIs use other methods for passing credentials. For example, the API for Pivotal Tracker, the agile project management tool, uses a “X-TrackerToken” request header for passing an API token. Our standard redactor doesn’t know about this header, so by default, this token would be written in our recorded responses.
So, in the pivotaltrackR package, which wraps this API, if we want to record mocks to use in tests with this API, we need to tell capture_requests
to scrub its special header. To do this, we’ll use set_redactor()
to supply a custom function.
set_redactor(~ redact_headers(., "X-TrackerToken"))
Note the formula shorthand: this follows the syntax in the purrr
package for defining anonymous functions. It is equivalent to function (response) redact_headers(response, "X-TrackerToken")
.
Valid inputs to set_redactor()
include:
response
, and returning a valid response
object.
as the “response” argumentNULL
, to override the default redact_auth()
and do no redactingTo illustrate, here’s a recording of a GET on the stories/
endpoint. We can see that the real token is included in the “X-TrackerToken” header in the real request:
library(httptest)
library(pivotaltrackR)
options(
pivotal.project="my-project-name",
pivotal.token="8fe3452ac4e3"
)
set_redactor(~ redact_headers(., "X-TrackerToken"))
capture_requests(simplify=FALSE, {
active_stories <- getStories(state="started")
})
active_stories$request$headers
## Accept
## "application/json, text/xml, application/xml, */*"
## user-agent
## "libcurl/7.51.0 curl/2.3 httr/1.2.1 pivotaltrackR/0.1.0"
## X-TrackerToken
## "8fe3452ac4e3"
Because we set ~ redact_headers(., "X-TrackerToken")
as the redactor, it should have been removed from the mock file that was written out. Let’s read in that file and inspect it.
mockfile <- "www.pivotaltracker.com/services/v5/projects/my-project-name/stories-c0a029.R"
mock <- source(mockfile)$value
mock$request$headers
## Accept
## "application/json, text/xml, application/xml, */*"
## user-agent
## "libcurl/7.51.0 curl/2.3 httr/1.2.1 pivotaltrackR/0.1.0"
## X-TrackerToken
## "REDACTED"
The actual token was removed, and in its place is the string “REDACTED”. I am now safe to commit this mock file to my repository and publish it without exposing my real credentials.
Sensitive or personal information can also found in other parts of the request and response. Sometimes personal identifiers are built into URLs or response bodies. These may be less sensitive than auth tokens, but you probably want to conceal or anonymize your data that is included in test fixtures.
Redacting functions can help with this personal information as well. You can use redactors on any part of the response object, not just the headers and cookies. A redactor is just a function that takes a response as input and returns a response object, so anything is possible if you write a custom redactor.
Keeping with the pivotaltrackR
example, note that the Pivotal project id, stored in options(pivotal.project)
in the R session, appears in the mock file path. It’s in the file path because it’s in the request URL, and it turns out that many API responses also include it in the response body. We’d rather not have that information leak in our test fixtures, so let’s write a function to remove it.
All of the redactors in httptest
take the “response” object as their first argument and return the response object modified in some way. This lends them to pipelining, as with the magrittr
package. Let’s use that, and let’s start our function with that “X-TrackerToken” header redacting:
redact_pivotal <- function (response) {
require(magrittr)
response %>%
redact_headers("X-TrackerToken")
}
To remove the project ID, we can use the gsub_response()
function, which takes response
as its first argument and then passes the rest to gsub()
, which is called on the response URL, the response body, and the request object as well (because response
s include the request
object in them):
redact_pivotal <- function (response) {
require(magrittr)
response %>%
redact_headers("X-TrackerToken") %>%
gsub_response(getOption("pivotal.project"), "123")
}
To see this in action, let’s record a request:
set_redactor(redact_pivotal)
start_capturing()
s <- getStories(search="mnt")
stop_capturing()
Note that the actual project id appears in the data returned from the search.
s[[1]]$project_id
## [1] "my-project-name"
However, the project id won’t be found in the recorded file. If we load the recorded response in with_mock_api
, we’ll see the value we substituted in:
with_mock_api({
s <- getStories(search="mnt")
})
s[[1]]$project_id
## [1] "123"
Nor will the project id appear in the file path: since the redactor is evaluated before determining the file path to write to, if you alter the response URL, the destination file path will be generated based on the modified URL. In this case, our mock is written to “…/projects/123/stories-fb1776.json”, not “…/projects/my-project-name/stories-fb1776.json”.
If you’re writing a package that wraps an API and you need a custom redactor to safely record API responses, you’ll want to make sure that you always record with that redactor. You don’t want to forget to call set_redactor()
in your R session and end up recording fixtures that contain your auth secrets.
To make sure that your redactor is “always on” for your package, httptest
enables you to define a package-level redactor. To do this, put a redacting function in inst/httptest/redact.R
in your package. Any time you record requests while your package is loaded, as when running tests or building vignettes, this function will be called on the response
object before writing it to disk. It’s automatic: set it there once and you never have to remember.
For example, here is the one for pivotaltrackR
:
function (response) {
require(magrittr, quietly=TRUE)
response %>%
# Remove the auth token
redact_headers("X-TrackerToken") %>%
# Shorten the URL
gsub_response("https://www.pivotaltracker.com/services/v5/", "", fixed=TRUE) %>%
# Remove my project ID
gsub_response(getOption("pivotal.project"), "123")
}
Finally, depending on how long the URLs are in the API requests you make, you may need to programmatically shorten them if you’re planning on submitting your package to CRAN because CRAN requires file names to be 100 characters or less. Long file names throw a “non-portable file paths” message in R CMD check
.
A good way to solve this problem is to use a request preprocessor: a function that alters the content of your ‘httr’ request
before mapping it to a mock file. It’s like a redactor but for the request object. Just as you can provide a custom function to modify responses that are recorded, you can provide a function to tweak the request being made in order to map the request to the right file in the mocked context. Importantly, this lets you truncate the URLs, which then map to files.
For example, if all of your API endpoints sit beneath https://language.googleapis.com/v1/
, you could set a request preprocessor like:
set_requester(~ gsub_request(., "https\\://language.googleapis.com/v1/", "api/"))
and then all mocked requests would look for a path starting with “api/” rather than “language.googleapis.com/v1/”, saving you (in this case) 23 characters.
You can also provide this function in inst/httptest/request.R
, just as you can for the redactor, and any time your package is loaded and you’re reading mock (previously recorded) responses, this function will be called on the request
object before mapping it to a file. For example, here is the one from pivotaltrackR
:
function (request) {
require(magrittr, quietly=TRUE)
request %>%
gsub_request("https://www.pivotaltracker.com/services/v5/", "", fixed=TRUE) %>%
gsub_request(getOption("pivotal.project"), "123")
}