Tuesday, August 30, 2016

Setting up Spectron with Mocha for testing Electron apps before packaging



So you've decided to unit-test your electron app? great! you've made the right decision.
The thing is, it can be a little confusing figuring out where to start, as Electron development has a different work flow than traditional web apps.

Spectron is a young -official- project for testing Electron apps, it is great once you get to know it but it's so young that its documentation is a little not clear when it comes to using it while developing (i.e. not having an executable for your app).
This post will help you setup your testing environment. It assumes that you're going to use Mocha, but it should be no different for any other testing framework.
note: this post assumes that you use electron (formerly electron-prebuilt) for developing your app.

Lets get started

First things first, install Spectron as a dev dependency
$ npm install --save-dev spectron

And install Mocha, also as a dev dependency
$ npm install --save-dev mocha

If you've ever been to Spectron documentation, they provide an example for using Mocha, let's use it with a little twist.
var Application = require('spectron').Application
var assert = require('assert')

describe('application launch', function () {
  this.timeout(10000)

  beforeEach(function () {
    this.app = new Application({
      path: `${__dirname}/../node_modules/.bin/electron`,
      args: ['main.js']
    })
    return this.app.start()
  })

  afterEach(function () {
    if (this.app && this.app.isRunning()) {
      return this.app.stop()
    }
  })

  it('shows an initial window', function () {
    return this.app.client.getWindowCount().then(function (count) {
      assert.equal(count, 1)
    })
  })
})

Update: as Mats Lindblad pointed out in his comment, if you're using Windows, you might need to add ".cmd" to the end of the path property at line 9, so it becomes
path: `${__dirname}/../node_modules/.bin/electron.cmd`,
.
It's your usual Mocha tests, just added the part of Spectron that creates and launches the app before each test.
So in order to run your tests during development (executing your app's main script), look at lines 9 and 10, the api documentation mentions tht the path option is required, so to start with the main script, point the path to the electron executable (if it's installed globally, just type electron) and provide the first argument in the args array as your main script.

