sails
Version:
API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)
925 lines (773 loc) • 32.5 kB
JavaScript
/**
* Test dependencies
*/
var util = require('util');
var path = require('path');
var _ = require('@sailshq/lodash');
var assert = require('assert');
var async = require('async');
var socketHelper = require('./helpers/socketHelper.js');
var appHelper = require('./helpers/appHelper');
var fs = require('fs-extra');
/**
* Errors
*/
var Err = {
badResponse: function(response) {
return 'Wrong server response! Response :::\n' + util.inspect(response);
}
};
describe('pubsub :: ', function() {
describe('Model events', function() {
describe('when a socket is watching Users ', function() {
var socket1;
var socket2;
var appName = 'testApp';
var sailsApp;
var bootstrapModels = {};
var bootstrappedData = {};
before(appName, function(done) {
appHelper.build(done);
});
beforeEach(function (done) {
appHelper.liftWithTwoSockets({
log: {level: 'warn'},
models: {
migrate: 'drop'
}
}, function(err, sails, _socket1, _socket2) {
if (err) {
return done(err);
}
sailsApp = sails;
socket1 = _socket1;
socket2 = _socket2;
async.eachSeries(_.keys(bootstrapModels), function(model, nextModel) {
sailsApp.models[model].createEach(bootstrapModels[model]).meta({fetch: true}).exec(function(err, records) {
if (err) {
return nextModel(err);
}
bootstrappedData[model] = records;
return nextModel();
});
}, function(err) {
if (err) {return done(err);}
// Subscribe to all users and new user notifications.
socket1.get('/user', function(body, jwr) {
if (jwr.error) { return done(new Error('Error in tests. Details:' + JSON.stringify(jwr))); }
// Subscribe to all pets and new pet notifications.
socket1.get('/pet', function(body, jwr) {
if (jwr.error) { return done(new Error('Error in tests. Details:' + JSON.stringify(jwr))); }
done();
});
});
});
});
});
afterEach(function(done) {
bootstrapModels = {};
bootstrappedData = {};
socket1.removeAllListeners();
socket2.removeAllListeners();
var dir = path.resolve('.tmp', 'localDiskDb');
if (fs.existsSync(dir)) {
fs.removeSync(dir);
}
setTimeout(function(){sailsApp.lower(done);},100);
});
after(function(done) {
// Add a delay before killing the app to account for any queries that
// haven't been run by the blueprints yet; otherwise they might casue an error
setTimeout(function() {
process.chdir('../');
appHelper.teardown(appName);
return done();
}, 500);
});//</after>
// ██████╗██████╗ ███████╗ █████╗ ████████╗███████╗
// ██╔════╝██╔══██╗██╔════╝██╔══██╗╚══██╔══╝██╔════╝
// ██║ ██████╔╝█████╗ ███████║ ██║ █████╗
// ██║ ██╔══██╗██╔══╝ ██╔══██║ ██║ ██╔══╝
// ╚██████╗██║ ██║███████╗██║ ██║ ██║ ███████╗
// ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝
describe('creating a new user with POST /user', function() {
it('should cause a `created` notification to be received by all `user` subscribers', function(done) {
expectNotifications({
user: {
created: {
verb: 'created',
id: 1,
'data.name': 'bert'
}
}
}, done);
socket2.post('/user', { name: 'bert' }, function (body, jwr) {
if (jwr.error) { return done(jwr.error); }
// Otherwise, the event handler above should fire (or this test will time out and fail).
});
});
});
describe('creating a new pet with POST /pet that includes a value for a singular association in a one-to-many relationship', function () {
before(function() {
bootstrapModels = {
user: [{name: 'bob'}]
};
});
it('should cause an `addedTo` notification to be received by all subscribers to the child record', function(done) {
expectNotifications({
pet: {
created: {
verb: 'created',
id: 1,
'data.name': 'alice'
}
},
user: {
addedTo: {
verb: 'addedTo',
id: 1,
addedId: 1,
attribute: 'pets'
}
}
}, done);
socket2.post('/pet', { name: 'alice', owner: 1 }, function (body, jwr) {
if (jwr.error) { return done(jwr.error); }
// Otherwise, the event handler above should fire (or this test will time out and fail).
});
});
});
describe('creating a new user with POST /user that includes a value for a plural association in a many-to-one relationship', function () {
describe('and the other side was not already linked to a record', function() {
before(function() {
bootstrapModels = {
pet: [{ name: 'alice'}, {name: 'mr. bailey'}, {name: 'tex'}]
};
});
it('should cause an `updated` notification to be received by all subscribers to the child record', function(done) {
expectNotifications({
user: {
created: {
verb: 'created',
id: 1,
'data.name': 'bert'
}
},
pet: {
updatedAlice: {
verb: 'updated',
id: 1,
'data.owner': 1
},
updatedBailey: {
verb: 'updated',
id: 2,
'data.owner': 1
},
updatedTex: {
verb: 'updated',
id: 3,
'data.owner': 1
}
}
}, done);
socket2.post('/user', { name: 'bert', pets: [1, 2, 3] }, function (body, jwr) {
if (jwr.error) { return done(jwr.error); }
// Otherwise, the event handler above should fire (or this test will time out and fail).
});
});
});
describe('and the other side was already linked to a record', function() {
before(function() {
bootstrapModels = {
user: [{ name: 'bert' }],
pet: [{ name: 'alice', owner: 1}, {name: 'mr. bailey'}, {name: 'tex', owner: 1}]
};
});
it('should cause an `updated` notification to be received by all subscribers to the child record, and a `removedFrom` notification to be received by all subscribers to the child\'s former parent record', function(done) {
expectNotifications({
user: {
created: {
verb: 'created',
id: 2,
'data.name': 'ernie'
},
removedAlice: {
verb: 'removedFrom',
id: 1,
removedId: 1,
attribute: 'pets'
},
removedText: {
verb: 'removedFrom',
id: 1,
removedId: 3,
attribute: 'pets'
}
},
pet: {
updatedAlice: {
verb: 'updated',
id: 1,
'data.owner': 2
},
updatedBailey: {
verb: 'updated',
id: 2,
'data.owner': 2
},
updatedTex: {
verb: 'updated',
id: 3,
'data.owner': 2
}
}
}, done);
socket2.post('/user', { name: 'ernie', pets: [1, 2, 3] }, function (body, jwr) {
if (jwr.error) { return done(jwr.error); }
// Otherwise, the event handler above should fire (or this test will time out and fail).
});
});
});
});
describe('creating a new user with POST /user that includes a value for a plural association in a many-to-many relationship', function () {
before(function() {
bootstrapModels = {
pet: [{ name: 'alice' }, {name: 'mr. bailey'}, {name: 'tex' }]
};
});
it('should cause an `addedTo` notification to be received by all subscribers to the child record', function(done) {
expectNotifications({
user: {
created: {
verb: 'created',
id: 1,
'data.name': 'bert'
}
},
pet: {
addedToAlice: {
verb: 'addedTo',
attribute: 'vets',
id: 1,
addedId: 1
},
addedToBailey: {
verb: 'addedTo',
attribute: 'vets',
id: 2,
addedId: 1
},
addedToTex: {
verb: 'addedTo',
attribute: 'vets',
id: 3,
addedId: 1
}
}
}, done);
socket2.post('/user', { name: 'bert', patients: [1, 2, 3] }, function (body, jwr) {
if (jwr.error) { return done(jwr.error); }
// Otherwise, the event handler above should fire (or this test will time out and fail).
});
});
});
// ██╗ ██╗██████╗ ██████╗ █████╗ ████████╗███████╗
// ██║ ██║██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝██╔════╝
// ██║ ██║██████╔╝██║ ██║███████║ ██║ █████╗
// ██║ ██║██╔═══╝ ██║ ██║██╔══██║ ██║ ██╔══╝
// ╚██████╔╝██║ ██████╔╝██║ ██║ ██║ ███████╗
// ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝
//
describe('updating a record with PATCH /user', function() {
before(function() {
bootstrapModels = {
user: [{ name: 'bert' }]
};
});
it('should cause an `updated` notification to be received by all subscribers to the parent record', function(done) {
expectNotifications({
user: {
updated: {
verb: 'updated',
id: 1,
'data.name': 'ernie',
'previous.name': 'bert'
}
}
}, done);
socket2.patch('/user/1', { name: 'ernie' }, function (body, jwr) {
if (jwr.error) { return done(jwr.error); }
// Otherwise, the event handler above should fire (or this test will time out and fail).
});
});
});
describe('updating a record with PUT /pet with a new value for a singular association', function() {
describe('where the previous value was `null`', function() {
before(function() {
bootstrapModels = {
user: [{ name: 'bert' }],
pet: [{name: 'alice', owner: null}]
};
});
it('should cause an `addedTo` notification to be sent to all subscribers to the new parent record', function(done) {
expectNotifications({
pet: {
updated: {
verb: 'updated',
id: 1,
'data.owner': 1,
'previous.owner': null
}
},
user: {
addedTo: {
verb: 'addedTo',
id: 1,
attribute: 'pets',
addedId: 1
}
}
}, done);
socket2.patch('/pet/1', { owner: 1 }, function (body, jwr) {
if (jwr.error) { return done(jwr.error); }
// Otherwise, the event handler above should fire (or this test will time out and fail).
});
});
});
describe('where the previous value was not `null`', function() {
before(function() {
bootstrapModels = {
user: [{ name: 'bert' }, {name: 'ernie'}],
pet: [{name: 'alice', owner: 1}]
};
});
it('should cause a `removedFrom` notification to be sent to all subscribers to the old parent record', function(done) {
expectNotifications({
pet: {
updated: {
verb: 'updated',
id: 1,
'data.owner': 2,
'previous.owner.user_id': 1
}
},
user: {
addedTo: {
verb: 'addedTo',
id: 2,
attribute: 'pets',
addedId: 1
},
removedFrom: {
verb: 'removedFrom',
id: 1,
attribute: 'pets',
removedId: 1
}
}
}, done);
socket2.patch('/pet/1', { owner: 2 }, function (body, jwr) {
if (jwr.error) { return done(jwr.error); }
// Otherwise, the event handler above should fire (or this test will time out and fail).
});
});
});
});
// █████╗ ██████╗ ██████╗
// ██╔══██╗██╔══██╗██╔══██╗
// ███████║██║ ██║██║ ██║
// ██╔══██║██║ ██║██║ ██║
// ██║ ██║██████╔╝██████╔╝
// ╚═╝ ╚═╝╚═════╝ ╚═════╝
//
describe('adding a pet to a user with PUT /user/1/pets/1 where pets->owner is a many-to-one relationship', function () {
describe('and the other side was not already linked to a record', function() {
before(function() {
bootstrapModels = {
user: [{name: 'bert'}],
pet: [{ name: 'alice'}]
};
});
it('should cause an `addedTo` notification to be received by all subscribers to the parent record, and an `updated` notification to be received by all subscribers to the child record', function(done) {
expectNotifications({
user: {
addedTo: {
verb: 'addedTo',
id: 1,
attribute: 'pets',
addedId: 1
}
},
pet: {
updatedAlice: {
verb: 'updated',
id: 1,
'data.owner': 1
}
}
}, done);
socket2.put('/user/1/pets/1', {}, function (body, jwr) {
if (jwr.error) { return done(jwr.error); }
// Otherwise, the event handler above should fire (or this test will time out and fail).
});
});
});
describe('and the other side was already linked to a record', function() {
before(function() {
bootstrapModels = {
user: [{ name: 'bert' }, { name: 'ernie' }],
pet: [{ name: 'alice', owner: 1}]
};
});
it('should cause an `updated` notification to be received by all subscribers to the child record, and a `removedFrom` notification to be received by all subscribers to the child\'s former parent record', function(done) {
expectNotifications({
user: {
addedTo: {
verb: 'addedTo',
id: 2,
attribute: 'pets',
addedId: 1
},
removedFrom: {
verb: 'removedFrom',
id: 1,
removedId: 1,
attribute: 'pets'
}
},
pet: {
updatedAlice: {
verb: 'updated',
id: 1,
'data.owner': 2
}
}
}, done);
socket2.put('/user/2/pets/1', {}, function (body, jwr) {
if (jwr.error) { return done(jwr.error); }
// Otherwise, the event handler above should fire (or this test will time out and fail).
});
});
});
});
describe('adding a patient to a user with PUT /user/1/patients/1 where patients->vets is a many-to-many relationship', function () {
before(function() {
bootstrapModels = {
user: [{name: 'bert'}],
pet: [{ name: 'alice' }]
};
});
it('should cause an `addedTo` notification to be received by all subscribers to the child record', function(done) {
expectNotifications({
pet: {
addedTo: {
verb: 'addedTo',
id: 1,
attribute: 'vets',
addedId: 1
}
},
user: {
addedTo: {
verb: 'addedTo',
id: 1,
attribute: 'patients',
addedId: 1
}
}
}, done);
socket2.put('/user/1/patients/1', {}, function (body, jwr) {
if (jwr.error) { return done(jwr.error); }
// Otherwise, the event handler above should fire (or this test will time out and fail).
});
});
});
// ██████╗ ███████╗███╗ ███╗ ██████╗ ██╗ ██╗███████╗
// ██╔══██╗██╔════╝████╗ ████║██╔═══██╗██║ ██║██╔════╝
// ██████╔╝█████╗ ██╔████╔██║██║ ██║██║ ██║█████╗
// ██╔══██╗██╔══╝ ██║╚██╔╝██║██║ ██║╚██╗ ██╔╝██╔══╝
// ██║ ██║███████╗██║ ╚═╝ ██║╚██████╔╝ ╚████╔╝ ███████╗
// ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝ ╚══════╝
//
describe('removing a pet from a user with DELETE /user/1/pets/1 where pets->owner is a many-to-one relationship', function () {
before(function() {
bootstrapModels = {
user: [{ name: 'bert' }],
pet: [{ name: 'alice', owner: 1}]
};
});
it('should cause a `removedFrom` notification to be received by all subscribers to the parent record, and an `updated` notification to be received by all subscribers to the child record', function(done) {
expectNotifications({
user: {
removedFrom: {
verb: 'removedFrom',
id: 1,
attribute: 'pets',
removedId: 1
},
},
pet: {
updatedAlice: {
verb: 'updated',
id: 1,
'data.owner': null
}
}
}, done);
socket2.delete('/user/1/pets/1', {}, function (body, jwr) {
if (jwr.error) { return done(jwr.error); }
// Otherwise, the event handler above should fire (or this test will time out and fail).
});
});
});
describe('removing a patient from a user with DELETE /user/1/patients/1 where patients->vets is a many-to-many relationship', function () {
before(function() {
bootstrapModels = {
user: [{name: 'bert'}],
pet: [{ name: 'alice', vets: [1] }]
};
});
it('should cause a `removedFrom` notification to be received by all subscribers to the child record', function(done) {
expectNotifications({
pet: {
removedFrom: {
verb: 'removedFrom',
id: 1,
attribute: 'vets',
removedId: 1
}
},
user: {
removedFrom: {
verb: 'removedFrom',
id: 1,
attribute: 'patients',
removedId: 1
}
}
}, done);
socket2.delete('/user/1/patients/1', {}, function (body, jwr) {
if (jwr.error) { return done(jwr.error); }
// Otherwise, the event handler above should fire (or this test will time out and fail).
});
});
});
// ██████╗ ███████╗██████╗ ██╗ █████╗ ██████╗███████╗
// ██╔══██╗██╔════╝██╔══██╗██║ ██╔══██╗██╔════╝██╔════╝
// ██████╔╝█████╗ ██████╔╝██║ ███████║██║ █████╗
// ██╔══██╗██╔══╝ ██╔═══╝ ██║ ██╔══██║██║ ██╔══╝
// ██║ ██║███████╗██║ ███████╗██║ ██║╚██████╗███████╗
// ╚═╝ ╚═╝╚══════╝╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝╚══════╝
//
describe('replacing pets of a user with PUT /user/1/pets where pets->owner is a many-to-one relationship', function () {
describe('and some of the replacement pets were already linked to owners', function() {
before(function() {
bootstrapModels = {
user: [{ name: 'bert' }, { name: 'ernie' }],
pet: [{ name: 'alice', owner: 1}, {name: 'mr. bailey', owner: 2}, {name: 'tex'}]
};
});
it('should cause an `updated` notification to be received by all subscribers to the replacement child records, an `addedTo` notification to be received by all subscribers to the new parent record, a `removedFrom` notification to be received by all subscribers to the new parent record (about replaced children) and a `removedFrom` notification to be received by all subscribers to any "stolen" child\'s former parent record', function(done) {
expectNotifications({
user: {
addedMrBailey: {
verb: 'addedTo',
id: 1,
attribute: 'pets',
addedId: 2
},
addedTex: {
verb: 'addedTo',
id: 1,
attribute: 'pets',
addedId: 3
},
removedAlice: {
verb: 'removedFrom',
id: 1,
removedId: 1,
attribute: 'pets'
},
removedBailey: {
verb: 'removedFrom',
id: 2,
removedId: 2,
attribute: 'pets'
},
},
pet: {
updatedAlice: {
verb: 'updated',
id: 1,
'data.owner': null
},
updatedBailey: {
verb: 'updated',
id: 2,
'data.owner': 1
},
updatedTex: {
verb: 'updated',
id: 3,
'data.owner': 1
},
}
}, done);
socket2.put('/user/1/pets', [2,3], function (body, jwr) {
if (jwr.error) { return done(jwr.error); }
// Otherwise, the event handler above should fire (or this test will time out and fail).
});
});
});
});
describe('replacing patients of a user with PUT /user/1/patients where patients->vets is a many-to-many relationship', function () {
before(function() {
bootstrapModels = {
pet: [{ name: 'alice' }, { name: 'mr. bailey'}, {name: 'tex'}],
user: [{name: 'bert', patients: [1,2]}],
};
});
it('should cause an `updated` notification to be received by all subscribers to the replacement child records, an `addedTo` notification to be received by all subscribers to the new parent record, and a `removedFrom` notification to be received by all subscribers to the new parent record (about replaced children)', function(done) {
expectNotifications({
pet: {
addedTex: {
verb: 'addedTo',
id: 3,
attribute: 'vets',
addedId: 1
},
removedAlice: {
verb: 'removedFrom',
id: 1,
attribute: 'vets',
removedId: 1
}
},
user: {
addedTex: {
verb: 'addedTo',
id: 1,
attribute: 'patients',
addedId: 3
},
removedAlice: {
verb: 'removedFrom',
id: 1,
attribute: 'patients',
removedId: 1
}
}
}, done);
socket2.put('/user/1/patients', [2,3], function (body, jwr) {
if (jwr.error) { return done(jwr.error); }
// Otherwise, the event handler above should fire (or this test will time out and fail).
});
});
});
// ██████╗ ███████╗███████╗████████╗██████╗ ██████╗ ██╗ ██╗
// ██╔══██╗██╔════╝██╔════╝╚══██╔══╝██╔══██╗██╔═══██╗╚██╗ ██╔╝
// ██║ ██║█████╗ ███████╗ ██║ ██████╔╝██║ ██║ ╚████╔╝
// ██║ ██║██╔══╝ ╚════██║ ██║ ██╔══██╗██║ ██║ ╚██╔╝
// ██████╔╝███████╗███████║ ██║ ██║ ██║╚██████╔╝ ██║
// ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝
//
describe('destroying a user with DELETE /user/1 where the user has pets and pets->owner is a many-to-one relationship', function () {
before(function() {
bootstrapModels = {
user: [{ name: 'bert' }],
pet: [{ name: 'alice', owner: 1}]
};
});
it('should cause a `destroyed` notification to be received by all subscribers to the parent record, and an `updated` notification to be received by all subscribers to the child records', function(done) {
expectNotifications({
user: {
destroyed: {
verb: 'destroyed',
id: 1,
},
},
pet: {
updatedAlice: {
verb: 'updated',
id: 1,
'data.owner': null
}
}
}, done);
socket2.delete('/user/1', {}, function (body, jwr) {
if (jwr.error) { return done(jwr.error); }
// Otherwise, the event handler above should fire (or this test will time out and fail).
});
});
});
describe('destroying a user with DELETE /user/1 where the user has patients and patients->vets is a many-to-many relationship', function () {
before(function() {
bootstrapModels = {
user: [{name: 'bert'}],
pet: [{ name: 'alice', vets: [1] }]
};
});
it('should cause a `removedFrom` notification to be received by all subscribers to the child record', function(done) {
expectNotifications({
user: {
destroyed: {
verb: 'destroyed',
id: 1
}
},
pet: {
removedFrom: {
verb: 'removedFrom',
id: 1,
attribute: 'vets',
removedId: 1
}
}
}, done);
socket2.delete('/user/1', {}, function (body, jwr) {
if (jwr.error) { return done(jwr.error); }
// Otherwise, the event handler above should fire (or this test will time out and fail).
});
});
});
function expectNotifications(notifications, done) {
var checklist = {};
var errored = false;
_.each(notifications, function(modelNotifications, model) {
_.each(modelNotifications, function(validator, identifier) {
checklist[model + '.' + identifier] = false;
});
socket1.on(model, function(notification){
// console.log(notification);
if (errored) {return;}
try {
if (!_.any(modelNotifications, function(validator, identifier) {
if (_.all(validator, function(val, path) {
return _.get(notification, path) === val;
})) {
if (checklist[model + '.' + identifier] === true) {
errored = true;
throw new Error('Got duplicate `' + identifier + '` notification for model `' + model + '`' );
}
checklist[model + '.' + identifier] = true;
if (_.all(checklist, function(flag) {
return flag === true;
})) {
done();
}
return true;
}
})) {
throw new Error('Unexpected `' + model + '` notification: ' + util.inspect(notification, {depth: null}));
}
} catch (e) {
errored = true;
return done(e);
}
});
});
}
});//</describe>
});//</describe :: Model events>
});//</describe :: pubsub>