Angular 2 with Socket.IO Beer Tasting Party Study Application (Part 2)

Datetime:2016-08-22 22:31:02          Topic: SQL  AngularJS           Share

Seeding our project

Because I am new to Angular 2 and I want to learn from other people’s best practices, I am going to start with a popular Angular 2 seed application https://github.com/mgechev/angular2-seed . If you are following along install the seed as the directions under ‘How to Start.’ I named my project ‘bee.rs’ because I will be hosting it from a Raspberry Pi for the beer tasting party (using special routing rules on my home DD-WRT.) Don’t worry, when the project is done, I will give you a how-to on setting this up in your home / bar!

Database Access Layer

With the way our database is laid out, our database access layer is going to be able to perform most of the logic of our application. We’ve pretty much decided already everything the application can do and what objects will exist in our system. Now we will dive into the NodeJS code and start to build our data access layer. We know that because we are using Angular 2 to drive the applications UI the API layer will be fairly thin.

In the root directory of my application I create a new folder named ‘modules.’ In this directory I am going to keep all of my custom modules that will access the database and do all of my application logic. Because I chose to use MySQL instead of a NoSQL solution, I am going to be able to push a lot of my application logic down to the database layer. The upside is that my logic will not be split between two layers. The downside is that I will have to unit test the database and modules layer together. I have chosen to use a well tested npm mysql module , so I will install that now.

Tims-MBP:bee.rs tim$ npm install mysql --save

The low level MySQL object

First I’ll create a base class from which my modules will inherit. This base class will handle the abstract low level logic of querying the database and returning rows or objects. For this application, I don’t need a connection pool, I don’t need transactions, what I do need is simplicity! In my modules folder I create a new file mysqlob.js

mysqlob.js

'use strict';
var mysql = require('mysql');
var createConnection = mysql.createConnection;

module.exports = class MySQLOb {
	constructor(SQL) {
		this.log = false;
		this.connectionOpts = {
			host     : 'localhost',
			user     : 'beer_admin',
			database : 'beer_tasting_app',
			password : '_B33rZ!_'
		};
		this.connected = false;
		this.connection = createConnection(this.connectionOpts);
		this.SQL = SQL;
	}
	query(sql, values, cb) {
		if( !this.connected) {
			this.connection = createConnection(this.connectionOpts);
		}
		try {
			if(!values){
				this.connection.query(sql, cb);
			} else {
				this.connection.query({
					sql: sql,
					values: values
				}, cb);
			}
		} catch(e) {
			this.connected = false;
			// use try-catch to prevent from program exiting caused by mysql
			cb(e);
		}

		return this;
	}
	doSql(stmtRef,params,cb) {
		var stmt = this.SQL[stmtRef];
		if(typeof params === 'function') {
			cb = params;
			params = null;
		}
		if( this.log ) {
			console.log(mysql.format(stmt,params).replace(/\t/g,' '));
		}
		this.query(stmt,params,function(err,rows) {
			if(err) {
				cb(err);
			}
			if(stmt.indexOf('LIMIT 1;') !== -1 && rows.length ) {
				rows = rows[0];
			}
			if( cb ) {
				cb(null,rows);
			}

		});
	}
}

You’ll see the constructor sets up the connection, so when my module classes are instantiated a new connection is created automatically. The constructor will also set a local variable SQL which will contain an object with keyed queries. Once again, because my application logic can mostly be expressed in SQL, I am going to keep all of that logic in a succinct and self documenting object.

The query method simply runs a query on the mysql object and detects automatically if we are running a simple query or a query with an array of values to bind. Finally it passes the cb or callback function as the function to be executed after the query runs.

The doSql function is another layer of abstraction in which I grab the requested query statement from the shared SQL object, log the query after the statement has been parsed (if the inheriting object switches this on; good for debugging in the console). I then check for ‘LIMIT 1;’ and pass back a single row otherwise, I pass back the result of the query.

For the modules that drive the application logic I can now use this simple pattern to access the database.

