diff --git a/.jshintrc b/.jshintrc index 17430da..1b5282b 100644 --- a/.jshintrc +++ b/.jshintrc @@ -14,8 +14,9 @@ "quotmark": "single", "regexp": true, "undef": true, - "unused": true, + "unused": false, "trailing": true, "sub": true, - "maxlen": 80 + "maxlen": 80, + "predef": [ "describe", "beforeEach", "afterEach", "before", "after", "it" ] } diff --git a/Gruntfile.js b/Gruntfile.js index 5697bf3..adb79a6 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -7,7 +7,8 @@ module.exports = function(grunt) { banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' + '<%= grunt.template.today("yyyy-mm-dd") %>\n' + '<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' + - '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + + '* Copyright (c) <%= grunt.template.today("yyyy") %> ' + + '<%= pkg.author.name %>;' + ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */\n', // Task configuration. jshint: { @@ -17,7 +18,7 @@ module.exports = function(grunt) { gruntfile: { src: 'Gruntfile.js' }, - lib_test: { + libTest: { src: ['lib/**/*.js', 'test/**/*.js'] } }, diff --git a/lib/relations.js b/lib/relations.js index dbd67e2..ca68294 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -41,14 +41,15 @@ function RelationMixin() { * // Save the new chapter * chapter.save(); * - * // you can also call the Chapter.create method with the `chapters` property which will build a chapter - * // instance and save the it in the data source. + * // you can also call the Chapter.create method with the `chapters` property + * // which will build a chapter instance and save the it in the data source. * book.chapters.create({name: 'Chapter 2'}, function(err, savedChapter) { * // this callback is optional * }); * * // Query chapters for the book - * book.chapters(function(err, chapters) { // all chapters with bookId = book.id + * book.chapters(function(err, chapters) { + * // all chapters with bookId = book.id * console.log(chapters); * }); * @@ -58,9 +59,11 @@ function RelationMixin() { * }); * }); *``` - * @param {Object|String} modelTo Model object (or String name of model) to which you are creating the relationship. + * @param {Object|String} modelTo Model object (or String name of model) to + * which you are creating the relationship. * @options {Object} parameters Configuration parameters; see below. - * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. + * @property {String} as Name of the property in the referring model that + * corresponds to the foreign key field in the related model. * @property {String} foreignKey Property name of foreign key field. * @property {Object} model Model object */ @@ -71,12 +74,14 @@ RelationMixin.hasMany = function hasMany(modelTo, params) { }; /** - * Declare "belongsTo" relation that sets up a one-to-one connection with another model, such that each - * instance of the declaring model "belongs to" one instance of the other model. + * Declare "belongsTo" relation that sets up a one-to-one connection with + * another model, such that each instance of the declaring model "belongs + * to" one instance of the other model. * - * For example, if an application includes users and posts, and each post can be written by exactly one user. - * The following code specifies that `Post` has a reference called `author` to the `User` model via the `userId` property of `Post` - * as the foreign key. + * For example, if an application includes users and posts, and each post can be + * written by exactly one user. The following code specifies that `Post` has a + * reference called `author` to the `User` model via the `userId` property of + * `Post` as the foreign key. * ``` * Post.belongsTo(User, {as: 'author', foreignKey: 'userId'}); * ``` @@ -94,7 +99,8 @@ RelationMixin.hasMany = function hasMany(modelTo, params) { * ``` * Examples: * - * Suppose the model Post has a *belongsTo* relationship with User (the author of the post). You could declare it this way: + * Suppose the model Post has a *belongsTo* relationship with User (the author + * of the post). You could declare it this way: * ```js * Post.belongsTo(User, {as: 'author', foreignKey: 'userId'}); * ``` @@ -106,18 +112,22 @@ RelationMixin.hasMany = function hasMany(modelTo, params) { * }); * ``` * - * The related object is cached, so if later you try to get again the author, no additional request will be made. - * But there is an optional boolean parameter in first position that set whether or not you want to reload the cache: + * The related object is cached, so if later you try to get again the author, no + * additional request will be made. But there is an optional boolean parameter + * in first position that set whether or not you want to reload the cache: * ```js * post.author(true, function(err, user) { * // The user is reloaded, even if it was already cached. * }); * ``` - * This optional parameter default value is false, so the related object will be loaded from cache if available. + * This optional parameter default value is false, so the related object will + * be loaded from cache if available. * - * @param {Class|String} modelTo Model object (or String name of model) to which you are creating the relationship. + * @param {Class|String} modelTo Model object (or String name of model) + * to which you are creating the relationship. * @options {Object} params Configuration parameters; see below. - * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. + * @property {String} as Name of the property in the referring model that + * corresponds to the foreign key field in the related model. * @property {String} foreignKey Name of foreign key property. * */ @@ -128,9 +138,10 @@ RelationMixin.belongsTo = function(modelTo, params) { }; /** - * A hasAndBelongsToMany relation creates a direct many-to-many connection with another model, with no intervening model. - * For example, if your application includes users and groups, with each group having many users and each user appearing - * in many groups, you could declare the models this way: + * A hasAndBelongsToMany relation creates a direct many-to-many connection with + * another model, with no intervening model. For example, if your application + * includes users and groups, with each group having many users and each user + * appearing in many groups, you could declare the models this way: * ``` * User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'}); * ``` @@ -151,10 +162,11 @@ RelationMixin.belongsTo = function(modelTo, params) { * user.groups.remove(group, callback); * ``` * - * @param {String|Object} modelTo Model object (or String name of model) to which you are creating the relationship. - * the relation + * @param {String|Object} modelTo Model object (or String name of model) to + * which you are creating the relationship. * @options {Object} params Configuration parameters; see below. - * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model. + * @property {String} as Name of the property in the referring model that + * corresponds to the foreign key field in the related model. * @property {String} foreignKey Property name of foreign key field. * @property {Object} model Model object */ @@ -202,7 +214,7 @@ function defineRelationProperty(modelClass, def) { scope.create = function() { return that['__create__' + def.name].apply(that, arguments); }; - scope.deleteById = destroyById = function() { + scope.deleteById = scope.destroyById = function() { return that['__destroyById__' + def.name].apply(that, arguments); }; scope.exists = function() { diff --git a/lib/remote-connector.js b/lib/remote-connector.js index 6c833f6..624ee31 100644 --- a/lib/remote-connector.js +++ b/lib/remote-connector.js @@ -24,7 +24,7 @@ function RemoteConnector(settings) { 'cannot initiaze RemoteConnector without a settings object'); this.client = settings.client; this.adapter = settings.adapter || 'rest'; - this.protocol = settings.protocol || 'http' + this.protocol = settings.protocol || 'http'; this.root = settings.root || ''; this.host = settings.host || 'localhost'; this.port = settings.port || 3000; diff --git a/package.json b/package.json index 6b1c260..b01716b 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "devDependencies": { "assert": "^1.1.2", "grunt": "~0.4.5", + "grunt-cli": "^0.1.13", "grunt-contrib-jshint": "~0.10.0", "grunt-mocha-test": "^0.11.0", "loopback": "^2.2.0", diff --git a/test/helper.js b/test/helper.js new file mode 100644 index 0000000..f43df80 --- /dev/null +++ b/test/helper.js @@ -0,0 +1,55 @@ +var loopback = require('loopback'); +var remoteConnector = require('..'); + +exports.createMemoryDataSource = createMemoryDataSource; +exports.createModel = createModel; +exports.createRemoteDataSource = createRemoteDataSource; +exports.createRestAppAndListen = createRestAppAndListen; +exports.getUserProperties = getUserProperties; + +function createRestAppAndListen(port) { + var app = loopback(); + + app.set('host', '127.0.0.1'); + if (port) app.set('port', port); + + app.use(loopback.rest()); + app.locals.handler = app.listen(); + + return app; +} + +function createMemoryDataSource() { + return loopback.createDataSource({connector: 'memory'}); +} + +function createRemoteDataSource(remoteApp) { + return loopback.createDataSource({ + url: 'http://' + remoteApp.get('host') + ':' + remoteApp.get('port'), + connector: remoteConnector + }); +} + +/** + * Used to create models based on a set of options. May associate or link to an + * app. + */ +function createModel(options) { + var Model = loopback.PersistedModel.extend(options.parent, options.properties, + options.options); + if (options.app) options.app.model(Model); + if (options.datasource) Model.attachTo(options.datasource); + return Model; +} + +function getUserProperties() { + return { + 'first': String, + 'last': String, + 'age': Number, + 'password': String, + 'gender': String, + 'domain': String, + 'email': String + }; +} diff --git a/test/models.test.js b/test/models.test.js new file mode 100644 index 0000000..879b516 --- /dev/null +++ b/test/models.test.js @@ -0,0 +1,218 @@ +var assert = require('assert'); +var helper = require('./helper'); +var TaskEmitter = require('strong-task-emitter'); + +describe('Model tests', function() { + var User; + + beforeEach(function() { + User = helper.createModel({ + parent: 'user', + app: helper.createRestAppAndListen(), + datasource: helper.createMemoryDataSource(), + properties: helper.getUserProperties() + }); + }); + + describe('Model.validatesPresenceOf(properties...)', function() { + it('should require a model to include a property to be considered valid', + function() { + User.validatesPresenceOf('first', 'last', 'age'); + var joe = new User({first: 'joe'}); + assert(joe.isValid() === false, 'model should not validate'); + assert(joe.errors.last, 'should have a missing last error'); + assert(joe.errors.age, 'should have a missing age error'); + }); + }); + + describe('Model.validatesLengthOf(property, options)', function() { + it('should require a property length to be within a specified range', + function() { + User.validatesLengthOf('password', {min: 5, message: {min: + 'Password is too short'}}); + var joe = new User({password: '1234'}); + assert(joe.isValid() === false, 'model should not be valid'); + assert(joe.errors.password, 'should have password error'); + }); + }); + + describe('Model.validatesInclusionOf(property, options)', function() { + it('should require a value for `property` to be in the specified array', + function() { + User.validatesInclusionOf('gender', {in: ['male', 'female']}); + var foo = new User({gender: 'bar'}); + assert(foo.isValid() === false, 'model should not be valid'); + assert(foo.errors.gender, 'should have gender error'); + }); + }); + + describe('Model.validatesExclusionOf(property, options)', function() { + it('should require a value for `property` to not exist in the specified ' + + 'array', function() { + User.validatesExclusionOf('domain', {in: ['www', 'billing', 'admin']}); + var foo = new User({domain: 'www'}); + var bar = new User({domain: 'billing'}); + var bat = new User({domain: 'admin'}); + assert(foo.isValid() === false); + assert(bar.isValid() === false); + assert(bat.isValid() === false); + assert(foo.errors.domain, 'model should have a domain error'); + assert(bat.errors.domain, 'model should have a domain error'); + assert(bat.errors.domain, 'model should have a domain error'); + }); + }); + + describe('Model.validatesNumericalityOf(property, options)', function() { + it('should require a value for `property` to be a specific type of ' + + '`Number`', function() { + User.validatesNumericalityOf('age', {int: true}); + var joe = new User({age: 10.2}); + assert(joe.isValid() === false); + var bob = new User({age: 0}); + assert(bob.isValid() === true); + assert(joe.errors.age, 'model should have an age error'); + }); + }); + + describe('myModel.isValid()', function() { + it('should validate the model instance', function() { + User.validatesNumericalityOf('age', {int: true}); + var user = new User({first: 'joe', age: 'flarg'}); + var valid = user.isValid(); + assert(valid === false); + assert(user.errors.age, 'model should have age error'); + }); + + it('should validate the model asynchronously', function(done) { + User.validatesNumericalityOf('age', {int: true}); + var user = new User({first: 'joe', age: 'flarg'}); + user.isValid(function(valid) { + assert(valid === false); + assert(user.errors.age, 'model should have age error'); + done(); + }); + }); + }); + + describe('Model.create([data], [callback])', function() { + it('should create an instance and save to the attached data source', + function(done) { + User.create({first: 'Joe', last: 'Bob'}, function(err, user) { + assert(user instanceof User); + done(); + }); + }); + }); + + describe('model.save([options], [callback])', function() { + it('should save an instance of a Model to the attached data source', + function(done) { + var joe = new User({first: 'Joe', last: 'Bob'}); + joe.save(function(err, user) { + assert(user.id); + assert(!err); + assert(!user.errors); + done(); + }); + }); + }); + + describe('model.updateAttributes(data, [callback])', function() { + it('should save specified attributes to the attached data source', + function(done) { + User.create({first: 'joe', age: 100}, function(err, user) { + assert(!err); + assert.equal(user.first, 'joe'); + + user.updateAttributes({ + first: 'updatedFirst', + last: 'updatedLast' + }, function(err, updatedUser) { + assert(!err); + assert.equal(updatedUser.first, 'updatedFirst'); + assert.equal(updatedUser.last, 'updatedLast'); + assert.equal(updatedUser.age, 100); + done(); + }); + }); + }); + }); + + describe('Model.upsert(data, callback)', function() { + it('should update when a record with id=data.id is found, insert otherwise', + function(done) { + User.upsert({first: 'joe', id: 7}, function(err, user) { + assert(!err); + assert.equal(user.first, 'joe'); + + User.upsert({first: 'bob', id: 7}, function(err, updatedUser) { + assert(!err); + assert.equal(updatedUser.first, 'bob'); + done(); + }); + }); + }); + }); + + describe('model.destroy([callback])', function() { + it('should remove a model from the attached data source', function(done) { + User.create({first: 'joe', last: 'bob'}, function(err, user) { + User.findById(user.id, function(err, foundUser) { + assert.equal(user.id, foundUser.id); + foundUser.destroy(function() { + User.findById(user.id, function(err, notFound) { + assert.equal(notFound, null); + done(); + }); + }); + }); + }); + }); + }); + + describe('Model.deleteById(id, [callback])', function() { + it('should delete a model instance from the attached data source', + function(done) { + User.create({first: 'joe', last: 'bob'}, function(err, user) { + User.deleteById(user.id, function(err) { + User.findById(user.id, function(err, notFound) { + assert.equal(notFound, null); + done(); + }); + }); + }); + }); + }); + + describe('Model.findById(id, callback)', function() { + it('should find an instance by id', function(done) { + User.create({first: 'michael', last: 'jordan', id: 23}, function() { + User.findById(23, function(err, user) { + assert.equal(user.id, 23); + assert.equal(user.first, 'michael'); + assert.equal(user.last, 'jordan'); + done(); + }); + }); + }); + }); + + describe('Model.count([query], callback)', function() { + it('should return the count of Model instances in data source', + function(done) { + var taskEmitter = new TaskEmitter(); + taskEmitter + .task(User, 'create', {first: 'jill', age: 100}) + .task(User, 'create', {first: 'bob', age: 200}) + .task(User, 'create', {first: 'jan'}) + .task(User, 'create', {first: 'sam'}) + .task(User, 'create', {first: 'suzy'}) + .on('done', function() { + User.count({age: {gt: 99}}, function(err, count) { + assert.equal(count, 2); + done(); + }); + }); + }); + }); +}); diff --git a/test/remote-connector.test.js b/test/remote-connector.test.js index 2eabfd6..60453ec 100644 --- a/test/remote-connector.test.js +++ b/test/remote-connector.test.js @@ -1,123 +1,109 @@ -var loopback = require('loopback'); -var defineModelTestsWithDataSource = require('./util/model-tests'); var assert = require('assert'); -var Remote = require('..'); - -function createAppWithRest() { - var app = loopback(); - app.set('host', '127.0.0.1'); - app.use(loopback.rest()); - return app; -} - -function listenAndSetupRemoteDS(test, app, remoteName, cb) { - app.listen(0, function() { - test[remoteName] = loopback.createDataSource({ - host: '127.0.0.1', - port: app.get('port'), - connector: Remote, - }); - cb(); - }); -} +var helper = require('./helper'); describe('RemoteConnector', function() { - var remoteApp; - var remote; + var ctx = this; - defineModelTestsWithDataSource({ - beforeEach: function(done) { - var test = this; - remoteApp = createAppWithRest(); - listenAndSetupRemoteDS(test, remoteApp, 'dataSource', done); - }, - onDefine: function(Model) { - var RemoteModel = Model.extend(Model.modelName); - RemoteModel.attachTo(loopback.createDataSource({ - connector: loopback.Memory - })); - remoteApp.model(RemoteModel); - } + before(function() { + ctx.serverApp = helper.createRestAppAndListen(3001); + ctx.ServerModel = helper.createModel({ + parent: 'TestModel', + app: ctx.serverApp, + datasource: helper.createMemoryDataSource() + }); + ctx.remoteApp = helper.createRestAppAndListen(3002); + ctx.RemoteModel = helper.createModel({ + parent: 'TestModel', + app: ctx.remoteApp, + datasource: helper.createRemoteDataSource(ctx.serverApp) + }); }); - beforeEach(function(done) { - var test = this; - var ServerModel = this.ServerModel = - loopback.PersistedModel.extend('TestModel'); - - remoteApp = test.remoteApp = createAppWithRest(); - remoteApp.model(ServerModel); - - listenAndSetupRemoteDS(test, remoteApp, 'remote', done); + after(function() { + ctx.serverApp.locals.handler.close(); + ctx.remoteApp.locals.handler.close(); + ctx.ServerModel = null; + ctx.RemoteModel = null; }); it('should support the save method', function(done) { var calledServerCreate = false; - var RemoteModel = loopback.PersistedModel.extend('TestModel'); - RemoteModel.attachTo(this.remote); - var ServerModel = this.ServerModel; - - ServerModel.create = function(data, cb) { + ctx.ServerModel.create = function(data, cb, callback) { calledServerCreate = true; data.id = 1; - cb(null, data); - } + if (callback) callback(null, data); + else cb(null, data); + }; - ServerModel.setupRemoting(); - - var m = new RemoteModel({foo: 'bar'}); - m.save(function(err, inst) { - assert(inst instanceof RemoteModel); + var m = new ctx.RemoteModel({foo: 'bar'}); + m.save(function(err, instance) { + if (err) return done(err); + assert(instance); + assert(instance instanceof ctx.RemoteModel); assert(calledServerCreate); done(); }); }); it('should support aliases', function(done) { - var RemoteModel = loopback.PersistedModel.extend('TestModel'); - RemoteModel.attachTo(this.remote); - - var ServerModel = this.ServerModel; - - ServerModel.upsert = function(id, cb) { - done(); + var calledServerUpsert = false; + ctx.ServerModel.upsert = function(id, cb) { + calledServerUpsert = true; + cb(); }; - RemoteModel.updateOrCreate({}, function(err, inst) { + ctx.RemoteModel.updateOrCreate({}, function(err, instance) { if (err) return done(err); + assert(instance); + assert(instance instanceof ctx.RemoteModel); + assert(calledServerUpsert); + done(); }); }); }); describe('Custom Path', function() { - var test = this; + var ctx = this; before(function(done) { - var ServerModel = loopback.PersistedModel.extend('TestModel', {}, { - http: {path: '/custom'} + ctx.serverApp = helper.createRestAppAndListen(3001); + ctx.ServerModel = helper.createModel({ + parent: 'TestModel', + app: ctx.serverApp, + datasource: helper.createMemoryDataSource(), + options: { + http: {path: '/custom'} + } }); - server = test.server = createAppWithRest(); - server.dataSource('db', { - connector: loopback.Memory, - name: 'db' + ctx.remoteApp = helper.createRestAppAndListen(3002); + ctx.RemoteModel = helper.createModel({ + parent: 'TestModel', + app: ctx.remoteApp, + datasource: helper.createRemoteDataSource(ctx.serverApp), + options: { + dataSource: 'remote', + http: {path: '/custom'} + } }); - server.model(ServerModel, {dataSource: 'db'}); + done(); + }); - listenAndSetupRemoteDS(test, server, 'remote', done); + after(function(done) + { + ctx.serverApp.locals.handler.close(); + ctx.remoteApp.locals.handler.close(); + ctx.ServerModel = null; + ctx.RemoteModel = null; + done(); }); it('should support http.path configuration', function(done) { - var RemoteModel = loopback.PersistedModel.extend('TestModel', {}, { - dataSource: 'remote', - http: {path: '/custom'} - }); - RemoteModel.attachTo(test.remote); - - RemoteModel.create({}, function(err, instance) { - if (err) return assert(err); + ctx.RemoteModel.create({}, function(err, instance) { + if (err) return done(err); + assert(instance); done(); }); }); -}); \ No newline at end of file +}); diff --git a/test/remote-models.test.js b/test/remote-models.test.js new file mode 100644 index 0000000..78ad69e --- /dev/null +++ b/test/remote-models.test.js @@ -0,0 +1,141 @@ +var assert = require('assert'); +var helper = require('./helper'); +var TaskEmitter = require('strong-task-emitter'); + +describe('Remote model tests', function() { + var ctx = this; + + beforeEach(function() { + ctx.serverApp = helper.createRestAppAndListen(3001); + ctx.ServerModel = helper.createModel({ + parent: 'TestModel', + app: ctx.serverApp, + datasource: helper.createMemoryDataSource(), + properties: helper.userProperties + }); + + ctx.remoteApp = helper.createRestAppAndListen(3002); + ctx.RemoteModel = helper.createModel({ + parent: 'TestModel', + app: ctx.remoteApp, + datasource: helper.createRemoteDataSource(ctx.serverApp), + properties: helper.userProperties + }); + }); + + afterEach(function() { + ctx.serverApp.locals.handler.close(); + ctx.remoteApp.locals.handler.close(); + ctx.ServerModel = null; + ctx.RemoteModel = null; + }); + + describe('Model.create([data], [callback])', function() { + it('should create an instance and save to the attached data source', + function(done) { + ctx.RemoteModel.create({first: 'Joe', last: 'Bob'}, function(err, user) { + assert(user instanceof ctx.RemoteModel); + done(); + }); + }); + }); + + describe('model.save([options], [callback])', function() { + it('should save an instance of a Model to the attached data source', + function(done) { + var joe = new ctx.RemoteModel({first: 'Joe', last: 'Bob'}); + joe.save(function(err, user) { + assert(user.id); + assert(!err); + assert(!user.errors); + done(); + }); + }); + }); + + describe('model.updateAttributes(data, [callback])', function() { + it('should save specified attributes to the attached data source', + function(done) { + ctx.ServerModel.create({first: 'joe', age: 100}, function(err, user) { + assert(!err); + assert.equal(user.first, 'joe'); + + user.updateAttributes({ + first: 'updatedFirst', + last: 'updatedLast' + }, function(err, updatedUser) { + assert(!err); + assert.equal(updatedUser.first, 'updatedFirst'); + assert.equal(updatedUser.last, 'updatedLast'); + assert.equal(updatedUser.age, 100); + done(); + }); + }); + }); + }); + + describe('Model.upsert(data, callback)', function() { + it('should update when a record with id=data.id is found, insert otherwise', + function(done) { + ctx.RemoteModel.upsert({first: 'joe', id: 7}, function(err, user) { + assert(!err); + assert.equal(user.first, 'joe'); + + ctx.RemoteModel.upsert({first: 'bob', id: 7}, function(err, + updatedUser) { + assert(!err); + assert.equal(updatedUser.first, 'bob'); + done(); + }); + }); + }); + }); + + describe('Model.deleteById(id, [callback])', function() { + it('should delete a model instance from the attached data source', + function(done) { + ctx.ServerModel.create({first: 'joe', last: 'bob'}, function(err, user) { + ctx.RemoteModel.deleteById(user.id, function(err) { + ctx.RemoteModel.findById(user.id, function(err, notFound) { + assert.equal(notFound, null); + done(); + }); + }); + }); + }); + }); + + describe('Model.findById(id, callback)', function() { + it('should find an instance by id from the attached data source', + function(done) { + ctx.ServerModel.create({first: 'michael', last: 'jordan', id: 23}, + function() { + ctx.RemoteModel.findById(23, function(err, user) { + assert.equal(user.id, 23); + assert.equal(user.first, 'michael'); + assert.equal(user.last, 'jordan'); + done(); + }); + }); + }); + }); + + describe('Model.count([query], callback)', function() { + it('should return the count of Model instances from both data source', + function(done) { + var taskEmitter = new TaskEmitter(); + taskEmitter + .task(ctx.ServerModel, 'create', {first: 'jill', age: 100}) + .task(ctx.RemoteModel, 'create', {first: 'bob', age: 200}) + .task(ctx.RemoteModel, 'create', {first: 'jan'}) + .task(ctx.ServerModel, 'create', {first: 'sam'}) + .task(ctx.ServerModel, 'create', {first: 'suzy'}) + .on('done', function() { + ctx.RemoteModel.count({age: {gt: 99}}, function(err, count) { + assert.equal(count, 2); + done(); + }); + }); + }); + }); +}); diff --git a/test/util/model-tests.js b/test/util/model-tests.js deleted file mode 100644 index 51b42c9..0000000 --- a/test/util/model-tests.js +++ /dev/null @@ -1,259 +0,0 @@ -var assert = require('assert'); -var loopback = require('loopback'); -var PersistedModel = loopback.PersistedModel; -var TaskEmitter = require('strong-task-emitter'); - -module.exports = function defineModelTestsWithDataSource(options) { - - describe('Model Tests', function() { - - var User, dataSource; - - if (options.beforeEach) { - beforeEach(options.beforeEach); - } - - beforeEach(function() { - var test = this; - - // setup a model / datasource - dataSource = - this.dataSource || loopback.createDataSource(options.dataSource); - - var extend = PersistedModel.extend; - - // create model hook - PersistedModel.extend = function() { - var extendedModel = extend.apply(PersistedModel, arguments); - - if (options.onDefine) { - options.onDefine.call(test, extendedModel); - } - - return extendedModel; - } - - User = PersistedModel.extend('user', { - 'first': String, - 'last': String, - 'age': Number, - 'password': String, - 'gender': String, - 'domain': String, - 'email': String - }, { - trackChanges: true - }); - - // enable destroy all for testing - User.destroyAll.shared = true; - User.attachTo(dataSource); - }); - - describe('Model.validatesPresenceOf(properties...)', function() { - it("Require a model to include a property to be considered valid", - function() { - User.validatesPresenceOf('first', 'last', 'age'); - var joe = new User({first: 'joe'}); - assert(joe.isValid() === false, 'model should not validate'); - assert(joe.errors.last, 'should have a missing last error'); - assert(joe.errors.age, 'should have a missing age error'); - }); - }); - - describe('Model.validatesLengthOf(property, options)', function() { - it("Require a property length to be within a specified range", - function() { - User.validatesLengthOf('password', - {min: 5, message: {min: 'Password is too short'}}); - var joe = new User({password: '1234'}); - assert(joe.isValid() === false, 'model should not be valid'); - assert(joe.errors.password, 'should have password error'); - }); - }); - - describe('Model.validatesInclusionOf(property, options)', function() { - it("Require a value for `property` to be in the specified array", - function() { - User.validatesInclusionOf('gender', {in: ['male', 'female']}); - var foo = new User({gender: 'bar'}); - assert(foo.isValid() === false, 'model should not be valid'); - assert(foo.errors.gender, 'should have gender error'); - }); - }); - - describe('Model.validatesExclusionOf(property, options)', function() { - it("Require a value for `property` to not exist in the specified array", - function() { - User.validatesExclusionOf('domain', - {in: ['www', 'billing', 'admin']}); - var foo = new User({domain: 'www'}); - var bar = new User({domain: 'billing'}); - var bat = new User({domain: 'admin'}); - assert(foo.isValid() === false); - assert(bar.isValid() === false); - assert(bat.isValid() === false); - assert(foo.errors.domain, 'model should have a domain error'); - assert(bat.errors.domain, 'model should have a domain error'); - assert(bat.errors.domain, 'model should have a domain error'); - }); - }); - - describe('Model.validatesNumericalityOf(property, options)', function() { - it("Require a value for `property` to be a specific type of `Number`", - function() { - User.validatesNumericalityOf('age', {int: true}); - var joe = new User({age: 10.2}); - assert(joe.isValid() === false); - var bob = new User({age: 0}); - assert(bob.isValid() === true); - assert(joe.errors.age, 'model should have an age error'); - }); - }); - - describe('myModel.isValid()', function() { - it("Validate the model instance", function() { - User.validatesNumericalityOf('age', {int: true}); - var user = new User({first: 'joe', age: 'flarg'}) - var valid = user.isValid(); - assert(valid === false); - assert(user.errors.age, 'model should have age error'); - }); - - it('Asynchronously validate the model', function(done) { - User.validatesNumericalityOf('age', {int: true}); - var user = new User({first: 'joe', age: 'flarg'}); - user.isValid(function(valid) { - assert(valid === false); - assert(user.errors.age, 'model should have age error'); - done(); - }); - }); - }); - - describe('Model.create([data], [callback])', function() { - it("Create an instance of Model with given data and save to the attached data source", - function(done) { - User.create({first: 'Joe', last: 'Bob'}, function(err, user) { - assert(user instanceof User); - done(); - }); - }); - }); - - describe('model.save([options], [callback])', function() { - it("Save an instance of a Model to the attached data source", - function(done) { - var joe = new User({first: 'Joe', last: 'Bob'}); - joe.save(function(err, user) { - assert(user.id); - assert(!err); - assert(!user.errors); - done(); - }); - }); - }); - - describe('model.updateAttributes(data, [callback])', function() { - it("Save specified attributes to the attached data source", - function(done) { - User.create({first: 'joe', age: 100}, function(err, user) { - assert(!err); - assert.equal(user.first, 'joe'); - - user.updateAttributes({ - first: 'updatedFirst', - last: 'updatedLast' - }, function(err, updatedUser) { - assert(!err); - assert.equal(updatedUser.first, 'updatedFirst'); - assert.equal(updatedUser.last, 'updatedLast'); - assert.equal(updatedUser.age, 100); - done(); - }); - }); - }); - }); - - describe('Model.upsert(data, callback)', function() { - it("Update when record with id=data.id found, insert otherwise", - function(done) { - User.upsert({first: 'joe', id: 7}, function(err, user) { - assert(!err); - assert.equal(user.first, 'joe'); - - User.upsert({first: 'bob', id: 7}, function(err, updatedUser) { - assert(!err); - assert.equal(updatedUser.first, 'bob'); - done(); - }); - }); - }); - }); - - describe('model.destroy([callback])', function() { - it("Remove a model from the attached data source", function(done) { - User.create({first: 'joe', last: 'bob'}, function(err, user) { - User.findById(user.id, function(err, foundUser) { - assert.equal(user.id, foundUser.id); - foundUser.destroy(function() { - User.findById(user.id, function(err, notFound) { - assert.equal(notFound, null); - done(); - }); - }); - }); - }); - }); - }); - - describe('Model.deleteById(id, [callback])', function() { - it("Delete a model instance from the attached data source", - function(done) { - User.create({first: 'joe', last: 'bob'}, function(err, user) { - User.deleteById(user.id, function(err) { - User.findById(user.id, function(err, notFound) { - assert.equal(notFound, null); - done(); - }); - }); - }); - }); - }); - - describe('Model.findById(id, callback)', function() { - it("Find an instance by id", function(done) { - User.create({first: 'michael', last: 'jordan', id: 23}, function() { - User.findById(23, function(err, user) { - assert.equal(user.id, 23); - assert.equal(user.first, 'michael'); - assert.equal(user.last, 'jordan'); - done(); - }); - }); - }); - }); - - describe('Model.count([query], callback)', function() { - it("Query count of Model instances in data source", function(done) { - (new TaskEmitter()).task(User, - 'create', - {first: 'jill', age: 100}).task(User, - 'create', - {first: 'bob', age: 200}).task(User, - 'create', - {first: 'jan'}).task(User, 'create', {first: 'sam'}).task(User, - 'create', - {first: 'suzy'}).on('done', function() { - User.count({age: {gt: 99}}, function(err, count) { - assert.equal(count, 2); - done(); - }); - }); - }); - }); - - }); - - -}