Test Coverage for Your WP REST API Project

30
Ensure Security and Reliability with Test Coverage Test Coverage for Your WP REST API Project

Transcript of Test Coverage for Your WP REST API Project

Ensure Security and Reliability with Test Coverage

Test Coverage for Your WP REST API Project

Daniel Bachhuber, Author

Daniel Bachhuber knows a thing or two about WordPress.

In addition to working on the WP REST API, Bachhuber founded Handbuilt, a shop providing WordPress development and consulting services. He also founded Runcommand and is an active maintainer of the WP-CLI.

Daniel Bachhuber, Author

Bachhuber wrote this tutorial to help developers working with the WP REST API ensure a secure, performant site. Whether you are currently working on a REST API project or not, check out Daniel’s tips for securing endpoints as you go.

Alex the developer is pretty excited about the WordPress REST API. Because the infrastructural components were introduced in WordPress 4.4, they too can use register_rest_route() to easily register their own WP REST API endpoints. In fact, they love registering routes so much that they’re creating API endpoints for every project they work on.

Sound like you too? Are you writing full test coverage for your endpoints as you go? If not, you absolutely need to be, for two primary reasons: security and reliability. If you aren’t writing test coverage for your endpoints, sorry Charlie—your endpoints are probably insecure, and probably behave unexpectedly for clients.

This tutorial is everything you need to get started.

Ensure Security and Reliability

To start at the beginning, “writing tests” is a way for you, as the developer of a complex application, to define assertions of how the application’s functionality is expected to work.

Pairing your tests with a continuous integration system like Travis CI means your suite of tests will be run automatically on every push or pull request, making it much easier to incorporate tests into your development workflow.

What Are We Talking About?

As it relates to your WP REST API endpoints, there are two common ways to think about test coverage.

• “Unit tests” test the smallest testable part of your application (e.g. the phone formatting function in this tutorial).

• “Integration tests” test groups of application functionality (e.g. the WP REST API endpoints in this tutorial).

What Are We Talking About?

Invest in Security and Performance

Test coverage is additive; the only place to start is at the very beginning. Continual investment over time leads to an increasing amount of test coverage, and greater confidence that your application isn’t breaking unexpectedly as it becomes more complex.

Say, for instance, you’ve written a rad_format_phone_number( $input )function to format phone numbers within your WordPress application. Your first pass at the function produces something like this:

function rad_format_phone_number( $input ) {

$bits = explode( '-', $input );

return "({$bits[0]}) {$bits[1]}-{$bits[2]}";

}

To ensure the function works as expected, you write a test case for it like this:

You run phpunit to see if the test passes—and it does!

Invest in Security and Performance