'use strict';
let MySQLOb = require('./mysqlob');

var SQL = {
	someQuery: `SELECT *
		FROM somewhere
		WHERE id = ?
		LIMIT 1;`
};

module.exports = class Module extends MySQLOb {
	constructor() {
		super(SQL);
	}
	getSomething(id,cb) {
		this.doSql('someQuery',[id],cb);
	}
}

Building unit tests and modules together

To prepare for my upcoming modules I install unit.js which I will pair alongside Mocha .

Tims-MBP:bee.rs tim$ npm install -g mocha
Tims-MBP:bee.rs tim$ npm install unit.js --save-dev

Sessions module

The base level module is sessions. Sessions have no foreign dependencies and drive our application state, so we will start here. I create a new file modules/sessions.js .

sessions.js

'use strict';
let MySQLOb = require('./mysqlob');

var SQL = {
	getOpenSession : `SELECT
		s.id,
		s.created_at as session_started,
		s.name as session_name,
		b.id as current_beer,
		b.unique_code as beer_code
		FROM sessions s
		LEFT JOIN beers b ON (
			b.session_id = s.id AND
			b.tasting_in_process = 1
		)
		WHERE session_open = 1
		LIMIT 1;`,
	createSession : `INSERT INTO sessions (name,session_open) VALUES (?,1)`,
	closeSession: `UPDATE sessions SET session_open = 0 WHERE id = ?`,
	deleteSession: `DELETE FROM sessions WHERE id = ?`
};

module.exports = class Sessions extends MySQLOb {
	constructor() {
		super(SQL);
	}
	getOpenSession(cb) {
		this.doSql('getOpenSession', cb);
	}
	createSession(session,cb) {
		this.doSql('createSession',[session.name],cb);
	}
	closeSession(sessionId,cb) {
		this.doSql('closeSession',[sessionId],cb);
	}
	deleteSession(sessionId,cb) {
		this.doSql('deleteSession',[sessionId],cb);
	}
}

The verbs in the method names tell it all. This module should now handle CRUD on my sessions. Let’s put that to the test! I create a new folder in the root of my application called tests and inside I create a new file sessions_tests.js

sessions_tests.js

'use strict';
// http://unitjs.com/
var test = require('unit.js');

var sessions = require(__dirname+'/../modules/sessions');
sessions = new sessions();

describe('Testing Sessions Model', () => {
	// there should be no sessions to begin with
	it('sessions suite', (done) => {
		sessions.getOpenSession((err,res) => {
			if( err ) throw err;
			// test that there are no open sessions
			test.array(res).is([]);
			// test session creation
			sessions.createSession({name:'Beer Tasting 2016'}, (err,res) => {
				if(err) throw err;
				sessions.getOpenSession((err,res) => {
					if(err) throw err;
					var session = res;
					test.object(session);
					test.string(session.session_name).is('Beer Tasting 2016');
					test.date(session.session_started);
					// test closing the session
					sessions.closeSession(session.id, (err,res) => {
						if(err) throw err;
						sessions.getOpenSession((err,res) => {
							if(err) throw err;
							test.array(res).is([]);
							sessions.deleteSession(session.id, (err,res) => {
								if(err) throw err;
								done();
							});
						});
					});
				});
			});
		});
	});
});

This test covers all of the CRUD methods that I have created thus far. It checks the database for open sessions (expecting none in testing). Creates a new session and tests again for open sessions. Tests that the open session created returned the expected session that we created. Closes the session. Finally the test session we created is deleted. Now I run the test using mocha from my terminal!

Tims-MBP:bee.rs tim$ mocha tests/sessions_tests.js


  Testing Sessions Model
    ✓ sessions suite (38ms)


  1 passing (43ms)

Tims-MBP:bee.rs tim$

Everything went as expected! Now I can move on and build out the rest of the base application logic and have it tested before I even start on the API interface. This will make building out the API smooth and clean. I will not be tempted to put application logic where it shouldn’t be!

