Testing Authenticated REST Calls with Behat and Drupal

Retro beekeeper hat

Prerequisites: Drupal, Behat, Composer, REST

There are plenty of posts out there describing how to make REST calls from a Behat test apparatus. What if, however, you need to take things a step further and make these calls as an authenticated user instead of just an anonymous user? As it turns out, this is both more involved and easier than you might think.

The back end for this project is a composer-based Drupal 8 site, hosted on Pantheon and initially setup with Pantheon’s Build Tools plugin for their Terminus utility. The site exposes a set of endpoints intended for use by a private app and so most of the functionality is restricted to authenticated users. The site also uses cookie-based authentication and requires a JSON request format for incoming calls.

In order to write tests for this scenario, we have to overcome two main obstacles:

  1. We need to be able to specify the payload, method and endpoint for the outgoing REST call and also inspect the responses.
  2. We need to be able to make calls as a user with a particular Drupal role.

As you may already know, Behat (plus Mink) combined with the Behat Drupal Extension has some very nice conveniences, including Behat Steps like

[Given|*] I am logged in as a/an :role

This step creates a user with the given role, then deletes the user when the test is over. All steps in between are executed as the newly created user.

If you search the internet for “Behat REST plugin”, you’ll find a number of solutions, some of which directly use Guzzle to make HTTP calls and some of which extend MinkContext. Choosing one that extends MinkContext is key in this scenario because we want to leverage the benefits given to us by the “…logged in as…” step. We went with the nifty Behatch extension. As you can see from the Behatch instructions, installation of the this extension is pretty simple, requiring only a composer command and some minor edits to your behat.yml file.

So far, so good. We can now make REST calls using steps like

Then I send a "GET" request to "my/cool/rest/api"

or even make a POST request, specifying the raw body with a PyString. We can also inspect the responses.

There are a couple of complications, however. First, our calls need to add a Content-Type header equal to application/json. Luckily, Behatch includes the step

[Then|*] I add :name header equal to :value

The second issue is that we need to maintain the CSRF token required by Drupal web services. Luckily, Drupal 8 has a default way to get the token, so all you have to do is write some custom steps in your Behat context to get it and add it to your outgoing calls. Your custom context might contain something like this:

  /**
   * @Given I get the CSRF Token
   */
  public function getCSRF() {
    $this->getSession()->visit('/session/token');
    $this->csrf = $this->getSession()->getPage()->getContent();
  }

  /**
   * @Then I add CSRF Token to request
   */
  public function addCSRF() {
    $this->request->setHttpHeader('X-CSRF-TOKEN', $this->csrf);
  }

Finally, a feature might contain something like this:

  Given I am logged in as an "authenticated"
  Given I get the CSRF Token

  Then I add CSRF Token to request
  Then I add "Content-Type" header equal to "application/json"
  When I send a "GET" request to "/my/cool/api?_format=json"
  Then I should get a "200" HTTP response

  Then I add CSRF Token to request
  Then I add "Content-Type" header equal to "application/json"
  When I send a "POST" request to "/my/cool/api?_format=json" with body:
    """
    {
      "name": [{
        "value": "mitsuha"
      }],
      "title": [{
        "value": "Your Name"
      }]
    }
    """
  Then I should get a "201" HTTP response

  Then I add CSRF Token to request
  Then I add "Content-Type" header equal to "application/json"
  When I send a "DELETE" request to "/my/cool/api?_format=json"
  Then I should get a "204" HTTP response

And so, with just some minor custom coding, we are now able to write Behat automated tests for our REST endpoints that require authentication in Drupal.