Mocking a REST API With Mojolicious

Tags:

I need to test a UI for a REST JSON API. I don't want to set up a database. I can't set up the dozens of remote machines that my UI interacts with each with different hardware and revealing different potential problems in my code. So I need a mock API that can test my UI without actually doing anything.

So let's set up a simple Mojolicious::Lite app that responds to a path with a JSON response:

# test-api.pl
use Mojolicious::Lite;
get '/servers' => sub {
    my ( $c ) = @_;
    return $c->render(
        json => [
            { ip => '10.0.0.1', os => 'Debian 9' },
            { ip => '10.0.0.2', os => 'Debian 8' }
        ],
    );
};
app->start;

Now I can fetch that JSON response by starting the web application and going to /servers or by using the get command:

$ perl test-api.pl get /servers
[{"ip":"10.0.0.1","os":"Debian 9"},{"ip":"10.0.0.2","os":"Debian 8"}

$ perl test-api.pl daemon
Server available at http://127.0.0.1:3000

That's pretty easy and shows how easy Mojolicious can be to get started. But I have dozens of routes in my application! Combined with all the possible data and its thousands of routes. How do I make all of them work without copy-pasting code for every single route?

Let's match the whole path of the route and then create a template with the given path. Mojolicious lets us match the whole path using the * placeholder in the route path. Then we can use that path to look up the template, which we'll put in the __DATA__ section.

# test-api.pl
use Mojolicious::Lite;
any '/*path' => sub {
    my ( $c ) = @_;
    return $c->render(
        template => $c->stash( 'path' ),
        format => 'json',
    );
};
app->start;
__DATA__
@@ servers.json.ep
[
    { "ip": "10.0.0.1", "os": "Debian 9" },
    { "ip": "10.0.0.2", "os": "Debian 8" }
]

Again, we can use the get command to test that we get the right data:

$ perl test-api.pl get /servers
[
    { "ip": "10.0.0.1", "os": "Debian 9" },
    { "ip": "10.0.0.2", "os": "Debian 8" }
]

So now I can write a bunch of JSON in my script and it will be exposed as an API. But I'd like it to be easier to make lists of things: REST APIs often have one endpoint as a list and another as an individual item in that list. We can make a list by composing our individual parts using Mojolicious templates and the include template helper:

# test-api.pl
use Mojolicious::Lite;
any '/*path' => sub {
    my ( $c ) = @_;
    return $c->render(
        template => $c->stash( 'path' ),
        format => 'json',
    );
};
app->start;
__DATA__
@@ servers.json.ep
[
    <%= include 'servers/1' %>,
    <%= include 'servers/2' %>
]
@@ servers/1.json.ep
{ "ip": "10.0.0.1", "os": "Debian 9" }
@@ servers/2.json.ep
{ "ip": "10.0.0.2", "os": "Debian 8" }

Now I can test the list endpoint again:

$ perl test-api.pl get /servers
[
    { "ip": "10.0.0.1", "os": "Debian 9" }
,
    { "ip": "10.0.0.2", "os": "Debian 8" }
]

And also one of the individual item endpoints:

$ perl test-api.pl get /servers/1
{ "ip": "10.0.0.1", "os": "Debian 9" }

Currently we handle all request methods (GET, POST, PUT, DELETE) the same, but my API doesn't work like that. So, I need to be able to provide different data for different request methods. To do that, let's add the request method to the template path:

# test-api.pl
use Mojolicious::Lite;
any '/*path' => sub {
    my ( $c ) = @_;
    return $c->render(
        template => join( '/', uc $c->req->method, $c->stash( 'path' ) ),
        format => 'json',
    );
};
app->start;
__DATA__
@@ GET/servers.json.ep
[
    <%= include 'get/servers/1' %>,
    <%= include 'get/servers/2' %>
]
@@ GET/servers/1.json.ep
{ "ip": "10.0.0.1", "os": "Debian 9" }
@@ GET/servers/2.json.ep
{ "ip": "10.0.0.2", "os": "Debian 8" }
@@ POST/servers.json.ep
{ "status": "success", "id": 3, "server": <%== $c->req->body %> }

Now all our template paths start with the HTTP request method (GET), allowing us to add different routes for POST requests and other HTTP methods.

We also added a POST/servers.json.ep template that shows us getting a successful response from adding a new server via the API. It even correctly gives us back the data we submitted, like our original API might do.

We can test our added POST /servers method with the get command again:

$ perl test-api.pl get -M POST -c '{ "ip": "10.0.0.3" }' /servers
{ "status": "success", "id": 3, "server": { "ip": "10.0.0.3" } }

Now what if I want to test what happens when the API gives me an error? Mojolicious has an easy way to layer on additional templates to use for certain routes: Template variants. These variant templates will be used instead of the original template, but only if they are available.

By setting the template variant to the application "mode", we can easily switch between multiple sets of templates by adding -m <mode> to the command we run.

# test-api.pl
use Mojolicious::Lite;
any '/*path' => sub {
    my ( $c ) = @_;
    return $c->render(
        template => join( '/', uc $c->req->method, $c->stash( 'path' ) ),
        variant => $c->app->mode,
        format => 'json',
    );
};
app->start;
__DATA__
@@ GET/servers.json.ep
[
    <%= include 'get/servers/1' %>,
    <%= include 'get/servers/2' %>
]
@@ GET/servers/1.json.ep
{ "ip": "10.0.0.1", "os": "Debian 9" }
@@ GET/servers/2.json.ep
{ "ip": "10.0.0.2", "os": "Debian 8" }
@@ POST/servers.json.ep
{ "status": "success", "id": 3, "server": <%== $c->req->body %> }
@@ POST/servers.json+error.ep
% $c->res->code( 400 );
{ "status": "error", "error": "Bad request" }
$ perl test-api.pl get -m error -M POST -c '{}' /servers
{ "status": "error", "error": "Bad request" }

And finally, since I'm using this to test an AJAX web application, I need to allow the preflight OPTIONS request to succeed and I need to make sure that all of the correct Access-Control-* headers are set to allow for cross-origin requests.

# test-api.pl
use Mojolicious::Lite;
hook after_build_tx => sub {
my ($tx, $app) = @_;
    $tx->res->headers->header( 'Access-Control-Allow-Origin' => '*' );
    $tx->res->headers->header( 'Access-Control-Allow-Methods' => 'GET, POST, PUT, PATCH, DELETE, OPTIONS' );
    $tx->res->headers->header( 'Access-Control-Max-Age' => 3600 );
    $tx->res->headers->header( 'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-Requested-With' );
};
any '/*path' => sub {
    my ( $c ) = @_;
    # Allow preflight OPTIONS request for XmlHttpRequest to succeed
    return $c->rendered( 204 ) if $c->req->method eq 'OPTIONS';
    return $c->render(
        template => join( '/', uc $c->req->method, $c->stash( 'path' ) ),
        variant => $c->app->mode,
        format => 'json',
    );
};
app->start;
__DATA__
@@ GET/servers.json.ep
[
    <%= include 'get/servers/1' %>,
    <%= include 'get/servers/2' %>
]
@@ GET/servers/1.json.ep
{ "ip": "10.0.0.1", "os": "Debian 9" }
@@ GET/servers/2.json.ep
{ "ip": "10.0.0.2", "os": "Debian 8" }
@@ POST/servers.json.ep
{ "status": "success", "id": 3, "server": <%== $c->req->body %> }
@@ POST/servers.json+error.ep
% $c->res->code( 400 );
{ "status": "error", "error": "Bad request" }

Now I have 20 lines of code that can be made to mock any JSON API I write. Mojolicious makes everything easy!

Using Template Variants For a Beta Landing Page

Tags:

CPAN Testers is a pretty big project with a long, storied history. At its heart is a data warehouse holding all the test reports made by people installing CPAN modules. Around that exists an ecosystem of tools and visualizations that use this data to provide useful insight into the status of CPAN distributions.

For the CPAN Testers webapp project, I needed a way to show off some pre-release tools with some context about what they are and how they might be made ready for release. I needed a "beta" website with a front page that introduced the beta projects. But, I also needed the same Mojolicious application to serve (in the future) as a production website. The front page of the production website would be completely different from the front page of the beta testing website.

To achieve this, I used Mojolicious's template variants feature. First, I created a variant of my index.html template for my beta site and called it index.html+beta.ep.

<h1>CPAN Testers Beta</h1>
<p>This site shows off some new features currently being tested.</p>
<h2><a href="/chart.html">Release Dashboard</a></h2>

Next, I told Mojolicious to use the "beta" variant when in "beta" mode by passing $app->mode to the variant stash variable.

# myapp.pl
use Mojolicious::Lite;
get '/*path', { path => 'index' }, sub {
    my ( $c ) = @_;
    return $c->render(
        template => $c->stash( 'path' ),
        variant => $c->app->mode,
    );
};
app->start;

The mode is set by passing the -m beta option to Mojolicious's daemon or prefork command.

$ perl myapp.pl daemon -m beta

This gives me the new landing page for beta.cpantesters.org.

$ perl myapp.pl get / -m beta
<h1>CPAN Testers Beta</h1>
<p>This site shows off some new features currently being tested.</p>
<h2><a href="/chart.html">Release Dashboard</a></h2>

But now I also need to replace the original landing page (index.html.ep) so it can still be seen on the beta website. I do this with a simple trick: I created a new template called web.html+beta.ep that imports the original template and unsets the variant stash variable. Now I can see the main index page on the beta site at http://beta.cpantesters.org/web.

%= include 'index', variant => undef
$ perl myapp.pl get /web -m beta
<h1>CPAN Testers</h1>
<p>This is the main CPAN Testers application.</p>

Template variants are a useful feature in some edge cases, and this isn't the first time I've found a good use for them. I've also used them to provide a different layout template in "development" mode to display a banner saying "You're on the development site". Useful for folks who are undergoing user acceptance testing. The best part is that if the desired variant for that specific template is not found, Mojolicious falls back to the main template. I built a mock JSON API application which made extensive use of this fallback feature, but that's another blog post for another time.