Users Module

Moving right along I create a new file modules/users.js that will handle users CRUD and more.

modules/users.js

'use strict';
let MySQLOb = require('./mysqlob');

var SQL = {
	upsertUser: `INSERT INTO users (id,session_id,name,user_type)
		VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE
		name = VALUES(name),
		user_type = VALUES(user_type)`,
	deleteUser: `DELETE FROM users WHERE id = ?`,
	getHostForSession: `SELECT * FROM users
		WHERE user_type = 'host'
		AND session_id = ?
		LIMIT 1;`,
	getUser: `SELECT * FROM users
		WHERE id = ?
		LIMIT 1;`
};

module.exports = class Users extends MySQLOb {
	constructor() {
		super(SQL);
	}
	upsertUser(user,cb) {
		this.doSql('upsertUser',[
			user.id,
			user.session_id,
			user.name,
			user.user_type
		],cb);
	}
	getHost(sessionId,cb) {
		this.doSql('getHostForSession',[sessionId],cb);
	}
	getUser(userId,cb) {
		this.doSql('getUser',[userId],cb);
	}
}

Now I am starting to see that even though my objects are decoupled and not dependent on each-other, the database model dictates that my users are tied to a session. Since users are ephemeral, they only last as long as a session, I don’t worry about storing too much information about them. Notice also, I am not doing any data validation that you might expect on a low level model like this. Since this is a study application, I’m going to keep it light and enforce data validation at the Angular level so as not to duplicate logic.

tests/users_tests.js

'use strict';
// docs http://unitjs.com/
var test = require('unit.js');

var sessions = require(__dirname+'/../modules/sessions');
sessions = new sessions();

var users = require(__dirname+'/../modules/users');
users = new users();

describe('Create Taster', () => {
	it('should create a session and add a user to the session.', (done) => {
		var user = {
			name: 'Charley',
			user_type: 'taster'
		};
		sessions.createSession({name:'User Creation Test'}, (err,res) => {
			if(err) throw err;
			var sessionId= res.insertId;
			user.session_id = sessionId;
			users.upsertUser(user,(err,res) => {
				if(err) throw err;
				var userId = res.insertId;
				test.number(userId);
				users.getUser(userId,(err,user) => {
					if(err) throw err;
					test.string(user.name).is('Charley');
					test.string(user.user_type).is('taster');
					sessions.deleteSession(sessionId,(err,res) => {
						if(err) throw err;
						done();
					});
				});
			});
		});
	});
});

describe('Create Host', () => {
	it('should create a session and add a user to the session.', (done) => {
		var user = {
			name: 'Donald',
			user_type: 'host'
		};
		sessions.createSession({name:'Host Creation Test'}, (err,res) => {
			if(err) throw err;
			var sessionId= res.insertId;
			user.session_id = sessionId;
			users.upsertUser(user,(err,res) => {
				if(err) throw err;
				var userId = res.insertId;
				test.number(userId);
				users.getHost(sessionId,(err,host) => {
					if(err) throw err;
					test.string(host.name).is('Donald');
					test.string(host.user_type).is('host');
					sessions.deleteSession(sessionId,(err,res) => {
						if(err) throw err;
						done();
					});
				});
			});
		});
	});
});

For the user test I create 2 base tests. I create a taster user, validate they have been inserted and that I get the expected objects back. Then clean up. Next I do the exact same process for a host. At this level, we’ll notice there isn’t much of a difference between the two. Once I get to routing, that’s gonna change!

Run the user tests.

Tims-MBP:bee.rs tim$ mocha tests/user_tests.js


  Create Taster
    ✓ should create a session and add a user to the session.

  Create Host
    ✓ should create a session and add a user to the session.


  2 passing (44ms)

Tims-MBP:bee.rs tim$

Beers

Next up, the fun part. BEERS! I create a new module modules/beers.js and add the CRUD functionality for my beer lists, generate unguessable ID’s to mask my beers, and an iterator for randomly iterating through my list of beers.

