Quantcast
Channel: Matthew Daly
Viewing all 158 articles
Browse latest View live

Broadcasting events with Laravel and Socket.io

$
0
0

PHP frameworks like Laravel aren’t really set up to handle real-time events properly, so if you want to build a real-time app, you’re generally better off with another platform, such as Node.js. However, if that only forms a small part of your application, you may still prefer to work with PHP. Fortunately it’s fairly straightforward to hand off the real-time aspects of your application to a dedicated microservice written using Node.js and still use Laravel to handle the rest of the functionality.

Here I’ll show you how I built a Laravel app that uses a separate Node.js script to handle sending real-time updates to the user.

Events in Laravel

In this case, I was building a REST API to serve as the back end for a Phonegap app that allowed users to message each other. The API includes an endpoint that allows users to create and fetch messages. Now, in theory, we could just repeatedly poll the endpoint for new messages, but that would be inefficient. What we needed was a way to notify users of new messages in real time, which seemed like the perfect opportunity to use Socket.io.

Laravel comes with a simple, but robust system that allows you to broadcast events to a Redis server. Another service can then listen for these events and carry out jobs on them, and there is no reason why this service has to be written in PHP. This makes it easy to decouple your application into smaller parts. In essence the functionality we wanted was as follows:

  • Receive message
  • Push message to Redis
  • Have a separate service pick up message on Redis
  • Push message to clients

First off, we need to define an event in our Laravel app. You can create a boilerplate with the following Artisan command:

$ php artisan make:event NewMessage

This will create the file app/Events/NewMessage.php. You can then customise this as follows:

<?php
namespace App\Events;
use App\Events\Event;
use App\Message;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class NewMessage extends Event implements ShouldBroadcast
{
use SerializesModels;
public $message;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Message $message)
{
// Get message
$this->message = $message;
}
/**
* Get the channels the event should be broadcast on.
*
* @return array
*/
public function broadcastOn()
{
return ['room_'.$this->message->room_id];
}
}

This particular event is a class that accepts a single argument, which is an instance of the Message model. This model includes an attribute of room_id that is used to determine which room the message is posted to - note that this is returned in the broadcastOn() method.

When we want to trigger our new event, we can do so as follows:

use App\Events\NewMessage;
Event::fire(new NewMessage($message));

Here, $message is the saved Eloquent object containing the message. Note the use of SerializesModels - this means that the Eloquent model is serialized into JSON when broadcasting the event.

We also need to make sure Redis is set as our broadcast driver. Ensure the Composer package predis/predis is installed, and set BROADCAST_DRIVER=redis in your .env file. Also, please note that I found that setting QUEUE_DRIVER=redis in .env as well broke the broadcasting system, so it looks like you can’t use Redis as both a queue and a broadcasting system unless you set up multiple connections.

Next, we need another server-side script to handle processing the received events and pushing the messages out. In my case, this was complicated by the fact that we were using HTTPS, courtesy of Let’s Encrypt. I installed the required dependencies for the Node.js script as follows:

$ npm install socket.io socket.io-client ioredis --save-dev

Here’s an example Node.js script for processing the events:

var fs = require('fs');
var pkey = fs.readFileSync('/etc/letsencrypt/live/example.com/privkey.pem');
var pcert = fs.readFileSync('/etc/letsencrypt/live/example.com/fullchain.pem')
var options = {
key: pkey,
cert: pcert
};
var app = require('https').createServer(options);
var io = require('socket.io')(app);
var Redis = require('ioredis');
var redis = new Redis();
app.listen(9000, function() {
console.log('Server is running!');
});
function handler(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.writeHead(200);
res.end('');
}
io.on('connection', function(socket) {
//
});
redis.psubscribe('*', function(err, count) {
//
});
redis.on('pmessage', function(subscribed, channel, message) {
message = JSON.parse(message);
console.log('Channel is ' + channel + ' and message is ' + message);
io.emit(channel, message.data);
});

Note we use the https module instead of the http one, and we pass the key and certificate as options to the server. This server runs on port 9000, but feel free to move it to any arbitrary port you wish. In production, you’d normally use something like Supervisor or systemd to run a script like this as a service.

Next, we need a client-side script to connect to the Socket.io instance and handle any incoming messages. Here’s a very basic example that just dumps them to the browser console:

var url = window.location.protocol + '//' + window.location.hostname;
var socket = io(url, {
'secure': true,
'reconnect': true,
'reconnection delay': 500,
'max reconnection attempts': 10
});
var chosenEvent = 'room_' + room.id;
socket.on(chosenEvent, function (data) {
console.log(data);
});

Finally, we need to configure our web server. I’m using Nginx with PHP-FPM and PHP 7, and this is how I configured it:

upstream websocket {
server 127.0.0.1:9000;
}
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
ssl on;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
client_max_body_size 50M;
server_tokens off;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
root /var/www/public;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
gzip on;
gzip_proxied any;
gzip_types text/plain text/css application/javascript application/x-javascript text/xml application/xml application/xml-rss text/javascript text/js application/json;
expires 1y;
charset utf-8;
}
location ~ \.php$ {
try_files $uri /index.php =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /.well-known {
root /var/www/public;
allow all;
}
location /socket.io {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass https://websocket;
}
}

Any requests to /socket.io are proxied to port 9000, where our chat handling script is listening. Note that we allow the HTTPS connection to be upgraded to a WebSocket one.

Once that’s done, you just need to restart your PHP application and Nginx, and start running your chat script, and everything should be working fine. If it isn’t, the command redis-cli monitor is invaluable in verifying that the event is being published correctly.

Summary

Getting this all working together did take quite a bit of trial and error, but that was mostly a matter of configuration. Actually implementing this is pretty straightforward, and it’s an easy way to add some basic real-time functionality to an existing Laravel application.


Adding Google AMP support to my site

$
0
0

You may have heard of Google’s AMP Project, which allows you to create mobile-optimized pages using a subset of HTML. After seeing the sheer speed at which you can load an AMP page (practically instantaneous in many cases), I was eager to see if I could apply it to my own site.

I still wanted to retain the existing functionality for my site, such as comments and search, so I elected not to rewrite the whole thing to make it AMP-compliant. Instead, I opted to create AMP versions of every blog post, and link to them from the original. This preserves the advantages of AMP since search engines will be able to discover it from the header of the original, while allowing those wanting a richer experience to view the original, where the comments are hosted. You can now view the AMP version of any post by appending amp/ to its URL.

The biggest problem was the images in the post body, as the <img> tag needs to be replaced by the <amp-img> tag, which also requires an explicit height and width. I wound up amending the renderer for AMP pages to render an image tag as an empty string, since I have only ever used one image in the post body and I think I can live without them.

It’s also a bit of a pain styling it as it will be awkward to use Bootstrap. I’ve therefore opted to skip Bootstrap for now and write my own fairly basic theme for the AMP pages instead.

It’ll be interesting to see what effect having the AMP versions of the pages available will have on my site in terms of search results. It obviously takes some time before the page gets crawled, and until then the AMP version won’t be served from the CDN used by AMP, so I really can’t guess what effect it will have right now.

Using Jenkins pipelines

$
0
0

I use Jenkins as my main continuous integration solution at work, largely for two reasons:

  • It generally works out cheaper to host it ourselves than to use one of the paid CI solutions for closed-source projects
  • The size of the plugin ecosystem

However, we also use Travis CI for testing one or two open-source projects, and one distinct advantage Travis has is the way you can configure it using a single text file.

With the Pipeline plugin, it’s possible to define the steps required to run your tests in a Jenkinsfile and then set up a Pipeline job which reads that file from the version control system and runs it accordingly. Here’s a sample Jenkinsfile for a Laravel project:

node {
// Mark the code checkout 'stage'....
stage 'Checkout'
// Get some code from a Bitbucket repository
git credentialsId: '5239c33e-10ab-4c1b-a4a0-91b96a07955e', url: 'git@bitbucket.org:matthewbdaly/my-app.git'
// Install dependencies
stage 'Install dependencies'
// Run Composer
sh 'composer install'
// Test stage
stage 'Test'
// Run the tests
sh "vendor/bin/phpunit"
}

Note the steps it’s broken down into:

  • stage defines the start of a new stage in the build
  • git defines a point where we check out the code from the repository
  • sh defines a point where we run a shell command

Using these three commands it’s straightforward to define a fairly simple build process for your application in a way that’s more easily repeatable when creating new projects - for instance, you can copy this over to a new project and change the source repository URL and you’re pretty much ready to go.

Unfortunately, support for the Pipeline plugin is missing from a lot of Jenkins plugins - for instance, I can’t publish the XML coverage reports. This is something of a deal-breaker for most of my projects as I use these kind of report plugins a lot - it’s one of the reasons I chose Jenkins over Travis. Still, this is definitely a big step forward, and if you don’t need this kind of reporting then there’s no reason not to consider using the Pipeline plugin for your Jenkins jobs. Hopefully in future more plugins will be amended to work with Pipeline so that it’s more widely usable.

Testing your API documentation with Dredd

$
0
0

Documenting your API is something most developers agree is generally a Good Thing, but it’s a pain in the backside, and somewhat boring to do. What you really need is a tool that allows you to specify the details of your API before you start work, generate documentation from that specification, and test your implementation against that specification.

Fortunately, such a tool exists. The Blueprint specification allows you to document your API using a Markdown-like syntax. You can then create HTML documentation using a tool like Aglio or Apiary, and test it against your implementation using Dredd.

In this tutorial we’ll implement a very basic REST API using the Lumen framework. We’ll first specify our API, then we’ll implement routes to match the implementation. In the process, we’ll demonstrate the Blueprint specification in action.

Getting started

Assuming you already have PHP 5.6 or better and Composer installed, run the following command to create our Lumen app skeleton:

$ composer create-project --prefer-dist laravel/lumen demoapi

Once it has finished installing, we’ll also need to add the Dredd hooks:

$ cd demoapi
$ composer require ddelnano/dredd-hooks-php

We need to install Dredd. It’s a Node.js tool, so you’ll need to have that installed. We’ll also install Aglio to generate HTML versions of our documentation:

$ npm install -g aglio dredd

We also need to create a configuration file for Dredd, which you can do by running dredd init. Or you can just copy the one below:

dry-run: null
hookfiles: null
language: php
sandbox: false
server: 'php -S localhost:3000 -t public/'
server-wait: 3
init: false
custom:
apiaryApiKey: ''
names: false
only: []
reporter: apiary
output: []
header: []
sorted: false
user: null
inline-errors: false
details: false
method: []
color: true
level: info
timestamp: false
silent: false
path: []
hooks-worker-timeout: 5000
hooks-worker-connect-timeout: 1500
hooks-worker-connect-retry: 500
hooks-worker-after-connect-wait: 100
hooks-worker-term-timeout: 5000
hooks-worker-term-retry: 500
hooks-worker-handler-host: localhost
hooks-worker-handler-port: 61321
config: ./dredd.yml
blueprint: apiary.apib
endpoint: 'http://localhost:3000'

If you choose to run dredd init, you’ll see prompts for a number of things, including:

  • The server command
  • The blueprint file name
  • The endpoint
  • Any Apiary API key
  • The language you want to use

There are Dredd hooks for many languages, so if you’re planning on building a REST API in a language other than PHP, don’t worry - you can still test it with Dredd, you’ll just get prompted to install different hooks.

Note the hookfiles section, which specifies a hookfile to run during the test in order to set up the API. We’ll touch on that in a moment. Also, note the server setting - this specifies the command we should call to run the server. In this case we’re using the PHP development server.

If you’re using Apiary with your API (which I highly recommend), you can also set the following parameter to ensure that every time you run Dredd, it submits the results to Apiary:

custom:
apiaryApiKey: <API KEY HERE>
apiaryApiName: <API NAME HERE>

Hookfiles

As mentioned, the hooks allow you to set up your API. In our case, we’ll need to set up some fixtures for our tests. Save this file at tests/dredd/hooks/hookfile.php:

<?php
use Dredd\Hooks;
use Illuminate\Support\Facades\Artisan;
require __DIR__ . '/../../../vendor/autoload.php';
$app = require __DIR__ . '/../../../bootstrap/app.php';
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
Hooks::beforeAll(function (&$transaction) use ($app) {
putenv('DB_CONNECTION=sqlite');
putenv('DB_DATABASE=:memory:');
Artisan::call('migrate:refresh');
Artisan::call('db:seed');
});
Hooks::beforeEach(function (&$transaction) use ($app) {
Artisan::call('migrate:refresh');
Artisan::call('db:seed');
});

Before the tests run, we set the environment up to use an in-memory SQLite database. We also migrate and seed the database, so we’re working with a clean database. As part of this tutorial, we’ll create seed files for the fixtures we need in the database.

This hookfile assumes that the user does not need to be authenticated to communicate with the API. If that’s not the case for your API, you may want to include something like this in your hookfile’s beforeEach callback:

$user = App\User::first();
$token = JWTAuth::fromUser($user);
$transaction->request->headers->Authorization = 'Bearer ' . $token;

Here we’re using the JWT Auth package for Laravel to authenticate users of our API, and we need to set the Authorization header to contain a valid JSON web token for the given user. If you’re using a different method, such as HTTP Basic authentication, you’ll need to amend this code to reflect that.

With that done, we need to create the Blueprint file for our API. Recall the following line in dredd.yml:

blueprint: apiary.apib

This specifies the path to our documentation. Let’s create that file:

$ touch apiary.apib

Once this is done, you should be able to run Dredd:

$ dredd
info: Configuration './dredd.yml' found, ignoring other arguments.
info: Using apiary reporter.
info: Starting server with command: php -S localhost:3000 -t public/
info: Waiting 3 seconds for server command to start...
warn: Parser warning in file 'apiary.apib': (warning code undefined) Could not recognize API description format. Falling back to API Blueprint by default.
info: Beginning Dredd testing...
complete: Tests took 619ms
complete: See results in Apiary at: https://app.apiary.io/public/tests/run/4aab4155-cfc4-4fda-983a-fea280933ad4
info: Sending SIGTERM to the backend server
info: Backend server was killed

With that done, we’re ready to start work on our API.

Our first route

Dredd is not a testing tool in the usual sense. Under no circumstances should you use it as a substitute for something like PHPUnit - that’s not what it’s for. It’s for ensuring that your documentation and your implementation remain in sync. However, it’s not entirely impractical to use it as a Behaviour-driven development tool in the same vein as Cucumber or Behat - you can use it to plan out the endpoints your API will have, the requests they accept, and the responses they return, and then verify your implementation against the documentation.

We will only have a single endpoint, in order to keep this tutorial as simple and concise as possible. Our endpoint will expose products for a shop, and will allow users to fetch, create, edit and delete products. Note that we won’t be implementing any kind of authentication, which in production is almost certainly not what you want - we’re just going for the simplest possible implementation.

First, we’ll implement getting a list of products:

FORMAT: 1A
# Demo API
# Products [/api/products]
Product object representation
## Get products [GET /api/products]
Get a list of products
+ Request (application/json)
+ Response 200 (application/json)
+ Body
{
"id": 1,
"name": "Purple widget",
"description": "A purple widget",
"price": 5.99,
"attributes": {
"colour": "Purple",
"size": "Small"
}
}

A little explanation is called for. First the FORMAT section denotes the version of the API. Then, the # Demo API section denotes the name of the API.

Next, we define the Products endpoint, followed by our first method. Then we define what should be contained in the request, and what the response should look like. Blueprint is a little more complex than that, but that’s sufficient to get us started.

Then we run dredd again:

$ dredd.yml
info: Configuration './dredd.yml' found, ignoring other arguments.
info: Using apiary reporter.
info: Starting server with command: php -S localhost:3000 -t public/
info: Waiting 3 seconds for server command to start...
info: Beginning Dredd testing...
fail: GET /api/products duration: 61ms
info: Displaying failed tests...
fail: GET /api/products duration: 61ms
fail: headers: Header 'content-type' has value 'text/html; charset=UTF-8' instead of 'application/json'
body: Can't validate real media type 'text/plain' against expected media type 'application/json'.
statusCode: Status code is not '200'
request:
method: GET
uri: /api/products
headers:
Content-Type: application/json
User-Agent: Dredd/1.5.0 (Linux 4.4.0-31-generic; x64)
body:
expected:
headers:
Content-Type: application/json
body:
{
"id": 1,
"name": "Purple widget",
"description": "A purple widget",
"price": 5.99,
"attributes": {
"colour": "Purple",
"size": "Small"
}
}
statusCode: 200
actual:
statusCode: 404
headers:
host: localhost:3000
connection: close
x-powered-by: PHP/7.0.8-0ubuntu0.16.04.2
cache-control: no-cache
date: Mon, 08 Aug 2016 10:30:33 GMT
content-type: text/html; charset=UTF-8
body:
<!DOCTYPE html>
<html>
<head>
<meta name="robots" content="noindex,nofollow" />
<style>
/* Copyright (c) 2010, Yahoo! Inc. All rights reserved. Code licensed under the BSD License: http://developer.yahoo.com/yui/license.html */
html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:text-top;}sub{vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;}
html { background: #eee; padding: 10px }
img { border: 0; }
#sf-resetcontent { width:970px; margin:0 auto; }
.sf-reset { font: 11px Verdana, Arial, sans-serif; color: #333 }
.sf-reset .clear { clear:both; height:0; font-size:0; line-height:0; }
.sf-reset .clear_fix:after { display:block; height:0; clear:both; visibility:hidden; }
.sf-reset .clear_fix { display:inline-block; }
.sf-reset * html .clear_fix { height:1%; }
.sf-reset .clear_fix { display:block; }
.sf-reset, .sf-reset .block { margin: auto }
.sf-reset abbr { border-bottom: 1px dotted #000; cursor: help; }
.sf-reset p { font-size:14px; line-height:20px; color:#868686; padding-bottom:20px }
.sf-reset strong { font-weight:bold; }
.sf-reset a { color:#6c6159; cursor: default; }
.sf-reset a img { border:none; }
.sf-reset a:hover { text-decoration:underline; }
.sf-reset em { font-style:italic; }
.sf-reset h1, .sf-reset h2 { font: 20px Georgia, "Times New Roman", Times, serif }
.sf-reset .exception_counter { background-color: #fff; color: #333; padding: 6px; float: left; margin-right: 10px; float: left; display: block; }
.sf-reset .exception_title { margin-left: 3em; margin-bottom: 0.7em; display: block; }
.sf-reset .exception_message { margin-left: 3em; display: block; }
.sf-reset .traces li { font-size:12px; padding: 2px 4px; list-style-type:decimal; margin-left:20px; }
.sf-reset .block { background-color:#FFFFFF; padding:10px 28px; margin-bottom:20px;
-webkit-border-bottom-right-radius: 16px;
-webkit-border-bottom-left-radius: 16px;
-moz-border-radius-bottomright: 16px;
-moz-border-radius-bottomleft: 16px;
border-bottom-right-radius: 16px;
border-bottom-left-radius: 16px;
border-bottom:1px solid #ccc;
border-right:1px solid #ccc;
border-left:1px solid #ccc;
}
.sf-reset .block_exception { background-color:#ddd; color: #333; padding:20px;
-webkit-border-top-left-radius: 16px;
-webkit-border-top-right-radius: 16px;
-moz-border-radius-topleft: 16px;
-moz-border-radius-topright: 16px;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
border-top:1px solid #ccc;
border-right:1px solid #ccc;
border-left:1px solid #ccc;
overflow: hidden;
word-wrap: break-word;
}
.sf-reset a { background:none; color:#868686; text-decoration:none; }
.sf-reset a:hover { background:none; color:#313131; text-decoration:underline; }
.sf-reset ol { padding: 10px 0; }
.sf-reset h1 { background-color:#FFFFFF; padding: 15px 28px; margin-bottom: 20px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
border: 1px solid #ccc;
}
</style>
</head>
<body>
<div id="sf-resetcontent" class="sf-reset">
<h1>Sorry, the page you are looking for could not be found.</h1>
<h2 class="block_exception clear_fix">
<span class="exception_counter">1/1</span>
<span class="exception_title"><abbr title="Symfony\Component\HttpKernel\Exception\NotFoundHttpException">NotFoundHttpException</abbr> in <a title="/home/matthew/Projects/demoapi/vendor/laravel/lumen-framework/src/Concerns/RoutesRequests.php line 450" ondblclick="var f=this.innerHTML;this.innerHTML=this.title;this.title=f;">RoutesRequests.php line 450</a>:</span>
<span class="exception_message"></span>
</h2>
<div class="block">
<ol class="traces list_exception">
<li> in <a title="/home/matthew/Projects/demoapi/vendor/laravel/lumen-framework/src/Concerns/RoutesRequests.php line 450" ondblclick="var f=this.innerHTML;this.innerHTML=this.title;this.title=f;">RoutesRequests.php line 450</a></li>
<li>at <abbr title="Laravel\Lumen\Application">Application</abbr>->handleDispatcherResponse(<em>array</em>('0')) in <a title="/home/matthew/Projects/demoapi/vendor/laravel/lumen-framework/src/Concerns/RoutesRequests.php line 387" ondblclick="var f=this.innerHTML;this.innerHTML=this.title;this.title=f;">RoutesRequests.php line 387</a></li>
<li>at <abbr title="Laravel\Lumen\Application">Application</abbr>->Laravel\Lumen\Concerns\{closure}() in <a title="/home/matthew/Projects/demoapi/vendor/laravel/lumen-framework/src/Concerns/RoutesRequests.php line 636" ondblclick="var f=this.innerHTML;this.innerHTML=this.title;this.title=f;">RoutesRequests.php line 636</a></li>
<li>at <abbr title="Laravel\Lumen\Application">Application</abbr>->sendThroughPipeline(<em>array</em>(), <em>object</em>(<abbr title="Closure">Closure</abbr>)) in <a title="/home/matthew/Projects/demoapi/vendor/laravel/lumen-framework/src/Concerns/RoutesRequests.php line 389" ondblclick="var f=this.innerHTML;this.innerHTML=this.title;this.title=f;">RoutesRequests.php line 389</a></li>
<li>at <abbr title="Laravel\Lumen\Application">Application</abbr>->dispatch(<em>null</em>) in <a title="/home/matthew/Projects/demoapi/vendor/laravel/lumen-framework/src/Concerns/RoutesRequests.php line 334" ondblclick="var f=this.innerHTML;this.innerHTML=this.title;this.title=f;">RoutesRequests.php line 334</a></li>
<li>at <abbr title="Laravel\Lumen\Application">Application</abbr>->run() in <a title="/home/matthew/Projects/demoapi/public/index.php line 28" ondblclick="var f=this.innerHTML;this.innerHTML=this.title;this.title=f;">index.php line 28</a></li>
</ol>
</div>
</div>
</body>
</html>
complete: 0 passing, 1 failing, 0 errors, 0 skipped, 1 total
complete: Tests took 533ms
[Mon Aug 8 11:30:33 2016] 127.0.0.1:44472 [404]: /api/products
complete: See results in Apiary at: https://app.apiary.io/public/tests/run/0153d5bf-6efa-4fdb-b02a-246ddd75cb14
info: Sending SIGTERM to the backend server
info: Backend server was killed

Our route is returning HTML, not JSON, and is also raising a 404 error. So let’s fix that. First, let’s create our Product model at app/Product.php:

<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
//
}

Next, we need to create a migration for the database tables for the Product model:

$ php artisan make:migration create_product_table
Created Migration: 2016_08_08_105737_create_product_table

This will create a new file under database/migrations. Open this file and paste in the following:

<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateProductTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Create products table
Schema::create('products', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->text('description');
$table->float('price');
$table->json('attributes');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// Drop products table
Schema::drop('products');
}
}

Note that we create fields that map to the attributes our API exposes. Also, note the use of the JSON field. In databases that support it, like PostgreSQL, it uses the native JSON support, otherwise it works like a text field. Next, we run the migration to create the table:

$ php artisan migrate
Migrated: 2016_08_08_105737_create_product_table

With our model done, we now need to ensure that when Dredd runs, there is some data in the database, so we’ll create a seeder file at database/seeds/ProductSeeder:

<?php
use Illuminate\Database\Seeder;
use Carbon\Carbon;
class ProductSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
// Add product
DB::table('products')->insert([
'name' => 'Purple widget',
'description' => 'A purple widget',
'price' => 5.99,
'attributes' => json_encode([
'colour' => 'purple',
'size' => 'Small'
]),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
]);
}
}

You also need to amend database/seeds/DatabaseSeeder to call it:

<?php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$this->call('ProductSeeder');
}
}

I found I also had to run the following command to find the new seeder:

$ composer dump-autoload

Then, call the seeder:

$ php artisan db:seed
Seeded: ProductSeeder

We also need to enable Eloquent, as Lumen disables it by default. Uncomment the following line in bootstrap/app.php:

$app->withEloquent();

With that done, we can move onto the controller.

Creating the controller

Create the following file at app/Http/Controllers/ProductController:

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Product;
class ProductController extends Controller
{
private $product;
public function __construct(Product $product) {
$this->product = $product;
}
public function index()
{
// Get all products
$products = $this->product->all();
// Send response
return response()->json($products, 200);
}
}

This implements the index route. Note that we inject the Product instance into the controller. Next, we need to hook it up in app/Http/routes.php:

<?php
/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It is a breeze. Simply tell Lumen the URIs it should respond to
| and give it the Closure to call when that URI is requested.
|
*/
$app->get('/api/products', 'ProductController@index');

Then we run Dredd again:

$ dredd
info: Configuration './dredd.yml' found, ignoring other arguments.
info: Using apiary reporter.
info: Starting server with command: php -S localhost:3000 -t public/
info: Waiting 3 seconds for server command to start...
info: Beginning Dredd testing...
[Mon Aug 8 12:36:28 2016] 127.0.0.1:45466 [200]: /api/products
fail: GET /api/products duration: 131ms
info: Displaying failed tests...
fail: GET /api/products duration: 131ms
fail: body: At '' Invalid type: array (expected object)
request:
method: GET
uri: /api/products
headers:
Content-Type: application/json
User-Agent: Dredd/1.5.0 (Linux 4.4.0-31-generic; x64)
body:
expected:
headers:
Content-Type: application/json
body:
{
"id": 1,
"name": "Purple widget",
"description": "A purple widget",
"price": 5.99,
"attributes": {
"colour": "Purple",
"size": "Small"
}
}
statusCode: 200
actual:
statusCode: 200
headers:
host: localhost:3000
connection: close
x-powered-by: PHP/7.0.8-0ubuntu0.16.04.2
cache-control: no-cache
content-type: application/json
date: Mon, 08 Aug 2016 11:36:28 GMT
body:
[
{
"id": 1,
"name": "Purple widget",
"description": "A purple widget",
"price": "5.99",
"attributes": "{\"colour\":\"purple\",\"size\":\"Small\"}",
"created_at": "2016-08-08 11:32:24",
"updated_at": "2016-08-08 11:32:24"
}
]
complete: 0 passing, 1 failing, 0 errors, 0 skipped, 1 total
complete: Tests took 582ms
complete: See results in Apiary at: https://app.apiary.io/public/tests/run/83da2d67-c846-4356-a3b8-4d7c32daa7ef
info: Sending SIGTERM to the backend server
info: Backend server was killed

Whoops, looks like we made a mistake here. The index route returns an array of objects, but we’re looking for a single object in the blueprint. We also need to wrap our attributes in quotes, and add the created_at and updated_at attributes. Let’s fix the blueprint:

FORMAT: 1A
# Demo API
# Products [/api/products]
Product object representation
## Get products [GET /api/products]
Get a list of products
+ Request (application/json)
+ Response 200 (application/json)
+ Body
[
{
"id": 1,
"name": "Purple widget",
"description": "A purple widget",
"price": 5.99,
"attributes": "{\"colour\": \"Purple\",\"size\": \"Small\"}",
"created_at": "*",
"updated_at": "*"
}
]

Let’s run Dredd again:

$ dredd
info: Configuration './dredd.yml' found, ignoring other arguments.
info: Using apiary reporter.
info: Starting server with command: php -S localhost:3000 -t public/
info: Waiting 3 seconds for server command to start...
info: Beginning Dredd testing...
pass: GET /api/products duration: 65ms
complete: 1 passing, 0 failing, 0 errors, 0 skipped, 1 total
complete: Tests took 501ms
[Mon Aug 8 13:05:54 2016] 127.0.0.1:45618 [200]: /api/products
complete: See results in Apiary at: https://app.apiary.io/public/tests/run/7c23d4ae-aff2-4daf-bbdf-9fd76fc58b97
info: Sending SIGTERM to the backend server
info: Backend server was killed

And now we can see that our test passes.

Next, we’ll implement a test for fetching a single product:

## Get a product [GET /api/products/1]
Get a single product
+ Request (application/json)
+ Response 200 (application/json)
+ Body
{
"id": 1,
"name": "Purple widget",
"description": "A purple widget",
"price": 5.99,
"attributes": "{\"colour\": \"Purple\",\"size\": \"Small\"}",
"created_at": "*",
"updated_at": "*"
}

Note the same basic format - we define the URL that should be fetched, the content of the request, and the response, including the status code.

Let’s hook up our route in app/Http/routes.php:

$app->get('/api/products/{id}', 'ProductController@show');

And add the show() method to the controller:

public function show($id)
{
// Get individual product
$product = $this->product->findOrFail($id);
// Send response
return response()->json($product, 200);
}

Running Dredd again should show this method has been implemented:

$ dredd
info: Configuration './dredd.yml' found, ignoring other arguments.
info: Using apiary reporter.
info: Starting server with command: php -S localhost:3000 -t public/
info: Waiting 3 seconds for server command to start...
info: Beginning Dredd testing...
pass: GET /api/products duration: 66ms
[Mon Aug 8 13:21:31 2016] 127.0.0.1:45750 [200]: /api/products
pass: GET /api/products/1 duration: 17ms
complete: 2 passing, 0 failing, 0 errors, 0 skipped, 2 total
complete: Tests took 521ms
[Mon Aug 8 13:21:31 2016] 127.0.0.1:45752 [200]: /api/products/1
complete: See results in Apiary at: https://app.apiary.io/public/tests/run/bb6d03c3-8fad-477c-b140-af6e0cc8b96c
info: Sending SIGTERM to the backend server
info: Backend server was killed

That’s our read support done. We just need to add support for POST, PATCH and DELETE methods.

Our remaining methods

Let’s set up the test for our POST method first:

## Create products [POST /api/products]
Create a new product
+ name (string) - The product name
+ description (string) - The product description
+ price (float) - The product price
+ attributes (string) - The product attributes
+ Request (application/json)
+ Body
{
"name": "Blue widget",
"description": "A blue widget",
"price": 5.99,
"attributes": "{\"colour\": \"blue\",\"size\": \"Small\"}"
}
+ Response 201 (application/json)
+ Body
{
"id": 2,
"name": "Blue widget",
"description": "A blue widget",
"price": 5.99,
"attributes": "{\"colour\": \"blue\",\"size\": \"Small\"}",
"created_at": "*",
"updated_at": "*"
}

Note we specify the format of the parameters that should be passed through, and that our status code should be 201, not 200 - this is arguably a more correct choice for creating a resource. Be careful of the whitespace - I had some odd issues with it. Next, we add our route:

$app->post('/api/products', 'ProductController@store');

And the store() method in the controller:

public function store(Request $request)
{
// Validate request
$valid = $this->validate($request, [
'name' => 'required|string',
'description' => 'required|string',
'price' => 'required|numeric',
'attributes' => 'string',
]);
// Create product
$product = new $this->product;
$product->name = $request->input('name');
$product->description = $request->input('description');
$product->price = $request->input('price');
$product->attributes = $request->input('attributes');
// Save product
$product->save();
// Send response
return response()->json($product, 201);
}

Note that we validate the attributes, to ensure they are correct and that the required ones exist. Running Dredd again should show the route is now in place:

$ dredd
info: Configuration './dredd.yml' found, ignoring other arguments.
info: Using apiary reporter.
info: Starting server with command: php -S localhost:3000 -t public/
info: Waiting 3 seconds for server command to start...
info: Beginning Dredd testing...
pass: GET /api/products duration: 69ms
[Mon Aug 8 15:17:35 2016] 127.0.0.1:47316 [200]: /api/products
pass: GET /api/products/1 duration: 18ms
[Mon Aug 8 15:17:35 2016] 127.0.0.1:47318 [200]: /api/products/1
pass: POST /api/products duration: 42ms
complete: 3 passing, 0 failing, 0 errors, 0 skipped, 3 total
complete: Tests took 575ms
[Mon Aug 8 15:17:35 2016] 127.0.0.1:47322 [201]: /api/products
complete: See results in Apiary at: https://app.apiary.io/public/tests/run/cb5971cf-180d-47ed-abf4-002378941134
info: Sending SIGTERM to the backend server
info: Backend server was killed

Next, we’ll implement PATCH. This targets an existing object, but accepts parameters in the same way as POST:

## Update existing products [PATCH /api/products/1]
Update an existing product
+ name (string) - The product name
+ description (string) - The product description
+ price (float) - The product price
+ attributes (string) - The product attributes
+ Request (application/json)
+ Body
{
"name": "Blue widget",
"description": "A blue widget",
"price": 5.99,
"attributes": "{\"colour\": \"blue\",\"size\": \"Small\"}"
}
+ Response 200 (application/json)
+ Body
{
"id": 2,
"name": "Blue widget",
"description": "A blue widget",
"price": 5.99,
"attributes": "{\"colour\": \"blue\",\"size\": \"Small\"}",
"created_at": "*",
"updated_at": "*"
}

We add our new route:

$app->patch('/api/products/{id}', 'ProductController@update');

And our update() method:

public function update(Request $request, $id)
{
// Validate request
$valid = $this->validate($request, [
'name' => 'string',
'description' => 'string',
'price' => 'numeric',
'attributes' => 'string',
]);
// Get product
$product = $this->product->findOrFail($id);
// Update it
if ($request->has('name')) {
$product->name = $request->input('name');
}
if ($request->has('description')) {
$product->description = $request->input('description');
}
if ($request->has('price')) {
$product->price = $request->input('price');
}
if ($request->has('attributes')) {
$product->attributes = $request->input('attributes');
}
// Save product
$product->save();
// Send response
return response()->json($product, 200);
}

Here we can’t guarantee every parameter will exist, so we test for it. We run Dredd again:

$ dredd
info: Configuration './dredd.yml' found, ignoring other arguments.
info: Using apiary reporter.
info: Starting server with command: php -S localhost:3000 -t public/
info: Waiting 3 seconds for server command to start...
info: Beginning Dredd testing...
pass: GET /api/products duration: 74ms
[Mon Aug 8 15:27:14 2016] 127.0.0.1:47464 [200]: /api/products
pass: GET /api/products/1 duration: 19ms
[Mon Aug 8 15:27:14 2016] 127.0.0.1:47466 [200]: /api/products/1
pass: POST /api/products duration: 36ms
[Mon Aug 8 15:27:14 2016] 127.0.0.1:47470 [201]: /api/products
[Mon Aug 8 15:27:14 2016] 127.0.0.1:47474 [200]: /api/products/1
pass: PATCH /api/products/1 duration: 34ms
complete: 4 passing, 0 failing, 0 errors, 0 skipped, 4 total
complete: Tests took 2579ms
complete: See results in Apiary at: https://app.apiary.io/public/tests/run/eae98644-44ad-432f-90fc-5f73fa674f66
info: Sending SIGTERM to the backend server
info: Backend server was killed

One last method to implement - the DELETE method. Add this to apiary.apib:

## Delete products [DELETE /api/products/1]
Delete an existing product
+ Request (application/json)
+ Response 200 (application/json)
+ Body
{
"status": "Deleted"
}

Next, add the route:

$app->delete('/api/products/{id}', 'ProductController@destroy');

And the destroy() method in the controller:

public function destroy($id)
{
// Get product
$product = $this->product->findOrFail($id);
// Delete product
$product->delete();
// Return empty response
return response()->json(['status' => 'deleted'], 200);
}

And let’s run Dredd again:

$ dredd
info: Configuration './dredd.yml' found, ignoring other arguments.
info: Using apiary reporter.
info: Starting server with command: php -S localhost:3000 -t public/
info: Waiting 3 seconds for server command to start...
info: Beginning Dredd testing...
pass: GET /api/products duration: 66ms
[Mon Aug 8 15:57:44 2016] 127.0.0.1:48664 [200]: /api/products
pass: GET /api/products/1 duration: 19ms
[Mon Aug 8 15:57:44 2016] 127.0.0.1:48666 [200]: /api/products/1
pass: POST /api/products duration: 45ms
[Mon Aug 8 15:57:44 2016] 127.0.0.1:48670 [201]: /api/products
pass: PATCH /api/products/1 duration: 24ms
[Mon Aug 8 15:57:44 2016] 127.0.0.1:48674 [200]: /api/products/1
pass: DELETE /api/products/1 duration: 27ms
complete: 5 passing, 0 failing, 0 errors, 0 skipped, 5 total
complete: Tests took 713ms
[Mon Aug 8 15:57:44 2016] 127.0.0.1:48678 [200]: /api/products/1
complete: See results in Apiary at: https://app.apiary.io/public/tests/run/a3e11d59-1dad-404b-9319-61ca5c0fcd15
info: Sending SIGTERM to the backend server
info: Backend server was killed

Our REST API is now finished.

Generating HTML version of your documentation

Now we have finished documenting and implementing our API, we need to generate an HTML version of it. One way is to use aglio:

$ aglio -i apiary.apib -o output.html

This will write the documentation to output.html. There’s also scope for choosing different themes if you wish.

You can also use Apiary, which has the advantage that they’ll create a stub of your API so that if you need to work with the API before it’s finished being implemented, you can use that as a placeholder.

Summary

The Blueprint language is a useful way of documenting your API, and makes it simple enough that it’s hard to weasel out of doing so. It’s worth taking a closer look at the specification as it goes into quite a lot of detail. It’s hard to ensure that the documentation and implementation remain in sync, so it’s a good idea to use Dredd to ensure that any changes you make don’t invalidate the documentation. With Aglio or Apiary, you can easily convert the documentation into a more attractive format.

You’ll find the source code for this demo API on Github, so if you get stuck, take a look at that. I did have a fair few issues with whitespace, so bear that in mind if it behaves oddly. I’ve also noticed a few quirks, such as Dredd not working properly if a route returns a 204 response code, which is why I couldn’t use that for deleting - this appears to be a bug, but hopefully this will be resolved soon.

I’ll say it again, Dredd is not a substitute for proper unit tests, and under no circumstances should you use it as one. However, it can be very useful as a way to plan how your API will work and ensure that it complies with that plan, and to ensure that the implementation and documentation don’t diverge. Used as part of your normal continuous integration setup, Dredd can make sure that any divergence between the docs and the application is picked up on and fixed as quickly as possible, while also making writing documentation less onerous.

An introduction to managing your servers with Ansible

$
0
0

If, like me, you’re a web developer who sometimes also has to wear a sysadmin’s hat, then you’ll probably be coming across the same set of tasks each time you set up a new server. These may include:

  • Provisioning new servers on cloud hosting providers such as Digital Ocean
  • Setting up Cloudflare
  • Installing a web server, database and other required packages
  • Installing an existing web application, such as Wordpress
  • Configuring the firewall and Fail2ban
  • Keeping existing servers up to date

These can get tedious and repetitive fairly quickly - who genuinely wants to SSH into each server individually and run the updates regularly? Also, if done manually, there’s a danger of the setup for each server being inconsistent. Shell scripts will do this, but aren’t easy to read and not necessarily easy to adapt to different operating systems. You need a way to be able to manage multiple servers easily, maintain a series of reusable “recipes” and do it all in a way that’s straightforward to read - in other words, a configuration management system.

There are others around, such as Chef, Puppet, and Salt, but my own choice is Ansible. Here’s why I went for Ansible:

  • Playbooks and roles are defined as YAML, making them fairly straightforward to read and understand
  • It’s written in Python, making it easy to create your own modules that leverage existing Python modules to get things done
  • It’s distributed via pip, making it easy to install
  • It doesn’t require you to install anything new on the servers, so you can get started straight away as soon as you can access a new server
  • It has modules for interfacing with cloud services such as Digital Ocean and Amazon Web Services

Ansible is very easy to use, but you do still need to know what is actually going on to get the best out of it. It’s intended as a convenient abstraction on top of the underlying commands, not a replacement, and you should know how to do what you want to do manually before you write an Ansible playbook to do it.

Setting up

You need to have Python 2 available. Ansible doesn’t yet support Python 3 (Grr…) so if you’re using an operating system that has switched to Python 3, such as Arch Linux, you’ll need to have Python 2 installed as well. Assuming you have pip installed, then run this command to install it:

$ sudo pip install ansible

Or for users on systems with Python 3 as the main Python:

$ sudo pip2 install ansible

For Windows users, you’ll want to drop sudo. On Unix-like OS’s that don’t have sudo installed, drop it and run the command as root.

Our first Ansible command

We’ll demonstrate Ansible in action with a Vagrant VM. Drop the following Vagrantfile into your working directory:

# -*- mode: ruby -*-
# vi: set ft=ruby :
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "debian/jessie64"
config.vm.network "forwarded_port", guest: 80, host: 8080
end

Then fire up the VM:

$ vagrant up

This VM will be our test bed for running Ansible. If you prefer, you can use a remote server instead.

Next, we’ll configure Ansible. Save this as ansible.cfg:

[defaults]
hostfile = inventory
remote_user = vagrant
private_key_file = .vagrant/machines/default/virtualbox/private_key

In this case the remote user is vagrant because we’re using Vagrant, but to manage remote machines you would need to change this to the name of the account that you use on the server. The value of private_key_file will also normally be something like /home/matthew/.ssh/id_rsa.pub, but here we’re using the Vagrant-specific key.

Note the hostfile entry - this points to the list of hosts you want to manage with Ansible. Let’s create this next. Save the following as inventory:

testserver ansible_ssh_host=127.0.0.1 ansible_ssh_port=2222

Note that we explicitly need to set the port here because we’re using Vagrant. Normally it will default to port 22. A typical entry for a remote server might look like this:

example.com ansible_ssh_host=192.168.56.101

Note also that we can refer to hosts by the name we give it, which can be as meaningful (or not) as you want.

Let’s run our first command:

$ ansible all -m ping
testserver | SUCCESS => {
"changed": false,
"ping": "pong"
}

We called Ansible with the hosts set to all, therefore every host in the inventory was contacted. We used the -m flag to say we were calling a module, and then specified the ping module. Ansible therefore pinged each server in turn.

We can call ad-hoc commands using the -a flag, as in this example:

$ ansible all -a "uptime"
testserver | SUCCESS | rc=0 >>
17:26:57 up 19 min, 1 user, load average: 0.00, 0.04, 0.13

This command gets the uptime for the server. If you only want to run the command on a single server, you can specify it by name:

$ ansible testserver -a "uptime"
testserver | SUCCESS | rc=0 >>
17:28:21 up 20 min, 1 user, load average: 0.02, 0.04, 0.13

Here we specified the server as testserver. What about if you want to specify more than one server, but not all of them? You can create groups of servers in inventory, as in this example:

[webservers]
testserver ansible_ssh_host=127.0.0.1 ansible_ssh_port=2222
example.com ansible_ssh_host=192.168.56.101

You could then call the following to run the uptime command on all the servers in the webservers group:

$ ansible webservers -a 'uptime'

If you want to run the command as a different user, you can do so:

$ ansible webservers -a 'uptime' -u bob

Note that for running uptime we haven’t specified the -m flag. This is because the command module is the default, but it’s very basic and doesn’t support shell variables. For more complex interactions you might need to use the shell module, as in this example:

$ ansible testserver -m shell -a 'echo $PATH'
testserver | SUCCESS | rc=0 >>
/usr/local/bin:/usr/bin:/bin:/usr/games

For installing a package on Debian or Ubuntu, you might use the apt module:

$ ansible testserver -m apt -a "name=git state=present" --become
testserver | SUCCESS => {
"cache_update_time": 0,
"cache_updated": false,
"changed": true,
"stderr": "",
"stdout": "Reading package lists...\nBuilding dependency tree...\nReading state information...\nThe following extra packages will be installed:\n git-man liberror-perl\nSuggested packages:\n git-daemon-run git-daemon-sysvinit git-doc git-el git-email git-gui gitk\n gitweb git-arch git-cvs git-mediawiki git-svn\nThe following NEW packages will be installed:\n git git-man liberror-perl\n0 upgraded, 3 newly installed, 0 to remove and 83 not upgraded.\nNeed to get 4552 kB of archives.\nAfter this operation, 23.5 MB of additional disk space will be used.\nGet:1 http://httpredir.debian.org/debian/ jessie/main liberror-perl all 0.17-1.1 [22.4 kB]\nGet:2 http://httpredir.debian.org/debian/ jessie/main git-man all 1:2.1.4-2.1+deb8u2 [1267 kB]\nGet:3 http://httpredir.debian.org/debian/ jessie/main git amd64 1:2.1.4-2.1+deb8u2 [3262 kB]\nFetched 4552 kB in 1s (3004 kB/s)\nSelecting previously unselected package liberror-perl.\r\n(Reading database ... \r(Reading database ... 5%\r(Reading database ... 10%\r(Reading database ... 15%\r(Reading database ... 20%\r(Reading database ... 25%\r(Reading database ... 30%\r(Reading database ... 35%\r(Reading database ... 40%\r(Reading database ... 45%\r(Reading database ... 50%\r(Reading database ... 55%\r(Reading database ... 60%\r(Reading database ... 65%\r(Reading database ... 70%\r(Reading database ... 75%\r(Reading database ... 80%\r(Reading database ... 85%\r(Reading database ... 90%\r(Reading database ... 95%\r(Reading database ... 100%\r(Reading database ... 32784 files and directories currently installed.)\r\nPreparing to unpack .../liberror-perl_0.17-1.1_all.deb ...\r\nUnpacking liberror-perl (0.17-1.1) ...\r\nSelecting previously unselected package git-man.\r\nPreparing to unpack .../git-man_1%3a2.1.4-2.1+deb8u2_all.deb ...\r\nUnpacking git-man (1:2.1.4-2.1+deb8u2) ...\r\nSelecting previously unselected package git.\r\nPreparing to unpack .../git_1%3a2.1.4-2.1+deb8u2_amd64.deb ...\r\nUnpacking git (1:2.1.4-2.1+deb8u2) ...\r\nProcessing triggers for man-db (2.7.0.2-5) ...\r\nSetting up liberror-perl (0.17-1.1) ...\r\nSetting up git-man (1:2.1.4-2.1+deb8u2) ...\r\nSetting up git (1:2.1.4-2.1+deb8u2) ...\r\n",
"stdout_lines": [
"Reading package lists...",
"Building dependency tree...",
"Reading state information...",
"The following extra packages will be installed:",
" git-man liberror-perl",
"Suggested packages:",
" git-daemon-run git-daemon-sysvinit git-doc git-el git-email git-gui gitk",
" gitweb git-arch git-cvs git-mediawiki git-svn",
"The following NEW packages will be installed:",
" git git-man liberror-perl",
"0 upgraded, 3 newly installed, 0 to remove and 83 not upgraded.",
"Need to get 4552 kB of archives.",
"After this operation, 23.5 MB of additional disk space will be used.",
"Get:1 http://httpredir.debian.org/debian/ jessie/main liberror-perl all 0.17-1.1 [22.4 kB]",
"Get:2 http://httpredir.debian.org/debian/ jessie/main git-man all 1:2.1.4-2.1+deb8u2 [1267 kB]",
"Get:3 http://httpredir.debian.org/debian/ jessie/main git amd64 1:2.1.4-2.1+deb8u2 [3262 kB]",
"Fetched 4552 kB in 1s (3004 kB/s)",
"Selecting previously unselected package liberror-perl.",
"(Reading database ... ",
"(Reading database ... 5%",
"(Reading database ... 10%",
"(Reading database ... 15%",
"(Reading database ... 20%",
"(Reading database ... 25%",
"(Reading database ... 30%",
"(Reading database ... 35%",
"(Reading database ... 40%",
"(Reading database ... 45%",
"(Reading database ... 50%",
"(Reading database ... 55%",
"(Reading database ... 60%",
"(Reading database ... 65%",
"(Reading database ... 70%",
"(Reading database ... 75%",
"(Reading database ... 80%",
"(Reading database ... 85%",
"(Reading database ... 90%",
"(Reading database ... 95%",
"(Reading database ... 100%",
"(Reading database ... 32784 files and directories currently installed.)",
"Preparing to unpack .../liberror-perl_0.17-1.1_all.deb ...",
"Unpacking liberror-perl (0.17-1.1) ...",
"Selecting previously unselected package git-man.",
"Preparing to unpack .../git-man_1%3a2.1.4-2.1+deb8u2_all.deb ...",
"Unpacking git-man (1:2.1.4-2.1+deb8u2) ...",
"Selecting previously unselected package git.",
"Preparing to unpack .../git_1%3a2.1.4-2.1+deb8u2_amd64.deb ...",
"Unpacking git (1:2.1.4-2.1+deb8u2) ...",
"Processing triggers for man-db (2.7.0.2-5) ...",
"Setting up liberror-perl (0.17-1.1) ...",
"Setting up git-man (1:2.1.4-2.1+deb8u2) ...",
"Setting up git (1:2.1.4-2.1+deb8u2) ..."
]
}

Here we specify that a particular package should be state=present or state=absent. Also, note the --become flag, which allows us to become root. If you’re using an RPM-based Linux distro, you can use the yum module in the same way.

Finally, let’s use the git module to check out a project on the server:

$ ansible testserver -m git -a "repo=https://github.com/matthewbdaly/django_tutorial_blog_ng.git dest=/home/vagrant/example version=HEAD"
testserver | SUCCESS => {
"after": "3542098e3b01103db4d9cfc724ba3c71c45cb314",
"before": null,
"changed": true,
"warnings": []
}

Here we check out a Git repository. We specify the repo, destination and version.

You can call any installed Ansible module in an ad-hoc fashion in the same way. Refer to the documentation for a list of modules.

Playbooks

Ad-hoc commands are useful, but they don’t offer much extra over using SSH. Playbooks allow you to define a repeatable set of commands for a particular use case. In this example, I’ll show you how to write a playbook that does the following:

  • Installs and configures Nginx
  • Clones the repository for my site into the web root

This is sufficiently complex to demonstrate some more of the functionality of Ansible, while also demonstrating playbooks in action.

Create a new folder called playbooks, and inside it save the following as sitecopy.yml:

---
- name: Copy personal website
hosts: testserver
become: True
tasks:
- name: Install Nginx
apt: name=nginx update_cache=yes
- name: Copy config
copy: >
src=files/nginx.conf
dest=/etc/nginx/sites-available/default
- name: Activate config
file: >
dest=/etc/nginx/sites-enabled/default
src=/etc/nginx/sites-available/default
state=link
- name: Delete /var/www directory
file: >
path=/var/www
state=absent
- name: Clone repository
git: >
repo=https://github.com/matthewbdaly/matthewbdaly.github.io.git
dest=/var/www
version=HEAD
- name: Restart Nginx
service: name=nginx state=restarted

Note the name fields - these are comments that will show up in the output when each step is run. First we use the apt module to install Nginx, then we copy over the config file and activate it, then we empty the existing /var/www and clone the repository, and finally we restart Nginx.

Also, note the following fields:

  • hosts defines the hosts affected
  • become specifies that the commands are run using sudo

We also need to create the config for Nginx. Create the files directory under playbooks and save this file as playbooks/files/nginx.conf:

server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
root /var/www;
index index.html index.htm;
server_name localhost;
location / {
try_files $uri $uri/ =404;
}
}

Obviously if your Nginx config will be different, feel free to amend it as necessary. Finally, we run the playbook using the ansible-playbook command:

$ ansible-playbook playbooks/sitecopy.yml
PLAY [Copy personal website] ***************************************************
TASK [setup] *******************************************************************
ok: [testserver]
TASK [Install Nginx] ***********************************************************
changed: [testserver]
TASK [Copy config] *************************************************************
changed: [testserver]
TASK [Activate config] *********************************************************
changed: [testserver]
TASK [Delete /var/www directory] ***********************************************
changed: [testserver]
TASK [Clone repository] ********************************************************
changed: [testserver]
TASK [Restart Nginx] ***********************************************************
changed: [testserver]
PLAY RECAP *********************************************************************
testserver : ok=7 changed=6 unreachable=0 failed=0

If we had a playbook that we wanted to run on only a subset of the hosts it applied to, we could use the -l flag, as in this example:

$ ansible-playbook playbooks/sitecopy.yml -l testserver

Using these same basic concepts, you can invoke many different Ansible modules to achieve many different tasks. You can spin up new servers on supported cloud hosting companies, you can set up a known good fail2ban config, you can configure your firewall, and many more tasks. As your playbooks get bigger, it’s worth moving sections into separate roles that get invoked within multiple playbooks, in order to reduce repetition.

Finally, I mentioned earlier that you can use Ansible to update all of your servers regularly. Here’s the playbook I use for that:

---
- name: Update system
hosts: all
become: True
tasks:
- name: update system
apt: upgrade=full update_cache=yes

This connects to all hosts using the all shortcut we saw earlier, and upgrades all existing packages. Using this method is a lot easier than connecting to each one in turn via SSH and updating it manually.

Summary

Ansible is an extremely useful tool for managing servers, but to get the most out of it you have to put in a fair bit of work reading the documentation and writing your own playbooks for your own use cases. It’s simple to get started with, and if you’re willing to put in the time writing your own playbooks then in the long run you’ll save yourself a lot of time and grief by making it easy to set up new servers and administer existing ones. Hopefully this has given you a taster of what you can do with Ansible - from here on the documentation is worth a look as it lists all of the modules that ship with Ansible. If there’s a particular task you dread, such as setting up a mail server, then Ansible is a very good way to automate that away so it’s easier next time.

My experience is that it’s best to make an effort to try to standardise on two or three different stacks for different purposes, and create Ansible playbooks for those stacks. For instance, I’ve tended to use PHP 5, Apache, MySQL, Memcached and Varnish for Wordpress sites, and PHP 7, Nginx, Redis and PostgreSQL for Laravel sites. That way I know that any sites I build with Laravel will be using that stack. Knowing my servers are more consistent makes it easier to work with them and identify problems.

Creating a personal dashboard with React and Webpack

$
0
0

The Raspberry Pi is a great device for running simple web apps at home on a permanent basis, and you can pick up a small touchscreen for it quite cheaply. This makes it easy to build and host a small personal dashboard that pulls important data from various APIs or RSS feeds and displays it. You’ll often see dashboards like this on Raspberry Pi forums and subreddits. As I’m currently between jobs, and have some time to spare before my new job starts, I decided to start creating my own version of it. It was obvious that React.js is a good fit for this as it allows you to break up your user interface into multiple independent components and keep the functionality close to the UI. It also makes it easy to reuse widgets by passing different parameters through each time.

In this tutorial I’ll show you how to start building a simple personal dashboard using React and Webpack. You can then install Nginx on your Raspberry Pi and host it from there. In the process, you’ll be able to pick up a bit of knowledge about Webpack and ECMAScript 2015 (using Babel). Our initial implementation will have only two widgets, a clock and a feed, but those should show you enough of the basics that you should then be able to build other widgets you may have in mind.

Installing our dependencies

First, let’s create our package.json:

$ npm init -y

Then install the dependencies:

$ npm install --save-dev babel-cli babel-register babel-core babel-eslint babel-loader babel-preset-es2015 babel-preset-react chai css-loader eslint eslint-loader eslint-plugin-react file-loader istanbul@^1.0.0-alpha.2 jquery jsdom mocha moment node-sass react react-addons-pure-render-mixin react-addons-test-utils react-dom react-hot-loader request sass-loader style-loader url-loader webpack webpack-dev-server

Note that we need to install a specific version of Istanbul to get code coverage.

Next, we create our Webpack config. Save this as webpack.config.js:

var webpack = require('webpack');
module.exports = {
entry: [
'webpack/hot/only-dev-server',
"./js/app.js"
],
debug: true,
devtool: 'source-map',
output: {
path: __dirname + '/static',
filename: "bundle.js"
},
module: {
preLoaders: [
{
test: /(\.js$|\.jsx$)/,
exclude: /node_modules/,
loader: "eslint-loader"
}
],
loaders: [
{ test: /\.jsx?$/, loaders: ['react-hot', 'babel'], exclude: /node_modules/ },
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'},
{ test: /\.woff2?$/, loader: "url-loader?limit=25000" },
{ test: /\.(eot|svg|ttf)?$/, loader: "file-loader" },
{ test: /\.scss$/, loader: "style!css!sass" }
]
},
eslint: {
configFile: '.eslintrc.yml'
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin()
]
};

Note the various loaders we’re using. We use ESLint to lint our Javascript files for code quality, and the build will fail if they do not match the required standards. We’re also using loaders for CSS, Sass, Babel (so we can use ES2015 for our Javascript) and fonts. Also, note the hot module replacement plugin - this allows us to reload the application automatically. If you haven’t used Webpack before, this config should be sufficient to get you started, but I recommend reading the documentation.

We also need to configure ESLint how we want. Here is the configuration we will be using, which should be saved as .eslintrc.yml:

rules:
no-debugger:
- 0
no-console:
- 0
no-unused-vars:
- 0
indent:
- 2
- 2
quotes:
- 2
- single
linebreak-style:
- 2
- unix
semi:
- 2
- always
env:
es6: true
browser: true
node: true
extends: 'eslint:recommended'
parserOptions:
sourceType: module
ecmaFeatures:
jsx: true
experimentalObjectRestSpread: true
modules: true
plugins:
- react

We also need a base HTML file. Save this as index.html:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Personal Dashboard</title>
</head>
<body>
<div id="view"></section>
<script src="bundle.js"></script>
</body>
</html>

We also need to set the commands for building and testing our app in package.json:

"scripts": {
"test": "istanbul cover _mocha -- --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'",
"test:watch": "npm run test -- --watch",
"start": "webpack-dev-server --progress --colors",
"build": "webpack --progress --colors"
},
"babel": {
"presets": [
"es2015",
"react"
]
},

The npm test command will call Mocha to run the tests, but will also use Istanbul to generate test coverage. For the sake of brevity, our tests won’t be terribly comprehensive. The npm start command will run a development server, while npm run build will build our application.

We also need to create the test/ folder and the test/setup.js file:

import jsdom from 'jsdom';
import chai from 'chai';
const doc = jsdom.jsdom('<!doctype html><html><body></body></html>');
const win = doc.defaultView;
global.document = doc;
global.window = win;
Object.keys(window).forEach((key) => {
if (!(key in global)) {
global[key] = window[key];
}
});

This sets up Chai and creates a dummy DOM for our tests. We also need to create the folder js/ and the file js/app.js. You can leave that file empty for now.

If you now run npm start and navigate to http://localhost:8080/webpack-dev-server/, you can see the current state of the application.

Our dashboard component

Our first React component will be a wrapper for all the other ones. Each of the rest of the components will be a self-contained widget that will populate itself without the need for a centralized data store like Redux. I will mention that Redux is a very useful library, and for larger React applications it makes a lot of sense to use it, but here we’re better off having each widget manage its own data internally, rather than have it be passed down from a single data store.

Save the following as test/components/dashboard.js:

import TestUtils from 'react-addons-test-utils';
import React from 'react';
import {findDOMNode} from 'react-dom';
import Dashboard from '../../js/components/dashboard';
import {expect} from 'chai';
const {renderIntoDocument, scryRenderedDOMComponentsWithClass, Simulate} = TestUtils;
describe('Dashboard', () => {
it('renders the dashboard', () => {
const component = renderIntoDocument(
<Dashboard title="My Dashboard" />
);
const title = findDOMNode(component.refs.title);
expect(title).to.be.ok;
expect(title.textContent).to.contain('My Dashboard');
});
}

This tests that we can set the title of our dashboard component. Let’s run our tests:

$ npm test
> personal-dashboard@1.0.0 test /home/matthew/Projects/personal-dashboard
> istanbul cover _mocha -- --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'
No coverage information was collected, exit without writing coverage information
module.js:327
throw err;
^
Error: Cannot find module '../../js/components/dashboard'
at Function.Module._resolveFilename (module.js:325:15)
at Function.Module._load (module.js:276:25)
at Module.require (module.js:353:17)
at require (internal/module.js:12:17)
at Object.<anonymous> (dashboard.js:4:1)
at Module._compile (module.js:409:26)
at loader (/home/matthew/Projects/personal-dashboard/node_modules/babel-register/lib/node.js:148:5)
at Object.require.extensions.(anonymous function) [as .js] (/home/matthew/Projects/personal-dashboard/node_modules/babel-register/lib/node.js:158:7)
at Module.load (module.js:343:32)
at Function.Module._load (module.js:300:12)
at Module.require (module.js:353:17)
at require (internal/module.js:12:17)
at /home/matthew/Projects/personal-dashboard/node_modules/mocha/lib/mocha.js:220:27
at Array.forEach (native)
at Mocha.loadFiles (/home/matthew/Projects/personal-dashboard/node_modules/mocha/lib/mocha.js:217:14)
at Mocha.run (/home/matthew/Projects/personal-dashboard/node_modules/mocha/lib/mocha.js:485:10)
at Object.<anonymous> (/home/matthew/Projects/personal-dashboard/node_modules/mocha/bin/_mocha:403:18)
at Module._compile (module.js:409:26)
at Object.Module._extensions..js (module.js:416:10)
at Object.Module._extensions.(anonymous function) (/home/matthew/Projects/personal-dashboard/node_modules/istanbul/lib/hook.js:109:37)
at Module.load (module.js:343:32)
at Function.Module._load (module.js:300:12)
at Function.Module.runMain (module.js:441:10)
at runFn (/home/matthew/Projects/personal-dashboard/node_modules/istanbul/lib/command/common/run-with-cover.js:122:16)
at /home/matthew/Projects/personal-dashboard/node_modules/istanbul/lib/command/common/run-with-cover.js:251:17
at /home/matthew/Projects/personal-dashboard/node_modules/istanbul/lib/util/file-matcher.js:68:16
at /home/matthew/Projects/personal-dashboard/node_modules/async/lib/async.js:52:16
at /home/matthew/Projects/personal-dashboard/node_modules/async/lib/async.js:361:13
at /home/matthew/Projects/personal-dashboard/node_modules/async/lib/async.js:52:16
at done (/home/matthew/Projects/personal-dashboard/node_modules/async/lib/async.js:246:17)
at /home/matthew/Projects/personal-dashboard/node_modules/async/lib/async.js:44:16
at /home/matthew/Projects/personal-dashboard/node_modules/async/lib/async.js:358:17
at LOOP (fs.js:1530:14)
at nextTickCallbackWith0Args (node.js:420:9)
at process._tickCallback (node.js:349:13)
npm ERR! Test failed. See above for more details.

Our dashboard file doesn’t exist. So let’s create it:

$ mkdir js/components
$ touch js/components/dashboard.js

And run our test again:

$ npm test
> personal-dashboard@1.0.0 test /home/matthew/Projects/personal-dashboard
> istanbul cover _mocha -- --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'
Dashboard
Warning: React.createElement: type should not be null, undefined, boolean, or number. It should be a string (for DOM elements) or a ReactClass (for composite components).
1) renders the dashboard
0 passing (31ms)
1 failing
1) Dashboard renders the dashboard:
Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
at invariant (node_modules/fbjs/lib/invariant.js:38:15)
at [object Object].instantiateReactComponent [as _instantiateReactComponent] (node_modules/react/lib/instantiateReactComponent.js:86:134)
at [object Object].ReactCompositeComponentMixin.performInitialMount (node_modules/react/lib/ReactCompositeComponent.js:388:22)
at [object Object].ReactCompositeComponentMixin.mountComponent (node_modules/react/lib/ReactCompositeComponent.js:262:21)
at Object.ReactReconciler.mountComponent (node_modules/react/lib/ReactReconciler.js:47:35)
at mountComponentIntoNode (node_modules/react/lib/ReactMount.js:105:32)
at ReactReconcileTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
at batchedMountComponentIntoNode (node_modules/react/lib/ReactMount.js:126:15)
at ReactDefaultBatchingStrategyTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
at Object.ReactDefaultBatchingStrategy.batchedUpdates (node_modules/react/lib/ReactDefaultBatchingStrategy.js:63:19)
at Object.batchedUpdates (node_modules/react/lib/ReactUpdates.js:98:20)
at Object.ReactMount._renderNewRootComponent (node_modules/react/lib/ReactMount.js:285:18)
at Object.ReactMount._renderSubtreeIntoContainer (node_modules/react/lib/ReactMount.js:371:32)
at Object.ReactMount.render (node_modules/react/lib/ReactMount.js:392:23)
at ReactTestUtils.renderIntoDocument (node_modules/react/lib/ReactTestUtils.js:85:21)
at Context.<anonymous> (dashboard.js:11:23)
No coverage information was collected, exit without writing coverage information
npm ERR! Test failed. See above for more details.