Now in your package.json file point Mocha to your tests folder (or if you don't specify a folder, it will default to "test" as the folder holding your test files)

...
"scripts": {
    "test": "mocha mytests"
...
Put the code of the example above in your mytests folder and run npm test

That's it, now dive into the api documentation and write your usual Mocha tests.

Monday, August 22, 2016

OAuth 1.0 in electron apps


Authentication with OAuth 1.0 in an electron application might get a little tricky, mostly because of the mandatory oauth_callback parameter and because we may want to use local storage to store the access token.

In this post, I will discuss one possible way to achieve the authentication with electron in a single window.

I’ve tried this flow with a single service but OAuth is a standard, so it should all be similar.

The full source code for this tutorial is available Here , keep it as your guide through the tutorial, clone it, run npm install then npm start.
Update: This post was updated to use webview . As it turned out, opening external sites is better done using webview instead of directly loading the URL into the renderer window. It also has the advantage of rendering everything just like in a browser (for example, jQuery never loaded until a webview was used).

Step 1: setting up the work environment

You will need electron as well as the request module, easily installed with these two commands from inside your project’s folder

npm install electron request

Step 2: setting up the app

I’m going to ignore most of the boilerplate stuff, which are conventions (you can find them in the repo anyway).

The app will have a flat structure because it has only 4 files, the structure is as follows:
|- your-app
  |- auth.js
  |- content.html
  |- index.html
  |- main.js

auth.js will hold all the authentication logic.

Now in your main.js require the needed modules from electron, and the url module as well.

const {app, BrowserWindow, protocol} = require('electron');
const url = require(‘url');

Now let’s reserve us a window..

let win;

And make the function that will be called when the app is ready, I will call it prepareApp, and create the window that will allow us to check local storage as the first step of the application.

function prepareApp() {
  win = new BrowserWindow({width: 600, height: 600});
  win.loadURL(`file://${__dirname}/index.html`);
}

The characters inside the loadURL ( `` ) are javascript template literals, and they are awesome.

While we are here (in main.js) I will introduce how to make a custom protocol scheme for our app to handle the oauth_callback.

the protocol module is what allows us to make a custom protocol in electron, which will be used to hand the oauth_callback to one of our functions.
To register the new protocol scheme, insert this line right above the let win line in main.js, I’m calling the protocol “electroauth” because I like how it sounds.

protocol.registerStandardSchemes(['electroauth']);

Now inside the prepareApp function, we catch the callback and hand it to the function that will get the access token for us (we will write it shortly in auth.js).

I’ve chosen registerStringProtocol, for the most part, it won’t matter which protocol we choose because we are not using the callback parameter here anyway
protocol.registerStringProtocol('electroauth', (req, callback) => {
    if (url.parse(req.url).host === 'storetoken'){
      require('./auth').storeToken(url.parse(req.url).query, win);
    }
  });
“storetoken” is the name of the call we are going to make, it will be: electroauth://storetoken (that’s why we check for the “host” part of the URL). that indeed looks funny but it does its job.

Now Just tell the app to run this guy whenever it’s ready and then we are done with main.js. Woohoo!
app.on('ready', prepareApp);

Step 3: index.html
create a file named “index.html” which will run the first thing when the app loads, this is how we will be able to use local storage
<!DOCTYPE html>
<html>
  <head>
    <title>Checking token</title>
  </head>
  <body style="overflow:hidden;">
    <webview id="webview" src="data:text/html,<h3>Checking token status...</h3>"
        style="position:absolute;width:100%;height:100%;"></webview>
    <script>require('./auth').checkToken();</script>
  </body>
</html>
The src for the webview is just a placeholder that is going to be replaced when authorizing the token. As stated in the documentation, the src attribute can take Data URIs.

Step 4: auth.js
This is where we write the authentication logic

First, require request and querystring
querystring will help us parse the responses
const request = require('request');
const querystring = require('querystring');

const CONSUMER_KEY = '';
const CONSUMER_SECRET = '';
const ACCESS_TOKEN_URL = 'https://www.someapi.org/access_token';
const AUTHORIZE_TOKEN_URL = 'https://www.someapi.org/authorize?oauth_token=';
const REQUEST_TOKEN_URL = 'https://www.someapi.org/request_token?oauth_callback=electroauth://storetoken';
Make sure to replace these constants with the appropriate information for the service you’re using.

As mentioned, the authorization process should be similar in all services using this standard (OAuth 1.0) it goes through three steps
  • Get a request token
  • Authorize the request token
  • Get the access token
let’s do this step by step

Getting a request token
We will make the function that first checks the local storage for the token, if it finds it, we move to the normal flow of the app, if not, get the token and store it for later use.

Please consider dealing with the situation where the token has expired, this would most probably give a 401(unauthorized) error
function checkToken() {
  const {getCurrentWindow} = require('electron').remote;
  if (localStorage.getItem('token')){
    getCurrentWindow().loadURL(`file://${__dirname}/content.html`);
  } else {
    //the request module provides a way to make OAuth requests easily
    const oauth = {consumer_key: CONSUMER_KEY, consumer_secret: CONSUMER_SECRET};

    request.post({url: REQUEST_TOKEN_URL, oauth: oauth}, (e, r, body) => {
      const req_data = querystring.parse(body);
      document.getElementById('webview').setAttribute("src", AUTHORIZE_TOKEN_URL + req_data.oauth_token);
    });
  }
}
The function above gets called when the app loads, here’s what’s happening
  • first we get a reference to the active (and only in this case) window through the “remote” object
  • Check if the access token is already in local storage, if yes, no further action is required besides loading your content (which is your actual app, ready to make requests on behalf of the user)
  • If the access token is not in the local storage, that means we must go and hunt one, for that we use the “request” module which have a pretty good support for OAtuh calls
  • Make a request after that for the request token and very quickly pass it to the token authorization url which is loaded inside of the webview, and that’s when our custom protocol comes to work

Remember when we passed the result of the call to the protocol to the auth function? yeah me neither, here’s a reminder of it
require('./auth').storeToken(url.parse(req.url).query, win);
Actually here we are passing the query part of the URL (the ?x=y thingy) which has the information needed to make the final call to get the access token, we’re also passing the window object (a shorter way of getting the active window out of the remote object)

Here’s what the storeToken function looks like, we are still in auth.js
function storeToken(urlQuery, targetWindow) {
  const verify_data = querystring.parse(urlQuery);

  const oauth = {
    consumer_key: CONSUMER_KEY,
    consumer_secret: CONSUMER_SECRET,
    token: verify_data.oauth_token,
    token_secret: verify_data.oauth_token_secret,
    verifier: verify_data.oauth_verifier,
  };

  request.post({url: ACCESS_TOKEN_URL, oauth: oauth}, (e, r, body) => {
    const token_data = querystring.parse(body);
    targetWindow.loadURL(`file://${__dirname}/content.html`);
    targetWindow.webContents.on('did-finish-load', () => {
      targetWindow.webContents.executeJavaScript(
        `localStorage.setItem('token', "${token_data.oauth_token}");
         localStorage.setItem('token_secret', "${token_data.oauth_token_secret}");`
      );
    });
  });
}

And yet again here’s what’s happening in here
  • The query string of the url (that’s what the service has sent along with the redirection to our specified oauth_callback) is being parsed with the querystring module (no, really!)
  • Using this info, the app is ready to make its call to get its access token it has been waiting for all its life since it was just a little baby in the memory
  • After successfully getting the access token, we load the window with our actual content
  • But wait, we are working for the main process now, how is the token going to be stored in local storage? I know you probably haven’t asked that but here’s the answer, we wait for the did-finish-load event of the windowContents of our window, and then execute javascript there (in the content file) with the executeJavaScript function, that way we can access local storage.
Notice that the template literals are used in the call to executeJavaScript, that’s why we were able to use a multiline string in there.

Last thing you have to do is export these functions
module.exports = {checkToken: checkToken, storeToken: storeToken};

Here’s a sequence diagram in case the flow is not so clear

PS:
Someone suggested another way to handle the oauth_callback redirection, you may intercept the redirection with did-get-redirect-request instead of creating a custom protocol scheme, I haven’t tried it though but it’s worth mentioning.

That’s it, as always, please ask questions and provide feedback.