beers.js

'use strict';
let MySQLOb = require('./mysqlob');

var SQL = {
	upsertBeer: `INSERT INTO beers (id,unique_code,session_id,brand,name)
		VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE
		brand = VALUES(brand),
		name = VALUES(name);`,
	deleteBeer: `DELETE FROM beers WHERE id = ?`,
	openRound: `UPDATE beers
		SET tasting_in_process = 1
		WHERE id = ?;`,
	closeRound: `UPDATE beers
		SET tasting_complete = 1,
		tasting_in_process = 0
		WHERE id = ?;`,
	getNextRandomBeer: `SELECT
		id,
		unique_code,
		brand,
		name,
		tasting_in_process,
		tasting_complete
		FROM beers
		WHERE tasting_complete = 0
		AND session_id = ?
		ORDER BY RAND()
		LIMIT 1;`,
	getByUniqueCode: `SELECT id
		FROM beers
		WHERE unique_code = ?
		AND session_id = ? LIMIT 1;`
};

module.exports = class Beers extends MySQLOb {
	constructor() {
		super(SQL);
	}
	generateUniqueCode(sessionId,cb) {
		var self = this;
		var generateCode = function(cb) {
			let code = '',
				possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
			for( var i=0; i < 4; i++ )
				code += possible.charAt(Math.floor(Math.random() * possible.length));
			self.doSql('getByUniqueCode',[code,sessionId],(err,res) => {
				if( res.id ) {
					generateCode(cb);
				} else {
					cb(code);
				}
			});
		};
		generateCode(cb);
	}
	upsertBeer(beer,cb) {
		this.doSql('upsertBeer',[
			beer.id,
			beer.unique_code,
			beer.session_id,
			beer.brand,
			beer.name
		], cb);
	}
	nextBeer(beerId,sessionId,cb) {
		var self = this;
		var getAndOpen = function(cb) {
			self.doSql('getNextRandomBeer',[sessionId],(err,beer) => {
				if(err) throw err;
				if( beer.id ) {
					self.doSql('openRound',[beer.id],(err,res) => {
						if(err) throw err;
						beer.tasting_in_process = 1;
						cb(beer);
					});
				} else {
					cb(false);
				}
			});
		};
		// --> if a beer ID is supplied, we open that session and advance.
		if( beerId ) {
			self.doSql('closeRound',[beerId],(err,res) => {
				getAndOpen(cb);
			});
		} else {
			getAndOpen(cb);
		}
	}
}

This should be fun to test! I’m going to split my tests into two groups. Since the unique identifier is such a core piece of my requirements, I want to test that separately. Then I want to test through a whole tasting session with multiple beers to ensure my iterator works the way I expect. I create tests/beer_tests.js .

beer_tests.js

'use strict';
// docs http://unitjs.com/
var test = require('unit.js');

var beers = require(__dirname+'/../modules/beers');
beers = new beers();

var sessions = require(__dirname+'/../modules/sessions');
sessions = new sessions();

describe('Testing Unique Code Generator', () => {
	it('should generate a unique code per beer per session', (done) => {
		sessions.createSession({name:'Unique Code Test'}, (err,res) => {
			if(err) throw err;
			var sessionId = res.insertId;
			// --> test by creating with a non unique code
			var beer = {
				id:null,
				unique_code:'TEST',
				session_id:sessionId,
				brand:'Allagash',
				name:'Curieux'
			};
			beers.upsertBeer(beer,(err,res) => {
				if(err) throw err;
				beers.generateUniqueCode(sessionId,(unique_code) => {
					test.string(unique_code).hasLength(4).notMatch('TEST');
					sessions.deleteSession(sessionId,(err,res) => {
						if(err) throw err;
						done();
					});
				});
			});
		});
	});
});