function test_format_phone_number() {

$this->assertEquals( '(555) 212-

2121', rad_format_phone_number( '555-212-2121' ) );

}

Test-Driven Development

What if a user passes a value like 5552122121 or +1 (555) 212 2121? Or even an empty string? Make sure your function can handle these alternative formats, as well as the original input format you created the function for.

Using Test-Driven Development, you can actually write the test cases first, and then adapt your function until the tests pass.

function test_format_phone_number() {

$this->assertEquals( '(555) 212-2121', rad_format_phone_number( '555-212-2121' ) );

$this->assertEquals( '(555) 212-2121', rad_format_phone_number( '5552122121' ) );

$this->assertEquals( '(555) 212-2121', rad_format_phone_number( '+1 (555) 212 2121' ) );

$this->assertEquals( '', rad_format_phone_number( '' ) );

}

Twenty minutes of regex later, you’ve created a function to handle the assertions above:

Congratulations! You’ve introduced test coverage into your code.

Test-Driven Development

function rad_format_phone_number( $input ) {

if ( preg_match( '#([\d]{3})[^\d]*([\d]{3})[^\d]*([\d]{4})#', $input,

$matches ) ) {

return "({$matches[1]}) {$matches[2]}-{$matches[3]}";

}

return '';

}

Why Test Coverage Is Even More Important with a WP REST API Project

Test Coverage for Your WP REST API Project

Why Is it More Important?

Because the WP REST API offers a direct read/write interface into WordPress, you need to make absolutely sure you:

• Aren’t unintentionally disclosing private information to unauthorized requests.

• Aren’t unintentionally permitting unauthorized requests to perform write operations on your application.

You may be manually verifying the security of your endpoints while building your WordPress-based application, but test coverage enables you to make those security assertions explicit.

Furthermore, even if your WP REST API endpoints are read-only and don’t deal with private information, you want to make sure your application returns consistent responses. The clients built on top of your API expect consistent responses above all else—and can break unexpectedly when they receive unexpected data.

Why Is it More Important?

How Should I Write My Endpoints?

If you’re familiar with PHPUnit and the WordPress project’s PHPUnit test suite, then you’re already part of the way there. If you’re not, you’ll want to get yourself up to speed, and then come back to this tutorial. You can also open the entire test class in a separate tab if you’d like to refer to it as we go along.

How Should I Write My Endpoints?

To make it possible to test your registered WP REST API endpoint in a PHPUnit test, you’ll need to first set up a WP_REST_Server instance for your test class. If you just have one test class, you can perform this step in the Tests_REST_API_Demo::setUp() method:

public function setUp() {

parent::setUp();

global $wp_rest_server;

$this->server = $wp_rest_server = new WP_REST_Server;

do_action( 'rest_api_init' );

}

The call to rest_api_init ensures your routes are registered to the server within the test. Make sure you also reset the $wp_rest_server global on Tests_REST_API_Demo::tearDown():

How Should I Write My Endpoints?

public function tearDown() {

parent::tearDown();

global $wp_rest_server;

$wp_rest_server = null;

}

Let’s imagine we want to make this phone number accessible through the WP REST API. However, because a phone number is semi-private information, it should only editable by administrators.

How Should I Write My Endpoints?

register_rest_route( 'rad/v1', 'site-info', array(

array(

'methods' => 'GET',

'callback' => function( $request ) {

return array(

'phone_number' => get_option( 'phone_number' )

,

);

},

), Click for the full code.

Switching to the plugin file, our first attempt at registering our WP REST API endpoint looks like this:

Because we have $this→server available on our test class, we can create a WP_REST_Request object, dispatch it on WP_REST_Server, inspect what the server includes on WP_REST_Response.

How Should I Write My Endpoints?

public function test_get() {

$request = new WP_REST_Request( 'GET', '/rad/v1/site

-info' );

$response = $this->server->dispatch( $request );

$this->assertResponseStatus( 200, $response );

$this->assertResponseData( array(

'phone_number' => '(555) 212-2121',

), $response );

}

In this example, notice how we test both the response data and the response status.

Click for the full code.

Clients interpret HTTP status codes to have a higher-level understanding of the type of response, so we want to also make sure we’re returning the proper status code.

How Should I Write My Endpoints?

public function test_get() {

$request = new WP_REST_Request( 'GET', '/rad/v1/site

-info' );

$response = $this->server->dispatch( $request );

$this->assertResponseStatus( 200, $response );

$this->assertResponseData( array(

'phone_number' => '(555) 212-2121',

), $response );

} Click for the full code.

Uh oh! If the warning bells aren’t going off already, the endpoint we’ve registered is hugely insecure—any request, including logged-in and logged-out users can both read or update our phone number. We need to patch this right away.

How Should I Write My Endpoints?

public function test_get_unauthorized() {

wp_set_current_user( 0 );

$request = new WP_REST_Request( 'GET', '/rad/v1/site-

info' );

$response = $this->server->dispatch( $request );

$this->assertResponseStatus( 401, $response );

}

Click for the full code.

Because we’re practicing Test-Driven Development, we first write failing tests (changeset) for the security vulnerability (see the actual pull request on Github). Our tests of our WP REST API endpoints now look like this.

How Should I Write My Endpoints?

public function test_get_unauthorized() {

wp_set_current_user( 0 );

$request = new WP_REST_Request( 'GET', '/rad/v1/site-

info' );

$response = $this->server->dispatch( $request );

$this->assertResponseStatus( 401, $response );

}

Click for the full code.

A Few Key Details to Note

• wp_set_current_user() lets us set the scope of the test to a given user that already exists. Because our tests are against the endpoint itself, and not the authentication system WordPress uses to verify the response, we can safely assume the current user within the scope of the code is the actual user making the request. If authentication fails, WordPress will wp_set_current_user( 0 );, which is functionally equivalent to a logged out request.

• It’s incredibly important to take to heart the difference between authentication and authorization. Authentication refers to whether or not a request is associated with a valid user in the system. Authorization refers to whether or not a given user has permission to perform a given action. Even though a user may be authenticated, they might not be authorized. Your WP REST API endpoint should return a 401 when a user isn’t authenticated, and a 403 when a user isn’t authorized.

• assertResponseStatus() and assertResponseData() are helper methods you are more than welcome to copy into your own test suite.

A Few Key Details to Note

Given our new knowledge about authentication and authorization, we can update our endpoint to use thepermission_callback to authorize the request before our callback handles it.

A Few Key Details to Note

add_action( 'rest_api_init', function() {

register_rest_route( 'rad/v1', 'site-info', array(

array(

'methods' => 'GET',

'callback' => function( $request ) {

return array(

'phone_number' => get_option( 'phone_number' )

,

);

}, Click for the full code.

To be as helpful as possible to clients, let’s adapt our endpoint to only accept input when the data is close to a phone number, and ensure our response data is formatted as a phone number or empty string.

A Few Key Details to Note

add_action( 'rest_api_init', function() {

register_rest_route( 'rad/v1', 'site-info', array(

array(

'methods' => 'GET',

'callback' => function( $request ) {

return array(

'phone_number' => get_option( 'phone_number' )

,

);

}, Click for the full code.

Again, because we’re practicing Test-Driven Development, we first write failing tests (see the actual pull request on Github). These failing tests look like this:

A Few Key Details to Note

public function test_get_authorized_reformatted() {

update_option( 'phone_number', '555 555 5555' );

wp_set_current_user( $this->subscriber );

$request = new WP_REST_Request( 'GET', '/rad/v1/site

-info' );

$response = $this->server->dispatch( $request );

$this->assertResponseStatus( 200, $response );

$this->assertResponseData( array(

'phone_number' => '(555) 555-5555',

), $response );

}Click for the full code.

Given our new knowledge about making to sure consistently handle data, we can update our endpoint to register the phone_number resource argument with a validation callback, and make sure to return data through our rad_format_phone_number()function.

A Few Key Details to Note

register_rest_route( 'rad/v1', 'site-info', array(

array(

'methods' => 'GET',

'callback' => function( $request ) {

return array(

'phone_number' => rad_format_phone_number( get_

option( 'phone_number' ) ),

);

}, Click for the full code.

This is Only the Beginning…

• Test coverage is critically important for two reasons: security and reliability. You want to make triply sure your API isn’t disclosing private information, permitting unauthorized operations, and responds consistently to correct and incorrect client requests.

• Using the WordPress project’s PHPUnit test suite, you can write integration tests for your endpoints. Include assertions for both the response data and the response status. For every successful request test you write, include 4 or 5 permutations of erred requests.

• Clients will always send your application unexpected or incorrect data. If your endpoints can provide consistent, clear, and expected responses, then the client developer’s life will be greatly improved, as they won’t have to spend hours or days trying to debug cryptic errors from an application they don’t have access to.

This is Only the Beginning…

Run your WP REST API project on Pantheon.We’ve created a unique WordPress hosting platform. We provide elastic hosting and the best cloud-based development tools for teams.

Try it for free