Now we have a failing test, we can create our component. Save this as js/components/dashboard.js:

import React from 'react';
export default React.createClass({
render() {
return (
<div className="dashboard">
<h1 ref="title">{this.props.title}</h1>
<div className="wrapper">
</div>
</div>
);
}
});

And let’s run our tests again:

$ npm test
> personal-dashboard@1.0.0 test /home/matthew/Projects/personal-dashboard
> istanbul cover _mocha -- --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'
Dashboard
✓ renders the dashboard
1 passing (50ms)
No coverage information was collected, exit without writing coverage information

Our first component is in place. However, it isn’t getting loaded. We also need to start thinking about styling. Create the file scss/style.scss, but leave it blank for now. Then save this in js/app.js:

import React from 'react';
import ReactDOM from 'react-dom';
import Dashboard from './components/dashboard';
import styles from '../scss/style.scss';
ReactDOM.render(
<Dashboard title="My Dashboard" />,
document.getElementById('view')
);

Note that we’re importing CSS or Sass files in the same way as Javascript files. This is unique to Webpack, and while it takes a bit of getting used to, it has its advantages - if you import only the styles relating to each component, you can be sure there’s no orphaned CSS files. Here, we only have one CSS file anyway, so it’s a non-issue.

If you now run npm start, our dashboard gets loaded and the title is displayed. With our dashboard in place, we can now implement our first widget.

Creating the clock widget

Our first widget will be a simple clock. This demonstrates changing the state of the widget on an interval. First let’s write a test - save this as test/components/clockwidget.js:

import TestUtils from 'react-addons-test-utils';
import React from 'react';
import {findDOMNode} from 'react-dom';
import ClockWidget from '../../js/components/clockwidget';
import {expect} from 'chai';
const {renderIntoDocument, scryRenderedDOMComponentsWithClass, Simulate} = TestUtils;
describe('Clock Widget', () => {
it('renders the clock widget', () => {
const currentTime = 1465160300530;
const component = renderIntoDocument(
<ClockWidget time={currentTime} />
);
const time = findDOMNode(component.refs.time);
expect(time).to.be.ok;
expect(time.textContent).to.contain('Sunday');
});
});

And create an empty file at js/components/clockwidget.js. Then we run our tests again:

$ npm test
> personal-dashboard@1.0.0 test /home/matthew/Projects/personal-dashboard
> istanbul cover _mocha -- --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'
Clock Widget
Warning: React.createElement: type should not be null, undefined, boolean, or number. It should be a string (for DOM elements) or a ReactClass (for composite components).
1) renders the clock widget
Dashboard
✓ renders the dashboard
1 passing (46ms)
1 failing
1) Clock Widget renders the clock widget:
Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
at invariant (node_modules/fbjs/lib/invariant.js:38:15)
at [object Object].instantiateReactComponent [as _instantiateReactComponent] (node_modules/react/lib/instantiateReactComponent.js:86:134)
at [object Object].ReactCompositeComponentMixin.performInitialMount (node_modules/react/lib/ReactCompositeComponent.js:388:22)
at [object Object].ReactCompositeComponentMixin.mountComponent (node_modules/react/lib/ReactCompositeComponent.js:262:21)
at Object.ReactReconciler.mountComponent (node_modules/react/lib/ReactReconciler.js:47:35)
at mountComponentIntoNode (node_modules/react/lib/ReactMount.js:105:32)
at ReactReconcileTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
at batchedMountComponentIntoNode (node_modules/react/lib/ReactMount.js:126:15)
at ReactDefaultBatchingStrategyTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
at Object.ReactDefaultBatchingStrategy.batchedUpdates (node_modules/react/lib/ReactDefaultBatchingStrategy.js:63:19)
at Object.batchedUpdates (node_modules/react/lib/ReactUpdates.js:98:20)
at Object.ReactMount._renderNewRootComponent (node_modules/react/lib/ReactMount.js:285:18)
at Object.ReactMount._renderSubtreeIntoContainer (node_modules/react/lib/ReactMount.js:371:32)
at Object.ReactMount.render (node_modules/react/lib/ReactMount.js:392:23)
at ReactTestUtils.renderIntoDocument (node_modules/react/lib/ReactTestUtils.js:85:21)
at Context.<anonymous> (clockwidget.js:12:23)
No coverage information was collected, exit without writing coverage information
npm ERR! Test failed. See above for more details.

With a failing test in place, we can create our component:

import React from 'react';
import moment from 'moment';
export default React.createClass({
getInitialState() {
return {
time: this.props.time || moment()
};
},
render() {
const time = moment(this.state.time).format('dddd, Do MMMM YYYY, h:mm:ss a');
return (
<div className="clockwidget widget">
<div className="widget-content">
<h2 ref="time">{time}</h2>
</div>
</div>
);
}
});

Note that the component accepts a property of time. The getInitialState() method then converts this.props.time into this.state.time so that it can be displayed on render. Note we also set a default of the current time using Moment.js.

We also need to update the dashboard component to load this new component:

import React from 'react';
import ClockWidget from './clockwidget';
export default React.createClass({
render() {
return (
<div className="dashboard">
<h1 ref="title">{this.props.title}</h1>
<div className="wrapper">
<ClockWidget />
</div>
</div>
);
}
});

Now, if you try running npm start and viewing the dashboard in the browser, you will see that it displays the current time and date, but it’s not being updated. You can force the page to reload every now and then, but we can do better than that. We can set an interval in which the time will refresh. As the smallest unit we show is seconds, this interval should be 1 second.

Amend the clock component as follows:

import React from 'react';
import moment from 'moment';
export default React.createClass({
getInitialState() {
return {
time: this.props.time || moment()
};
},
tick() {
this.setState({
time: moment()
});
},
componentDidMount() {
this.interval = setInterval(this.tick, 1000);
},
componentWillUnmount() {
clearInterval(this.interval);
},
render() {
const time = moment(this.state.time).format('dddd, Do MMMM YYYY, h:mm:ss a');
return (
<div className="clockwidget widget">
<div className="widget-content">
<h2 ref="time">{time}</h2>
</div>
</div>
);
}
});

When our component has mounted, we set an interval of 1,000 milliseconds, and each time it elapses we call the tick() method. This method sets the state to the current time, and as a result the user interface is automatically re-rendered. On unmount, we clear the interval.

In this case we’re just calling a single function on a set interval. In principle, the same approach can be used to populate components in other ways, such as by making an AJAX request.

Creating an RSS widget

Our next widget will be a simple RSS feed reader. We’ll fetch the content with jQuery and render it using React. We’ll also reload it regularly. First, let’s create our test:

import TestUtils from 'react-addons-test-utils';
import React from 'react';
import {findDOMNode} from 'react-dom';
import FeedWidget from '../../js/components/feedwidget';
import {expect} from 'chai';
const {renderIntoDocument, scryRenderedDOMComponentsWithClass, Simulate} = TestUtils;
describe('Feed Widget', () => {
it('renders the Feed widget', () => {
const url = "http://feeds.bbci.co.uk/news/rss.xml?edition=uk"
const component = renderIntoDocument(
<FeedWidget feed={url} size={5} delay={60} />
);
const feed = findDOMNode(component.refs.feed);
expect(feed).to.be.ok;
expect(feed.textContent).to.contain(url);
});
});

Our feed widget will accept an external URL as an argument, and will then poll this URL regularly to populate the feed. It also allows us to specify the size attribute, which denotes the number of feed items, and the delay attribute, which denotes the number of seconds it should wait before fetching the data again.

We also need to amend the dashboard component to include this widget:

import React from 'react';
import ClockWidget from './clockwidget';
import FeedWidget from './feedwidget';
export default React.createClass({
render() {
return (
<div className="dashboard">
<h1 ref="title">{this.props.title}</h1>
<div className="wrapper">
<ClockWidget />
<FeedWidget feed="http://feeds.bbci.co.uk/news/rss.xml?edition=uk" size="5" delay="60" />
</div>
</div>
);
}
});

If we then create js/components/feedwidget.js and run npm test:

$ npm test
> personal-dashboard@1.0.0 test /home/matthew/Projects/personal-dashboard
> istanbul cover _mocha -- --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'
Clock Widget
✓ renders the clock widget (92ms)
Dashboard
Warning: React.createElement: type should not be null, undefined, boolean, or number. It should be a string (for DOM elements) or a ReactClass (for composite components). Check the render method of `dashboard`.
1) renders the dashboard
Feed Widget
Warning: React.createElement: type should not be null, undefined, boolean, or number. It should be a string (for DOM elements) or a ReactClass (for composite components).
2) renders the Feed widget
1 passing (286ms)
2 failing
1) Dashboard renders the dashboard:
Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object. Check the render method of `dashboard`.
at invariant (node_modules/fbjs/lib/invariant.js:38:15)
at instantiateReactComponent (node_modules/react/lib/instantiateReactComponent.js:86:134)
at instantiateChild (node_modules/react/lib/ReactChildReconciler.js:43:28)
at node_modules/react/lib/ReactChildReconciler.js:70:16
at traverseAllChildrenImpl (node_modules/react/lib/traverseAllChildren.js:69:5)
at traverseAllChildrenImpl (node_modules/react/lib/traverseAllChildren.js:85:23)
at traverseAllChildren (node_modules/react/lib/traverseAllChildren.js:164:10)
at Object.ReactChildReconciler.instantiateChildren (node_modules/react/lib/ReactChildReconciler.js:69:7)
at ReactDOMComponent.ReactMultiChild.Mixin._reconcilerInstantiateChildren (node_modules/react/lib/ReactMultiChild.js:194:41)
at ReactDOMComponent.ReactMultiChild.Mixin.mountChildren (node_modules/react/lib/ReactMultiChild.js:231:27)
at ReactDOMComponent.Mixin._createInitialChildren (node_modules/react/lib/ReactDOMComponent.js:715:32)
at ReactDOMComponent.Mixin.mountComponent (node_modules/react/lib/ReactDOMComponent.js:531:12)
at Object.ReactReconciler.mountComponent (node_modules/react/lib/ReactReconciler.js:47:35)
at ReactDOMComponent.ReactMultiChild.Mixin.mountChildren (node_modules/react/lib/ReactMultiChild.js:242:44)
at ReactDOMComponent.Mixin._createInitialChildren (node_modules/react/lib/ReactDOMComponent.js:715:32)
at ReactDOMComponent.Mixin.mountComponent (node_modules/react/lib/ReactDOMComponent.js:531:12)
at Object.ReactReconciler.mountComponent (node_modules/react/lib/ReactReconciler.js:47:35)
at [object Object].ReactCompositeComponentMixin.performInitialMount (node_modules/react/lib/ReactCompositeComponent.js:397:34)
at [object Object].ReactCompositeComponentMixin.mountComponent (node_modules/react/lib/ReactCompositeComponent.js:262:21)
at Object.ReactReconciler.mountComponent (node_modules/react/lib/ReactReconciler.js:47:35)
at [object Object].ReactCompositeComponentMixin.performInitialMount (node_modules/react/lib/ReactCompositeComponent.js:397:34)
at [object Object].ReactCompositeComponentMixin.mountComponent (node_modules/react/lib/ReactCompositeComponent.js:262:21)
at Object.ReactReconciler.mountComponent (node_modules/react/lib/ReactReconciler.js:47:35)
at mountComponentIntoNode (node_modules/react/lib/ReactMount.js:105:32)
at ReactReconcileTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
at batchedMountComponentIntoNode (node_modules/react/lib/ReactMount.js:126:15)
at ReactDefaultBatchingStrategyTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
at Object.ReactDefaultBatchingStrategy.batchedUpdates (node_modules/react/lib/ReactDefaultBatchingStrategy.js:63:19)
at Object.batchedUpdates (node_modules/react/lib/ReactUpdates.js:98:20)
at Object.ReactMount._renderNewRootComponent (node_modules/react/lib/ReactMount.js:285:18)
at Object.ReactMount._renderSubtreeIntoContainer (node_modules/react/lib/ReactMount.js:371:32)
at Object.ReactMount.render (node_modules/react/lib/ReactMount.js:392:23)
at ReactTestUtils.renderIntoDocument (node_modules/react/lib/ReactTestUtils.js:85:21)
at Context.<anonymous> (dashboard.js:11:23)
2) Feed Widget renders the Feed widget:
Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
at invariant (node_modules/fbjs/lib/invariant.js:38:15)
at [object Object].instantiateReactComponent [as _instantiateReactComponent] (node_modules/react/lib/instantiateReactComponent.js:86:134)
at [object Object].ReactCompositeComponentMixin.performInitialMount (node_modules/react/lib/ReactCompositeComponent.js:388:22)
at [object Object].ReactCompositeComponentMixin.mountComponent (node_modules/react/lib/ReactCompositeComponent.js:262:21)
at Object.ReactReconciler.mountComponent (node_modules/react/lib/ReactReconciler.js:47:35)
at mountComponentIntoNode (node_modules/react/lib/ReactMount.js:105:32)
at ReactReconcileTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
at batchedMountComponentIntoNode (node_modules/react/lib/ReactMount.js:126:15)
at ReactDefaultBatchingStrategyTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
at Object.ReactDefaultBatchingStrategy.batchedUpdates (node_modules/react/lib/ReactDefaultBatchingStrategy.js:63:19)
at Object.batchedUpdates (node_modules/react/lib/ReactUpdates.js:98:20)
at Object.ReactMount._renderNewRootComponent (node_modules/react/lib/ReactMount.js:285:18)
at Object.ReactMount._renderSubtreeIntoContainer (node_modules/react/lib/ReactMount.js:371:32)
at Object.ReactMount.render (node_modules/react/lib/ReactMount.js:392:23)
at ReactTestUtils.renderIntoDocument (node_modules/react/lib/ReactTestUtils.js:85:21)
at Context.<anonymous> (feedwidget.js:12:23)
=============================== Coverage summary ===============================
Statements : 83.33% ( 10/12 )
Branches : 50% ( 1/2 )
Functions : 66.67% ( 4/6 )
Lines : 83.33% ( 10/12 )
================================================================================
npm ERR! Test failed. See above for more details.

Our test fails, so we can start work on the widget proper. Here it is:

import React from 'react';
import jQuery from 'jquery';
window.jQuery = jQuery;
const FeedItem = React.createClass({
render() {
return (
<a href={this.props.link} target="_blank">
<li className="feeditem">{this.props.title}</li>
</a>
);
}
});
export default React.createClass({
getInitialState() {
return {
feed: [],
size: this.props.size || 5
};
},
componentDidMount() {
this.getFeed();
this.interval = setInterval(this.getFeed, (this.props.delay * 1000));
},
componentWillUnmount() {
clearInterval(this.interval);
},
getFeed() {
let that = this;
jQuery.ajax({
url: this.props.feed,
success: function (response) {
let xml = jQuery(response);
let feed = [];
xml.find('item').each(function () {
let item = {};
item.title = jQuery(this).find('title').text();
item.link = jQuery(this).find('guid').text();
feed.push(item);
});
that.setState({
feed: feed.slice(0,that.state.size)
});
}
});
},
render() {
let feedItems = this.state.feed.map(function (item, index) {
return (
<FeedItem title={item.title} link={item.link} key={item.link}></FeedItem>
);
});
return (
<div className="feedwidget widget">
<div className="widget-content">
<h2 ref="feed"> Fetched from {this.props.feed}</h2>
<ul>
{feedItems}
</ul>
</div>
</div>
);
}
});

This is by far the most complex component, so a little explanation is called for. We include jQuery as a dependency at the top of the file. Then we create a component for rendering an individual feed item, called FeedItem. This is very simple, consisting of an anchor tag wrapped around a list item. Note the use of the const keyword - in ES6 this denotes a constant.

Next, we move onto the feed widget proper. We set the initial state of the feed to be an empty array. Then, we define a componentDidMount() method that calls getFeed() and sets up an interval to call it again, based on the delay property. The getFeed() method fetches the URL in question and sets this.state.feed to an array of the most recent entries in the feed, with the size denoted by the size property passed through. We also clear that interval when the component is about to be umounted.

Note that you may have problems with the Access-Control-Allow-Origin HTTP header. It’s possible to disable this in your web browser, so if you want to run this as a dashboard you’ll probably need to do so. On Chrome there’s a useful plugin that allows you to disable this when needed.

Because our FeedWidget has been created in a generic manner, we can then include multiple feed widgets easily, as in this example:

import React from 'react';
import ClockWidget from './clockwidget';
import FeedWidget from './feedwidget';
export default React.createClass({
render() {
return (
<div className="dashboard">
<h1 ref="title">{this.props.title}</h1>
<div className="wrapper">
<ClockWidget />
<FeedWidget feed="http://feeds.bbci.co.uk/news/rss.xml?edition=uk" size="5" delay="60" />
<FeedWidget feed="https://www.sitepoint.com/feed/" size="10" delay="120" />
</div>
</div>
);
}
});

We also need to style our widgets. Save this as scss/_colours.scss:

$bgColour: #151515;
$txtColour: #cfcfcf;
$clockBg: #fa8c00;
$clockHoverBg: #0099ff;
$clockTxt: #fff;
$feedBg: #0099ff;
$feedTxt: #fff;
$feedHoverBg: #fa8c00;

And this as scss/style.scss:

@import 'colours';
html, body {
background-color: $bgColour;
color: $txtColour;
font-family: Arial, Helvetica, sans-serif;
}
div.dashboard {
padding: 10px;
}
div.wrapper {
-moz-column-count: 4;
-webkit-column-count: 4;
column-count: 4;
-moz-column-gap: 1em;
-webkit-column-gap: 1em;
column-gap: 1em;
}
div.widget {
display: inline-block;
margin: 0 0 1em;
width: 100%;
min-height: 100px;
margin: 5px;
opacity: 0.8;
transition: opacity 1s;
&:hover {
opacity: 1;
}
h2, h4 {
padding: 20px;
}
div.widget-content {
width: 100%;
}
}
div.clockwidget {
background-color: $clockBg;
color: $clockTxt;
}
div.feedwidget {
background-color: $feedBg;
color: $feedTxt;
h2 {
word-wrap: break-word;
}
ul {
margin-left: 0;
padding-left: 20px;
a {
text-decoration: none;
padding: 5px;
li {
list-style-type: none;
font-weight: bold;
color: $feedTxt;
}
}
}
}

The end result should look something like this:

The personal dashboard in action

With that done, feel free to add whatever other feeds you want to include.

Deploying our dashboard

The final step is deploying our dashboard to our Raspberry Pi or other device. Run the following command to generate the Javascript:

$ npm run build

This will create static/bundle.js. You can then copy that file over to your web server with index.html and place both files in the web root. I recommend using Nginx if you’re using a Raspberry Pi as it’s faster and simpler for static content. If you’re likely to make a lot of changes you might want to create a command in the scripts section of your package.json to deploy the files more easily.

These basic widgets should be enough to get you started. You should be able to use the feed widget with virtually any RSS feed, and you should be able to use a similar approach to poll third-party APIs, although you might need to authenticate in some way (if you do, you won’t want to expose your authentication details, so ensure that nobody from outside the network can view your application). I’ll leave it to you to see what kind of interesting widgets you come up with for your own dashboard, but some ideas to get you started include:

  • Public transport schedules/Traffic issues
  • Weather reports
  • Shopping lists/Todo lists, with HTML5 local storage used to persist them
  • Galleries of recent photos on social networks
  • Status of servers on cloud hosting providers

With a little thought, you can probably come up with a few more than that! I’ve created a Github repository with the source code so you can check your own implementation against it.

Maintaining your CV with Markdown and Emacs

$
0
0

I’ve recently been jobhunting, so that has meant having to update my CV. Fortunately, I’ve got into the habit of keeping it up to date easily by writing it in Markdown and generating it in the required format on demand. That way I can easily convert it to HTML, PDF or Microsoft DocX format as and when I need it. I thought I’d share this method as it works very well for me.

Maintaining your CV in Emacs?

Yes, you read that right! Although I’m a die-hard Vim user, I do use Emacs for a few things. One of them is time-tracking using org-mode, and another is maintaining my CV.

First of all you’ll need to install pandoc, texlive and markdown. On Ubuntu this is easily done using apt-get:

$ sudo apt-get install pandoc markdown texlive

You’ll also need to install Emacs and the appropriate packages, namely markdown-mode and markdown-mode+. To do so, first ensure this is in your .emacs.d/init.el:

(require 'package)
(add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/"))
(package-initialize)
;; Markdown support
(require 'markdown-mode)
(require 'markdown-mode+)
(setq markdown-command "/usr/bin/markdown")
(add-to-list 'auto-mode-alist '("\\.markdown$" . markdown-mode))
(add-to-list 'auto-mode-alist '("\\.md$" . markdown-mode))
(setq markdown-css-paths `(,(expand-file-name "Documents/markdown.css")))

Then fire up Emacs, ignoring the warnings you get, and run M-x package-list-packages to load the list of available packages. I’ll leave navigating and installing this list of packages to you, but once they’re done you should have everything you need.

This assumes the stylesheet you wish to use is at ~/Documents/markdown.css - adjust the path if necessary. You may also need to amend the path to your Markdown install if the location differs. You can put what you like in the stylesheet, but my advice is to keep it as simple as you can - it’s your CV, not a web page. Here’s what I use:

body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
width: 80%;
margin: auto;
background: #ffffff;
padding: 10px;
}
h2 {
font-size: 30px;
color: #757575;
text-align: center;
margin-bottom: 15px;
}
h1 {
font-size: 55px;
color: #757575;
text-align: center;
margin-bottom: 15px;
}
hr {
color: #000000;
}
ul li {
list-style-type: disc;
}
blockquote {
text-align: center;
}
a, a:visited, a:hover {
text-decoration: none;
color: #000000;
}
code {
white-space: pre-wrap;
word-wrap: break-word;
}

Next, we write our CV in Markdown. Here’s a sample one based on mine:

James Smith
============
About me
--------
I'm a full-stack web developer. I have built a wide variety of web applications (including single-page web apps), content based sites and REST APIs.
---
Skills
----------
* HTML5
* CSS, Sass and Compass
* Javascript, including Angular.js
* PHP, including Laravel and Lumen
---
Employment
----------
**Agency ltd**
June 2014 - present
I worked for a busy digital agency, building custom web apps using Laravel and Angular.js
---
Education
----------
* **2009-2014 My Secondary School, London** - 7 GCSEs:
---
Hobbies and Interests
---------------------
Real ale, learning more about webdev, reading, socialising.
---
Contact
-------
> **Mobile:** 01234 567890
> **[Email](mailto:user@example.com)** - **[Website](http://www.example.com)** - **[GitHub](https://github.com/username)**

Now, if you save this file as something like cv.md and then open it up in Emacs, you should be able to preview it in your browser with C-c C-c p. Nice, huh? To export it to HTML, run C-c C-c v instead.

What if you want to view it in other formats? Say a potential employer is asking for your CV in Microsoft DocX format (ugh…)? Just run this command in the shell:

$ pandoc -s -S cv.md -o cv.docx

Or how about PDF?

$ pandoc -s -S cv.md -o cv.pdf

Using this method it’s straightforward to maintain a single master copy of your CV which you can then convert to other formats on demand.

Keeping your CV backed up

If you want to keep your CV safe, there’s a couple of ways to do it. One is to keep it in a Git or Mercurial repository, and another is to use Dropbox to keep it in sync. I tend to use the latter approach, although I’m considering switching to the former. If you wanted to generate the various versions automatically, you could set up a hook to generate the various versions using Pandoc during the commit process.

I used to hate updating my CV, but that was largely because I left it too long, and often had nothing much to put on it. Nowadays I’m often learning something new so I quite often have reason to update it to reflect that, and adopting this workflow has made things a lot easier.

Deploying new versions of a Laravel app with Fabric

$
0
0

Envoy is the official way to run tasks on a remote server for Laravel apps. A typical Envoy task for deploying a new version might look like this:

@servers(['web' => 'matthew@server1.example.com'])
@task('deploy', ['on' => 'web'])
cd /var/www
sudo chown -R matthew:matthew .
git pull origin master
php artisan migrate
php artisan view:clear
composer dump-autoload
sudo chown -R www-data:www-data .
sudo supervisorctl restart mail-queue
@endtask

This would be defined in Envoy.blade.php. With this in place, and Envoy set up globally, you can then run envoy run deploy to run the deploy command.

However, Envoy requires the PHP SSH library, which I haven’t been able to get working with PHP 7. Fortunately I was already familiar with Fabric, which makes an excellent alternative as long as you don’t mind writing the task in Python.

The same kind of task might look like this in a Fabric script, saved as fabfile.py:

#!/usr/bin/env python
from fabric.api import local, env, run, sudo
from fabric.context_managers import cd, prefix
env.hosts = ['server1.example.com']
env.path = "/var/www"
env.user = "matthew"
env.password = "password"
# Or...
env.key_filename = '/path/to/ssh/key'
def deploy():
"""
Deploy the latest version
"""
# Push changes to Bitbucket
local("git push origin master")
# Switch to project directory
with cd(env.path):
# Change owner
sudo('chown -R matthew:matthew .')
# Pull changes to server
run('git pull origin master')
# Run migrations
run('php artisan migrate')
# Clear cached files
run('php artisan view:clear')
run('composer dump-autoload')
# Change owner back
sudo('chown -R www-data:www-data .')
# restart mail queue
sudo('supervisorctl restart mail-queue')

Then, assuming Fabric is already installed locally, you can run fab deploy to push up the latest revision.

Either of these solutions will do a fine job of deploying your app. If you do need to store user-specific data in your Fabric script, it’s probably prudent to keep it out of version control.

Whichever way you choose, it’s a really good idea to do what you can to automate deployment. It can be a boring, repetitive job, and both of these solutions make it much easier.


Building a Phonegap app with Laravel and Angular - Part 1

$
0
0

A lot of my work over the last few years has been involved Phonegap apps. Phonegap isn’t terribly hard to use, but the difference in context between that and a more conventional web app means that you have to move a lot of functionality to the client side, and unless you’ve used client-side Javascript frameworks before it can be a struggle.

In this series of tutorials I’ll show you how I might build a Phonegap app. The work involved will include:

  • Building a REST API using Laravel to expose the data
  • Building an admin interface to manage the data
  • Building a Phonegap app using Angular.js
  • Testing and deploying it

In the process we’ll cover issues like authentication, authorization, real-time notifications and working with REST APIs. Note that we won’t cover the app submission process - you can find plenty of resources on that. We will, however, be using Phonegap Build to build the app.

The brief

Let’s say our new client is an animal shelter. The brief for the app is as follows:

My New Animal Friend will be an app for finding a new pet. Once a user signs in, they’ll be able to choose what type of pet they’re looking for, then look through a list of pets available to adopt. They can reject them by swiping left or save them by swiping right. They can see more about the ones they swipe right on, and arrange to meet them, from within the app. Users can also message the staff to ask questions about a pet.

Nice idea, but there’s a lot of work involved! Our very first task is to build the REST API, since everything else relies on that. Before starting, make sure you have the following installed:

  • PHP (I’m using PHP 7, but 5.6 should be fine)
  • Composer
  • Git
  • A compatible relational database (I use PostgreSQL)
  • Redis
  • Your usual text editor
  • Node.js

As long as you have this, you should be ready to go. Using Homestead is the simplest way to get started if you don’t have all this stuff already.

Starting the API

To start building our REST API, run the following command from the shell:

$ composer create-project --prefer-dist laravel/laravel mynewanimalfriend-backend

We also have some other dependencies we need to install, so switch into the new directory and run the following command:

$ composer require barryvdh/laravel-cors tymon/jwt-auth predis/predis

Next, we need to add the new packages to the Laravel config. Open up config/app.php and add the following to the providers array:

Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class,
Barryvdh\Cors\ServiceProvider::class,

And the following to the aliases array:

'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class,

We also need to ensure that the CORS middleware is applied to all API routes. Open up app/Http/Kernel.php and under the api array in protected $middlewareGroups paste the following:

   \Barryvdh\Cors\HandleCors::class,

Now that the packages are included, we can publish the files for them:

$ php artisan vendor:publish

Next, we need to set a key for our API authentication:

$ php artisan jwt:generate

And set a custom namespace:

$ php artisan app:name AnimalFriend

I had to change the namespace for the user model in config/jwt.php as well:

'user' => 'AnimalFriend\User',

I also tend to amend the settings in phpunit.xml as follows so that it uses an in-memory SQLite database for tests:

<env name="APP_ENV" value="testing"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
<env name="CACHE_DRIVER" value="redis"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>

Also, delete tests/ExampleTest.php and amend tests/TestCase.php as follows in order to use database migrations in tests:

<?php
use Illuminate\Foundation\Testing\DatabaseMigrations;
abstract class TestCase extends Illuminate\Foundation\Testing\TestCase
{
use DatabaseMigrations;
/**
* The base URL to use while testing the application.
*
* @var string
*/
protected $baseUrl = 'http://localhost';
/**
* Creates the application.
*
* @return \Illuminate\Foundation\Application
*/
public function createApplication()
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
return $app;
}
}

With that in place, we can start work on our API proper.

Authenticating our API

We’re going to start out with a very limited subset of our API. First, we’ll implement the authentication for our app, then we’ll add the facility to view a list of pets or an individual pet. Other functionality will come later. This will be sufficient to get the app working.

First, we need to create our user model. As we’ll be practicing TDD throughout, we write a test for the user model first. Save the following as tests/UserModelTest.php:

<?php
use AnimalFriend\User;
class UserModelTest extends TestCase
{
/**
* Test creating a user
*
* @return void
*/
public function testCreatingAUser()
{
// Create a User
$user = factory(AnimalFriend\User::class)->create([
'name' => 'bobsmith',
'email' => 'bob@example.com',
]);
$this->seeInDatabase('users', ['email' => 'bob@example.com']);
// Verify it works
$saved = User::where('email', 'bob@example.com')->first();
$this->assertEquals($saved->id, 1);
$this->assertEquals($saved->name, 'bobsmith');
}
}

If we run the tests:

$ vendor/bin/phpunit
PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 169 ms, Memory: 12.00MB
OK (1 test, 3 assertions)

We already have a perfectly good User model and the appropriate migrations, so our test already passes.

Next, we need to implement the authentication system. Save this as tests/AuthTest.php:

<?php
use Illuminate\Foundation\Testing\DatabaseMigrations;
class AuthTest extends TestCase
{
use DatabaseMigrations;
/**
* Test the auth
*
* @return void
*/
public function testAuth()
{
// Create a User
$user = factory(AnimalFriend\User::class)->create([
'name' => 'bobsmith',
'email' => 'bob@example.com',
'password' => bcrypt('password')
]);
// Create request
$data = array(
'email' => $user->email,
'password' => 'password',
);
$response = $this->call('POST', 'api/authenticate', $data);
$this->assertResponseStatus(200);
$content = json_decode($response->getContent());
$this->assertTrue(array_key_exists('token', $content));
}
/**
* Test the auth when user does not exist
*
* @return void
*/
public function testAuthFailure()
{
// Create data for request
$data = array(
'email' => 'user@example.com',
'password' => 'password',
);
$response = $this->call('POST', 'api/authenticate', $data);
// Check the status code
$this->assertResponseStatus(401);
}
}

The first test creates a user and sends an authentication request, then confirms that it returns the JSON Web Token. The second checks that a user that doesn’t exist cannot log in.

Let’s run the tests:

$ vendor/bin/phpunit
PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
FF. 3 / 3 (100%)
Time: 328 ms, Memory: 14.00MB
There were 2 failures:
1) AuthTest::testAuth
Expected status code 200, got 404.
Failed asserting that 404 matches expected 200.
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:648
/home/matthew/Projects/mynewanimalfriend-backend/tests/AuthTest.php:29
2) AuthTest::testAuthFailure
Expected status code 401, got 404.
Failed asserting that 404 matches expected 401.
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:648
/home/matthew/Projects/mynewanimalfriend-backend/tests/AuthTest.php:49
FAILURES!
Tests: 3, Assertions: 5, Failures: 2.

With a failing test in place, we can implement login. First let’s create our controller at app/Http/Controllers/AuthenticateController.php:

<?php
namespace AnimalFriend\Http\Controllers;
use Illuminate\Http\Request;
use AnimalFriend\Http\Requests;
use AnimalFriend\Http\Controllers\Controller;
use JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
use AnimalFriend\User;
use Hash;
class AuthenticateController extends Controller
{
private $user;
public function __construct(User $user) {
$this->user = $user;
}
public function authenticate(Request $request)
{
// Get credentials
$credentials = $request->only('email', 'password');
// Get user
$user = $this->user->where('email', $credentials['email'])->first();
try {
// attempt to verify the credentials and create a token for the user
if (! $token = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'invalid_credentials'], 401);
}
} catch (JWTException $e) {
// something went wrong whilst attempting to encode the token
return response()->json(['error' => 'could_not_create_token'], 500);
}
// all good so return the token
return response()->json(compact('token'));
}
}

And we need to set up the route in routes/api.php:

<?php
use Illuminate\Http\Request;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::post('authenticate', 'AuthenticateController@authenticate');

Note that because it’s an API route, it’s automatically prefixed with api/ without us having to do anything.

Now if we run our tests, they should pass:

$ vendor/bin/phpunit
PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
... 3 / 3 (100%)
Time: 402 ms, Memory: 14.00MB
OK (3 tests, 6 assertions)

Now we can obtain a JSON Web Token to authenticate users with. To start with we’ll only support existing users, but later we’ll add a method to sign up. However, we need at least one user to test with, so we’ll create a seeder for that at database/seeds/UserTableSeeder.php:

<?php
use Illuminate\Database\Seeder;
use Carbon\Carbon;
class UserTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
// Add user
DB::table('users')->insert([
'name' => 'bobsmith',
'email' => 'bob@example.com',
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
'password' => Hash::make("password")
]);
}
}

You can run php artisan make:seeder UserTableSeeder to generate the file, or just paste it in. You also need to amend database/seeds/DatabaseSeeder.php as follows:

<?php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$this->call(UserTableSeeder::class);
}
}

This ensures the seeder will actually be called. Then, run the following commands:

$ php artisan migrate
$ php artisan db:seed

That sets up our user in the database.

Adding the Pets endpoint

Our next step is to add the pets model and endpoint. Our Pet model should have the following fields:

  • ID
  • Timestamps (created_at and updated_at)
  • Name
  • Path to photo
  • Availability
  • Type (eg cat, dog)

Let’s create a test for that model:

<?php
use AnimalFriend\Pet;
class PetModelTest extends TestCase
{
/**
* Test creating a pet
*
* @return void
*/
public function testCreatingAPet()
{
// Create a Pet
$pet = factory(AnimalFriend\Pet::class)->create([
'name' => 'Freddie',
'type' => 'Cat',
]);
$this->seeInDatabase('pets', ['type' => 'Cat']);
// Verify it works
$saved = Pet::where('name', 'Freddie')->first();
$this->assertEquals($saved->id, 1);
$this->assertEquals($saved->name, 'Freddie');
$this->assertEquals($saved->type, 'Cat');
$this->assertEquals($saved->available, 1);
$this->assertEquals($saved->picture, '1.jpg');
}
}

Save this as tests/PetModelTest.php. Then run the tests:

$ vendor/bin/phpunit
PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
..E. 4 / 4 (100%)
Time: 414 ms, Memory: 16.00MB
There was 1 error:
1) PetModelTest::testCreatingAUser
InvalidArgumentException: Unable to locate factory with name [default] [AnimalFriend\Pet].
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:126
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:2280
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:139
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:106
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:84
/home/matthew/Projects/mynewanimalfriend-backend/tests/PetModelTest.php:16
ERRORS!
Tests: 4, Assertions: 6, Errors: 1.

First we need to create a factory for creating a pet in database/factories/ModelFactory.php:

$factory->define(AnimalFriend\Pet::class, function (Faker\Generator $faker) {
return [
'name' => $faker->firstNameMale,
'type' => 'Cat',
'available' => 1,
'picture' => '1.jpg'
];
});

Then, we create the model:

$ php artisan make:model Pet

Next, we create a migration for the Pet model:

$ php artisan make:migration create_pets_table
Created Migration: 2016_09_11_145010_create_pets_table

And paste in the following code:

<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreatePetsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('pets', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('type');
$table->string('available');
$table->string('picture')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('pets');
}
}

Time to run the tests again:

$ vendor/bin/phpunit
PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
.... 4 / 4 (100%)
Time: 412 ms, Memory: 16.00MB
OK (4 tests, 12 assertions)

With that done, we can start work on implementing the endpoint. We need to check that unauthorised users cannot retrieve the data, and that authorised users can. First, let’s create tests/PetControllerTest.php:

<?php
use Illuminate\Foundation\Testing\DatabaseMigrations;
class PetControllerTest extends TestCase
{
use DatabaseMigrations;
/**
* Test fetching pets when unauthorised
*
* @return void
*/
public function testFetchingPetsWhenUnauthorised()
{
// Create a Pet
$pet = factory(AnimalFriend\Pet::class)->create([
'name' => 'Freddie',
'type' => 'Cat',
]);
$this->seeInDatabase('pets', ['type' => 'Cat']);
// Create request
$response = $this->call('GET', '/api/pets');
$this->assertResponseStatus(400);
}
/**
* Test fetching pets when authorised
*
* @return void
*/
public function testFetchingPets()
{
// Create a Pet
$pet = factory(AnimalFriend\Pet::class)->create([
'name' => 'Freddie',
'type' => 'Cat',
]);
$this->seeInDatabase('pets', ['type' => 'Cat']);
// Create a User
$user = factory(AnimalFriend\User::class)->create([
'name' => 'bobsmith',
'email' => 'bob@example.com',
]);
$this->seeInDatabase('users', ['email' => 'bob@example.com']);
// Create request
$token = JWTAuth::fromUser($user);
$headers = array(
'Authorization' => 'Bearer '.$token
);
// Send it
$this->json('GET', '/api/pets', [], $headers)
->seeJsonStructure([
'*' => [
'id',
'name',
'type',
'available',
'picture',
'created_at',
'updated_at'
]
]);
$this->assertResponseStatus(200);
}
}

First, we create a pet, make an HTTP request to /api/pets, and check we are not authorised. Next, we do the same, but also create a user and a JSON Web Token, and pass the token through in the request. Then we verify the response data and that it was successful.

Let’s run the tests:

$ vendor/bin/phpunit
PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
..FF.. 6 / 6 (100%)
Time: 509 ms, Memory: 16.00MB
There were 2 failures:
1) PetControllerTest::testFetchingPetsWhenUnauthorised
Expected status code 400, got 404.
Failed asserting that 404 matches expected 400.
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:648
/home/matthew/Projects/mynewanimalfriend-backend/tests/PetControllerTest.php:25
2) PetControllerTest::testFetchingPets
Failed asserting that null is of type "array".
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:295
/home/matthew/Projects/mynewanimalfriend-backend/tests/PetControllerTest.php:67
FAILURES!
Tests: 6, Assertions: 17, Failures: 2.

That looks correct, so we can start building our endpoint. We can generate a boilerplate for it as follows:

$ $ php artisan make:controller PetController --resource

Note the --resource flag - this tells Laravel to set it up to be a RESTful controller with certain predefined functions. Next, let’s amend the new file at app\Http\Controllers/PetController.php as follows:

<?php
namespace AnimalFriend\Http\Controllers;
use Illuminate\Http\Request;
use AnimalFriend\Http\Requests;
use AnimalFriend\Pet;
class PetController extends Controller
{
private $pet;
public function __construct(Pet $pet) {
$this->pet = $pet;
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
// Get all pets
$pets = $this->pet->get();
// Send response
return response()->json($pets, 200);
}
}

This implements an index route that shows all pets. Next, we hook up the route in routes/api.php:

// Auth routes
Route::group(['middleware' => ['jwt.auth']], function () {
Route::resource('pets', 'PetController');
});

Note that we wrap this resource in the jwt.auth middleware to prevent access by unauthorised users. Implementing this as middleware makes it very easy to reuse. Also note that we can specify it as a resource, meaning we don’t have to explicitly hook up each route to a controller method.

Let’s run the tests again:

$ vendor/bin/phpunit
PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
..EE.. 6 / 6 (100%)
Time: 511 ms, Memory: 16.00MB
There were 2 errors:
1) PetControllerTest::testFetchingPetsWhenUnauthorised
ReflectionException: Class jwt.auth does not exist
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Container/Container.php:734
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Container/Container.php:629
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:709
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:173
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:517
/home/matthew/Projects/mynewanimalfriend-backend/tests/PetControllerTest.php:24
2) PetControllerTest::testFetchingPets
ReflectionException: Class jwt.auth does not exist
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Container/Container.php:734
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Container/Container.php:629
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:709
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:173
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:517
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:72
/home/matthew/Projects/mynewanimalfriend-backend/tests/PetControllerTest.php:56
ERRORS!
Tests: 6, Assertions: 15, Errors: 2.

Looks like JWT isn’t configured correctly. We can fix that in app/Http/Kernel.php by adding it to $routeMiddleware:

'jwt.auth' => 'Tymon\JWTAuth\Middleware\GetUserFromToken',
'jwt.refresh' => 'Tymon\JWTAuth\Middleware\RefreshToken',

And run the tests again:

$ vendor/bin/phpunit
PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
...... 6 / 6 (100%)
Time: 514 ms, Memory: 16.00MB
OK (6 tests, 25 assertions)

Our final task for today on the API is building a route for fetching a single pet. Our tests need to handle three situations:

  • An unauthorised request
  • A request for a pet that does not exist
  • A request for a pet that does exist

Add these methods to tests/PetControllerTest.php:

/**
* Test fetching pet when unauthorised
*
* @return void
*/
public function testFetchingPetWhenUnauthorised()
{
// Create a Pet
$pet = factory(AnimalFriend\Pet::class)->create([
'name' => 'Freddie',
'type' => 'Cat',
]);
$this->seeInDatabase('pets', ['type' => 'Cat']);
// Send request
$response = $this->call('GET', '/api/pets/'.$pet->id);
$this->assertResponseStatus(400);
}
/**
* Test fetching pet which does not exist
*
* @return void
*/
public function testFetchingPetDoesNotExist()
{
// Create a User
$user = factory(AnimalFriend\User::class)->create([
'name' => 'bobsmith',
'email' => 'bob@example.com',
]);
$this->seeInDatabase('users', ['email' => 'bob@example.com']);
// Create request
$token = JWTAuth::fromUser($user);
$headers = array(
'Authorization' => 'Bearer '.$token
);
// Send it
$this->json('GET', '/api/pets/1', [], $headers);
$this->assertResponseStatus(404);
}
/**
* Test fetching pet when authorised
*
* @return void
*/
public function testFetchingPet()
{
// Create a Pet
$pet = factory(AnimalFriend\Pet::class)->create([
'name' => 'Freddie',
'type' => 'Cat',
]);
$this->seeInDatabase('pets', ['type' => 'Cat']);
// Create a User
$user = factory(AnimalFriend\User::class)->create([
'name' => 'bobsmith',
'email' => 'bob@example.com',
]);
$this->seeInDatabase('users', ['email' => 'bob@example.com']);
// Create request
$token = JWTAuth::fromUser($user);
$headers = array(
'Authorization' => 'Bearer '.$token
);
// Send it
$this->json('GET', '/api/pets/'.$pet->id, [], $headers)
->seeJsonStructure([
'id',
'name',
'type',
'available',
'picture',
'created_at',
'updated_at'
]);
$this->assertResponseStatus(200);
}

Let’s check our tests fail:

$ vendor/bin/phpunit
PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
.....FE.. 9 / 9 (100%)
Time: 974 ms, Memory: 16.00MB
There was 1 error:
1) PetControllerTest::testFetchingPet
PHPUnit_Framework_Exception: Argument #2 (No Value) of PHPUnit_Framework_Assert::assertArrayHasKey() must be a array or ArrayAccess
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:304
/home/matthew/Projects/mynewanimalfriend-backend/tests/PetControllerTest.php:145
--
There was 1 failure:
1) PetControllerTest::testFetchingPetDoesNotExist
Expected status code 404, got 400.
Failed asserting that 400 matches expected 404.
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:648
/home/matthew/Projects/mynewanimalfriend-backend/tests/PetControllerTest.php:112
ERRORS!
Tests: 9, Assertions: 31, Errors: 1, Failures: 1.

Now, we already have the show() method hooked up by default, so we just have to implement it in app/Http/Controllers/PetController.php:

/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
// Get pet
$pet = $this->pet->findOrFail($id);
// Send response
return response()->json($pet, 200);
}

And let’s run our tests again:

$ vendor/bin/phpunit
PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
......... 9 / 9 (100%)
Time: 693 ms, Memory: 16.00MB
OK (9 tests, 39 assertions)

Now we have all the endpoints we need to get started with the app. You can find the source code for this backend on Github - check out the lesson-1 tag.

That seems like a good place to stop for now. We have our first pass at the back end. It’s not complete by any means, but it’s a good start, and is sufficient for us to get some basic functionality up and running in the app. In the next instalment we’ll start working with Phonegap to build the first pass at the app itself. Later instalments will see us working with both the app and backend to build it into a more useful whole.

Building a Phonegap app with Laravel and Angular - Part 2

$
0
0

In this lesson, the initial scope of the app will be extremely simple. We will implement functionality that:

  • Allows users to log in and out
  • Displays the home page

That’s fairly simple, and easily achievable within a fairly short timeframe. We’ll also write automated tests for our app. By the end of this lesson, we’ll have built a first pass for our app using Angular.js.

NOTE: As at time of writing, Angular 2 has just come out. I’m using Angular 1 here, and the two are not compatible, so make sure you’re using Angular 1.

Creating our app

Start by creating a new folder, separate from the backend, for the app. Then, in there, run the following command:

$ npm init -y

Then let’s install our dependencies:

$ npm install --save-dev gulp karma karma-browserify karma-phantomjs-launcher browserify angular angular-route angular-mocks angular-animate angular-messages angular-sanitize angular-material angular-resource vinyl-buffer vinyl-source-stream gulp-sass karma-coverage karma-jasmine jasmine-core gulp-webserver

We’re going to use Angular Material for our user interface as it includes support out of the box for swiping left and right. You’ll notice it mentioned as one of the dependencies above.

We’ll also use Karma for running our tests. Save the following as karma.conf.js:

module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['browserify', 'jasmine'],
files: [
'node_modules/angular/angular.min.js',
'node_modules/angular-mocks/angular-mocks.js',
'node_modules/angular-material/angular-material-mocks.js',
'js/*.js',
'test/*.js'
],
exclude: [
],
preprocessors: {
'js/*.js': ['browserify', 'coverage'],
'tests/js': ['browserify']
},
browserify: {
debug: true
},
reporters: ['progress', 'coverage'],
port: 9876,
colors: true,
logLevel: config.LOG_DEBUG,
autoWatch: true,
browsers: ['PhantomJS'],
singleRun: true,
coverageReporter: {
dir : 'coverage/',
reporters: [
{ type: 'html', subdir: 'report-html' },
{ type: 'cobertura', subdir: 'report-cobertura' }
]
}
});
};

This is our Karma configuration. Karma can run the same test in multiple browsers. Here we’re going to use PhantomJS, but it’s trivial to amend the browsers section to add more. You just need to make sure you install the appropriate launchers for those browsers.

We’ll use Gulp to build the app. Here’s the gulpfile.js:

var gulp = require('gulp');
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
var browserify = require('browserify');
var sass = require('gulp-sass');
var server = require('gulp-webserver');
var paths = {
scripts: ['js/*.js'],
styles: ['sass/*.scss']
};
gulp.task('sass', function() {
gulp.src('sass/style.scss')
.pipe(sass().on('error', sass.logError))
.pipe(gulp.dest('www/css'));
});;
gulp.task('js', function () {
return browserify({ entries: ['js/main.js'], debug: true })
.bundle()
.pipe(source('bundle.js'))
.pipe(buffer())
.pipe(gulp.dest('www/js/'));
});
gulp.task('server', function () {
gulp.src('www/')
.pipe(server({
livereload: true,
open: true,
port: 5000
}));
});
gulp.task('watch', function () {
gulp.watch(paths.scripts, ['js']);
gulp.watch(paths.styles, ['sass']);
});
gulp.task('default', ['sass','js','server', 'watch']);

Note that we’re going to be using Browserify to handle our dependencies. If you haven’t used it before, it lets you use the require() syntax from Node.js to include other JavaScript files, including ones available via NPM such as jQuery or Angular, allowing you to compile them all into a single file.

We should be able to test and run the app using NPM, so add these scripts to package.json:

"scripts": {
"test": "karma start",
"run": "gulp"
},

We also need an HTML file. Save this as www/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
<title>My New Animal Friend</title>
<link href="/css/style.css" rel="stylesheet" type="text/css">
</head>
<body>
<div>
<div ng-app="mynewanimalfriend" ng-cloak>
<div ng-view></div>
</div>
</div>
</body>
<script language="javascript" type="text/javascript" src="/js/bundle.js"></script>
</html>

Note the use of the Angular directives. ng-app denotes the name of the app namespace, ng-cloak hides the application until it’s fully loaded, and ng-view denotes the area containing our content.

You should also create the files js/main.js, sass/style.scss, and the test folder.

Creating our first routes

Our first task is to create the routes we need. Our default route will be /, representing the home page. However, users will need to be logged in to see this. Otherwise, they should be redirected to the login route, which will be /login, appropriately enough. We’ll also have a /logout route, which should be self-explanatory.

Before we implement these routes, we need to write a test for them. We’ll start with our login route, and we’ll test that for this route, the controller will be LoginCtrl and the template will be templates/login.html. The significance of these will become apparent later. Save this as test/routes.spec.js:

'use strict';
describe('Routes', function () {
beforeEach(angular.mock.module('mynewanimalfriend'));
it('should map login route to login controller', function () {
inject(function ($route) {
expect($route.routes['/login'].controller).toBe('LoginCtrl');
expect($route.routes['/login'].templateUrl).toEqual('templates/login.html');
});
});
});

Note the beforeEach() hook. This is used to set up the application.

We can run this test with npm test as that calls Karma directly. Note that we’re using Jasmine to write our tests.

$ npm test
> mynewanimalfriend-app@1.0.0 test /home/matthew/Projects/mynewanimalfriend-app
> karma start
12 09 2016 22:22:34.168:DEBUG [config]: autoWatch set to false, because of singleRun
12 09 2016 22:22:34.172:DEBUG [plugin]: Loading karma-* from /home/matthew/Projects/mynewanimalfriend-app/node_modules
12 09 2016 22:22:34.176:DEBUG [plugin]: Loading plugin /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma-browserify.
12 09 2016 22:22:34.314:DEBUG [plugin]: Loading plugin /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma-coverage.
12 09 2016 22:22:34.484:DEBUG [plugin]: Loading plugin /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma-jasmine.
12 09 2016 22:22:34.485:DEBUG [plugin]: Loading plugin /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma-phantomjs-launcher.
12 09 2016 22:22:34.535:DEBUG [framework.browserify]: created browserify bundle: /tmp/f8c46bd8d72c5b8578e64552192273be.browserify
12 09 2016 22:22:34.553:DEBUG [framework.browserify]: add bundle to config.files at position 3
12 09 2016 22:22:34.559:DEBUG [web-server]: Instantiating middleware
12 09 2016 22:22:34.569:DEBUG [reporter]: Trying to load reporter: coverage
12 09 2016 22:22:34.570:DEBUG [reporter]: Trying to load color-version of reporter: coverage (coverage_color)
12 09 2016 22:22:34.571:DEBUG [reporter]: Couldn't load color-version.
12 09 2016 22:22:34.596:DEBUG [framework.browserify]: updating js/main.js in bundle
12 09 2016 22:22:34.597:DEBUG [framework.browserify]: building bundle
12 09 2016 22:22:35.302:DEBUG [framework.browserify]: bundling
12 09 2016 22:22:35.328:DEBUG [preprocessor.coverage]: Processing "/home/matthew/Projects/mynewanimalfriend-app/js/main.js".
12 09 2016 22:22:35.345:INFO [framework.browserify]: bundle built
12 09 2016 22:22:35.352:INFO [karma]: Karma v1.3.0 server started at http://localhost:9876/
12 09 2016 22:22:35.352:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency
12 09 2016 22:22:35.361:INFO [launcher]: Starting browser PhantomJS
12 09 2016 22:22:35.361:DEBUG [temp-dir]: Creating temp dir at /tmp/karma-17657666
12 09 2016 22:22:35.364:DEBUG [launcher]: /home/matthew/Projects/mynewanimalfriend-app/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs /tmp/karma-17657666/capture.js
12 09 2016 22:22:35.466:DEBUG [web-server]: serving: /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma/static/client.html
12 09 2016 22:22:35.478:DEBUG [web-server]: serving: /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma/static/karma.js
12 09 2016 22:22:35.541:DEBUG [karma]: A browser has connected on socket /#dQYjOD4F_HJwPXiYAAAA
12 09 2016 22:22:35.564:DEBUG [web-server]: upgrade /socket.io/?EIO=3&transport=websocket&sid=dQYjOD4F_HJwPXiYAAAA
12 09 2016 22:22:35.629:INFO [PhantomJS 2.1.1 (Linux 0.0.0)]: Connected on socket /#dQYjOD4F_HJwPXiYAAAA with id 17657666
12 09 2016 22:22:35.630:DEBUG [launcher]: PhantomJS (id 17657666) captured in 0.277 secs
12 09 2016 22:22:35.642:DEBUG [phantomjs.launcher]:
12 09 2016 22:22:35.643:DEBUG [middleware:karma]: custom files null null
12 09 2016 22:22:35.644:DEBUG [middleware:karma]: Serving static request /context.html
12 09 2016 22:22:35.646:DEBUG [web-server]: serving: /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma/static/context.html
12 09 2016 22:22:35.650:DEBUG [middleware:source-files]: Requesting /base/node_modules/jasmine-core/lib/jasmine-core/jasmine.js?b1682a1eb50e00abf147fc1fb28e31006d499aae /
12 09 2016 22:22:35.650:DEBUG [middleware:source-files]: Fetching /home/matthew/Projects/mynewanimalfriend-app/node_modules/jasmine-core/lib/jasmine-core/jasmine.js
12 09 2016 22:22:35.652:DEBUG [web-server]: serving (cached): /home/matthew/Projects/mynewanimalfriend-app/node_modules/jasmine-core/lib/jasmine-core/jasmine.js
12 09 2016 22:22:35.654:DEBUG [middleware:source-files]: Requesting /base/node_modules/angular-material/angular-material-mocks.js?9f31553e4bbbad4d6b52638351e3a274352311c2 /
12 09 2016 22:22:35.654:DEBUG [middleware:source-files]: Fetching /home/matthew/Projects/mynewanimalfriend-app/node_modules/angular-material/angular-material-mocks.js
12 09 2016 22:22:35.654:DEBUG [middleware:source-files]: Requesting /base/node_modules/karma-jasmine/lib/boot.js?945a38bf4e45ad2770eb94868231905a04a0bd3e /
12 09 2016 22:22:35.655:DEBUG [middleware:source-files]: Fetching /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma-jasmine/lib/boot.js
12 09 2016 22:22:35.655:DEBUG [middleware:source-files]: Requesting /base/node_modules/karma-jasmine/lib/adapter.js?7975a273517f1eb29d7bd018790fd4c7b9a485d5 /
12 09 2016 22:22:35.655:DEBUG [middleware:source-files]: Fetching /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma-jasmine/lib/adapter.js
12 09 2016 22:22:35.656:DEBUG [middleware:source-files]: Requesting /base/node_modules/angular/angular.min.js?78069f9f3a9ca9652cb04c13ccb0670d747666b8 /
12 09 2016 22:22:35.656:DEBUG [middleware:source-files]: Fetching /home/matthew/Projects/mynewanimalfriend-app/node_modules/angular/angular.min.js
12 09 2016 22:22:35.656:DEBUG [middleware:source-files]: Requesting /base/node_modules/angular-mocks/angular-mocks.js?cc56136dc551d94abe8195cf8475eb27a3aa3c4b /
12 09 2016 22:22:35.657:DEBUG [middleware:source-files]: Fetching /home/matthew/Projects/mynewanimalfriend-app/node_modules/angular-mocks/angular-mocks.js
12 09 2016 22:22:35.657:DEBUG [web-server]: serving (cached): /home/matthew/Projects/mynewanimalfriend-app/node_modules/angular-material/angular-material-mocks.js
12 09 2016 22:22:35.658:DEBUG [web-server]: serving (cached): /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma-jasmine/lib/boot.js
12 09 2016 22:22:35.658:DEBUG [web-server]: serving (cached): /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma-jasmine/lib/adapter.js
12 09 2016 22:22:35.659:DEBUG [web-server]: serving (cached): /home/matthew/Projects/mynewanimalfriend-app/node_modules/angular/angular.min.js
12 09 2016 22:22:35.659:DEBUG [web-server]: serving (cached): /home/matthew/Projects/mynewanimalfriend-app/node_modules/angular-mocks/angular-mocks.js
12 09 2016 22:22:35.660:DEBUG [web-server]: serving: /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma/static/context.js
12 09 2016 22:22:35.661:DEBUG [middleware:source-files]: Requesting /absolute/tmp/f8c46bd8d72c5b8578e64552192273be.browserify?8ffde4eef27d38e92cc62da4e8dd0ffa5a3a4a4c /
12 09 2016 22:22:35.661:DEBUG [middleware:source-files]: Fetching /tmp/f8c46bd8d72c5b8578e64552192273be.browserify
12 09 2016 22:22:35.662:DEBUG [middleware:source-files]: Requesting /base/js/main.js?41c850cecc07c24d7cd0421e914bd2420671e573 /
12 09 2016 22:22:35.662:DEBUG [middleware:source-files]: Fetching /home/matthew/Projects/mynewanimalfriend-app/js/main.js
12 09 2016 22:22:35.662:DEBUG [middleware:source-files]: Requesting /base/test/routes.spec.js?92b15bb7c24bc6ead636994fb1c737b91727d887 /
12 09 2016 22:22:35.662:DEBUG [middleware:source-files]: Fetching /home/matthew/Projects/mynewanimalfriend-app/test/routes.spec.js
12 09 2016 22:22:35.663:DEBUG [web-server]: serving (cached): /tmp/f8c46bd8d72c5b8578e64552192273be.browserify
12 09 2016 22:22:35.664:DEBUG [web-server]: serving (cached): /home/matthew/Projects/mynewanimalfriend-app/js/main.js
12 09 2016 22:22:35.664:DEBUG [web-server]: serving (cached): /home/matthew/Projects/mynewanimalfriend-app/test/routes.spec.js
PhantomJS 2.1.1 (Linux 0.0.0) Routes should map login route to login controller FAILED
Error: [$injector:modulerr] http://errors.angularjs.org/1.5.8/$injector/modulerr?p0=mynewanimalfriend&p1=%5B%24injector%3Anomod%5D%20http%3A%2F%2Ferrors.angularjs.org%2F1.5.8%2F%24injector%2Fnomod%3Fp0%3Dmynewanimalfriend%0Ahttp%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fangular%2Fangular.min.js%3F78069f9f3a9ca9652cb04c13ccb0670d747666b8%3A25%3A111%0Ab%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fangular%2Fangular.min.js%3F78069f9f3a9ca9652cb04c13ccb0670d747666b8%3A24%3A143%0Ahttp%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fangular%2Fangular.min.js%3F78069f9f3a9ca9652cb04c13ccb0670d747666b8%3A24%3A489%0Ahttp%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fangular%2Fangular.min.js%3F78069f9f3a9ca9652cb04c13ccb0670d747666b8%3A39%3A473%0Aq%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fangular%2Fangular.min.js%3F78069f9f3a9ca9652cb04c13ccb0670d747666b8%3A7%3A359%0Ag%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fangular%2Fangular.min.js%3F78069f9f3a9ca9652cb04c13ccb0670d747666b8%3A39%3A320%0Acb%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fangular%2Fangular.min.js%3F78069f9f3a9ca9652cb04c13ccb0670d747666b8%3A43%3A337%0AworkFn%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fangular-mocks%2Fangular-mocks.js%3Fcc56136dc551d94abe8195cf8475eb27a3aa3c4b%3A3074%3A60%0Ainject%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fangular-mocks%2Fangular-mocks.js%3Fcc56136dc551d94abe8195cf8475eb27a3aa3c4b%3A3054%3A46%0Ahttp%3A%2F%2Flocalhost%3A9876%2Fbase%2Ftest%2Froutes.spec.js%3F92b15bb7c24bc6ead636994fb1c737b91727d887%3A5%3A11%0AattemptSync%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fjasmine-core%2Flib%2Fjasmine-core%2Fjasmine.js%3Fb1682a1eb50e00abf147fc1fb28e31006d499aae%3A1942%3A28%0Arun%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fjasmine-core%2Flib%2Fjasmine-core%2Fjasmine.js%3Fb1682a1eb50e00abf147fc1fb28e31006d499aae%3A1930%3A20%0Aexecute%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fjasmine-core%2Flib%2Fjasmine-core%2Fjasmine.js%3Fb1682a1eb50e00abf147fc1fb28e31006d499aae%3A1915%3A13%0AqueueRunnerFactory%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fjasmine-core%2Flib%2Fjasmine-core%2Fjasmine.js%3Fb1682a1eb50e00abf147fc1fb28e31006d499aae%3A710%3A42%0Aexecute%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fjasmine-core%2Flib%2Fjasmine-core%2Fjasmine.js%3Fb1682a1eb50e00abf147fc1fb28e31006d499aae%3A367%3A28%0Afn%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fjasmine-core%2Flib%2Fjasmine-core%2Fjasmine.js%3Fb1682a1eb50e00abf147fc1fb28e31006d499aae%3A2568%3A44%0AattemptAsync%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fjasmine-core%2Flib%2Fjasmine-core%2Fjasmine.js%3Fb1682a1eb50e00abf147fc1fb28e31006d499aae%3A1972%3A28%0Arun%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fjasmine-core%2Flib%2Fjasmine-core%2Fjasmine.js%3Fb1682a1eb50e00abf147fc1fb28e31006d499aae%3A1927%3A21%0Aexecute%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fjasmine-core%2Flib%2Fjasmine-core%2Fjasmine.js%3Fb1682a1eb50e00abf147fc1fb28e31006d499aae%3A1915%3A13%0AqueueRunnerFactory%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fjasmine-core%2Flib%2Fjasmine-core%2Fjasmine.js%3Fb1682a1eb50e00abf147fc1fb28e31006d499aae%3A710%3A42%0Afn%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fjasmine-core%2Flib%2Fjasmine-core%2Fjasmine.js%3Fb1682a1eb50e00abf147fc1fb28e31006d499aae%3A2553%3A31%0AattemptAsync%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fjasmine-core%2Flib%2Fjasmine-core%2Fjasmine.js%3Fb1682a1eb50e00abf147fc1fb28e31006d499aae%3A1972%3A28%0Arun%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fjasmine-core%2Flib%2Fjasmine-core%2Fjasmine.js%3Fb1682a1eb50e00abf147fc1fb28e31006d499aae%3A1927%3A21%0Aexecute%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fjasmine-core%2Flib%2Fjasmine-core%2Fjasmine.js%3Fb1682a1eb50e00abf147fc1fb28e31006d499aae%3A1915%3A13%0AqueueRunnerFactory%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fjasmine-core%2Flib%2Fjasmine-core%2Fjasmine.js%3Fb1682a1eb50e00abf147fc1fb28e31006d499aae%3A710%3A42%0Aexecute%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fjasmine-core%2Flib%2Fjasmine-core%2Fjasmine.js%3Fb1682a1eb50e00abf147fc1fb28e31006d499aae%3A2415%3A25%0Aexecute%40http%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fjasmine-core%2Flib%2Fjasmine-core%2Fjasmine.js%3Fb1682a1eb50e00abf147fc1fb28e31006d499aae%3A772%3A24%0Ahttp%3A%2F%2Flocalhost%3A9876%2Fbase%2Fnode_modules%2Fkarma-jasmine%2Flib%2Fadapter.js%3F7975a273517f1eb29d7bd018790fd4c7b9a485d5%3A320%3A23%0Aloaded%40http%3A%2F%2Flocalhost%3A9876%2Fcontext.js%3A151%3A17%0Aglobal%20code%40http%3A%2F%2Flocalhost%3A9876%2Fcontext.html%3A50%3A28 in node_modules/angular/angular.min.js (line 40)
node_modules/angular/angular.min.js:40:260
q@node_modules/angular/angular.min.js:7:359
g@node_modules/angular/angular.min.js:39:320
cb@node_modules/angular/angular.min.js:43:337
workFn@node_modules/angular-mocks/angular-mocks.js:3074:60
inject@node_modules/angular-mocks/angular-mocks.js:3054:46
test/routes.spec.js:5:11
loaded@http://localhost:9876/context.js:151:17
PhantomJS 2.1.1 (Linux 0.0.0): Executed 1 of 1 (1 FAILED) ERROR (0.044 secs / 0.006 secs)
12 09 2016 22:22:35.778:DEBUG [karma]: Run complete, exiting.
12 09 2016 22:22:35.778:DEBUG [launcher]: Disconnecting all browsers
12 09 2016 22:22:35.778:DEBUG [framework.browserify]: cleaning up
12 09 2016 22:22:35.782:DEBUG [coverage]: Writing coverage to /home/matthew/Projects/mynewanimalfriend-app/coverage/report-html
12 09 2016 22:22:35.876:DEBUG [coverage]: Writing coverage to /home/matthew/Projects/mynewanimalfriend-app/coverage/report-cobertura
12 09 2016 22:22:35.880:DEBUG [launcher]: Process PhantomJS exited with code 0
12 09 2016 22:22:35.881:DEBUG [temp-dir]: Cleaning temp dir /tmp/karma-17657666
12 09 2016 22:22:35.884:DEBUG [launcher]: Finished all browsers
npm ERR! Test failed. See above for more details.

Now that we have a failing test, we can set about making it pass. Save this at js/main.js:

'use strict';
require('angular');
require('angular-route');
require('angular-animate');
require('angular-material');
angular.module('mynewanimalfriend', [
'ngRoute',
'ngAnimate',
'ngMaterial'
])
.config(function ($routeProvider) {
$routeProvider
.when('/login', {
templateUrl: 'templates/login.html',
controller: 'LoginCtrl'
});
});

As mentioned earlier, because we’re using Browserify, we can use the require() syntax to import our dependencies. Note we also give our module a name and specify the dependencies. Finally, note that we use $routeProvider to set up our first route, and we map the template URL and controller to match our test.

Let’s run the test again:

$ npm test
> mynewanimalfriend-app@1.0.0 test /home/matthew/Projects/mynewanimalfriend-app
> karma start
12 09 2016 22:35:51.231:DEBUG [config]: autoWatch set to false, because of singleRun
12 09 2016 22:35:51.235:DEBUG [plugin]: Loading karma-* from /home/matthew/Projects/mynewanimalfriend-app/node_modules
12 09 2016 22:35:51.237:DEBUG [plugin]: Loading plugin /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma-browserify.
12 09 2016 22:35:51.354:DEBUG [plugin]: Loading plugin /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma-coverage.
12 09 2016 22:35:51.496:DEBUG [plugin]: Loading plugin /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma-jasmine.
12 09 2016 22:35:51.497:DEBUG [plugin]: Loading plugin /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma-phantomjs-launcher.
12 09 2016 22:35:51.547:DEBUG [framework.browserify]: created browserify bundle: /tmp/02002698e6d413a542186462d3a0a6ce.browserify
12 09 2016 22:35:51.559:DEBUG [framework.browserify]: add bundle to config.files at position 3
12 09 2016 22:35:51.564:DEBUG [web-server]: Instantiating middleware
12 09 2016 22:35:51.581:DEBUG [reporter]: Trying to load reporter: coverage
12 09 2016 22:35:51.582:DEBUG [reporter]: Trying to load color-version of reporter: coverage (coverage_color)
12 09 2016 22:35:51.582:DEBUG [reporter]: Couldn't load color-version.
12 09 2016 22:35:51.602:DEBUG [framework.browserify]: updating js/main.js in bundle
12 09 2016 22:35:51.603:DEBUG [framework.browserify]: building bundle
12 09 2016 22:35:52.306:DEBUG [framework.browserify]: bundling
12 09 2016 22:35:54.095:DEBUG [preprocessor.coverage]: Processing "/home/matthew/Projects/mynewanimalfriend-app/js/main.js".
12 09 2016 22:35:54.170:INFO [framework.browserify]: bundle built
12 09 2016 22:35:54.189:INFO [karma]: Karma v1.3.0 server started at http://localhost:9876/
12 09 2016 22:35:54.189:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency
12 09 2016 22:35:54.197:INFO [launcher]: Starting browser PhantomJS
12 09 2016 22:35:54.198:DEBUG [temp-dir]: Creating temp dir at /tmp/karma-91342786
12 09 2016 22:35:54.201:DEBUG [launcher]: /home/matthew/Projects/mynewanimalfriend-app/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs /tmp/karma-91342786/capture.js
12 09 2016 22:35:54.300:DEBUG [web-server]: serving: /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma/static/client.html
12 09 2016 22:35:54.308:DEBUG [web-server]: serving: /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma/static/karma.js
12 09 2016 22:35:54.366:DEBUG [karma]: A browser has connected on socket /#FpcuZAJUT-u6Dl4sAAAA
12 09 2016 22:35:54.386:DEBUG [web-server]: upgrade /socket.io/?EIO=3&transport=websocket&sid=FpcuZAJUT-u6Dl4sAAAA
12 09 2016 22:35:54.442:INFO [PhantomJS 2.1.1 (Linux 0.0.0)]: Connected on socket /#FpcuZAJUT-u6Dl4sAAAA with id 91342786
12 09 2016 22:35:54.442:DEBUG [launcher]: PhantomJS (id 91342786) captured in 0.253 secs
12 09 2016 22:35:54.447:DEBUG [phantomjs.launcher]:
12 09 2016 22:35:54.448:DEBUG [middleware:karma]: custom files null null
12 09 2016 22:35:54.448:DEBUG [middleware:karma]: Serving static request /context.html
12 09 2016 22:35:54.449:DEBUG [web-server]: serving: /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma/static/context.html
12 09 2016 22:35:54.451:DEBUG [middleware:source-files]: Requesting /base/node_modules/jasmine-core/lib/jasmine-core/jasmine.js?b1682a1eb50e00abf147fc1fb28e31006d499aae /
12 09 2016 22:35:54.451:DEBUG [middleware:source-files]: Fetching /home/matthew/Projects/mynewanimalfriend-app/node_modules/jasmine-core/lib/jasmine-core/jasmine.js
12 09 2016 22:35:54.452:DEBUG [web-server]: serving (cached): /home/matthew/Projects/mynewanimalfriend-app/node_modules/jasmine-core/lib/jasmine-core/jasmine.js
12 09 2016 22:35:54.453:DEBUG [middleware:source-files]: Requesting /base/node_modules/angular-material/angular-material-mocks.js?9f31553e4bbbad4d6b52638351e3a274352311c2 /
12 09 2016 22:35:54.453:DEBUG [middleware:source-files]: Fetching /home/matthew/Projects/mynewanimalfriend-app/node_modules/angular-material/angular-material-mocks.js
12 09 2016 22:35:54.453:DEBUG [middleware:source-files]: Requesting /base/node_modules/karma-jasmine/lib/boot.js?945a38bf4e45ad2770eb94868231905a04a0bd3e /
12 09 2016 22:35:54.454:DEBUG [middleware:source-files]: Fetching /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma-jasmine/lib/boot.js
12 09 2016 22:35:54.454:DEBUG [middleware:source-files]: Requesting /base/node_modules/karma-jasmine/lib/adapter.js?7975a273517f1eb29d7bd018790fd4c7b9a485d5 /
12 09 2016 22:35:54.454:DEBUG [middleware:source-files]: Fetching /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma-jasmine/lib/adapter.js
12 09 2016 22:35:54.454:DEBUG [middleware:source-files]: Requesting /base/node_modules/angular-mocks/angular-mocks.js?cc56136dc551d94abe8195cf8475eb27a3aa3c4b /
12 09 2016 22:35:54.454:DEBUG [middleware:source-files]: Fetching /home/matthew/Projects/mynewanimalfriend-app/node_modules/angular-mocks/angular-mocks.js
12 09 2016 22:35:54.455:DEBUG [middleware:source-files]: Requesting /base/node_modules/angular/angular.min.js?78069f9f3a9ca9652cb04c13ccb0670d747666b8 /
12 09 2016 22:35:54.455:DEBUG [middleware:source-files]: Fetching /home/matthew/Projects/mynewanimalfriend-app/node_modules/angular/angular.min.js
12 09 2016 22:35:54.455:DEBUG [web-server]: serving (cached): /home/matthew/Projects/mynewanimalfriend-app/node_modules/angular-material/angular-material-mocks.js
12 09 2016 22:35:54.455:DEBUG [web-server]: serving (cached): /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma-jasmine/lib/boot.js
12 09 2016 22:35:54.455:DEBUG [web-server]: serving (cached): /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma-jasmine/lib/adapter.js
12 09 2016 22:35:54.456:DEBUG [web-server]: serving (cached): /home/matthew/Projects/mynewanimalfriend-app/node_modules/angular-mocks/angular-mocks.js
12 09 2016 22:35:54.457:DEBUG [web-server]: serving (cached): /home/matthew/Projects/mynewanimalfriend-app/node_modules/angular/angular.min.js
12 09 2016 22:35:54.458:DEBUG [middleware:source-files]: Requesting /absolute/tmp/02002698e6d413a542186462d3a0a6ce.browserify?f4c82dc0618d979f84c89967ea1c412e646a5fe5 /
12 09 2016 22:35:54.458:DEBUG [middleware:source-files]: Fetching /tmp/02002698e6d413a542186462d3a0a6ce.browserify
12 09 2016 22:35:54.458:DEBUG [middleware:source-files]: Requesting /base/js/main.js?41c850cecc07c24d7cd0421e914bd2420671e573 /
12 09 2016 22:35:54.459:DEBUG [middleware:source-files]: Fetching /home/matthew/Projects/mynewanimalfriend-app/js/main.js
12 09 2016 22:35:54.460:DEBUG [middleware:source-files]: Requesting /base/test/routes.spec.js?92b15bb7c24bc6ead636994fb1c737b91727d887 /
12 09 2016 22:35:54.461:DEBUG [middleware:source-files]: Fetching /home/matthew/Projects/mynewanimalfriend-app/test/routes.spec.js
12 09 2016 22:35:54.461:DEBUG [web-server]: serving (cached): /tmp/02002698e6d413a542186462d3a0a6ce.browserify
12 09 2016 22:35:54.496:DEBUG [web-server]: serving (cached): /home/matthew/Projects/mynewanimalfriend-app/js/main.js
12 09 2016 22:35:54.497:DEBUG [web-server]: serving (cached): /home/matthew/Projects/mynewanimalfriend-app/test/routes.spec.js
12 09 2016 22:35:54.497:DEBUG [web-server]: serving: /home/matthew/Projects/mynewanimalfriend-app/node_modules/karma/static/context.js
12 09 2016 22:35:54.582:DEBUG [phantomjs.launcher]: WARNING: Tried to load angular more than once.
PhantomJS 2.1.1 (Linux 0.0.0) LOG: 'WARNING: Tried to load angular more than once.'
PhantomJS 2.1.1 (Linux 0.0.0): Executed 1 of 1 SUCCESS (0.004 secs / 0.358 secs)
12 09 2016 22:35:55.003:DEBUG [karma]: Run complete, exiting.
12 09 2016 22:35:55.003:DEBUG [launcher]: Disconnecting all browsers
12 09 2016 22:35:55.003:DEBUG [framework.browserify]: cleaning up
12 09 2016 22:35:55.006:DEBUG [coverage]: Writing coverage to /home/matthew/Projects/mynewanimalfriend-app/coverage/report-html
12 09 2016 22:35:55.078:DEBUG [coverage]: Writing coverage to /home/matthew/Projects/mynewanimalfriend-app/coverage/report-cobertura
12 09 2016 22:35:55.082:DEBUG [launcher]: Process PhantomJS exited with code 0
12 09 2016 22:35:55.082:DEBUG [temp-dir]: Cleaning temp dir /tmp/karma-91342786
12 09 2016 22:35:55.085:DEBUG [launcher]: Finished all browsers

Our first test has passed. Let’s add tests for the other routes:

'use strict';
describe('Routes', function () {
beforeEach(angular.mock.module('mynewanimalfriend'));
it('should map default route to home controller', function () {
inject(function ($route) {
expect($route.routes['/'].controller).toBe('HomeCtrl');
expect($route.routes['/'].templateUrl).toEqual('templates/home.html');
});
});
it('should map login route to login controller', function () {
inject(function ($route) {
expect($route.routes['/login'].controller).toBe('LoginCtrl');
expect($route.routes['/login'].templateUrl).toEqual('templates/login.html');
});
});
it('should map logout route to logout controller', function () {
inject(function ($route) {
expect($route.routes['/logout'].controller).toBe('LogoutCtrl');
expect($route.routes['/logout'].templateUrl).toEqual('templates/login.html');
});
});
});

Note that the logout route uses the login template. This is because all it will do is redirect the user to the login form.

For the sake of brevity I won’t display the test output, but two of these tests should now fail. We can easily set up the new routes in js/main.js:

'use strict';
require('angular');
require('angular-route');
require('angular-animate');
require('angular-material');
angular.module('mynewanimalfriend', [
'ngRoute',
'ngAnimate',
'ngMaterial'
])
.config(function ($routeProvider) {
$routeProvider
.when('/login', {
templateUrl: 'templates/login.html',
controller: 'LoginCtrl'
})
.when('/', {
templateUrl: 'templates/home.html',
controller: 'HomeCtrl'
})
.when('/logout', {
templateUrl: 'templates/login.html',
controller: 'LogoutCtrl'
});
});

That’s looking good so far. But what if someone navigates to a URL that doesn’t exist? Our router should handle that. Add this to the test:

it('should redirect other or empty routes to the home controller', function () {
inject(function ($route) {
expect($route.routes[null].redirectTo).toEqual('/')
});
});

Once again, the test should fail. Fixing it is fairly straightforward - we’ll use the otherwise() method to define a fallback route:

'use strict';
require('angular');
require('angular-route');
require('angular-animate');
require('angular-material');
angular.module('mynewanimalfriend', [
'ngRoute',
'ngAnimate',
'ngMaterial'
])
.config(function ($routeProvider) {
$routeProvider
.when('/login', {
templateUrl: 'templates/login.html',
controller: 'LoginCtrl'
})
.when('/', {
templateUrl: 'templates/home.html',
controller: 'HomeCtrl'
})
.when('/logout', {
templateUrl: 'templates/login.html',
controller: 'LogoutCtrl'
})
.otherwise({
redirectTo: '/'
});
});

Now our routes are in place, we need to implement the three controllers we will need. However, as two of these controllers deal with authentication, we’ll first create some services to handle that, and they’ll need to be tested. Save this as test/services.spec.js:

'use strict';
describe('Services', function () {
beforeEach(function(){
jasmine.addMatchers({
toEqualData: function(util, customEqualityTesters) {
return {
compare: function(actual, expected) {
return {
pass: angular.equals(actual, expected)
};
}
};
}
});
});
beforeEach(angular.mock.module('mynewanimalfriend.services'));
describe('Token service', function () {
var Token;
beforeEach(inject(function (_Token_, _$httpBackend_) {
Token = _Token_;
mockBackend = _$httpBackend_;
}));
it('can create a new token', function () {
mockBackend.expectPOST('http://localhost:8000/api/authenticate', '{"email":"bob@example.com","password":"password"}').respond({token: 'mytoken'});
var token = new Token({
email: 'bob@example.com',
password: 'password'
});
token.$save(function (response) {
expect(response).toEqualData({token: 'mytoken'});
});
mockBackend.flush();
});
});
});

In this test we use the $httpBackend facility from ngMock to mock out our API endpoints. We already have a REST API capable of generating a token, and we set this test up to behave similarly. We specify that it should expect to receive a certain POST request, and should respond with the token mytoken. Run the test to make sure it fails, then save this as js/services.js:

'use strict';
require('angular');
require("angular-resource");
angular.module('mynewanimalfriend.services', ['ngResource'])
.factory('Token', function ($resource) {
return $resource('http://localhost:8000/api/authenticate/');
});

A little explanation is called for. In Angular, the $resource dependency represents an HTTP resource. By default it supports making HTTP requests to the denoted endpoint via GET, POST and DELETE, and it’s trivial to add support for PUT or PATCH methods. Using $resource, you can easily interface with a RESTful web service, and it’s one of my favourite things about Angular.

We also need to load services.js in our main.js file:

'use strict';
require('angular');
require('angular-route');
require('angular-animate');
require('angular-material');
require('./services');
angular.module('mynewanimalfriend', [
'ngRoute',
'ngAnimate',
'ngMaterial',
'mynewanimalfriend.services'
])
.config(function ($routeProvider) {
$routeProvider
.when('/login', {
templateUrl: 'templates/login.html',
controller: 'LoginCtrl'
})
.when('/', {
templateUrl: 'templates/home.html',
controller: 'HomeCtrl'
})
.when('/logout', {
templateUrl: 'templates/login.html',
controller: 'LogoutCtrl'
})
.otherwise({
redirectTo: '/'
});
});

Now, running the tests should show that they pass.

With that in place, we will also create an authentication service that lets the app determine if the user is logged in. Add this to test/services.spec.js:

describe('Auth service', function () {
var Auth;
beforeEach(inject(function (_Auth_) {
Auth = _Auth_;
}));
it('can set user', function () {
Auth.setUser('mytoken');
var token = localStorage.getItem('authHeader');
expect(token).toEqual('Bearer mytoken');
});
it('can return login status', function () {
localStorage.setItem('authHeader', 'Bearer mytoken');
expect(Auth.isLoggedIn()).toBeTruthy();
});
it('can log the user out', function () {
localStorage.setItem('authHeader', 'Bearer mytoken');
Auth.logUserOut();
expect(Auth.isLoggedIn()).toBeFalsy();
expect(localStorage.getItem('authHeader')).toBeFalsy();
});
});

This service is expected to do three things:

  • Set the current user’s details in local storage
  • Return whether the user is logged in
  • Log the user out

Make sure the test fails, then amend js/services.js as follows:

'use strict';
require('angular');
require("angular-resource");
angular.module('mynewanimalfriend.services', ['ngResource'])
.factory('Auth', function(){
return{
setUser : function (aUser) {
localStorage.setItem('authHeader', 'Bearer ' + aUser);
},
isLoggedIn: function () {
var user = localStorage.getItem('authHeader');
return(user)? user : false;
},
logUserOut: function () {
localStorage.removeItem('authHeader');
}
}
})
.factory('Token', function ($resource) {
return $resource('http://localhost:8000/api/authenticate/');
});

When the user is set, we store the authentication details we need in local storage. We can then use that to determine if they are logged in. When they log out, we simply clear local storage,

That should be enough to make these tests pass. Now we can move on to our controllers. We’ll do the login controller first. Save this as test/controllers.spec.js:

'use strict';
describe('Controllers', function () {
beforeEach(function(){
jasmine.addMatchers({
toEqualData: function(util, customEqualityTesters) {
return {
compare: function(actual, expected) {
return {
pass: angular.equals(actual, expected)
};
}
};
}
});
});
beforeEach(angular.mock.module('mynewanimalfriend.controllers'));
describe('Login Controller', function () {
var mockBackend, scope;
beforeEach(inject(function ($rootScope, $controller, _$httpBackend_) {
mockBackend = _$httpBackend_;
scope = $rootScope.$new();
$controller('LoginCtrl', {
$scope: scope
});
}));
// Test controller scope is defined
it('should define the scope', function () {
expect(scope).toBeDefined();
});
// Test doLogin is defined
it('should define the login method', function () {
expect(scope.doLogin).toBeDefined();
});
// Test doLogin works
it('should allow the user to log in', function () {
// Mock the backend
mockBackend.expectPOST('http://localhost:8000/api/authenticate', '{"email":"user@example.com","password":"password"}').respond({token: 123});
// Define login data
scope.credentials = {
email: 'user@example.com',
password: 'password'
};
// Submit the request
scope.doLogin();
// Flush the backend
mockBackend.flush();
// Check login complete
expect(localStorage.getItem('authHeader')).toEqual('Bearer 123');
});
});
});

We check that the scope and the doLogin() method are defined. We then mock the backend’s /api/authenticate route to respond with a dummy token when our credentials are provided. Then, we set the credentials in the variable $scope.credentials, call doLogin(), flush the backend, and check the authentication header has been set.

Once you’ve verified these tests fail, we can start making them pass. Save this as js/controllers.js:

'use strict';
require('angular');
require('angular-route');
require('./services');
angular.module('mynewanimalfriend.controllers', [
'mynewanimalfriend.services',
"ngMaterial"
])
.controller('LoginCtrl', function ($scope, $location, Token, Auth) {
$scope.doLogin = function () {
var token = new Token($scope.credentials);
token.$save(function (response) {
if (response.token) {
// Set up auth service
Auth.setUser(response.token);
// Redirect
$location.path('/');
}
}, function (err) {
alert('Unable to log in - please check your details are correct');
});
};
});

The LoginCtrl controller accepts the scope, location, and our two services. When doLogin() is alled, it picks up the values in $scope.credentials, which we will set in our template later. It then makes a POST request to our endpoint including those credentials. Our API backend should return the new token in the response, and the token is stored using the Auth service. Otherwise, it raises an error.

Check the test now passes before moving onto the logout functionality. Add this to test/controllers.spec.js:

describe('Logout Controller', function () {
var scope;
beforeEach(inject(function ($rootScope, $controller, Auth) {
Auth.setUser('Blah');
scope = $rootScope.$new();
$controller('LogoutCtrl', {
$scope: scope
});
}));
// Test controller scope is defined
it('should define the scope', function () {
expect(scope).toBeDefined();
});
// Test session cleared
it('should clear the session', function () {
expect(localStorage.getItem('authHeader')).toEqual(null);
});
});

We want to ensure that when the user navigates to the route managed by the LogoutCtrl controller, the session is cleared, so we set up an existing session, call the controller, check it’s defined, and then check that local storage is empty.

Once you’ve verified that the test fails, amend the controllers as follows:

'use strict';
require('angular');
require('angular-route');
require('./services');
angular.module('mynewanimalfriend.controllers', [
'mynewanimalfriend.services',
"ngMaterial"
])
.controller('LoginCtrl', function ($scope, $location, Token, Auth) {
$scope.doLogin = function () {
var token = new Token($scope.credentials);
token.$save(function (response) {
if (response.token) {
// Set up auth service
Auth.setUser(response.token);
// Redirect
$location.path('/');
}
}, function (err) {
alert('Unable to log in - please check your details are correct');
});
};
})
.controller('LogoutCtrl', function ($scope, $location, Auth) {
// Log user out
Auth.logUserOut();
// Redirect to login page
$location.path('/login');
});

Our LogoutCtrl controller is very simple - it just logs the user out and redirects them back to the login form. Our final controller is for the home page:

describe('Home Controller', function () {
var scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
$controller('HomeCtrl', {
$scope: scope
});
}));
// Test controller scope is defined
it('should define the scope', function () {
expect(scope).toBeDefined();
});
});

For now our home controller does nothing except define the scope, so it’s easy to implement:

'use strict';
require('angular');
require('angular-route');
require('./services');
angular.module('mynewanimalfriend.controllers', [
'mynewanimalfriend.services',
"ngMaterial"
])
.controller('LoginCtrl', function ($scope, $location, Token, Auth) {
$scope.doLogin = function () {
var token = new Token($scope.credentials);
token.$save(function (response) {
if (response.token) {
// Set up auth service
Auth.setUser(response.token);
// Redirect
$location.path('/');
}
}, function (err) {
alert('Unable to log in - please check your details are correct');
});
};
})
.controller('LogoutCtrl', function ($scope, $location, Auth) {
// Log user out
Auth.logUserOut();
// Redirect to login page
$location.path('/login');
})
.controller('HomeCtrl', function ($scope) {
});

Verify that the tests pass, and our controllers are done for now. However, we still have some work to do to hook the various elements up. First, of all, our main.js unnecessarily loads our services - since we only use those services in our controllers, we don’t need them there. We also need to be able to keep users out of routes other than login when not logged in. Here’s what you main.js should look like:

'use strict';
require('angular');
require('angular-route');
require('angular-animate');
require('angular-material');
require('./controllers');
angular.module('mynewanimalfriend', [
'ngRoute',
'ngAnimate',
'ngMaterial',
'mynewanimalfriend.controllers'
])
.run(['$rootScope', '$location', 'Auth', function ($rootScope, $location, Auth) {
$rootScope.$on('$routeChangeStart', function (event) {
if (!Auth.isLoggedIn()) {
if ($location.path() !== '/login') {
$location.path('/login');
}
}
});
}])
.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('sessionInjector');
$httpProvider.interceptors.push('authInterceptor');
}])
.config(function ($routeProvider) {
$routeProvider
.when('/login', {
templateUrl: 'templates/login.html',
controller: 'LoginCtrl'
})
.when('/', {
templateUrl: 'templates/home.html',
controller: 'HomeCtrl'
})
.when('/logout', {
templateUrl: 'templates/login.html',
controller: 'LogoutCtrl'
})
.otherwise({
redirectTo: '/'
});
});

Note that we set it up to intercept the HTTP request with the session injector and the auth interceptor. Next we need to create these in js/services.js:

'use strict';
require('angular');
require("angular-resource");
angular.module('mynewanimalfriend.services', ['ngResource'])
.factory('Auth', function(){
return{
setUser : function (aUser) {
localStorage.setItem('authHeader', 'Bearer ' + aUser);
},
isLoggedIn: function () {
var user = localStorage.getItem('authHeader');
return(user)? user : false;
},
logUserOut: function () {
localStorage.removeItem('authHeader');
}
}
})
.factory('Token', function ($resource) {
return $resource('http://localhost:8000/api/authenticate/');
})
.factory('sessionInjector', function (Auth) {
var sessionInjector = {
request: function (config) {
if (Auth.isLoggedIn()) {
config.headers.Authorization = Auth.isLoggedIn();
}
return config;
}
};
return sessionInjector;
})
.service('authInterceptor', function ($q, Auth, $location) {
var service = this;
service.responseError = function (response) {
if (response.status == 400) {
Auth.logUserOut();
$location.path('/login');
}
return $q.reject(response);
};
});

I’ll walk you through these. sessionInjector adds the authorization HTTP header to every request to the server if the user is logged in, so that it returns the right user’s details. authInterceptor catches any 400 errors, denoting that the user is not authenticated with a current JSON web token, and logs the user out. In this way we can handle the expiry of a user’s token.

Now the logic of our app is in place, but that’s no use without some content…

Angular templating

We have one very basic HTML template, but that’s just a boilerplate for inserting the rest of our content. For the rest of the HTML we’ll need to load templates dynamically, and we’ll use Angular Material to help us build a nice UI quickly. Run the following commands to create the files:

$ mkdir www/templates
$ touch www/templates/login.html
$ touch www/templates/home.html

We need to import the CSS for Angular Material. Add this to sass/style.scss:

// Angular Material
@import "node_modules/angular-material/angular-material.scss";

With that done, we need to configure theming in main.js:

'use strict';
require('angular');
require('angular-route');
require('angular-animate');
require('angular-material');
require('./controllers');
angular.module('mynewanimalfriend', [
'ngRoute',
'ngAnimate',
'ngMaterial',
'mynewanimalfriend.controllers'
])
.config(function ($mdThemingProvider) {
$mdThemingProvider.theme('default')
.primaryPalette('purple')
.accentPalette('cyan');
})
.run(['$rootScope', '$location', 'Auth', function ($rootScope, $location, Auth) {
$rootScope.$on('$routeChangeStart', function (event) {
if (!Auth.isLoggedIn()) {
if ($location.path() !== '/login') {
$location.path('/login');
}
}
});
}])
.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('sessionInjector');
$httpProvider.interceptors.push('authInterceptor');
}])
.config(function ($routeProvider) {
$routeProvider
.when('/login', {
templateUrl: 'templates/login.html',
controller: 'LoginCtrl'
})
.when('/', {
templateUrl: 'templates/home.html',
controller: 'HomeCtrl'
})
.when('/logout', {
templateUrl: 'templates/login.html',
controller: 'LogoutCtrl'
})
.otherwise({
redirectTo: '/'
});
});

You may want to look at the documentation for Angular Material to choose your own theme options. Next, let’s create our login template at www/templates/login.html:

<md-content md-theme="default" layout-gt-sm="row" layout-padding>
<div>
<md-input-container class="md-block">
<label>Email</label>
<input ng-model="credentials.email" type="email">
</md-input-container>
<md-input-container class="md-block">
<label>Password</label>
<input ng-model="credentials.password" type="password">
</md-input-container>
<md-button class="md-raised md-primary" ng-click="doLogin()">Submit</md-button>
</div>
</md-content>

We’re using Angular Material’s input and button directives to make our inputs look a bit nicer. Note that the ng-click handler calls the doLogin() method of our controller, and that the ng-model attributes contain the credentials object that gets passed to the API. If you haven’t used Angular before, ng-model essentially lets you bind a variable to an element’s value so, for instance, when an input is changed, it can be easily accessed via the variable.

Next, we’ll implement a placeholder for our home page with a log out button. Save this as www/templates/home.html:

<md-toolbar>
<div class="md-toolbar-tools">
<md-button aria-label="Log out" href="#logout">
Log out
</md-button>
</div>
</md-toolbar>

That should be all we need to demonstrate logging in and out of our app. Let’s try it out. First run the Gulp task to show the app in the browser:

$ gulp

Then, in another shell session, switch to the directory with the backend and run the server for that:

$ php artisan serve

You should already have a user account set up and ready to use thanks to the seeder we wrote. The browser should show the login page by default, and if you fill in the login form and click the button you should see the home page. You should then be able to log out again.

Congratulations! We’ve got authentication working.

Switching to HTML5 routing

You may note that the URLs use hashes - they are in the format http://localhost:5000/#/login. Wouldn’t it be better if we didn’t use the hash? Fortunately modern browsers support this via the HTML5 pushState API, and Angular has built-in support for this.

To enable it, we first need to declare a base URL in www/index.html. Amend it as follows:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
<title>My New Animal Friend</title>
<link href="/css/style.css" rel="stylesheet" type="text/css">
<base href="/">
</head>
<body>
<div>
<div ng-app="mynewanimalfriend" ng-cloak>
<div ng-view></div>
</div>
</div>
</body>
<script language="javascript" type="text/javascript" src="/js/bundle.js"></script>
</html>

Here we’ve added the <base href="/"> tag to denote our base URL. Next we configure Angular to use HTML5 routing in main.js:

.config(function($locationProvider) {
$locationProvider.html5Mode(true);
})

And amend the URL in the home template:

<md-toolbar>
<div class="md-toolbar-tools">
<md-button aria-label="Log out" href="/logout">
Log out
</md-button>
</div>
</md-toolbar>

Now, we should be using HTML5 routing throughout.

With that done, we can finish for today. We’ve got our basic app skeleton and authentication system up and running, and we’ll be in a good place to continue developing the rest of the app next time. I’ve put the source code on Github, and you can find this lesson’s work under the lesson-2 tag.

Next time we’ll develop the app further, including implementing the pet search functionality.

Building a Phonegap app with Laravel and Angular - Part 3

$
0
0

Apologies for how long it’s taken for this post to be appear. I’ve got a lot on my plate at present as I recently started a new job, so I haven’t been able to devote as much time to this series as I’d like.

In this instalment we’ll begin extending our app beyond the basic authentication we’ve already implemented. We’ll start by adding the means to sign up, before adding the list of pets.

Adding a signup method to our backend

We’ll create a controller for our users in the Laravel backend. First we’ll create our tests:

$ php artisan make:test UserControllerTest

We’ll create three tests. The first will check to see that an invalid request raises the correct status code (422). The second will check that a valid request returns the correct status code (201) and creates the user. The third will check that trying to create a duplicate user raises an error. Here they are - they should be saved in the new tests/UserControllerTest.php file:

<?php
use Illuminate\Foundation\Testing\DatabaseMigrations;
class UserControllerTest extends TestCase
{
/**
* Test creating a user - invalid
*
* @return void
*/
public function testPostingInvalidUser()
{
// Create a request
$data = array(
'name' => 'Bob Smith',
'email' => 'bob@example.com'
);
$this->json('POST', '/api/users', $data);
$this->assertResponseStatus(422);
}
/**
* Test creating a user
*
* @return void
*/
public function testPostingUser()
{
// Create a request
$data = array(
'name' => 'Bob Smith',
'email' => 'bob@example.com',
'password' => 'password',
'password_confirmation' => 'password'
);
$this->json('POST', '/api/users', $data);
$this->assertResponseStatus(201);
$this->seeInDatabase('users', ['email' => 'bob@example.com']);
// Check user exists
$saved = User::first();
$this->assertEquals($saved->email, 'bob@example.com');
$this->assertEquals($saved->name, 'Bob Smith');
}
/**
* Test creating a duplicate user
*
* @return void
*/
public function testPostingDuplicateUser()
{
// Create user
$user = factory(AnimalFriend\User::class)->create([
'name' => 'Bob Smith',
'email' => 'bob@example.com',
'password' => 'password'
]);
$this->seeInDatabase('users', ['email' => 'bob@example.com']);
// Create a request
$data = array(
'name' => 'Bob Smith',
'email' => 'bob@example.com',
'password' => 'password',
'password_confirmation' => 'password'
);
$this->json('POST', '/api/users', $data);
$this->assertResponseStatus(422);
}
}

Note the use of $this->json() to make the request. This method is ideal for testing a REST API.

Running our tests should confirm that they fail:

$ vendor/bin/phpunit
PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
........FFF. 12 / 12 (100%)
Time: 827 ms, Memory: 18.00MB
There were 3 failures:
1) UserControllerTest::testPostingInvalidUser
Expected status code 422, got 404.
Failed asserting that 404 matches expected 422.
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:648
/home/matthew/Projects/mynewanimalfriend-backend/tests/UserControllerTest.php:21
2) UserControllerTest::testPostingUser
Expected status code 201, got 404.
Failed asserting that 404 matches expected 201.
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:648
/home/matthew/Projects/mynewanimalfriend-backend/tests/UserControllerTest.php:39
3) UserControllerTest::testPostingDuplicateUser
Expected status code 422, got 404.
Failed asserting that 404 matches expected 422.
/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:648
/home/matthew/Projects/mynewanimalfriend-backend/tests/UserControllerTest.php:71
FAILURES!
Tests: 12, Assertions: 43, Failures: 3.

Next, we create our new controller:

$ php artisan make:controller UserController --resource

Let’s populate it:

<?php
namespace AnimalFriend\Http\Controllers;
use Illuminate\Http\Request;
use AnimalFriend\Http\Requests;
use AnimalFriend\User;
use JWTAuth;
use Hash;
class UserController extends Controller
{
private $user;
public function __construct(User $user) {
$this->user = $user;
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
// Validate request
$valid = $this->validate($request, [
'email' => 'required|email|unique:users,email',
'name' => 'required|string',
'password' => 'required|confirmed',
]);
// Create user
$user = new $this->user;
$user->email = $request->input('email');
$user->name = $request->input('name');
$user->password = Hash::make($request->input('password'));
$user->save();
// Create token
$token = JWTAuth::fromUser($user);
// Send response
return response()->json(['token' => $token], 201);
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}

For now we’ll leave the other methods blank, but we’ll be using them later so we won’t get rid of them. At the top, note we load not only the User model, but also the JWTAuth and Hash facades. We use JWTAuth::fromUser() to return a JSON web token for the given user model.

In the store() method we first of all use Laravel’s validation support to validate our input. We specify that the user must provide a unique email address, a username, and a password, which must be confirmed. Note that we don’t need to specify an action if the request is invalid, as Laravel will do that for us. Also, note that the confirmed rule means that the password field must be accompanied by a matching password_confirmation field.

Next, we create the user. Note that we hash the password before storing it, which is a best practice (storing passwords in plain text is a REALLY bad idea!). Then we create the token for the new user and return it. From then on, the user can use that token to authenticate their requests.

We also need to add this route in routes/api.php:

Route::resource('users', 'UserController');

Let’s check the test passes:

$ vendor/bin/phpunit
PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
............ 12 / 12 (100%)
Time: 905 ms, Memory: 20.00MB
OK (12 tests, 46 assertions)

Building the registration in the app

With registration in place on the server side, we can move back to the app. We need to create another route for the registration form. Add this to test/routes.spec.js:

it('should map register route to register controller', function () {
inject(function ($route) {
expect($route.routes['/register'].controller).toBe('RegisterCtrl');
expect($route.routes['/register'].templateUrl).toEqual('templates/register.html');
});
});

Running the tests should confirm that this fails. So next you should add this to the route provider section of js/main.js:

.when('/register', {
templateUrl: 'templates/register.html',
controller: 'RegisterCtrl'
})

We also need to allow the register path to be accessed when not logged in:

.run(['$rootScope', '$location', 'Auth', function ($rootScope, $location, Auth) {
$rootScope.$on('$routeChangeStart', function (event) {
if (!Auth.isLoggedIn()) {
if ($location.path() !== '/login' && $location.path() !== '/register') {
$location.path('/login');
}
}
});
}])

Our next step is to create a service representing the User endpoint. Here’s the test for it:

describe('User service', function () {
var mockBackend, User;
beforeEach(inject(function (_User_, _$httpBackend_) {
User = _User_;
mockBackend = _$httpBackend_;
}));
it('can create a new user', function () {
mockBackend.expectPOST('http://localhost:8000/api/users', '{"email":"bob@example.com","name":"bobsmith","password":"password","password_confirmation":"password"}').respond({token: 'mytoken'});
var user = new User({
email: 'bob@example.com',
name: 'bobsmith',
password: 'password',
password_confirmation: 'password'
});
user.$save(function (response) {
expect(response).toEqualData({token: 'mytoken'});
});
mockBackend.flush();
});
});

We’re only interested in using this model to create new users at this point, so this is the full scope of this test for now. Make sure the test fails, then we’re ready to create the new service in js/services.js:

.factory('User', function ($resource) {
return $resource('http://localhost:8000/api/users/:id', null, {
'update': { method: 'PATCH' }
});
})

Note that angular-resource does not support the PUT or PATCH methods by default, but as shown here it’s easy to implement it ourselves. That should be sufficient to make our test pass.

Next, we need to create the controller for registration. Here’s the test for it:

describe('Register Controller', function () {
var mockBackend, scope;
beforeEach(inject(function ($rootScope, $controller, _$httpBackend_) {
mockBackend = _$httpBackend_;
scope = $rootScope.$new();
$controller('RegisterCtrl', {
$scope: scope
});
}));
// Test controller scope is defined
it('should define the scope', function () {
expect(scope).toBeDefined();
});
// Test doRegister is defined
it('should define the register method', function () {
expect(scope.doRegister).toBeDefined();
});
// Test doRegister works
it('should allow the user to register', function () {
// Mock the backend
mockBackend.expectPOST('http://localhost:8000/api/users', '{"email":"user@example.com","name":"bobsmith","password":"password","password_confirmation":"password"}').respond({token: 123});
// Define login data
scope.credentials = {
email: 'user@example.com',
name: "bobsmith",
password: 'password',
password_confirmation: 'password'
};
// Submit the request
scope.doRegister();
// Flush the backend
mockBackend.flush();
// Check login complete
expect(localStorage.getItem('authHeader')).toEqual('Bearer 123');
});
});

Make sure the test fails before proceeding. Our RegisterCtrl is very similar to the login controller:

.controller('RegisterCtrl', function ($scope, $location, User, Auth) {
$scope.doRegister = function () {
var user = new User($scope.credentials);
user.$save(function (response) {
if (response.token) {
// Set up auth service
Auth.setUser(response.token);
// Redirect
$location.path('/');
}
}, function (err) {
alert('Unable to log in - please check your details are correct');
});
};
})

Check the tests pass,and we’re ready to move on to creating our HTML template. Save this as www/templates/register.html:

<md-content md-theme="default" layout-gt-sm="row" layout-padding>
<div>
<md-input-container class="md-block">
<label>Email</label>
<input ng-model="credentials.email" type="email">
</md-input-container>
<md-input-container class="md-block">
<label>Username</label>
<input ng-model="credentials.name" type="text">
</md-input-container>
<md-input-container class="md-block">
<label>Password</label>
<input ng-model="credentials.password" type="password">
</md-input-container>
<md-input-container class="md-block">
<label>Confirm Password</label>
<input ng-model="credentials.password_confirmation" type="password">
</md-input-container>
<md-button class="md-raised md-primary" ng-click="doRegister()">Submit</md-button>
<md-button class="md-raised md-primary" href="/login">Log in</md-button>
</div>
</md-content>

It’s very similar to our login template. Speaking of which, we need to add a link to this route there:

<md-content md-theme="default" layout-gt-sm="row" layout-padding>
<div>
<md-input-container class="md-block">
<label>Email</label>
<input ng-model="credentials.email" type="email" />
</md-input-container>
<md-input-container class="md-block">
<label>Password</label>
<input ng-model="credentials.password" type="password" />
</md-input-container>
<md-button class="md-raised md-primary" ng-click="doLogin()">Submit</md-button>
<md-button class="md-raised md-primary" href="register">Register</md-button>
</div>
</md-content>

With that done, you should now be able to run the Gulp server for the app with gulp and the Laravel backend with php artisan serve and create a new user account.

Adding pets to the home page

Our final task for this lesson is to display a list of pets on the home page. Later we’ll refine that functionality, but for now we’ll just get a list of all current pets and display them. First we need to write a test for our Pet service:

describe('Pet service', function () {
var mockBackend, Pet;
beforeEach(inject(function (_Pet_, _$httpBackend_) {
Pet = _Pet_;
mockBackend = _$httpBackend_;
}));
it('can fetch pets', function () {
mockBackend.expectGET('http://localhost:8000/api/pets').respond([{id:1,name:"Freddie",type:"Cat"}]);
expect(Pet).toBeDefined();
var pets = Pet.query();
mockBackend.flush();
expect(pets).toEqualData([{id: 1,name:"Freddie",type:"Cat"}]);
});
});

Once you know that fails, it’s time to implement the service:

.factory('Pet', function ($resource) {
return $resource('http://localhost:8000/api/pets/:id', null, {
'update': { method: 'PATCH' }
});
})

Next, we want to add the pets to the scope of the home controller. Amend the test for it as follows:

describe('Home Controller', function () {
var pets, scope;
beforeEach(inject(function ($rootScope, $controller, Pet) {
pets = Pet;
scope = $rootScope.$new();
$controller('HomeCtrl', {
$scope: scope,
pets: [{id:1},{id:2}]
});
}));
// Test controller scope is defined
it('should define the scope', function () {
expect(scope).toBeDefined();
});
// Test pets
it('should define the pets', function () {
expect(scope.pets).toEqualData([{id: 1}, {id: 2}]);
});
});

We check to see if the scope contains the pets variable. Once you have a failing test, amend the home controller as follows:

.controller('HomeCtrl', function ($scope, Pet, pets) {
$scope.pets = pets;
});

We could fetch the via AJAX inside the controller, but there’s a better way. We’ll create a loader for the pet data and have it resolve that before the page is displayed. To do so, first we need to add the loader service to js/services.js:

.factory('PetsLoader', ['Pet', '$q', function (Pet, $q) {
return function () {
var delay = $q.defer();
Pet.query(function (response) {
delay.resolve(response);
}, function () {
delay.reject('Unable to fetch pets');
});
return delay.promise;
};
}])

Then we set that route up to resolve it in js/main.js:

.when('/', {
templateUrl: 'templates/home.html',
controller: 'HomeCtrl',
resolve: {
pets: ['PetsLoader', function (PetsLoader) {
return PetsLoader();
}]
}
})

Now, when we load that route, it will first of all fetch those pets and populate $scope.pets with them.

Now, we need to have some pets in the database, so we’ll make a seeder for it. Head back to the backend and run this command:

$ php artisan make:seeder PetTableSeeder

Then amend the file at database/seeds/PetTableSeeder.php as follows:

<?php
use Illuminate\Database\Seeder;
use Carbon\Carbon;
class PetTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
// Add Pets
DB::table('pets')->insert([[
'name' => 'Freddie',
'type' => 'Cat',
'available' => 1,
'picture' => 'https://placekitten.com/300/300',
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
], [
'name' => 'Sophie',
'type' => 'Cat',
'available' => 1,
'picture' => 'https://placekitten.com/300/300',
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
]]);
}
}

And we need to update database/seeds/DatabaseSeeder.php to call our seeder:

<?php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$this->call(UserTableSeeder::class);
$this->call(PetTableSeeder::class);
}
}

For now we’ll use placeholder images, but at a later point our backend will be set up to use images uploaded from the admin. Then we need to refresh our migrations and apply the seeders:

$ php artisan migrate:refresh
$ php artisan db:seed

Now we just need to amend our home template to show the pets and we’re done for today:

<md-toolbar>
<div class="md-toolbar-tools">
<md-button aria-label="Log out" href="/logout">
Log out
</md-button>
</div>
</md-toolbar>
<div layout="column" flex="grow" layout-align="center stretch">
<md-card md-theme="default" ng-repeat="pet in pets">
<md-card-title>
<md-card-title-text>
<span class="md-headline">{{ pet.name }}</span>
<span class="md-subhead">{{ pet.type }}</span>
</md-card-title-text>
</md-card-title>
<md-card-content>
<img class="md-card-image md-media-lg" ng-src="{{ pet.picture }}"></img>
</md-card-content>
</md-card>
</div>

Now we can see our pets in the app.

Wrapping up

That’s enough for today - the fact that we can log in and out, register, and view the home page is sufficient as a proof of concept for a client. As usual, the results are on Github, tagged lesson-3.

Next time, we’ll concentrate exclusively on the back end. We’ll build upon what we already have using Laravel to create a full REST API for our app. In a later instalment, we’ll move on to build our admin interface for the staff, before switching back to finish off the app. I hope you’ll join me then.

Creating an Azure storage adapter for Laravel

$
0
0

About a year ago I was working on my first non-trivial Laravel application. The client had, for their own reasons, wanted to use Microsoft’s Azure platform, particularly for its blob storage functionality, which is somewhat comparable to Amazon S3. Now, Laravel has the excellent Storage facade that allows consistent access to both local files and those stored on various file hosting services, which is built on top of Flysystem. Flysystem has an Azure driver, but the Laravel storage doesn’t include support for it, so at the time I resigned myself to using Flysystem directly, which wasn’t actually that bad, but not ideal.

A few days ago I stumbled across this section of the Laravel documentation, which had me kicking myself. It’s actually trivially easy to implement a custom filesystem for Laravel if it already has a Flysystem adapter, as demonstrated in their Dropbox implementation in the docs. Using this as a guide, I was able to produce the following service provider for using Azure as a storage backend very quickly:

<?php
namespace App\Providers;
use Storage;
use League\Flysystem\Filesystem;
use Illuminate\Support\ServiceProvider;
use League\Flysystem\Azure\AzureAdapter;
use WindowsAzure\Common\ServicesBuilder;
class AzureStorageServiceProvider extends ServiceProvider
{
/**
* Perform post-registration booting of services.
*
* @return void
*/
public function boot()
{
Storage::extend('azure', function($app, $config) {
$endpoint = sprintf(
'DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=%s',
$config['name'],
$config['key']
);
$blobRestProxy = ServicesBuilder::getInstance()->createBlobService($endpoint);
return new Filesystem(new AzureAdapter($blobRestProxy, $config['container']));
});
}
/**
* Register bindings in the container.
*
* @return void
*/
public function register()
{
//
}
}

This should be saved as app/Providers/AzureStorageServiceProvider.php. You also need to add this to the list of service providers in config/app.php:

        App\Providers\AzureStorageServiceProvider::class,

And add this to config/filesystems.php:

'azure' => [
'driver' => 'azure',
'name' => env('STORAGE_NAME'),
'key' => env('STORAGE_KEY'),
'container' => env('STORAGE_CONTAINER'),
],

I like to also set my storage backend using environment variables in this file, as in this example:

'default' => env('STORAGE_BACKEND', 'local'),

That way we can easily set a different backend for testing, development and production so we don’t upload files when running PHPUnit. You can also keep your other config settings in your .env file, which is always a better idea than keeping it under version control. You also need to install the microsoft/windowsazure and league/flysystem-azure packages via Composer for this to work.

As I’ve since changed jobs it’s unlikely I’ll ever actually use this Azure integration in production - it’s not a service I’d choose of my own accord to use. However, since it’s so straightforward to implement an adapter like this I imagine I may be doing something similar - I’m currently working on a web app that uses MongoDB for some of its data and currently stores files locally, so it might make sense to create a GridFS integration along similar lines. It may also be useful for someone else, so feel free to use it if you wish.

Building a Phonegap App with Laravel and Angular - Part 4

$
0
0

In this instalment we’ll return to the back end. What we’ve done so far is typical of the kind of proof of concept we might do for a client early on, before going back and implementing the full set of features later on. Now we’ll go back and start to improve on that rather quick-and-dirty API by making sure we follow a few best practices.

For those of you who want to follow the Laravel Phonegap tutorials, I’ve created a dedicated category here for those tutorials. This category include RSS and Atom feeds, so if you only want to read those posts, you can do so. I’ve also done the same for the Django tutorials.

The Repository pattern

One of the issues we currently have with our API is that we’re passing our Eloquent models into our controllers. This may not seem like a huge issue, but it means that our controllers are tightly coupled to the Eloquent ORM, so if we wanted to switch to another ORM, or to a completely different database such as MongoDB, we’d have to amend our controllers. That’s not good.

However, using the Repository pattern we can first of all define an interface for our repository, and then create a repository class that implements that interface. That way we can interact with the repository class in our controllers, rather than using Eloquent models directly. Then, if we want to switch databases, we merely amend the repository class to change the implementation of those methods, without having to touch our controllers. Also, it makes it much easier to test our controllers in isolation, because we can easily mock our repository class using Mockery and hard-code the response, so our tests won’t touch the database and will therefore run more quickly. We won’t touch on that this time, but it’s a very significant advantage.

If you haven’t used interfaces before in PHP, they aren’t that hard. They merely specify what methods an object implementing that method must have and what arguments they must accept, but do not specify the details of the implementation. This makes it easy to determine if a class implements an interface correctly, because it will throw an exception if it doesn’t.

<?php
namespace AnimalFriend\Repositories\Interfaces;
interface PetRepositoryInterface {
public function all();
public function findOrFail($id);
public function create($input);
}

That’s all there is to it. We define it using the interface keyword and we specify the methods it must implement. Save this file at app/Repositories/Interfaces/PetRepositoryInterface.php.

Next, we implement the repository class:

<?php
namespace AnimalFriend\Repositories;
use AnimalFriend\Pet;
use AnimalFriend\Repositories\Interfaces\PetRepositoryInterface;
class EloquentPetRepository implements PetRepositoryInterface {
private $pet;
public function __construct(Pet $pet)
{
$this->pet = $pet;
}
public function all()
{
return $this->pet->all();
}
public function findOrFail($id)
{
return $this->pet->findOrFail($id);
}
public function create($input)
{
return $this->pet->create($input);
}
}

Save this to app/Repositories/EloquentPetRepository.php. Note how the methods closely mirror the underlying Eloquent methods, but they don’t need to - you could change the underlying implementation of each method, but the repository would still work in exactly the same way.

To make this work, we need to make a few changes elsewhere. In composer.json, we need to add the new Repositories folder to our classmap:

"autoload": {
"classmap": [
"database",
"app/Repositories"
],
"psr-4": {
"AnimalFriend\\": "app/"
}
},

And in app/Providers/AppServiceProvider.php, we need to bind our new files:

<?php
namespace AnimalFriend\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->bind(
'AnimalFriend\Repositories\Interfaces\PetRepositoryInterface',
'AnimalFriend\Repositories\EloquentPetRepository'
);
}
}

With that done, we can now update app/Http/Controllers/PetController.php to use the repository:

<?php
namespace AnimalFriend\Http\Controllers;
use Illuminate\Http\Request;
use AnimalFriend\Http\Requests;
use AnimalFriend\Repositories\Interfaces\PetRepositoryInterface as Pet;
class PetController extends Controller
{
private $pet;
public function __construct(Pet $pet) {
$this->pet = $pet;
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
// Get all pets
$pets = $this->pet->all();
// Send response
return response()->json($pets, 200);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
// Get pet
$pet = $this->pet->findOrFail($id);
// Send response
return response()->json($pet, 200);
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}

Our repository is now injected automatically into the controller. To make this work we need to run the following command:

$ composer dump-autoload

Running our tests should confirm that everything is still working:

$ vendor/bin/phpunit
PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
............ 12 / 12 (100%)
Time: 897 ms, Memory: 18.00MB
OK (12 tests, 46 assertions)

Let’s do the same for the User model. First we implement our interface in app/Repositories/Interfaces/UserRepositoryInterface.php:

<?php
namespace AnimalFriend\Repositories\Interfaces;
interface UserRepositoryInterface {
public function all();
public function findOrFail($id);
public function create($input);
}

Next we create our repository at app/Repositories/EloquentUserRepository.php:

<?php
namespace AnimalFriend\Repositories;
use AnimalFriend\User;
use AnimalFriend\Repositories\Interfaces\UserRepositoryInterface;
use JWTAuth;
use Hash;
class EloquentUserRepository implements UserRepositoryInterface {
private $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function all()
{
return $this->user->all();
}
public function findOrFail($id)
{
return $this->user->findOrFail($id);
}
public function create($input)
{
$user = new $this->user;
$user->email = $input['email'];
$user->name = $input['name'];
$user->password = Hash::make($input['password']);
$user->save();
// Create token
return JWTAuth::fromUser($user);
}
}

Note how we’ve moved much of the logic for creating a user into the create() method, and we return the token, not the user model. This makes sense as right now we only ever want to get a token back when we create a user. Later that may change, but there’s nothing stopping us adding a new method to implement that behaviour alongside this.

Then we update app/Http/Controllers/UserController.php to use our repository:

<?php
namespace AnimalFriend\Http\Controllers;
use Illuminate\Http\Request;
use AnimalFriend\Http\Requests;
use AnimalFriend\Repositories\Interfaces\UserRepositoryInterface as User;
use JWTAuth;
use Hash;
class UserController extends Controller
{
private $user;
public function __construct(User $user) {
$this->user = $user;
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
// Validate request
$valid = $this->validate($request, [
'email' => 'required|email|unique:users,email',
'name' => 'required|string',
'password' => 'required|confirmed'
]);
// Create token
$token = $this->user->create($request->only(
'email',
'name',
'password'
));
// Send response
return response()->json(['token' => $token], 201);
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}

And add a new binding in app/Providers/AppServiceProvider.php:

<?php
namespace AnimalFriend\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->bind(
'AnimalFriend\Repositories\Interfaces\PetRepositoryInterface',
'AnimalFriend\Repositories\EloquentPetRepository'
);
$this->app->bind(
'AnimalFriend\Repositories\Interfaces\UserRepositoryInterface',
'AnimalFriend\Repositories\EloquentUserRepository'
);
}
}

Note that we bind the two sets separately - this allows Laravel to figure out which one maps to which.

Let’s run our tests to make sure nothing is broken:

$ vendor/bin/phpunit
PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
............ 12 / 12 (100%)
Time: 956 ms, Memory: 18.00MB
OK (12 tests, 46 assertions)

Now that we’ve got our repositories in place, we’re no longer tightly coupled to Eloquent, and have a more flexible implementation which is easier to test.

Separating our models from our JSON with Fractal

Another problem with our API is that our representation of our data is tightly coupled to our underlying implementation of our models. We therefore can’t change our models without potentially changing the data returned by the API. We need to separate our representation of our data from our actual model so that we can more easily specify the exact data we want to return, regardless of the underlying database structure.

Enter Fractal. From the website:

Fractal provides a presentation and transformation layer for complex data output, the like found in RESTful APIs, and works really well with JSON. Think of this as a view layer for your JSON/YAML/etc.

In other words, Fractal lets you specify the format your data will take in one place so that it’s easier to return that data in a desired format. We’ll use Fractal to specify how we want our API responses to be formatted.

Install Fractal with the following command:

$ composer require league/fractal

Then amend the classmap in composer.json:

"autoload": {
"classmap": [
"database",
"app/Repositories",
"app/Transformers"
],
"psr-4": {
"AnimalFriend\\": "app/"
}
},

Then create the folder app/Transformers and run composer dump-autoload. We’re now ready to write our first transformer. Save this as app/Transformers/PetTransformer.php:

<?php
namespace AnimalFriend\Transformers;
use AnimalFriend\Pet;
use League\Fractal;
class PetTransformer extends Fractal\TransformerAbstract
{
public function transform(Pet $pet)
{
return [
'id' => (int) $pet->id,
'name' => (string) $pet->name,
'type' => (string) $pet->type,
'available' => (bool) $pet->available,
'picture' => (string) $pet->picture
];
}
}

The transform method specifies how we want to represent our objects with our API. We can return only those attributes we want to expose, and amend the structure as we see fit. We could easily represemt relations in whatever manner we see fit, whereas before we needed to amend our queries to return the data in the right format, which would potentially be cumbersome.

Now let’s amend PetController.php to use this:

<?php
namespace AnimalFriend\Http\Controllers;
use Illuminate\Http\Request;
use AnimalFriend\Http\Requests;
use AnimalFriend\Repositories\Interfaces\PetRepositoryInterface as Pet;
use AnimalFriend\Transformers\PetTransformer;
use League\Fractal;
use League\Fractal\Manager;
class PetController extends Controller
{
private $pet, $fractal;
public function __construct(Pet $pet, Manager $fractal) {
$this->pet = $pet;
$this->fractal = $fractal;
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
// Get all pets
$pets = $this->pet->all();
// Format it
$resource = new Fractal\Resource\Collection($pets, new PetTransformer);
$data = $this->fractal->createData($resource)->toArray();
// Send response
return response()->json($data, 200);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
// Get pet
$pet = $this->pet->findOrFail($id);
// Format it
$resource = new Fractal\Resource\Item($pet, new PetTransformer);
$data = $this->fractal->createData($resource)->toArray();
// Send response
return response()->json($data, 200);
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}

Note that by default, Fractal places our data inside a dedicated data namespace. This is good because it leaves a place for us to put metadata such as pagination links, but it does mean our controller test has been broken. Let’s fix it:

<?php
use Illuminate\Foundation\Testing\DatabaseMigrations;
class PetControllerTest extends TestCase
{
use DatabaseMigrations;
/**
* Test fetching pets when unauthorised
*
* @return void
*/
public function testFetchingPetsWhenUnauthorised()
{
// Create a Pet
$pet = factory(AnimalFriend\Pet::class)->create([
'name' => 'Freddie',
'type' => 'Cat',
]);
$this->seeInDatabase('pets', ['type' => 'Cat']);
// Create request
$response = $this->call('GET', '/api/pets');
$this->assertResponseStatus(400);
}
/**
* Test fetching pets when authorised
*
* @return void
*/
public function testFetchingPets()
{
// Create a Pet
$pet = factory(AnimalFriend\Pet::class)->create([
'name' => 'Freddie',
'type' => 'Cat',
]);
$this->seeInDatabase('pets', ['type' => 'Cat']);
// Create a User
$user = factory(AnimalFriend\User::class)->create([
'name' => 'bobsmith',
'email' => 'bob@example.com',
]);
$this->seeInDatabase('users', ['email' => 'bob@example.com']);
// Create request
$token = JWTAuth::fromUser($user);
$headers = array(
'Authorization' => 'Bearer '.$token
);
// Send it
$this->json('GET', '/api/pets', [], $headers)
->seeJsonStructure([
'data' => [
'*' => [
'id',
'name',
'type',
'available',
'picture'
]
]
]);
$this->assertResponseStatus(200);
}
/**
* Test fetching pet when unauthorised
*
* @return void
*/
public function testFetchingPetWhenUnauthorised()
{
// Create a Pet
$pet = factory(AnimalFriend\Pet::class)->create([
'name' => 'Freddie',
'type' => 'Cat',
]);
$this->seeInDatabase('pets', ['type' => 'Cat']);
// Send request
$response = $this->call('GET', '/api/pets/'.$pet->id);
$this->assertResponseStatus(400);
}
/**
* Test fetching pet which does not exist
*
* @return void
*/
public function testFetchingPetDoesNotExist()
{
// Create a User
$user = factory(AnimalFriend\User::class)->create([
'name' => 'bobsmith',
'email' => 'bob@example.com',
]);
$this->seeInDatabase('users', ['email' => 'bob@example.com']);
// Create request
$token = JWTAuth::fromUser($user);
$headers = array(
'Authorization' => 'Bearer '.$token
);
// Send it
$this->json('GET', '/api/pets/1', [], $headers);
$this->assertResponseStatus(404);
}
/**
* Test fetching pet when authorised
*
* @return void
*/
public function testFetchingPet()
{
// Create a Pet
$pet = factory(AnimalFriend\Pet::class)->create([
'name' => 'Freddie',
'type' => 'Cat',
]);
$this->seeInDatabase('pets', ['type' => 'Cat']);
// Create a User
$user = factory(AnimalFriend\User::class)->create([
'name' => 'bobsmith',
'email' => 'bob@example.com',
]);
$this->seeInDatabase('users', ['email' => 'bob@example.com']);
// Create request
$token = JWTAuth::fromUser($user);
$headers = array(
'Authorization' => 'Bearer '.$token
);
// Send it
$this->json('GET', '/api/pets/'.$pet->id, [], $headers)
->seeJsonStructure([
'data' => [
'id',
'name',
'type',
'available',
'picture'
]
]);
$this->assertResponseStatus(200);
}
}

We’re also going to amend our test settings to use the array backend for the cache, as this does not require any external dependencies, but still allows us to tag our cache keys (I’ll cover that in a future instalment). Change the cache settings in phpunit.xml as follows:

<env name="CACHE_DRIVER" value="array"/>

Let’s run our tests to make sure everything’s fine:

$ vendor/bin/phpunit
PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
............ 12 / 12 (100%)
Time: 859 ms, Memory: 18.00MB
OK (12 tests, 44 assertions)

At present our User controller doesn’t actually return anything, and the auth only ever returns the token, so it’s not worth while adding a transformer now.

Wrapping up

That ends this lesson. We haven’t added any functionality, but we have improved the design of our API, and we’re now ready to develop it further. As usual, the backend repository has been tagged as lesson-4.

Next time we’ll start adding the additional functionality we need to our API.

Easy static asset versioning in PHP

$
0
0

It’s prudent to cache static assets such as images, Javascript and CSS to improve performance, but that raises the issue of changes not being reflected in your site due to visitor’s browsers retaining the cached versions. Many content management systems and frameworks already handle this for you (such as Laravel’s Elixir build system), but what if you have to work with a legacy application that doesn’t do this?

Fortunately there’s a quite easy solution in PHP. Using the filemtime() function described here, we can get a Unix timestamp for when a file was last altered. This is perfect to use to identify when a file last changed, because by appending a new query string to the file name when loading it, we can trick the browser into thinking it’s a new file when it’s not, as in this example for a CodeIgniter application:

<link rel="stylesheet" type="text/css" href="<?=$path?>?v=<?=filemtime($path)?>">

Obviously, this is a bit repetitive, so you may want to refactor this into some kind of template helper to make it easier to use, but the underlying principle applies to most programming languages. For instance, if you wanted to do so in a Handlebars template, you might want to create a helper something like this:

var fs = require('fs');
var Handlebars = require('handlebars');
Handlebars.registerHelper('version', function (path) {
return path + '?v=' + fs.statSync(path).mtime.getTime();
});

Where more robust solutions such as Elixir are already available, I’d advise making full use of them. However, this technique is a quick and easy way to implement versioning for static assets in existing projects.

Testing Laravel Middleware

$
0
0

It’s widely accepted that high-level integration tests alone do not make for a good test suite. Ideally each individual component of your application should have unit tests, which test that component in isolation. These unit tests are usually much quicker to run, making it easier to practice test-driven development. However, it can sometimes be hard to grasp how to test that one component on its own.

The other day I had an issue with several middleware classes for a Laravel application and I wanted to verify that they were working as expected. Sounds like a job for dedicated unit tests, but I hadn’t tested custom middleware in isolation before, and figuring out how to do so took a while.

Laravel middleware accepts an instance of Illuminate\Http\Request, itself based on the Symfony request object, as well as a closure for the action to take next. Depending on what the middleware does, it may return a redirect or simply amend the existing request or response. So in theory you can instantiate a request object, pass it to the middleware, and check the response. For middleware that does something simple, such as redirecting users based on certain conditions, this is fairly straightforward.

In this example we have a fairly useless piece of middleware that checks to see what the route is for a request and redirects it if it matches a certain pattern:

<?php
namespace App\Http\Middleware;
use Closure;
class RedirectFromAdminMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->is('admin*')) {
return redirect('/');
}
return $next($request);
}
}

While this example is of limited use, it wouldn’t take much work to develop it to redirect conditionally based on an account type, and it’s simple enough to demonstrate the principles involved. In these tests, we create instances of Illuminate\Http\Request and pass them to the middleware’s handle() method, along with an empty closure representing the response. If the middleware does not amend the request, we get the empty response from the closure. If it does amend the request, we get a redirect response.

<?php
use Illuminate\Http\Request;
class RedirectFromAdminMiddlewareTest extends TestCase
{
public function testRedirectMiddlewareCalledOnAdmin()
{
// Create request
$request = Request::create('http://example.com/admin', 'GET');
// Pass it to the middleware
$middleware = new App\Http\Middleware\RedirectFromAdminMiddleware();
$response = $middleware->handle($request, function () {});
$this->assertEquals($response->getStatusCode(), 302);
}
public function testRedirectMiddlewareNotCalledOnNonAdmin()
{
// Create request
$request = Request::create('http://example.com/pages', 'GET');
// Pass it to the middleware
$middleware = new App\Http\Middleware\RedirectFromAdminMiddleware();
$response = $middleware->handle($request, function () {});
$this->assertEquals($response, null);
}
}

For middleware that fetches the response and acts on it, things are a little more complex. For instance, this is the Etag middleware I use on many projects:

<?php
namespace App\Http\Middleware;
use Closure;
class ETagMiddleware {
/**
* Implement Etag support
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
// Get response
$response = $next($request);
// If this was a GET request...
if ($request->isMethod('get')) {
// Generate Etag
$etag = md5($response->getContent());
$requestEtag = str_replace('"', '', $request->getETags());
// Check to see if Etag has changed
if($requestEtag && $requestEtag[0] == $etag) {
$response->setNotModified();
}
// Set Etag
$response->setEtag($etag);
}
// Send response
return $response;
}
}

This acts on the response object, so we need to pass that through as well. Fortunately, Mockery allows us to create a mock of our response object and set it up to handle only those methods we anticipate being called:

<?php
use Illuminate\Http\Request;
class ETagMiddlewareTest extends TestCase
{
/**
* Test new request not cached
*
* @return void
*/
public function testModified()
{
// Create mock response
$response = Mockery::mock('Illuminate\Http\Response')->shouldReceive('getContent')->once()->andReturn('blah')->getMock();
$response->shouldReceive('setEtag')->with(md5('blah'));
// Create request
$request = Request::create('http://example.com/admin', 'GET');
// Pass it to the middleware
$middleware = new App\Http\Middleware\ETagMiddleware();
$middlewareResponse = $middleware->handle($request, function () use ($response) {
return $response;
});
}
/**
* Test repeated request not modified
*
* @return void
*/
public function testNotModified()
{
// Create mock response
$response = Mockery::mock('Illuminate\Http\Response')->shouldReceive('getContent')->once()->andReturn('blah')->getMock();
$response->shouldReceive('setEtag')->with(md5('blah'));
$response->shouldReceive('setNotModified');
// Create request
$request = Request::create('http://example.com/admin', 'GET', [], [], [], [
'ETag' => md5('blah')
]);
// Pass it to the middleware
$middleware = new App\Http\Middleware\ETagMiddleware();
$middlewareResponse = $middleware->handle($request, function () use ($response) {
return $response;
});
}
public function teardown()
{
Mockery::close();
}
}

In the first example we mock out the getContent() and setEtag() methods of our response to make sure they get called, and then pass the request to the middleware, along with a closure that returns the response. In the second example, we also mock out setNotModified() to ensure that the correct status code of 304 is set, and add an ETag to our request. In this way we can easily test our middleware in isolation, rather than having to resort to building up our entire application just to test one small method.

Middleware is a convenient place to put functionality that’s needed for many routes, but you shouldn’t neglect testing it, and ideally you shouldn’t have to resort to writing a slow integration test to test it works as expected. By mocking out your dependencies, it’s generally not too hard to test it in isolation, resulting in faster and more robust test suites.


Integrating Behat with Laravel

$
0
0

The Gherkin format used by tools like Cucumber is a really great way of specifying how your application will work. It’s easy for even non-technical stakeholders to understand, it makes it natural to break your tests into easily reusable steps, and it encourages you to think about the application from an end-user’s perspective. It’s also one of the easiest ways to get started writing automated tests when you first start out - it’s much more intuitive to a junior developer than lower-level unit tests, and is easier to add to a legacy project that may not have been built with testability in mind - if you can drive a browser, you can test it.

Behat is a PHP equivalent. Combined with Mink, it allows for easy automated acceptance tests of a PHP application. However, out of the box it doesn’t integrate well with Laravel. There is Jeffrey Way’s Behat Laravel extension, but it doesn’t seem to be actively maintained and seems to be overkill for this purpose. I wanted something that I could use to run integration tests using PHPUnit’s assertions and Laravel’s testing utilities, and crucially, I wanted to do so as quickly as possible. That meant running a web server and using an automated web browser wasn’t an option. Also, I often work on REST API’s, and browser testing isn’t appropriate for those - in API tests I’m more interested in setting up the fixtures, making a single request, and verifying that it does what it’s meant to do, as quickly as possible.

As it turns out, integrating Behat and Laravel isn’t that hard. When using Behat, your FeatureContext.php file must implement the Behat\Behat\Context\Context interface, but as this interface does not implement any methods, you can extend any existing class and declare that it implements that interface. That means we can just extend the existing Tests\TestCase class in Laravel 5.4 and gain access to all the same testing utilities we have in our regular Laravel tests.

Then, in the constructor we can set environment variables using putenv() so that we can set it up to use an in-memory SQLite database for faster tests. We also use the @BeforeScenario hook to migrate the database before each scenario, and the @AfterScenario hook to roll it back afterwards.

Here’s the finished example:

<?php
use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use Tests\TestCase;
use Behat\Behat\Tester\Exception\PendingException;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use App\User;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Behat\Hook\Scope\AfterScenarioScope;
use Illuminate\Contracts\Console\Kernel;
/**
* Defines application features from the specific context.
*/
class FeatureContext extends TestCase implements Context
{
use DatabaseMigrations;
protected $content;
/**
* Initializes context.
*
* Every scenario gets its own context instance.
* You can also pass arbitrary arguments to the
* context constructor through behat.yml.
*/
public function __construct()
{
putenv('DB_CONNECTION=sqlite');
putenv('DB_DATABASE=:memory:');
parent::setUp();
}
/** @BeforeScenario */
public function before(BeforeScenarioScope $scope)
{
$this->artisan('migrate');
$this->app[Kernel::class]->setArtisan(null);
}
/** @AfterScenario */
public function after(AfterScenarioScope $scope)
{
$this->artisan('migrate:rollback');
}
/**
* @Given I visit the path :path
*/
public function iVisitThePath($path)
{
$response = $this->get('/');
$this->assertEquals(200, $response->getStatusCode());
$this->content = $response->getContent();
}
/**
* @Then I should see the text :text
*/
public function iShouldSeeTheText($text)
{
$this->assertContains($text, $this->content);
}
/**
* @Given a user called :user exists
*/
public function aUserCalledExists($user)
{
$user = factory(App\User::class)->create([
'name' => $user,
]);
}
/**
* @Given I am logged in as :user
*/
public function iAmLoggedInAs($user)
{
$user = User::where('name', $user)->first();
$this->be($user);
}
}

Note that I’ve added a few basic example methods for our tests. As you can see, we can call the same methods we normally use in Laravel tests to make assertions and HTTP requests. If you’re using Dusk, you can also call that in the same way you usually would.

We might then write the following feature file to demonstrate our application at work:

Feature: Login
Background:
Given a user called "Alan" exists
And a user called "Bob" exists
And a user called "Clare" exists
And a user called "Derek" exists
And a user called "Eric" exists
Scenario: Log in as Alan
Given I am logged in as "Alan"
And I visit the path "/"
Then I should see the text "Laravel"
Scenario: Log in as Bob
Given I am logged in as "Bob"
And I visit the path "/"
Then I should see the text "Laravel"
Scenario: Log in as Clare
Given I am logged in as "Clare"
And I visit the path "/"
Then I should see the text "Laravel"
Scenario: Log in as Derek
Given I am logged in as "Derek"
And I visit the path "/"
Then I should see the text "Laravel"
Scenario: Log in as Eric
Given I am logged in as "Eric"
And I visit the path "/"
Then I should see the text "Laravel"

We can then run these tests with vendor/bin/behat:

$ vendor/bin/behat
Feature: Login
Background: # features/auth.feature:3
Given a user called "Alan" exists # FeatureContext::aUserCalledExists()
And a user called "Bob" exists # FeatureContext::aUserCalledExists()
And a user called "Clare" exists # FeatureContext::aUserCalledExists()
And a user called "Derek" exists # FeatureContext::aUserCalledExists()
And a user called "Eric" exists # FeatureContext::aUserCalledExists()
Scenario: Log in as Alan # features/auth.feature:10
Given I am logged in as "Alan" # FeatureContext::iAmLoggedInAs()
And I visit the path "/" # FeatureContext::iVisitThePath()
Then I should see the text "Laravel" # FeatureContext::iShouldSeeTheText()
Scenario: Log in as Bob # features/auth.feature:15
Given I am logged in as "Bob" # FeatureContext::iAmLoggedInAs()
And I visit the path "/" # FeatureContext::iVisitThePath()
Then I should see the text "Laravel" # FeatureContext::iShouldSeeTheText()
Scenario: Log in as Clare # features/auth.feature:20
Given I am logged in as "Clare" # FeatureContext::iAmLoggedInAs()
And I visit the path "/" # FeatureContext::iVisitThePath()
Then I should see the text "Laravel" # FeatureContext::iShouldSeeTheText()
Scenario: Log in as Derek # features/auth.feature:25
Given I am logged in as "Derek" # FeatureContext::iAmLoggedInAs()
And I visit the path "/" # FeatureContext::iVisitThePath()
Then I should see the text "Laravel" # FeatureContext::iShouldSeeTheText()
Scenario: Log in as Eric # features/auth.feature:30
Given I am logged in as "Eric" # FeatureContext::iAmLoggedInAs()
And I visit the path "/" # FeatureContext::iVisitThePath()
Then I should see the text "Laravel" # FeatureContext::iShouldSeeTheText()
5 scenarios (5 passed)
40 steps (40 passed)
0m0.50s (19.87Mb)

Higher level tests can get very tedious if you’re not careful - you wind up setting up the same fixtures and making the same requests many times over. By using Behat in this way, not only are you writing your tests in a way that is easy to understand, but you’re also breaking it down into logical, repeatable steps, and by passing arguments in each step you limit the amount of repetition. It’s also fast if you aren’t running browser-based tests, making it particularly well-suited to API testing.

My first Laravel package

$
0
0

For some time now I’ve had a Laravel middleware I use extensively to add ETags to HTTP requests. I often use it for work projects, but obviously copying and pasting it all the time was a pain. I always meant to create a package for it, but I didn’t want to do so until such time as I had some proper tests for it. Now I’ve finally figured out how to test middleware in isolation and I’ve got around to adding tests and creating a proper package for it.

It’s available on Github and Packagist if you want to use it.

Decorating Laravel repositories

$
0
0

As mentioned previously, when building any nontrivial Laravel application, it’s prudent to decouple our controllers from the Eloquent ORM (or any other ORM or data source we may be using) by creating an interface, and then writing a repository that implements that interface. We can then resolve the interface to our repository, and use the repository to interact with our data source. Should we need to switch to a different implementation, we then need only create the new repository and amend how Laravel resolves that interface.

The same principle applies when it comes to caching. Database queries are typically a major bottleneck in a web application, and so it’s prudent to implement some form of caching for your queries. However, it’s a bad idea to do so in your controllers, because just as with Eloquent models, you’re tying yourself to one particular implementation and won’t be able to switch without rewriting a good chunk of your controllers, as well as possibly having to maintain large amounts of duplicate code for when a query is made in several places.

Alternatively, you could implement caching within the methods of your repository, which might make sense for smaller projects. However, it means that your repository is now dependent on both the ORM and cache you chose. If you decide you want to change your ORM but retain the same caching system, or vice versa, you’re stuck with writing a new repository to handle both, duplicating work you’ve already done.

Fortunately, there’s a more elegant solution. Using the decorator pattern, we can create a second repository that implements the same interface and “wraps” the original repository. Each of its methods will call its counterpart in the original, and if appropriate cache the response. That way, our caching is implemented separately from our database interactions, and we can easily create a repository for a new data source without affecting the caching in the slightest.

Say we have the following interface for our User model:

<?php
namespace App\Repositories\Interfaces;
interface UserRepositoryInterface {
public function all();
public function findOrFail($id);
public function create($input);
}

And the following repository implements that interface:

<?php
namespace App\Repositories;
use App\User;
use App\Repositories\Interfaces\UserRepositoryInterface;
use Hash;
class EloquentUserRepository implements UserRepositoryInterface {
private $model;
public function __construct(User $model)
{
$this->model = $model;
}
public function all()
{
return $this->model->all();
}
public function findOrFail($id)
{
return $this->model->findOrFail($id);
}
public function create($input)
{
$user = new $this->model;
$user->email = $input['email'];
$user->name = $input['name'];
$user->password = Hash::make($input['password']);
$user->save();
return $user;
}
}

We might implement the following repository class to handle caching:

<?php
namespace App\Repositories\Decorators;
use App\Repositories\Interfaces\UserRepositoryInterface;
use Illuminate\Contracts\Cache\Repository as Cache;
class CachingUserRepository implements UserRepositoryInterface {
protected $repository;
protected $cache;
public function __construct(UserRepositoryInterface $repository, Cache $cache)
{
$this->repository = $repository;
$this->cache = $cache;
}
public function all()
{
return $this->cache->tags('users')->remember('all', 60, function () {
return $this->repository->all();
});
}
public function findOrFail($id)
{
return $this->cache->tags('users')->remember($id, 60, function () use ($id) {
return $this->repository->findOrFail($id);
});
}
public function create($input)
{
$this->cache->tags('users')->flush();
return $this->repository->create($input);
}
}

Note how each method doesn’t actually do any querying. Instead, the constructor accepts an implementation of the same interface and the cache, and we defer all interactions with the database to that implementation. Each call that queries the database is wrapped in a callback so that it’s stored in Laravel’s cache when it’s returned, without touching the original implementation. When a user is created, the users tag is flushed from the cache so that stale results don’t get served.

To actually use this implementation, we need to update our service provider so that it resolves the interface to an implementation of our decorator:

<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->singleton('App\Repositories\Interfaces\UserRepositoryInterface', function () {
$baseRepo = new \App\Repositories\EloquentUserRepository(new \App\User);
$cachingRepo = new \App\Repositories\Decorators\CachingUserRepository($baseRepo, $this->app['cache.store']);
return $cachingRepo;
});
}
}

We instantiate the base repository, passing it the appropriate model. Then we instantiate the decorator, passing it the base repository and the cache, and return it. Now our controllers will start using the new decorator.

Testing the decorator

Now that we have a working decorator, how do we test it? Just as with the decorator itself, we want our tests to be completely decoupled from any particular implementation of the dependencies. If in future we’re asked to migrate the database to MongoDB, say, we’ll have plenty of work writing our new database repositories, so we don’t want to have to rewrite the tests for our decorator as well. Fortunately, using Mockery we can just mock the interface for the repository, and pass that mock into the constructor of the decorator in our test. That way we can have the mock return a known response and not involve either the database repository or the underlying models in any way.

We will also want to mock the cache itself, as this is a unit test and so as far as possible it should not be testing anything outside of the repository class. Here’s an example of how we might test the above decorator.

<?php
namespace Tests\Repositories\Decorators;
use Tests\TestCase;
use App\Repositories\Decorators\CachingUserRepository;
use Mockery as m;
class UserTest extends TestCase
{
/**
* Test fetching all items
*
* @return void
*/
public function testFetchingAll()
{
// Create mock of decorated repository
$repo = m::mock('App\Repositories\Interfaces\UserRepositoryInterface');
$repo->shouldReceive('all')->andReturn([]);
// Create mock of cache
$cache = m::mock('Illuminate\Contracts\Cache\Repository');
$cache->shouldReceive('tags')->with('users')->andReturn($cache);
$cache->shouldReceive('remember')->andReturn([]);
// Instantiate the repository
$repository = new CachingUserRepository($repo, $cache);
// Get all
$items = $repository->all();
$this->assertCount(0, $items);
}
/**
* Test fetching a single item
*
* @return void
*/
public function testFindOrFail()
{
// Create mock of decorated repository
$repo = m::mock('App\Repositories\Interfaces\UserRepositoryInterface');
$repo->shouldReceive('findOrFail')->with(1)->andReturn(null);
// Create mock of cache
$cache = m::mock('Illuminate\Contracts\Cache\Repository');
$cache->shouldReceive('tags')->with('users')->andReturn($cache);
$cache->shouldReceive('remember')->andReturn(null);
// Instantiate the repository
$repository = new CachingUserRepository($repo, $cache);
// Get all
$item = $repository->findOrFail(1);
$this->assertNull($item);
}
/**
* Test creating a single item
*
* @return void
*/
public function testCreate()
{
// Create mock of decorated repository
$repo = m::mock('App\Repositories\Interfaces\UserRepositoryInterface');
$repo->shouldReceive('create')->with(['email' => 'bob@example.com'])->andReturn(true);
// Create mock of cache
$cache = m::mock('Illuminate\Contracts\Cache\Repository');
$cache->shouldReceive('tags')->with('usersUser')->andReturn($cache);
$cache->shouldReceive('flush')->andReturn(true);
// Instantiate the repository
$repository = new CachingUserRepository($repo, $cache);
// Get all
$item = $repository->create(['email' => 'bob@example.com']);
$this->assertTrue($item);
}
public function tearDown()
{
m::close();
parent::tearDown();
}
}

As you can see, all we care about is that the underlying repository interface receives the correct method calls and arguments, nothing more. That way our test is fast and repository-agnositc.

Other applications

Here I’ve used this technique to cache the queries, but that’s not the only use case for decorating a repository. For instance, you could decorate a repository to fire events when certain methods are called, and write different decorators when reusing these repositories for different applications. You could create one to log interactions with the repository, or you could use an external library to cache your quaries, all without touching your existing repository. Should we need to switch back to our base repository, it’s just a matter of amending the service provider accordingly as both the decorator and the repository implement the same interface.

Creating decorators does mean you have to implement all of the interface’s methods again, but if you have a base repository that your other ones inherit from, you can easily create a base decorator in a similar fashion that wraps methods common to all the repositories, and then just implement the additional methods for each decorator as required. Also, each method is likely to be fairly limited in scope so it’s not generally too onerous.

Enforcing a coding standard with PHP CodeSniffer

$
0
0

We all start new projects with the best of intentions - it’ll be clean, fully tested and work perfectly. Sadly as deadlines loom, it’s all too easy to find yourself neglecting your code quality, and once it starts to degrade, getting it back becomes much harder. Many development teams try to adhere to a coding standard, but it can be hard to enforce on other people - it puts you in the uncomfortable position of nagging others all the time.

Fortunately, there’s an easy solution that doesn’t force everyone to use the same IDE. PHP CodeSniffer is a useful package that lets you specify a coding standard and then validate your code against it. That way, you can set up continuous integration and use that to remind people of errors. Better still, it also allows many errors to be fixed automatically.

To use it on your PHP project, run the following command:

$ composer require --dev squizlabs/php_codesniffer

As this will only ever be used in development, you should use the --dev flag. We also need to specify the settings for our project. This example is for a module to be used with a Laravel application and should be saved as phpcs.xml:

<?xml version="1.0"?>
<ruleset name="PHP_CodeSniffer">
<description>The coding standard for our project.</description>
<file>app</file>
<file>tests</file>
<exclude-pattern>*/migrations/*</exclude-pattern>
<arg value="np"/>
<rule ref="PSR2"/>
</ruleset>

Note the <rule /> tag - this specifies that this project should be validated as PSR2. Also, note the <file /> and <exclude-pattern /> tags - these specify what files should and should not be checked.

With this in place, we’re ready to run PHP CodeSniffer:

$ vendor/bin/phpcs
......................
Time: 45ms; Memory: 6Mb

In this case, our code validated successfully. However, if it doesn’t, there’s an easy way to tidy it up. Just run this command:

$ vendor/bin/phpcbf

That will fix many of the most common problems, and any others should be straightforward to fix.

PHP CodeSniffer makes it extremely straightforward to enforce a coding style. You can write custom rulesets or just use an existing one as you prefer, and it’s easy to fix many common problems automatically. In fact, it makes it so easy that there’s very little excuse not to meet the coding standard.

Snapshot test your Vue components with Jest

$
0
0

At work I’ve recently started using Vue as my main front-end framework instead of Angular 1. It has a relatively shallow learning curve and has enough similarities with both React and Angular 1 that if you’re familiar with one or both of them it feels quite familiar. We’re a Laravel shop and Laravel comes out of the box with a basic scaffolding for using Vue, so not only is it the path of least resistance, but many of my colleagues knew it already and it’s used on some existing projects (one of which I’ve been helping out on this week), so it made sense to learn it. Add to that the fact that the main alternative is Angular 2, which I vehemently dislike, and learning Vue was a no-brainer.

Snapshot tests are a really useful way of making sure your user interface doesn’t change unexpectedly. Facebook introduced them to their Jest testing framework last year, and they’ve started to appear in other testing frameworks too. In their words…

A typical snapshot test case for a mobile app renders a UI component, takes a screenshot, then compares it to a reference image stored alongside the test. The test will fail if the two images do not match: either the change is unexpected, or the screenshot needs to be updated to the new version of the UI component.

This makes it easy to make sure than a UI component, such as a React or Vue component, does not unexpectedly change how it is rendered. In the event that it does change, it will fail the test, and it’s up to the developer to confirm whether or not that’s expected - if so they can generate a new version of the snapshot and be on their way. Without it, you’re stuck manually testing that the right HTML tags get generated, which is a chore.

Jest’s documentation is aimed pretty squarely at React, but it’s not hard to adapt it to work with Vue components. Here I’ll show you how I got it working with Vue.

Setting up a new project

I used the Vue CLI boilerplate generator to set up my initial dependencies for this project. I then had to install some further packages:

$ npm install --save-dev jest babel-jest jest-vue-preprocessor

After that, I had to configure Jest to work with Vue. The finished package.json looked like this:

{
"name": "myproject",
"version": "1.0.0",
"description": "A project",
"author": "Matthew Daly <matthew@matthewdaly.co.uk>",
"private": true,
"scripts": {
"dev": "node build/dev-server.js",
"start": "node build/dev-server.js",
"build": "node build/build.js",
"lint": "eslint --ext .js,.vue src",
"test": "jest __test__/ --coverage"
},
"dependencies": {
"vue": "^2.3.3",
"vue-router": "^2.3.1"
},
"devDependencies": {
"autoprefixer": "^6.7.2",
"babel-core": "^6.22.1",
"babel-eslint": "^7.1.1",
"babel-jest": "^20.0.3",
"babel-loader": "^6.2.10",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"chalk": "^1.1.3",
"connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.0",
"eslint": "^3.19.0",
"eslint-config-standard": "^6.2.1",
"eslint-friendly-formatter": "^2.0.7",
"eslint-loader": "^1.7.1",
"eslint-plugin-html": "^2.0.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^2.0.1",
"eventsource-polyfill": "^0.9.6",
"express": "^4.14.1",
"extract-text-webpack-plugin": "^2.0.0",
"file-loader": "^0.11.1",
"friendly-errors-webpack-plugin": "^1.1.3",
"html-webpack-plugin": "^2.28.0",
"http-proxy-middleware": "^0.17.3",
"jest": "^20.0.4",
"jest-vue-preprocessor": "^1.0.1",
"opn": "^4.0.2",
"optimize-css-assets-webpack-plugin": "^1.3.0",
"ora": "^1.2.0",
"rimraf": "^2.6.0",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"url-loader": "^0.5.8",
"vue-loader": "^12.1.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.3.3",
"webpack": "^2.6.1",
"webpack-bundle-analyzer": "^2.2.1",
"webpack-dev-middleware": "^1.10.0",
"webpack-hot-middleware": "^2.18.0",
"webpack-merge": "^4.1.0"
},
"engines": {
"node": ">= 4.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
],
"jest": {
"testRegex": "spec.js$",
"moduleFileExtensions": [
"js",
"vue"
],
"transform": {
"^.+\\.js$": "<rootDir>/node_modules/babel-jest",
".*\\.(vue)$": "<rootDir>/node_modules/jest-vue-preprocessor"
}
}
}

I won’t include things like the Webpack config, because that’s all generated by Vue CLI. Note that we need to tell Jest what file extensions it should work with, including .vue, and we need to specify the appropriate transforms for different types of files. We use jest-vue-preprocessor for .vue files and babel-jest for .js files.

With that done, we can create a basic component. We’ll assume we’re writing a simple issue tracker here, and our first component will be at src/components/Issue.vue:

<template>
<div>
<h1>An Issue</h1>
</div>
</template>
<script>
export default {
data () {
return {}
}
}
</script>
<style scoped>
</style>

Next, we create a simple test for this component. Save this as __test__/components/issue.spec.js:

import Issue from '../../src/components/Issue.vue'
import Vue from 'vue'
const Constructor = Vue.extend(Issue)
const vm = new Constructor().$mount()
describe('Issue', () => {
it('should render', () => {
expect(vm.$el.querySelector('h1').textContent).toEqual('An Issue')
});
it('should match the snapshot', () => {
expect(vm.$el).toMatchSnapshot()
});
});

Constructor is what creates our Vue component, while vm is our actual newly-mounted Vue component. We can refer to the HTML inside the component through vm.$el, so we can then work with the virtual DOM easily.

In the first test we use the more traditional method of verifying our UI component has worked as expected - we fetch an HTML tag inside it and verify that the content inside is what we expect. This is fine for a small component, but as the components get larger we’ll find it more of a chore.

The second test is much simpler and more concise. We simply assert that it matches the snapshot. Not only is that easier, but it can scale to components of any size because we don’t have to check every little element.

Let’s run our tests:

$ npm test
> myproject@1.0.0 test /home/matthew/Projects/myproject
> jest __test__/ --coverage
PASS __test__/components/issue.spec.js
Issue
✓ should render (46ms)
✓ should match the snapshot (14ms)
Snapshot Summary
› 1 snapshot written in 1 test suite.
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 1 added, 1 total
Time: 8.264s
Ran all test suites matching "__test__/".
-----------------------------------------------------------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
-----------------------------------------------------------|----------|----------|----------|----------|----------------|
All files | 96.15 | 50 | 100 | 96 | |
root | 100 | 100 | 100 | 100 | |
unknown | 100 | 100 | 100 | 100 | |
root/home/matthew/Projects/myproject/__test__/components | 100 | 100 | 100 | 100 | |
issue.spec.js | 100 | 100 | 100 | 100 | |
root/home/matthew/Projects/myproject/src/components | 94.44 | 50 | 100 | 94.12 | |
Issue.vue | 94.44 | 50 | 100 | 94.12 | 39 |
-----------------------------------------------------------|----------|----------|----------|----------|----------------|

Note this section:

Snapshot Summary
› 1 snapshot written in 1 test suite.

This tells us that the snapshot has been successfully written. If we run the tests again we should see that it checks against the existing snapshot:

$ npm test
> myproject@1.0.0 test /home/matthew/Projects/myproject
> jest __test__/ --coverage
PASS __test__/components/issue.spec.js
Issue
✓ should render (40ms)
✓ should match the snapshot (12ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 1 passed, 1 total
Time: 3.554s
Ran all test suites matching "__test__/".
-----------------------------------------------------------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
-----------------------------------------------------------|----------|----------|----------|----------|----------------|
All files | 96.15 | 50 | 100 | 96 | |
root | 100 | 100 | 100 | 100 | |
unknown | 100 | 100 | 100 | 100 | |
root/home/matthew/Projects/myproject/__test__/components | 100 | 100 | 100 | 100 | |
issue.spec.js | 100 | 100 | 100 | 100 | |
root/home/matthew/Projects/myproject/src/components | 94.44 | 50 | 100 | 94.12 | |
Issue.vue | 94.44 | 50 | 100 | 94.12 | 39 |
-----------------------------------------------------------|----------|----------|----------|----------|----------------|

Great stuff. Now, if we make a minor change to our component, such as changing the text from An Issue to My Issue, does it pick that up?

$ npm test
> myproject@1.0.0 test /home/matthew/Projects/myproject
> jest __test__/ --coverage
FAIL __test__/components/issue.spec.js (5.252s)
● Issue › should render
expect(received).toEqual(expected)
Expected value to equal:
"An Issue"
Received:
"My Issue"
at Object.<anonymous> (__test__/components/issue.spec.js:9:52)
at Promise.resolve.then.el (node_modules/p-map/index.js:42:16)
● Issue › should match the snapshot
expect(value).toMatchSnapshot()
Received value does not match stored snapshot 1.
- Snapshot
+ Received
<div>
<h1>
- An Issue
+ My Issue
</h1>
</div>
at Object.<anonymous> (__test__/components/issue.spec.js:13:20)
at Promise.resolve.then.el (node_modules/p-map/index.js:42:16)
Issue
✕ should render (48ms)
✕ should match the snapshot (25ms)
Snapshot Summary
› 1 snapshot test failed in 1 test suite. Inspect your code changes or run with `npm test -- -u` to update them.
Test Suites: 1 failed, 1 total
Tests: 2 failed, 2 total
Snapshots: 1 failed, 1 total
Time: 7.082s
Ran all test suites matching "__test__/".
-----------------------------------------------------------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
-----------------------------------------------------------|----------|----------|----------|----------|----------------|
All files | 96.15 | 50 | 100 | 96 | |
root | 100 | 100 | 100 | 100 | |
unknown | 100 | 100 | 100 | 100 | |
root/home/matthew/Projects/myproject/__test__/components | 100 | 100 | 100 | 100 | |
issue.spec.js | 100 | 100 | 100 | 100 | |
root/home/matthew/Projects/myproject/src/components | 94.44 | 50 | 100 | 94.12 | |
Issue.vue | 94.44 | 50 | 100 | 94.12 | 39 |
-----------------------------------------------------------|----------|----------|----------|----------|----------------|

Yes, we can see that it’s picked up on the change and thrown an error. Note this line:

 › 1 snapshot test failed in 1 test suite. Inspect your code changes or run with `npm test -- -u` to update them.

Jest is telling us that our snapshot has changed, but if we expect that, we can just run npm test -- -u to replace the existing one with our new one. Then, our tests will pass again.

Now, this component is pretty useless. It doesn’t accept any external input whatsoever, so the response is always going to be the same. How do we test a more dynamic component? Amend the component to look like this:

<template>
<div>
<h1>{{ issue.name }}</h1>
</div>
</template>
<script>
export default {
props: {
issue: Object
},
data () {
return {}
}
}
</script>
<style scoped>
</style>

We’re now passing the issue object into our component as a prop, and getting the name from that. That will break our test, so we need to amend it to pass through the props:

import Issue from '../../src/components/Issue.vue'
import Vue from 'vue'
const Constructor = Vue.extend(Issue)
const issue = {
name: 'My Issue'
}
const vm = new Constructor({
propsData: { issue: issue }
}).$mount()
describe('Issue', () => {
it('should render', () => {
expect(vm.$el.querySelector('h1').textContent).toEqual('My Issue')
});
it('should match the snapshot', () => {
expect(vm.$el).toMatchSnapshot()
});
});

Here we pass our prop into the constructor for the component. Now, let’s run the tests again:

$ npm test
> myproject@1.0.0 test /home/matthew/Projects/myproject
> jest __test__/ --coverage
FAIL __test__/components/issue.spec.js
● Issue › should match the snapshot
expect(value).toMatchSnapshot()
Received value does not match stored snapshot 1.
- Snapshot
+ Received
<div>
<h1>
- An Issue
+ My Issue
</h1>
</div>
at Object.<anonymous> (__test__/components/issue.spec.js:18:20)
at Promise.resolve.then.el (node_modules/p-map/index.js:42:16)
Issue
✓ should render (39ms)
✕ should match the snapshot (25ms)
Snapshot Summary
› 1 snapshot test failed in 1 test suite. Inspect your code changes or run with `npm test -- -u` to update them.
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 1 failed, 1 total
Time: 3.717s
Ran all test suites matching "__test__/".
-----------------------------------------------------------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
-----------------------------------------------------------|----------|----------|----------|----------|----------------|
All files | 96.3 | 50 | 100 | 96.15 | |
root | 100 | 100 | 100 | 100 | |
unknown | 100 | 100 | 100 | 100 | |
root/home/matthew/Projects/myproject/__test__/components | 100 | 100 | 100 | 100 | |
issue.spec.js | 100 | 100 | 100 | 100 | |
root/home/matthew/Projects/myproject/src/components | 94.44 | 50 | 100 | 94.12 | |
Issue.vue | 94.44 | 50 | 100 | 94.12 | 39 |
-----------------------------------------------------------|----------|----------|----------|----------|----------------|

Jest has picked up on our changes and thrown an error. However, because we know the UI has changed, we’re happy with this situation, so we can tell Jest to replace the prior snapshot with npm test -- -u as mentioned earlier:

$ npm test -- -u
> myproject@1.0.0 test /home/matthew/Projects/myproject
> jest __test__/ --coverage "-u"
PASS __test__/components/issue.spec.js
Issue
✓ should render (39ms)
✓ should match the snapshot (14ms)
Snapshot Summary
› 1 snapshot updated in 1 test suite.
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 1 updated, 1 total
Time: 3.668s
Ran all test suites matching "__test__/".
-----------------------------------------------------------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
-----------------------------------------------------------|----------|----------|----------|----------|----------------|
All files | 96.3 | 50 | 100 | 96.15 | |
root | 100 | 100 | 100 | 100 | |
unknown | 100 | 100 | 100 | 100 | |
root/home/matthew/Projects/myproject/__test__/components | 100 | 100 | 100 | 100 | |
issue.spec.js | 100 | 100 | 100 | 100 | |
root/home/matthew/Projects/myproject/src/components | 94.44 | 50 | 100 | 94.12 | |
Issue.vue | 94.44 | 50 | 100 | 94.12 | 39 |
-----------------------------------------------------------|----------|----------|----------|----------|----------------|

Great, we now have a passing test suite again! That’s all we need to make sure that any regressions in the generated HTML of a component get caught.

Of course, this won’t help with the actual functionality of the component. However, Jest is pretty easy to use to write tests for the actual functionality of the application. If you prefer another testing framework, it’s possible to do the same with them, although I will leave setting them up as an exercise for the reader.

Viewing all 158 articles
Browse latest View live