describe('Testing Run Full Session', () => {
	var beerList = [
		{
			brand:'Allagash',
			name:'Curieaux'
		},
		{
			brand:'Chimay',
			name:'Grand Reserve'
		},
		{
			brand:'La Trappe',
			name:'Quadrupel'
		},
		{
			brand:'North Coast',
			name:'Brother Thelonious'
		}
	];
	// --> beers need a session.
	function generate_session(cb) {
		sessions.createSession({name:'Full Suite Test'}, (err,res) => {
			if(err) throw err;
			cb(res.insertId);
		});
	}
	// --> recursively insert beers.
	var insertBeers = function(beerList,sessionId,cb) {
		if( beerList.length === 0 ) {
			cb();
		} else {
			var beer = beerList.pop();
			beer.session_id = sessionId;
			beers.generateUniqueCode(sessionId,(unique_code) => {
				beer.unique_code = unique_code;
				beers.upsertBeer(beer,(err,res) => {
					if(err) throw err;
					insertBeers(beerList,sessionId,cb);
				});
			});
		}
	};
	// --> recursively pull random beers until session is empty.
	var runBeers = function(beerId,sessionId,cb) {
		beers.nextBeer(beerId,sessionId,(beer) => {
			if( beer ) {
				test.number(beer.tasting_in_process).is(1);
				test.number(beer.tasting_complete).is(0);
				runBeers(beer.id,sessionId,cb);
			} else {
				cb();
			}
		});
	};
	it('should generate a session, insert a list of beers, start the session, pick random beers until the session is complete', (done) => {
		generate_session((sessionId) => {
			insertBeers(beerList,sessionId,() => {
				runBeers(false,sessionId,() => {
					sessions.deleteSession(sessionId, () => {
						done();
					});
				});
			});
		});
	});
});

A couple of recursive functions make these tests much cleaner than they could have been. Insert beers recursively iterates through the beerList array I have created to simulate the way the host would add beers. Once the beers are in the system the runBeers function recursively iterates through the beers in the database (using the randomized iterator) to simulate how our host is going to step through the beers during the session. Run em!

Tims-MBP:bee.rs tim$ mocha tests/beer_tests.js 


  Testing Unique Code Generator
    ✓ should generate a unique code per beer per session

  Testing Run Full Session
    ✓ should generate a session, insert a list of beers, start the session, pick random beers until the session is complete (54ms)


  2 passing (82ms)

Tims-MBP:bee.rs tim$

Notes module

For now the notes module is going to be simple. Just handling CRUD should do it! I create a the file modules/notes.js .

notes.js

'use strict';
let MySQLOb = require('./mysqlob');

var SQL = {
	upsertNote: `INSERT INTO notes (id,beer_id,user_id,beer_guess,rating,notes)
		VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE
		beer_guess=VALUES(beer_guess),
		rating=VALUES(rating),
		notes=VALUES(notes)`,
	getNoteForUserForBeer: `SELECT
		IF(n.beer_guess = b.id,1,0) as guessed_correct,
		b.id,
		b.unique_code,
		b.brand as brand,
		b.name as name,
		n.rating,
		n.notes,
		g.name as guess_name,
		g.id as guess_id,
		s.session_open
		FROM notes n
		JOIN beers b ON b.id = n.beer_id
		JOIN sessions s ON s.id = b.session_id
		LEFT JOIN beers g ON g.id = n.beer_guess
		WHERE n.beer_id = ?
		AND n.user_id = ?
		LIMIT 1;`
};

module.exports = class Beers extends MySQLOb {
	constructor() {
		super(SQL);
	}
	upsertNote(note,cb) {
		this.doSql('upsertNote',[
			note.id,
			note.beer_id,
			note.user_id,
			note.beer_guess,
			note.rating,
			note.notes
		],cb);
	}
	getNote(beerId,userId,cb) {
		this.doSql('getNoteForUserForBeer',[beerId,userId], cb);
	}
}

Though the module is pretty simple, the tests will be a little harder. My relational map shows that notes require a user and a beer, and the beer requires a session. I’ll need all of these objects in place to properly test my module. I create tests/notes_test.js

notes_test.js

'use strict';
// docs http://unitjs.com/
var test = require('unit.js');

var sessions = require(__dirname+'/../modules/sessions');
sessions = new sessions();

var users = require(__dirname+'/../modules/users');
users = new users();

var beers = require(__dirname+'/../modules/beers');
beers = new beers();

var notes = require(__dirname+'/../modules/notes');
notes = new notes();

describe('Notes Creation Test', () => {
	it('should create a session, user, beer and add a note.', (done) => {
		// create a session
		sessions.createSession({name:'Note Test'}, (err, res) => {
			if(err) throw err;
			var sessionId = res.insertId;
			// create a user
			users.upsertUser({
				name:'Jessica',
				user_type:'taster',
				session_id:sessionId
			}, (err,res) => {
				if(err) throw err;
				var userId = res.insertId;
				// create a beer
				beers.upsertBeer({
					brand:'Allagash',
					name:'Curieux',
					session_id: sessionId
				}, (err,res) => {
					if(err) throw err;
					var beerId = res.insertId;
					notes.upsertNote({
						beer_id: beerId,
						user_id: userId,
						beer_guess: beerId,
						rating: 5,
						notes: 'Tastes like drinking pure sunshine.'
					}, (err,res) => {
						if(err) throw err;
						notes.getNote(beerId,userId, (err,note) => {
							if(err) throw err;
							test.number(note.guessed_correct).is(1);
							test.number(note.rating).is(5);
							test.string(note.notes).is('Tastes like drinking pure sunshine.');
							sessions.deleteSession(sessionId,(err,res) => {
								if(err) throw err;
								done();
							});
						});
					});
				});
			});
		});
	});
});

In this test I build a session, user and beer then add a note for the user for the beer. Then I get the note once again and test that the low level logic I am applying to validate the user’s guess is working correctly. Let’s run it!

Tims-MBP:bee.rs tim$ mocha tests/notes_tests.js


  Notes Creation Test
    ✓ should create a session, user, beer and add a note.


  1 passing (40ms)

Tims-MBP:bee.rs tim$

Application Context

One thing that is unique about this application is that it is a shared state. Most web applications with the exception of things like Google Docs and games require an individual state. Since the host is going to dictate what and when users navigate, I need an object that handles this cross-cutting concern . To decouple this piece of functionality as much as I can I create a new module called modules/applicationContext.js . My aim here is to infer from the state of the records in the database, what screen each user should be seeing and when.

applicationContext.js

'use strict';
let MySQLOb = require('./mysqlob');

var SQL = {
	getOpenSessionAndHost: `SELECT
		s.id,
		s.name,
		h.name as host_name,
		b.id as beer_id
		FROM sessions s
		LEFT JOIN users h ON (
			h.user_type = "host" AND
			h.session_id = s.id
		)
		LEFT JOIN beers b ON (
			b.tasting_in_process = 1 AND
			b.session_id = s.id
		)
		WHERE s.session_open = 1
		ORDER BY s.created_at DESC
		LIMIT 1;`,
	getUserContext: `SELECT
		u.user_type,
		u.id,
		u.name,
		COUNT(b.id) as beer_count,
		SUM(b.tasting_complete) as tastings_complete,
		IF(SUM(b.tasting_complete) = COUNT(b.id), 1, 0) as all_tastings_complete,
		cb.id as current_beer_id
		FROM users u
		LEFT JOIN beers b ON b.session_id = u.session_id
		LEFT JOIN beers cb ON (
			cb.session_id = u.session_id AND
			cb.tasting_in_process = 1
		)
		WHERE u.id = ?
		GROUP BY b.session_id
		LIMIT 1;`
};

module.exports = class ApplicationContext extends MySQLOb {
	constructor() {
		super(SQL);
	}
	getRoute(userId,cb) {
		var self = this;
		// --> if a user id is not supplied, we check to see if a session exists.
		self.doSql('getOpenSessionAndHost',(err,session) => {
			if( err ) throw err;
			if( !userId ) {
				if( !session.id ) {
					// --> in this case we are starting from step 1, create a session.
					cb('/new-session');
				} else if( session.id && !session.host_name ) {
					// --> in this case, a session was created but there is no host.
					cb('/new-host');
				} else {
					// --> else, a session is created, a host is manifesting beers, route
					// other users to create new taster profiles.
					cb('/new-taster');
				}
			} else {
				self.doSql('getUserContext', [userId], (err,userContext) => {
					if(err) throw err;
					if( userContext.user_type === 'host' ) {
						if( !userContext.current_beer_id && (userContext.beer_count === 0 || userContext.tastings_complete === 0) ) {
							// --> in this case the host is still manifesting beers.
							cb('/beer-manifest');
						} else if( userContext.current_beer_id ) {
							// --> a tasting round is in process.
							cb('/host-round/'+userContext.current_beer_id );
						} else if( userContext.all_tastings_complete ) {
							// --> all tastings have been completed, go to summary
							cb('/summary');
						} else {
							// --> something actually went wrong here...
							// the host should be manifesting, driving the rounds or the session is complete
							cb('/new-session');
						}
					} else {
						if( !userContext.current_beer_id && (userContext.beer_count === 0 || userContext.tastings_complete === 0) ) {
							// --> in this case the host is still manifesting beers. Tasters will wait until
							// the host is ready to start!
							cb('/please-wait');
						} else if( userContext.current_beer_id ) {
							// --> a tasting round is in process.
							cb('/tasting-round/'+userContext.current_beer_id );
						} else if( userContext.all_tastings_complete ) {
							// --> all tastings have been completed, go to summary
							cb('/summary');
						} else {
							// --> something actually went wrong here...
							// route back to new session.
							cb('/new-session');
						}
					}

				});
			}
		});
	}
}

You’ll notice this code is much more heavily commented than the rest of my code thus far. My reason for that is because this will be the most complicated part of the application. Knowing where a user should be based on everything else that is happening means knowing a lot about the current state of the application. The other strange thing you might notice is that I am dictating my user’s routing from such a low level piece of code. This will all come together when we glue the Angular 2 interface to this application logic down the line. For now, just trust me :).

Alright, so if we thought testing notes was crazy, the application context tests will blow your mind! Essentially we need to test every possible state of our application, which means creating and running through the whole user story of our host and taster.

test/context_tests.js

'use strict';
// docs http://unitjs.com/
var test = require('unit.js');

var sessions = require(__dirname+'/../modules/sessions');
sessions = new sessions();

var users = require(__dirname+'/../modules/users');
users = new users();

var beers = require(__dirname+'/../modules/beers');
beers = new beers();

var notes = require(__dirname+'/../modules/notes');
notes = new notes();

var appContext = require(__dirname+'/../modules/applicationContext');
appContext = new appContext();

describe('Host Route Contexts', () => {
	it('Should follow the host through the lifecycle of routes.', (done) => {
		// first user opens the app.
		appContext.getRoute(null,(route) => {
			test.string(route).is('/new-session');
			// first user creates a new session
			sessions.createSession({name: 'Host Route Context Test'}, (err,res) => {
				if(err) throw err;
				var sessionId = res.insertId;
				// first user should become the host
				appContext.getRoute(null,(route) => {
					test.string(route).is('/new-host');
					users.upsertUser({
						name:'Frank',
						session_id: sessionId,
						user_type: 'host'
					}, (err,res) => {
						if(err) throw err;
						var userId = res.insertId;
						// once we know the host, they should be routed to the manifest.
						appContext.getRoute(userId,(route) => {
							test.string(route).is('/beer-manifest');
							beers.upsertBeer({
								brand:'Allagash',
								name:'Curieux',
								session_id:sessionId
							}, (err, res) => {
								if(err) throw err;
								// should still be adding beers to the manifest.
								appContext.getRoute(userId,(route) => {
									test.string(route).is('/beer-manifest');
									beers.upsertBeer({
										brand:'North Coast',
										name:'Old Stock',
										session_id:sessionId
									}, (err, res) => {
										// beer manifesting done, host will start the first session.
										beers.nextBeer(null,sessionId,(beer) => {
											appContext.getRoute(userId,(route) => {
												test.string(route).is('/host-round/'+beer.id);
												// host moves to the next beer.
												beers.nextBeer(beer.id,sessionId,(beer) => {
													appContext.getRoute(userId,(route) => {
														test.string(route).is('/host-round/'+beer.id);
														// host requests another beer, and the session is over!
														beers.nextBeer(beer.id,sessionId,(beer) => {
															test.bool(beer).isFalse();
															appContext.getRoute(userId,(route) => {
																test.string(route).is('/summary');
																sessions.deleteSession(sessionId,(err,res) => {
																	done();
																});
															});
														});
													});
												});
											});
										});
									});
								});
							});
						});
					});
				});
			});
		});
	});
});

describe('Taster Route Contexts', () => {
	it('Should follow the taster through the lifecycle of routes.', (done) => {
		sessions.createSession({name: 'Taster Route Context Test'}, (err,res) => {
			if(err) throw err;
			var sessionId = res.insertId;
			users.upsertUser({
				name:'Sally',
				session_id: sessionId,
				user_type: 'host'
			}, (err, res) => {
				if(err) throw err;
				// a new user comes on the scene when a host has created a new session.
				appContext.getRoute(null,(route) => {
					test.string(route).is('/new-taster');
					users.upsertUser({
						name:'Bob',
						session_id: sessionId,
						user_type: 'taster'
					}, (err, res) => {
						if(err) throw err;
						var userId = res.insertId;
						appContext.getRoute(userId,(route) => {
							test.string(route).is('/please-wait');
							beers.upsertBeer({
								brand:'North Coast',
								name:'Old Stock',
								session_id:sessionId
							}, (err, res) => {
								// host may still be manifesting beers.
								appContext.getRoute(userId,(route) => {
									test.string(route).is('/please-wait');
									beers.upsertBeer({
										brand:'Deschutes',
										name:'Not the Stoic',
										session_id:sessionId
									}, (err, res) => {
										//  host finishes manifesting beers, and
										// starts a session.
										beers.nextBeer(null,sessionId,(beer) => {
											appContext.getRoute(userId,(route) => {
												test.string(route).is('/tasting-round/'+beer.id);
												beers.nextBeer(beer.id,sessionId,(beer) => {
													appContext.getRoute(userId,(route) => {
														test.string(route).is('/tasting-round/'+beer.id);
														beers.nextBeer(beer.id,sessionId,(beer) => {
															test.bool(beer).isFalse();
															// host finishes all rounds and session is complete
															appContext.getRoute(userId,(route) => {
																test.string(route).is('/summary');
																sessions.deleteSession(sessionId,(err,res) => {
																	done();
																});
															});
														});
													});
												});
											});
										});
									});
								});
							});
						});
					});
				});
			});
		});
	});
});

Alright, before you kill me. I know these are the pyramids of doom.

In a production application, I would probably spend a bit more time and clean up this test code, however, here I simply don’t have the time. It may not be pretty, but this code can test the whole life cycle of my application. Let’s run it!

Tims-MBP:bee.rs tim$ mocha tests/context_tests.js


  Host Route Contexts
    ✓ Should follow the host through the lifecycle of routes. (71ms)

  Taster Route Contexts
    ✓ Should follow the taster through the lifecycle of routes. (52ms)


  2 passing (130ms)

Tims-MBP:bee.rs tim$

Wrap up

In round 2 we built out the business logic of our application by pushing the logic to the lowest layer accessible SQL. We’ve set up a base framework that we can come back to and easily construct the WebSocket API.

In the next section we will build out or Angular 2 components along with our WebSocket API. Things are really starting to come together!





About List