diff --git a/.travis.yml b/.travis.yml index 168369451..d431fa429 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,5 +3,6 @@ language: node_js node_js: - "0.10" - "0.12" - - "iojs" + - "4" + - "6" diff --git a/CHANGES.md b/CHANGES.md index ea87f5de4..aa42725cf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,47 @@ +2016-05-02, Version 2.28.0 +========================== + + * Add new feature to emit a `remoteMethodDisabled` event when disabling a remote method. (Supasate Choochaisri) + + * Fix typo in Model.nestRemoting (Tim Needham) + + * Allow built-in token middleware to run repeatedly (Benjamin Kröger) + + * Improve error message on connector init error (Miroslav Bajtoš) + + * application: correct spelling of "cannont" (Sam Roberts) + + +2016-02-19, Version 2.27.0 +========================== + + * Remove sl-blip from dependency (Candy) + + * Fix race condition in replication tests (Miroslav Bajtoš) + + * test: remove errant console.log from test (Ryan Graham) + + * Promisify Model Change (Jue Hou) + + * Fix race condition in error handler test (Miroslav Bajtoš) + + * Travis: drop iojs, add v4.x and v5.x (Miroslav Bajtoš) + + * Correct JSDoc findOrCreate() callback in PersistedModel (Miroslav Bajtoš) + + * Hide verificationToken (Miroslav Bajtoš) + + * test: use ephemeral port for e2e server (Ryan Graham) + + * test: fail on error instead of crash (Ryan Graham) + + * ensure app is booted before integration tests (Ryan Graham) + + * Checkpoint speedup (Amir Jafarian) + + * Pull in API doc fix from PR into master #1910 (crandmck) + + 2015-12-22, Version 2.26.2 ========================== @@ -989,8 +1033,6 @@ 2014-07-15, Version 2.0.0-beta6 =============================== - * 2.0.0-beta6 (Miroslav Bajtoš) - * lib/application: publish Change models to REST API (Miroslav Bajtoš) * models/change: fix typo (Miroslav Bajtoš) @@ -1001,8 +1043,6 @@ 2014-07-03, Version 2.0.0-beta5 =============================== - * 2.0.0-beta5 (Miroslav Bajtoš) - * app: update `url` on `listening` event (Miroslav Bajtoš) * Fix "ReferenceError: loopback is not defined" in registry.memory(). (Guilherme Cirne) @@ -1021,8 +1061,6 @@ 2014-06-26, Version 2.0.0-beta4 =============================== - * 2.0.0-beta4 (Miroslav Bajtoš) - * package: upgrade juggler to 2.0.0-beta2 (Miroslav Bajtoš) * Fix loopback in PhantomJS, fix karma tests (Miroslav Bajtoš) @@ -1149,8 +1187,6 @@ 2014-05-28, Version 2.0.0-beta3 =============================== - * 2.0.0-beta3 (Miroslav Bajtoš) - * package.json: fix malformed json (Miroslav Bajtoš) * 2.0.0-beta2 (Ritchie Martori) diff --git a/Gruntfile.js b/Gruntfile.js index a7a755c81..756bede3a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*global module:false*/ module.exports = function(grunt) { @@ -217,7 +222,10 @@ module.exports = function(grunt) { grunt.registerTask('e2e-server', function() { var done = this.async(); var app = require('./test/fixtures/e2e/app'); - app.listen(3000, done); + app.listen(0, function() { + process.env.PORT = this.address().port; + done(); + }); }); grunt.registerTask('e2e', ['e2e-server', 'karma:e2e']); diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..a95641b20 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2013,2016. All Rights Reserved. +Node module: loopback +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 29d781523..000000000 --- a/LICENSE.md +++ /dev/null @@ -1,9 +0,0 @@ -Copyright (c) 2013-2015 StrongLoop, Inc and other contributors. - -loopback uses a dual license model. - -You may use this library under the terms of the [MIT License][], -or under the terms of the [StrongLoop Subscription Agreement][]. - -[MIT License]: http://opensource.org/licenses/MIT -[StrongLoop Subscription Agreement]: http://strongloop.com/license diff --git a/browser/current-context.js b/browser/current-context.js index cdf1d8a28..97d4a1a70 100644 --- a/browser/current-context.js +++ b/browser/current-context.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + module.exports = function(loopback) { loopback.getCurrentContext = function() { return null; diff --git a/common/models/access-token.js b/common/models/access-token.js index 27cf5206d..750c21f8b 100644 --- a/common/models/access-token.js +++ b/common/models/access-token.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module Dependencies. */ diff --git a/common/models/acl.js b/common/models/acl.js index 2a7306b17..d4823cec8 100644 --- a/common/models/acl.js +++ b/common/models/acl.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! Schema ACL options diff --git a/common/models/application.js b/common/models/application.js index 617798c15..3286410a5 100644 --- a/common/models/application.js +++ b/common/models/application.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var assert = require('assert'); var utils = require('../../lib/utils'); diff --git a/common/models/change.js b/common/models/change.js index be353a039..605caa23d 100644 --- a/common/models/change.js +++ b/common/models/change.js @@ -1,9 +1,15 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module Dependencies. */ var PersistedModel = require('../../lib/loopback').PersistedModel; var loopback = require('../../lib/loopback'); +var utils = require('../../lib/utils'); var crypto = require('crypto'); var CJSON = {stringify: require('canonical-json')}; var async = require('async'); @@ -77,6 +83,8 @@ module.exports = function(Change) { var Change = this; var errors = []; + callback = callback || utils.createPromiseCallback(); + var tasks = modelIds.map(function(id) { return function(cb) { Change.findOrCreateChange(modelName, id, function(err, change) { @@ -111,6 +119,7 @@ module.exports = function(Change) { } callback(); }); + return callback.promise; }; /** @@ -138,6 +147,7 @@ module.exports = function(Change) { Change.findOrCreateChange = function(modelName, modelId, callback) { assert(loopback.findModel(modelName), modelName + ' does not exist'); + callback = callback || utils.createPromiseCallback(); var id = this.idForModel(modelName, modelId); var Change = this; @@ -155,6 +165,7 @@ module.exports = function(Change) { Change.updateOrCreate(ch, callback); } }); + return callback.promise; }; /** @@ -171,9 +182,7 @@ module.exports = function(Change) { change.debug('rectify change'); - cb = cb || function(err) { - if (err) throw new Error(err); - }; + cb = cb || utils.createPromiseCallback(); change.currentRevision(function(err, rev) { if (err) return cb(err); @@ -194,6 +203,7 @@ module.exports = function(Change) { } ); }); + return cb.promise; function doRectify(checkpoint, rev) { if (rev) { @@ -248,6 +258,7 @@ module.exports = function(Change) { */ Change.prototype.currentRevision = function(cb) { + cb = cb || utils.createPromiseCallback(); var model = this.getModelCtor(); var id = this.getModelId(); model.findById(id, function(err, inst) { @@ -258,6 +269,7 @@ module.exports = function(Change) { cb(null, null); } }); + return cb.promise; }; /** @@ -390,8 +402,11 @@ module.exports = function(Change) { */ Change.diff = function(modelName, since, remoteChanges, callback) { + callback = callback || utils.createPromiseCallback(); + if (!Array.isArray(remoteChanges) || remoteChanges.length === 0) { - return callback(null, {deltas: [], conflicts: []}); + callback(null, {deltas: [], conflicts: []}); + return callback.promise; } var remoteChangeIndex = {}; var modelIds = []; @@ -455,6 +470,7 @@ module.exports = function(Change) { conflicts: conflicts }); }); + return callback.promise; }; /** diff --git a/common/models/checkpoint.js b/common/models/checkpoint.js index 304f83148..b48cb1866 100644 --- a/common/models/checkpoint.js +++ b/common/models/checkpoint.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /** * Module Dependencies. */ @@ -27,43 +32,45 @@ module.exports = function(Checkpoint) { * Get the current checkpoint id * @callback {Function} callback * @param {Error} err - * @param {Number} checkpointId The current checkpoint id + * @param {Number} checkpoint The current checkpoint seq */ - Checkpoint.current = function(cb) { var Checkpoint = this; - this.find({ - limit: 1, - order: 'seq DESC' - }, function(err, checkpoints) { - if (err) return cb(err); - var checkpoint = checkpoints[0]; - if (checkpoint) { - cb(null, checkpoint.seq); - } else { - Checkpoint.create({ seq: 1 }, function(err, checkpoint) { - if (err) return cb(err); - cb(null, checkpoint.seq); - }); - } + Checkpoint._getSingleton(function(err, cp) { + cb(err, cp.seq); }); }; - Checkpoint.observe('before save', function(ctx, next) { - if (!ctx.instance) { - // Example: Checkpoint.updateAll() and Checkpoint.updateOrCreate() - return next(new Error('Checkpoint does not support partial updates.')); - } + Checkpoint._getSingleton = function(cb) { + var query = {limit: 1}; // match all instances, return only one + var initialData = {seq: 1}; + this.findOrCreate(query, initialData, cb); + }; - var model = ctx.instance; - if (!model.getId() && model.seq === undefined) { - model.constructor.current(function(err, seq) { - if (err) return next(err); - model.seq = seq + 1; - next(); + /** + * Increase the current checkpoint if it already exists otherwise initialize it + * @callback {Function} callback + * @param {Error} err + * @param {Object} checkpoint The current checkpoint + */ + Checkpoint.bumpLastSeq = function(cb) { + var Checkpoint = this; + Checkpoint._getSingleton(function(err, cp) { + if (err) return cb(err); + var originalSeq = cp.seq; + cp.seq++; + // Update the checkpoint but only if it was not changed under our hands + Checkpoint.updateAll({id: cp.id, seq: originalSeq}, {seq: cp.seq}, function(err, info) { + if (err) return cb(err); + // possible outcomes + // 1) seq was updated to seq+1 - exactly what we wanted! + // 2) somebody else already updated seq to seq+1 and our call was a no-op. + // That should be ok, checkpoints are time based, so we reuse the one created just now + // 3) seq was bumped more than once, so we will be using a value that is behind the latest seq. + // @bajtos is not entirely sure if this is ok, but since it wasn't handled by the current implementation either, + // he thinks we can keep it this way. + cb(null, cp); }); - } else { - next(); - } - }); + }); + }; }; diff --git a/common/models/email.js b/common/models/email.js index f73628027..6a6736dc6 100644 --- a/common/models/email.js +++ b/common/models/email.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /** * Email model. Extends LoopBack base [Model](#model-new-model). * @property {String} to Email addressee. Required. diff --git a/common/models/role-mapping.js b/common/models/role-mapping.js index ee728483c..53af71f9a 100644 --- a/common/models/role-mapping.js +++ b/common/models/role-mapping.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../lib/loopback'); /** diff --git a/common/models/role.js b/common/models/role.js index a176fec17..c4e7e8b3e 100644 --- a/common/models/role.js +++ b/common/models/role.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../lib/loopback'); var debug = require('debug')('loopback:security:role'); var assert = require('assert'); @@ -123,8 +128,9 @@ module.exports = function(Role) { /** * Add custom handler for roles. * @param {String} role Name of role. - * @param {Function} resolver Function that determines if a principal is in the specified role. - * Signature must be `function(role, context, callback)` + * @param {Function} resolver Function that determines + * if a principal is in the specified role. + * Should provide a callback or return a promise. */ Role.registerResolver = function(role, resolver) { if (!Role.resolvers) { @@ -147,12 +153,10 @@ module.exports = function(Role) { }); function isUserClass(modelClass) { - if (modelClass) { - return modelClass === loopback.User || - modelClass.prototype instanceof loopback.User; - } else { - return false; - } + if (!modelClass) return false; + var User = modelClass.modelBuilder.models.User; + if (!User) return false; + return modelClass == User || modelClass.prototype instanceof User; } /*! @@ -292,7 +296,14 @@ module.exports = function(Role) { var resolver = Role.resolvers[role]; if (resolver) { debug('Custom resolver found for role %s', role); - resolver(role, context, callback); + + var promise = resolver(role, context, callback); + if (promise && typeof promise.then === 'function') { + promise.then( + function(result) { callback(null, result); }, + callback + ); + } return; } @@ -442,4 +453,6 @@ module.exports = function(Role) { if (callback) callback(err, roles); }); }; + + Role.validatesUniquenessOf('name', { message: 'already exists' }); }; diff --git a/common/models/scope.js b/common/models/scope.js index 3c713b535..478124d2a 100644 --- a/common/models/scope.js +++ b/common/models/scope.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var assert = require('assert'); var loopback = require('../../lib/loopback'); diff --git a/common/models/user.js b/common/models/user.js index b91b3ca18..4263e354e 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module Dependencies. */ @@ -415,7 +420,7 @@ module.exports = function(User) { options.text = options.text || 'Please verify your email by opening this link in a web browser:\n\t{href}'; - options.text = options.text.replace('{href}', options.verifyHref); + options.text = options.text.replace(/\{href\}/g, options.verifyHref); options.to = options.to || user.email; diff --git a/common/models/user.json b/common/models/user.json index d70a89d3f..8ff1847b9 100644 --- a/common/models/user.json +++ b/common/models/user.json @@ -32,7 +32,7 @@ "options": { "caseSensitiveEmail": true }, - "hidden": ["password"], + "hidden": ["password", "verificationToken"], "acls": [ { "principalType": "ROLE", @@ -87,6 +87,12 @@ "permission": "ALLOW", "property": "resetPassword", "accessType": "EXECUTE" + }, + { + "principalType": "ROLE", + "principalId": "$owner", + "permission": "ALLOW", + "property": "replaceById" } ], "relations": { diff --git a/example/client-server/client.js b/example/client-server/client.js index 436e266b8..48c098f5e 100644 --- a/example/client-server/client.js +++ b/example/client-server/client.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../'); var client = loopback(); var CartItem = require('./models').CartItem; diff --git a/example/client-server/models.js b/example/client-server/models.js index 34d5c8bac..ac1b14320 100644 --- a/example/client-server/models.js +++ b/example/client-server/models.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../'); var CartItem = exports.CartItem = loopback.PersistedModel.extend('CartItem', { diff --git a/example/client-server/server.js b/example/client-server/server.js index 7e466a563..52c738d3a 100644 --- a/example/client-server/server.js +++ b/example/client-server/server.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../'); var server = module.exports = loopback(); var CartItem = require('./models').CartItem; diff --git a/example/colors/app.js b/example/colors/app.js index e182f926b..3e57b373b 100644 --- a/example/colors/app.js +++ b/example/colors/app.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../'); var app = loopback(); diff --git a/example/context/app.js b/example/context/app.js index 12cedc078..fa35eacca 100644 --- a/example/context/app.js +++ b/example/context/app.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../'); var app = loopback(); diff --git a/example/mobile-models/app.js b/example/mobile-models/app.js index e7e3c582b..abdf34c15 100644 --- a/example/mobile-models/app.js +++ b/example/mobile-models/app.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var models = require('../../lib/models'); var loopback = require('../../'); diff --git a/example/replication/app.js b/example/replication/app.js index ab6e69870..32e32299a 100644 --- a/example/replication/app.js +++ b/example/replication/app.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../'); var app = loopback(); var db = app.dataSource('db', {connector: loopback.Memory}); diff --git a/example/simple-data-source/app.js b/example/simple-data-source/app.js index 3964df77b..234baeb05 100644 --- a/example/simple-data-source/app.js +++ b/example/simple-data-source/app.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../'); var app = loopback(); diff --git a/index.js b/index.js index 1512239a7..bd558efa0 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /** * loopback ~ public api */ diff --git a/lib/access-context.js b/lib/access-context.js index 75ec50165..af04bdbba 100644 --- a/lib/access-context.js +++ b/lib/access-context.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var assert = require('assert'); var loopback = require('./loopback'); var debug = require('debug')('loopback:security:access-context'); diff --git a/lib/application.js b/lib/application.js index bd3375539..1f7b5c64a 100644 --- a/lib/application.js +++ b/lib/application.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module dependencies. */ @@ -153,6 +158,11 @@ app.model = function(Model, config) { this.emit('modelRemoted', Model.sharedClass); } + var self = this; + Model.on('remoteMethodDisabled', function(model, methodName) { + self.emit('remoteMethodDisabled', model, methodName); + }); + Model.shared = isPublic; Model.app = this; Model.emit('attached', this); @@ -219,11 +229,19 @@ app.models = function() { * @param {Object} config The data source config */ app.dataSource = function(name, config) { - var ds = dataSourcesFromConfig(name, config, this.connectors, this.registry); - this.dataSources[name] = - this.dataSources[classify(name)] = - this.dataSources[camelize(name)] = ds; - return ds; + try { + var ds = dataSourcesFromConfig(name, config, this.connectors, this.registry); + this.dataSources[name] = + this.dataSources[classify(name)] = + this.dataSources[camelize(name)] = ds; + return ds; + } catch (err) { + if (err.message) { + err.message = 'Cannot create data source ' + JSON.stringify(name) + + ': ' + err.message; + } + throw err; + } }; /** @@ -397,7 +415,7 @@ function dataSourcesFromConfig(name, config, connectorRegistry, registry) { var connectorPath; assert(typeof config === 'object', - 'cannont create data source without config object'); + 'can not create data source without config object'); if (typeof config.connector === 'string') { name = config.connector; @@ -410,6 +428,8 @@ function dataSourcesFromConfig(name, config, connectorRegistry, registry) { config.connector = require(connectorPath); } } + if (!config.connector.name) + config.connector.name = name; } return registry.createDataSource(config); @@ -551,7 +571,11 @@ app.listen = function(cb) { (arguments.length == 1 && typeof arguments[0] == 'function'); if (useAppConfig) { - server.listen(this.get('port'), this.get('host'), cb); + var port = this.get('port'); + // NOTE(bajtos) port:undefined no longer works on node@6, + // we must pass port:0 explicitly + if (port === undefined) port = 0; + server.listen(port, this.get('host'), cb); } else { server.listen.apply(server, arguments); } diff --git a/lib/browser-express.js b/lib/browser-express.js index 2a7dbe912..3b4237202 100644 --- a/lib/browser-express.js +++ b/lib/browser-express.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var EventEmitter = require('events').EventEmitter; var util = require('util'); diff --git a/lib/builtin-models.js b/lib/builtin-models.js index 78bf63990..59f0ff655 100644 --- a/lib/builtin-models.js +++ b/lib/builtin-models.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + module.exports = function(registry) { // NOTE(bajtos) we must use static require() due to browserify limitations diff --git a/lib/connectors/base-connector.js b/lib/connectors/base-connector.js index c1e37b7ba..a11dcce31 100644 --- a/lib/connectors/base-connector.js +++ b/lib/connectors/base-connector.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /** * Expose `Connector`. */ diff --git a/lib/connectors/mail.js b/lib/connectors/mail.js index a36984c39..3271c145e 100644 --- a/lib/connectors/mail.js +++ b/lib/connectors/mail.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /** * Dependencies. */ diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index 6a34417cd..f62448f61 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /** * Expose `Memory`. */ diff --git a/lib/express-middleware.js b/lib/express-middleware.js index f058a74a6..cc7596ff7 100644 --- a/lib/express-middleware.js +++ b/lib/express-middleware.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var path = require('path'); var middlewares = exports; diff --git a/lib/loopback.js b/lib/loopback.js index fd770e20d..656db41c2 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module dependencies. */ diff --git a/lib/model.js b/lib/model.js index f3ec47b25..5ffd82fb5 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module Dependencies. */ @@ -431,6 +436,7 @@ module.exports = function(registry) { Model.disableRemoteMethod = function(name, isStatic) { this.sharedClass.disableMethod(name, isStatic || false); + this.emit('remoteMethodDisabled', this.sharedClass, name); }; Model.belongsToRemoting = function(relationName, relation, define) { @@ -806,8 +812,8 @@ module.exports = function(registry) { listenerTree.before = listenerTree.before || {}; listenerTree.after = listenerTree.after || {}; - var beforeListeners = remotes.listenerTree.before[toModelName] || {}; - var afterListeners = remotes.listenerTree.after[toModelName] || {}; + var beforeListeners = listenerTree.before[toModelName] || {}; + var afterListeners = listenerTree.after[toModelName] || {}; sharedClass.methods().forEach(function(method) { var delegateTo = method.rest && method.rest.delegateTo; diff --git a/lib/persisted-model.js b/lib/persisted-model.js index b3bb62281..e7eb46051 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module Dependencies. */ @@ -111,17 +116,46 @@ module.exports = function(registry) { }; /** - * Find one record matching the optional `where` filter. The same as `find`, but limited to one object. - * Returns an object, not collection. - * If not found, create the object using data provided as second argument. + * Replace or insert a model instance + * @param {Object} data The model instance data to insert. + * @callback {Function} callback Callback function called with `cb(err, obj)` signature. + * @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object). + * @param {Object} model Replaced model instance. + */ + + PersistedModel.replaceOrCreate = function replaceOrCreate(data, cb) { + throwNotAttached(this.modelName, 'replaceOrCreate'); + }; + + /** + * Finds one record matching the optional filter object. If not found, creates + * the object using the data provided as second argument. In this sense it is + * the same as `find`, but limited to one object. Returns an object, not + * collection. If you don't provide the filter object argument, it tries to + * locate an existing object that matches the `data` argument. * - * @param {Object} where Where clause, such as `{test: 'me'}` - *
see - * [Where filter](https://docs.strongloop.com/display/LB/Where+filter#Wherefilter-Whereclauseforothermethods). + * @options {Object} [filter] Optional Filter object; see below. + * @property {String|Object|Array} fields Identify fields to include in return result. + *
See [Fields filter](http://docs.strongloop.com/display/LB/Fields+filter). + * @property {String|Object|Array} include See PersistedModel.include documentation. + *
See [Include filter](http://docs.strongloop.com/display/LB/Include+filter). + * @property {Number} limit Maximum number of instances to return. + *
See [Limit filter](http://docs.strongloop.com/display/LB/Limit+filter). + * @property {String} order Sort order: either "ASC" for ascending or "DESC" for descending. + *
See [Order filter](http://docs.strongloop.com/display/LB/Order+filter). + * @property {Number} skip Number of results to skip. + *
See [Skip filter](http://docs.strongloop.com/display/LB/Skip+filter). + * @property {Object} where Where clause, like + * ``` + * {where: {key: val, key2: {gt: val2}, ...}} + * ``` + *
See + * [Where filter](https://docs.strongloop.com/display/LB/Where+filter#Wherefilter-Whereclauseforqueries). * @param {Object} data Data to insert if object matching the `where` filter is not found. - * @callback {Function} callback Callback function called with `cb(err, instance)` arguments. Required. + * @callback {Function} callback Callback function called with `cb(err, instance, created)` arguments. Required. * @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object). * @param {Object} instance Model instance matching the `where` filter, if found. + * @param {Boolean} created True if the instance matching the `where` filter was created. */ PersistedModel.findOrCreate = function findOrCreate(query, data, callback) { @@ -460,6 +494,19 @@ module.exports = function(registry) { throwNotAttached(this.modelName, 'updateAttributes'); }; + /** + * Replace set of attributes. Performs validation before replacing. + * + * @param {Object} data Data to replace. + * @callback {Function} callback Callback function called with `(err, instance)` arguments. Required. + * @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object). + * @param {Object} instance Repalced instance. + */ + + PersistedModel.replaceById = function replaceById(data, cb) { + throwNotAttached(this.modelName, 'replaceById'); + }; + /** * Reload object from persistence. Requires `id` member of `object` to be able to call `find`. * @callback {Function} callback Callback function called with `(err, instance)` arguments. Required. @@ -527,6 +574,21 @@ module.exports = function(registry) { var PersistedModel = this; var typeName = PersistedModel.modelName; var options = PersistedModel.settings; + // This is the defualt verb used for 2.X + var configurableVerb = { + updateAttributes: 'put', + updateOrCreate: 'put', + replaceOrCreate: 'patch', + replaceById: 'patch', + }; + + // we check options.newMapping when backporting to `2.x`. Thought? + if (options.newMapping) { + configurableVerb.replaceById = 'put'; + configurableVerb.replaceOrCreate = 'put'; + configurableVerb.updateAttributes = 'patch'; + configurableVerb.updateOrCreate = 'patch'; + } function setRemoting(scope, name, options) { var fn = scope[name]; @@ -549,7 +611,16 @@ module.exports = function(registry) { accessType: 'WRITE', accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}}, returns: {arg: 'data', type: typeName, root: true}, - http: {verb: 'put', path: '/'} + http: { verb: configurableVerb.updateOrCreate, path: '/' }, + }); + + setRemoting(PersistedModel, 'replaceOrCreate', { + description: 'Replace an existing model instance or insert a new one into the data source.', + accessType: 'WRITE', + accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description: + 'Model instance data' }, + returns: { arg: 'data', type: typeName, root: true }, + http: { verb: configurableVerb.replaceOrCreate, path: '/' }, }); setRemoting(PersistedModel, 'exists', { @@ -597,6 +668,20 @@ module.exports = function(registry) { rest: {after: convertNullToNotFoundError} }); + setRemoting(PersistedModel, 'replaceById', { + description: 'Replace attributes for a model instance and persist it into the data source.', + accessType: 'WRITE', + accepts: [ + { arg: 'id', type: 'any', description: 'Model id', required: true, + http: { source: 'put' }}, + { arg: 'data', type: 'object', http: { source: 'body' }, description: + 'An object of model property name/value pairs' }, + ], + returns: { arg: 'data', type: typeName, root: true }, + http: { verb: configurableVerb.replaceById, path: '/:id' }, + rest: { after: convertNullToNotFoundError }, + }); + setRemoting(PersistedModel, 'find', { description: 'Find all instances of the model matched by filter from the data source.', accessType: 'READ', @@ -670,7 +755,7 @@ module.exports = function(registry) { accessType: 'WRITE', accepts: {arg: 'data', type: 'object', http: {source: 'body'}, description: 'An object of model property name/value pairs'}, returns: {arg: 'data', type: typeName, root: true}, - http: {verb: 'put', path: '/'} + http: {verb: configurableVerb.updateAttributes, path: '/'} }); if (options.trackChanges || options.enableRemoteReplication) { @@ -882,12 +967,7 @@ module.exports = function(registry) { PersistedModel.checkpoint = function(cb) { var Checkpoint = this.getChangeModel().getCheckpointModel(); - this.getSourceId(function(err, sourceId) { - if (err) return cb(err); - Checkpoint.create({ - sourceId: sourceId - }, cb); - }); + Checkpoint.bumpLastSeq(cb); }; /** diff --git a/lib/registry.js b/lib/registry.js index ade0e2e88..ccce6248f 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var assert = require('assert'); var extend = require('util')._extend; var juggler = require('loopback-datasource-juggler'); diff --git a/lib/runtime.js b/lib/runtime.js index 7e791f5b1..8799447bb 100644 --- a/lib/runtime.js +++ b/lib/runtime.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /* * This is an internal file that should not be used outside of loopback. * All exported entities can be accessed via the `loopback` object. diff --git a/lib/server-app.js b/lib/server-app.js index 237a62540..290c9b5ac 100644 --- a/lib/server-app.js +++ b/lib/server-app.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var assert = require('assert'); var express = require('express'); var merge = require('util')._extend; diff --git a/lib/utils.js b/lib/utils.js index 306a1764a..555a18616 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + exports.createPromiseCallback = createPromiseCallback; function createPromiseCallback() { diff --git a/package.json b/package.json index bccf39157..2eb05c8b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback", - "version": "2.26.2", + "version": "2.28.0", "description": "LoopBack: Open Source Framework for Node.js", "homepage": "http://loopback.io", "keywords": [ @@ -78,13 +78,15 @@ "karma-html2js-preprocessor": "^0.1.0", "karma-junit-reporter": "^0.2.2", "karma-mocha": "^0.1.10", - "karma-phantomjs-launcher": "^0.1.4", + "karma-phantomjs-launcher": "^1.0.0", "karma-script-launcher": "^0.1.0", "loopback-boot": "^2.7.0", "loopback-datasource-juggler": "^2.19.1", "loopback-testing": "~1.1.0", "mocha": "^2.1.0", + "phantomjs-prebuilt": "^2.1.7", "sinon": "^1.13.0", + "sinon-chai": "^2.8.0", "strong-task-emitter": "^0.0.6", "supertest": "^0.15.0" }, @@ -102,8 +104,10 @@ "depd": "loopback-datasource-juggler/lib/browser.depd.js", "bcrypt": false }, - "license": "MIT", - "optionalDependencies": { - "sl-blip": "http://blip.strongloop.com/loopback@2.26.2" - } + "config": { + "ci": { + "debug": "*,-mocha:*,-eslint:*" + } + }, + "license": "MIT" } diff --git a/server/current-context.js b/server/current-context.js index 6b8304e22..4da00bf4c 100644 --- a/server/current-context.js +++ b/server/current-context.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var juggler = require('loopback-datasource-juggler'); var remoting = require('strong-remoting'); var cls = require('continuation-local-storage'); diff --git a/server/middleware/context.js b/server/middleware/context.js index 95352018f..73948bd76 100644 --- a/server/middleware/context.js +++ b/server/middleware/context.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../../lib/loopback'); module.exports = context; diff --git a/server/middleware/error-handler.js b/server/middleware/error-handler.js index c549944bf..1d30ae289 100644 --- a/server/middleware/error-handler.js +++ b/server/middleware/error-handler.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var expressErrorHandler = require('errorhandler'); expressErrorHandler.title = 'Loopback'; diff --git a/server/middleware/favicon.js b/server/middleware/favicon.js index d2e1fa40d..b5cf10cec 100644 --- a/server/middleware/favicon.js +++ b/server/middleware/favicon.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /** * Serve the LoopBack favicon. * @header loopback.favicon() diff --git a/server/middleware/rest.js b/server/middleware/rest.js index 9c7e23a28..2ec5a200d 100644 --- a/server/middleware/rest.js +++ b/server/middleware/rest.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module dependencies. */ diff --git a/server/middleware/static.js b/server/middleware/static.js index c01a538df..6f253dfa5 100644 --- a/server/middleware/static.js +++ b/server/middleware/static.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /** * Serve static assets of a LoopBack application. * diff --git a/server/middleware/status.js b/server/middleware/status.js index 3e9308115..f064a9d83 100644 --- a/server/middleware/status.js +++ b/server/middleware/status.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Export the middleware. */ diff --git a/server/middleware/token.js b/server/middleware/token.js index e80eb560b..b5038df23 100644 --- a/server/middleware/token.js +++ b/server/middleware/token.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Module dependencies. */ @@ -62,6 +67,8 @@ function escapeRegExp(str) { * @property {Array} [headers] Array of header names. * @property {Array} [params] Array of param names. * @property {Boolean} [searchDefaultTokenKeys] Use the default search locations for Token in request + * @property {Boolean} [enableDoublecheck] Execute middleware although an instance mounted earlier in the chain didn't find a token + * @property {Boolean} [overwriteExistingToken] only has effect in combination with `enableDoublecheck`. If truthy, will allow to overwrite an existing accessToken. * @property {Function|String} [model] AccessToken model name or class to use. * @property {String} [currentUserLiteral] String literal for the current user. * @header loopback.token([options]) @@ -80,6 +87,9 @@ function token(options) { currentUserLiteral = escapeRegExp(currentUserLiteral); } + var enableDoublecheck = !!options.enableDoublecheck; + var overwriteExistingToken = !!options.overwriteExistingToken; + return function(req, res, next) { var app = req.app; var registry = app.registry; @@ -97,8 +107,19 @@ function token(options) { 'loopback.token() middleware requires a AccessToken model'); if (req.accessToken !== undefined) { - rewriteUserLiteral(req, currentUserLiteral); - return next(); + if (!enableDoublecheck) { + // req.accessToken is defined already (might also be "null" or "false") and enableDoublecheck + // has not been set --> skip searching for credentials + rewriteUserLiteral(req, currentUserLiteral); + return next(); + } + if (req.accessToken && req.accessToken.id && !overwriteExistingToken) { + // req.accessToken.id is defined, which means that some other middleware has identified a valid user. + // when overwriteExistingToken is not set to a truthy value, skip searching for credentials. + rewriteUserLiteral(req, currentUserLiteral); + return next(); + } + // continue normal operation (as if req.accessToken was undefined) } TokenModel.findForRequest(req, options, function(err, token) { req.accessToken = token || null; diff --git a/server/middleware/url-not-found.js b/server/middleware/url-not-found.js index dd696d79f..d204f1575 100644 --- a/server/middleware/url-not-found.js +++ b/server/middleware/url-not-found.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*! * Export the middleware. * See discussion in Connect pull request #954 for more details diff --git a/test/access-control.integration.js b/test/access-control.integration.js index 3125a6535..b32152573 100644 --- a/test/access-control.integration.js +++ b/test/access-control.integration.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*jshint -W030 */ var loopback = require('../'); @@ -11,6 +16,12 @@ var CURRENT_USER = {email: 'current@test.test', password: 'test'}; var debug = require('debug')('loopback:test:access-control.integration'); describe('access control - integration', function() { + before(function(done) { + if (app.booting) { + return app.once('booted', done); + } + done(); + }); lt.beforeEach.withApp(app); @@ -126,6 +137,7 @@ describe('access control - integration', function() { var userCounter; function newUserData() { userCounter = userCounter ? ++userCounter : 1; + return { email: 'new-' + userCounter + '@test.test', password: 'test' @@ -203,6 +215,7 @@ describe('access control - integration', function() { balance: 100 }, function(err, act) { self.url = '/api/accounts/' + act.id; + done(); }); diff --git a/test/access-token.test.js b/test/access-token.test.js index 9e57cba2a..573cae5a4 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../'); var extend = require('util')._extend; var Token = loopback.AccessToken.extend('MyToken'); @@ -65,7 +70,7 @@ describe('loopback.token(options)', function() { .end(done); }); - describe('populating req.toen from HTTP Basic Auth formatted authorization header', function() { + describe('populating req.token from HTTP Basic Auth formatted authorization header', function() { it('parses "standalone-token"', function(done) { var token = this.token.id; token = 'Basic ' + new Buffer(token).toString('base64'); @@ -144,7 +149,8 @@ describe('loopback.token(options)', function() { .set('authorization', id) .end(function(err, res) { assert(!err); - assert.deepEqual(res.body, {userId: userId}); + assert.deepEqual(res.body, { userId: userId }); + done(); }); }); @@ -159,7 +165,8 @@ describe('loopback.token(options)', function() { .set('authorization', id) .end(function(err, res) { assert(!err); - assert.deepEqual(res.body, {userId: userId, state: 1}); + assert.deepEqual(res.body, { userId: userId, state: 1 }); + done(); }); }); @@ -174,7 +181,8 @@ describe('loopback.token(options)', function() { .set('authorization', id) .end(function(err, res) { assert(!err); - assert.deepEqual(res.body, {userId: userId, state: 1}); + assert.deepEqual(res.body, { userId: userId, state: 1 }); + done(); }); }); @@ -183,6 +191,7 @@ describe('loopback.token(options)', function() { var tokenStub = { id: 'stub id' }; app.use(function(req, res, next) { req.accessToken = tokenStub; + next(); }); app.use(loopback.token({ model: Token })); @@ -195,10 +204,105 @@ describe('loopback.token(options)', function() { .expect(200) .end(function(err, res) { if (err) return done(err); + expect(res.body).to.eql(tokenStub); + done(); }); }); + + describe('loading multiple instances of token middleware', function() { + it('should skip when req.token is already present and no further options are set', + function(done) { + var tokenStub = { id: 'stub id' }; + app.use(function(req, res, next) { + req.accessToken = tokenStub; + + next(); + }); + app.use(loopback.token({ model: Token })); + app.get('/', function(req, res, next) { + res.send(req.accessToken); + }); + + request(app).get('/') + .set('Authorization', this.token.id) + .expect(200) + .end(function(err, res) { + if (err) return done(err); + + expect(res.body).to.eql(tokenStub); + + done(); + }); + }); + + it('should not overwrite valid existing token (has "id" property) ' + + ' when overwriteExistingToken is falsy', + function(done) { + var tokenStub = { id: 'stub id' }; + app.use(function(req, res, next) { + req.accessToken = tokenStub; + + next(); + }); + app.use(loopback.token({ + model: Token, + enableDoublecheck: true, + })); + app.get('/', function(req, res, next) { + res.send(req.accessToken); + }); + + request(app).get('/') + .set('Authorization', this.token.id) + .expect(200) + .end(function(err, res) { + if (err) return done(err); + + expect(res.body).to.eql(tokenStub); + + done(); + }); + }); + + it('should overwrite existing token when enableDoublecheck ' + + 'and overwriteExistingToken options are truthy', + function(done) { + var token = this.token; + var tokenStub = { id: 'stub id' }; + + app.use(function(req, res, next) { + req.accessToken = tokenStub; + + next(); + }); + app.use(loopback.token({ + model: Token, + enableDoublecheck: true, + overwriteExistingToken: true, + })); + app.get('/', function(req, res, next) { + res.send(req.accessToken); + }); + + request(app).get('/') + .set('Authorization', token.id) + .expect(200) + .end(function(err, res) { + if (err) return done(err); + + expect(res.body).to.eql({ + id: token.id, + ttl: token.ttl, + userId: token.userId, + created: token.created.toJSON(), + }); + + done(); + }); + }); + }); }); describe('AccessToken', function() { @@ -217,6 +321,7 @@ describe('AccessToken', function() { it('should be validateable', function(done) { this.token.validate(function(err, isValid) { assert(isValid); + done(); }); }); @@ -232,7 +337,9 @@ describe('AccessToken', function() { Token.findForRequest(req, function(err, token) { if (err) return done(err); + expect(token.id).to.eql(expectedTokenId); + done(); }); }); @@ -266,9 +373,11 @@ describe('app.enableAuth()', function() { if (err) { return done(err); } + var errorResponse = res.body.error; assert(errorResponse); assert.equal(errorResponse.code, 'AUTHORIZATION_REQUIRED'); + done(); }); }); @@ -282,9 +391,11 @@ describe('app.enableAuth()', function() { if (err) { return done(err); } + var errorResponse = res.body.error; assert(errorResponse); assert.equal(errorResponse.code, 'ACCESS_DENIED'); + done(); }); }); @@ -298,9 +409,11 @@ describe('app.enableAuth()', function() { if (err) { return done(err); } + var errorResponse = res.body.error; assert(errorResponse); assert.equal(errorResponse.code, 'MODEL_NOT_FOUND'); + done(); }); }); @@ -314,9 +427,11 @@ describe('app.enableAuth()', function() { if (err) { return done(err); } + var errorResponse = res.body.error; assert(errorResponse); assert.equal(errorResponse.code, 'AUTHORIZATION_REQUIRED'); + done(); }); }); @@ -347,7 +462,9 @@ describe('app.enableAuth()', function() { .expect('Content-Type', /json/) .end(function(err, res) { if (err) return done(err); + expect(res.body.token.id).to.eql(token.id); + done(); }); }); @@ -357,7 +474,9 @@ function createTestingToken(done) { var test = this; Token.create({userId: '123'}, function(err, token) { if (err) return done(err); + test.token = token; + done(); }); } diff --git a/test/acl.test.js b/test/acl.test.js index d8706eec3..3022ebe6b 100644 --- a/test/acl.test.js +++ b/test/acl.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var assert = require('assert'); var loopback = require('../index'); var Scope = loopback.Scope; @@ -382,7 +387,9 @@ describe('access check', function() { MyTestModel.beforeRemote('find', function(ctx, next) { // ensure this is called after checkAccess if (!checkAccessCalled) return done(new Error('incorrect order')); + beforeHookCalled = true; + next(); }); @@ -391,6 +398,7 @@ describe('access check', function() { .end(function(err, result) { assert(beforeHookCalled, 'the before hook should be called'); assert(checkAccessCalled, 'checkAccess should have been called'); + done(); }); }); diff --git a/test/app.test.js b/test/app.test.js index 2701ef9d2..fd8d03735 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + /*jshint -W030 */ var async = require('async'); @@ -34,10 +39,12 @@ describe('app', function() { executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql([ 'initial', 'session', 'auth', 'parse', 'main', 'routes', 'files', 'final' ]); + done(); }); }); @@ -48,7 +55,9 @@ describe('app', function() { executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql(['first', 'second']); + done(); }); }); @@ -60,7 +69,9 @@ describe('app', function() { executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql(['routes:before', 'main', 'routes:after']); + done(); }); }); @@ -80,7 +91,9 @@ describe('app', function() { expect(found).have.property('phase', 'routes:before'); executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql(['my-handler', 'extra-handler']); + done(); }); }); @@ -98,7 +111,9 @@ describe('app', function() { expect(found).have.property('phase', 'routes:before'); executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql(['my-handler']); + done(); }); }); @@ -116,7 +131,9 @@ describe('app', function() { expect(found).have.property('phase', 'routes:before'); executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql(['my-handler']); + done(); }); }); @@ -126,6 +143,7 @@ describe('app', function() { app.middleware('initial', function(req, res, next) { steps.push('initial'); + next(expectedError); }); @@ -133,12 +151,15 @@ describe('app', function() { app.use(function errorHandler(err, req, res, next) { expect(err).to.equal(expectedError); steps.push('error'); + next(); }); executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql(['initial', 'error']); + done(); }); }); @@ -152,6 +173,7 @@ describe('app', function() { executeMiddlewareHandlers(app, function(err) { expect(err).to.equal(expectedError); + done(); }); }); @@ -170,12 +192,15 @@ describe('app', function() { app.middleware('initial', function(err, req, res, next) { handledError = err; + next(); }); executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(handledError).to.equal(expectedError); + done(); }); }); @@ -188,7 +213,9 @@ describe('app', function() { function(url, next) { executeMiddlewareHandlers(app, url, next); }, function(err) { if (err) return done(err); + expect(steps).to.eql(['/scope', '/scope/item']); + done(); }); }); @@ -201,7 +228,9 @@ describe('app', function() { function(url, next) { executeMiddlewareHandlers(app, url, next); }, function(err) { if (err) return done(err); + expect(steps).to.eql(['/a', '/b']); + done(); }); }); @@ -214,7 +243,9 @@ describe('app', function() { function(url, next) { executeMiddlewareHandlers(app, url, next); }, function(err) { if (err) return done(err); + expect(steps).to.eql(['/a', '/b', '/scope']); + done(); }); }); @@ -222,12 +253,15 @@ describe('app', function() { it('sets req.url to a sub-path', function(done) { app.middleware('initial', ['/scope'], function(req, res, next) { steps.push(req.url); + next(); }); executeMiddlewareHandlers(app, '/scope/id', function(err) { if (err) return done(err); + expect(steps).to.eql(['/id']); + done(); }); }); @@ -239,11 +273,13 @@ describe('app', function() { app.middleware('initial', function(rq, rs, next) { req = rq; res = rs; + next(); }); executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(getObjectAndPrototypeKeys(req), 'request').to.include.members([ 'accepts', 'get', @@ -273,12 +309,15 @@ describe('app', function() { var reqProps; app.middleware('initial', function(req, res, next) { reqProps = { baseUrl: req.baseUrl, originalUrl: req.originalUrl }; + next(); }); executeMiddlewareHandlers(app, '/test/url', function(err) { if (err) return done(err); + expect(reqProps).to.eql({ baseUrl: '', originalUrl: '/test/url' }); + done(); }); }); @@ -290,7 +329,9 @@ describe('app', function() { executeMiddlewareHandlers(app, '/test', function(err) { if (err) return done(err); + expect(steps).to.eql(['route', 'files']); + done(); }); }); @@ -310,7 +351,9 @@ describe('app', function() { executeMiddlewareHandlers(app, function(err) { if (err) return done; + expect(steps).to.eql(numbers); + done(); }); }); @@ -324,6 +367,7 @@ describe('app', function() { mountpath: req.app.mountpath, parent: req.app.parent }; + next(); }); subapp.on('mount', function() { mountWasEmitted = true; }); @@ -332,11 +376,13 @@ describe('app', function() { executeMiddlewareHandlers(app, '/mountpath/test', function(err) { if (err) return done(err); + expect(mountWasEmitted, 'mountWasEmitted').to.be.true; expect(data).to.eql({ mountpath: '/mountpath', parent: app }); + done(); }); }); @@ -350,25 +396,30 @@ describe('app', function() { subapp.use(function verifyTestAssumptions(req, res, next) { expect(req.__proto__).to.not.equal(expected.req); expect(res.__proto__).to.not.equal(expected.res); + next(); }); app.middleware('initial', function saveOriginalValues(req, res, next) { expected.req = req.__proto__; expected.res = res.__proto__; + next(); }); app.middleware('routes', subapp); app.middleware('final', function saveActualValues(req, res, next) { actual.req = req.__proto__; actual.res = res.__proto__; + next(); }); executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(actual.req, 'req').to.equal(expected.req); expect(actual.res, 'res').to.equal(expected.res); + done(); }); }); @@ -383,6 +434,7 @@ describe('app', function() { function pathSavingHandler() { return function(req, res, next) { steps.push(req.originalUrl); + next(); }; } @@ -406,6 +458,7 @@ describe('app', function() { var args = Array.prototype.slice.apply(arguments); return function(req, res, next) { steps.push(args); + next(); }; }; @@ -456,12 +509,14 @@ describe('app', function() { executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql([ ['before'], [expectedConfig], ['after', 2], [{x: 1}] ]); + done(); }); }); @@ -472,6 +527,7 @@ describe('app', function() { function factory() { return function(req, res, next) { steps.push(req.originalUrl); + next(); }; }, @@ -485,7 +541,9 @@ describe('app', function() { function(url, next) { executeMiddlewareHandlers(app, url, next); }, function(err) { if (err) return done(err); + expect(steps).to.eql(['/a', '/b', '/scope']); + done(); }); }); @@ -542,13 +600,16 @@ describe('app', function() { names.forEach(function(it) { app.middleware(it, function(req, res, next) { steps.push(it); + next(); }); }); executeMiddlewareHandlers(app, function(err) { if (err) return done(err); + expect(steps).to.eql(names); + done(); }); } @@ -606,6 +667,22 @@ describe('app', function() { expect(remotedClass).to.eql(Color.sharedClass); }); + it('emits a `remoteMethodDisabled` event', function() { + var Color = PersistedModel.extend('color', { name: String }); + Color.shared = true; + var remoteMethodDisabledClass, disabledRemoteMethod; + app.on('remoteMethodDisabled', function(sharedClass, methodName) { + remoteMethodDisabledClass = sharedClass; + disabledRemoteMethod = methodName; + }); + app.model(Color); + app.models.Color.disableRemoteMethod('findOne'); + expect(remoteMethodDisabledClass).to.exist; + expect(remoteMethodDisabledClass).to.eql(Color.sharedClass); + expect(disabledRemoteMethod).to.exist; + expect(disabledRemoteMethod).to.eql('findOne'); + }); + it.onServer('updates REST API when a new model is added', function(done) { app.use(loopback.rest()); request(app).get('/colors').expect(404, function(err, res) { @@ -732,6 +809,16 @@ describe('app', function() { app.dataSource('custom', { connector: 'custom' }); expect(app.dataSources.custom.name).to.equal(loopback.Memory.name); }); + + it('adds data source name to error messages', function() { + app.connector('throwing', { + initialize: function() { throw new Error('expected test error'); }, + }); + + expect(function() { + app.dataSource('bad-ds', { connector: 'throwing' }); + }).to.throw(/bad-ds.*throwing/); + }); }); describe.onServer('listen()', function() { @@ -755,6 +842,7 @@ describe('app', function() { app.listen(function() { expect(app.get('port'), 'port').to.not.equal(0); + done(); }); }); @@ -768,6 +856,7 @@ describe('app', function() { var host = process.platform === 'win32' ? 'localhost' : app.get('host'); var expectedUrl = 'http://' + host + ':' + app.get('port') + '/'; expect(app.get('url'), 'url').to.equal(expectedUrl); + done(); }); }); @@ -778,6 +867,7 @@ describe('app', function() { app.listen(0, '127.0.0.1', function() { expect(app.get('port'), 'port').to.not.equal(0).and.not.equal(1); expect(this.address().address).to.equal('127.0.0.1'); + done(); }); }); @@ -788,6 +878,7 @@ describe('app', function() { app.set('port', 1); app.listen(0).on('listening', function() { expect(app.get('port'), 'port') .to.not.equal(0).and.not.equal(1); + done(); }); } @@ -802,6 +893,7 @@ describe('app', function() { app.listen() .on('listening', function() { expect(this.address().address).to.equal('127.0.0.1'); + done(); }); }); diff --git a/test/change-stream.test.js b/test/change-stream.test.js index ab7405214..cbd24ca23 100644 --- a/test/change-stream.test.js +++ b/test/change-stream.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + describe('PersistedModel.createChangeStream()', function() { describe('configured to source changes locally', function() { before(function() { @@ -17,6 +22,7 @@ describe('PersistedModel.createChangeStream()', function() { changes.on('data', function(change) { expect(change.type).to.equal('create'); changes.destroy(); + done(); }); @@ -31,6 +37,7 @@ describe('PersistedModel.createChangeStream()', function() { changes.on('data', function(change) { expect(change.type).to.equal('update'); changes.destroy(); + done(); }); newScore.updateAttributes({ @@ -47,6 +54,7 @@ describe('PersistedModel.createChangeStream()', function() { changes.on('data', function(change) { expect(change.type).to.equal('remove'); changes.destroy(); + done(); }); diff --git a/test/change.test.js b/test/change.test.js index 55d551c72..220d8055f 100644 --- a/test/change.test.js +++ b/test/change.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var async = require('async'); var expect = require('chai').expect; @@ -28,9 +33,11 @@ describe('Change', function() { }; TestModel.create(test.data, function(err, model) { if (err) return done(err); + test.model = model; test.modelId = model.id; test.revisionForModel = Change.revisionForInst(model); + done(); }); }); @@ -61,6 +68,7 @@ describe('Change', function() { var test = this; Change.rectifyModelChanges(this.modelName, [this.modelId], function(err, trackedChanges) { if (err) return done(err); + done(); }); }); @@ -69,6 +77,7 @@ describe('Change', function() { var test = this; Change.find(function(err, trackedChanges) { assert.equal(trackedChanges[0].modelId, test.modelId.toString()); + done(); }); }); @@ -76,12 +85,47 @@ describe('Change', function() { it('should only create one change', function(done) { Change.count(function(err, count) { assert.equal(count, 1); + done(); }); }); }); }); + describe('Change.rectifyModelChanges - promise variant', function() { + describe('using an existing untracked model', function() { + beforeEach(function(done) { + var test = this; + Change.rectifyModelChanges(this.modelName, [this.modelId]) + .then(function(trackedChanges) { + done(); + }) + .catch(done); + }); + + it('should create an entry', function(done) { + var test = this; + Change.find() + .then(function(trackedChanges) { + assert.equal(trackedChanges[0].modelId, test.modelId.toString()); + + done(); + }) + .catch(done); + }); + + it('should only create one change', function(done) { + Change.count() + .then(function(count) { + assert.equal(count, 1); + + done(); + }) + .catch(done); + }); + }); + }); + describe('Change.findOrCreateChange(modelName, modelId, callback)', function() { describe('when a change doesnt exist', function() { @@ -89,7 +133,9 @@ describe('Change', function() { var test = this; Change.findOrCreateChange(this.modelName, this.modelId, function(err, result) { if (err) return done(err); + test.result = result; + done(); }); }); @@ -98,7 +144,33 @@ describe('Change', function() { var test = this; Change.findById(this.result.id, function(err, change) { if (err) return done(err); + assert.equal(change.id, test.result.id); + + done(); + }); + }); + }); + + describe('when a change doesnt exist - promise variant', function() { + beforeEach(function(done) { + var test = this; + Change.findOrCreateChange(this.modelName, this.modelId) + .then(function(result) { + test.result = result; + + done(); + }) + .catch(done); + }); + + it('should create an entry', function(done) { + var test = this; + Change.findById(this.result.id, function(err, change) { + if (err) return done(err); + + assert.equal(change.id, test.result.id); + done(); }); }); @@ -112,6 +184,7 @@ describe('Change', function() { modelId: test.modelId }, function(err, change) { test.existingChange = change; + done(); }); }); @@ -120,7 +193,9 @@ describe('Change', function() { var test = this; Change.findOrCreateChange(this.modelName, this.modelId, function(err, result) { if (err) return done(err); + test.result = result; + done(); }); }); @@ -128,6 +203,7 @@ describe('Change', function() { it('should find the entry', function(done) { var test = this; assert.equal(test.existingChange.id, test.result.id); + done(); }); }); @@ -143,6 +219,7 @@ describe('Change', function() { }, function(err, ch) { change = ch; + done(err); }); }); @@ -151,6 +228,7 @@ describe('Change', function() { var test = this; change.rectify(function(err, ch) { assert.equal(ch.rev, test.revisionForModel); + done(); }); }); @@ -174,6 +252,7 @@ describe('Change', function() { expect(change.type(), 'type').to.equal('update'); expect(change.prev, 'prev').to.equal(originalRev); expect(change.rev, 'rev').to.equal(test.revisionForModel); + next(); } ], done); @@ -185,7 +264,9 @@ describe('Change', function() { function checkpoint(next) { TestModel.checkpoint(function(err, inst) { if (err) return next(err); + cp = inst.seq; + next(); }); } @@ -196,6 +277,7 @@ describe('Change', function() { model.name += 'updated'; model.save(function(err) { test.revisionForModel = Change.revisionForInst(model); + next(err); }); } @@ -211,14 +293,40 @@ describe('Change', function() { change.rectify(function(err, c) { if (err) return done(err); + expect(c.rev, 'rev').to.equal(originalRev); // sanity check expect(c.checkpoint, 'checkpoint').to.equal(originalCheckpoint); + done(); }); }); }); }); + describe('change.rectify - promise variant', function() { + var change; + beforeEach(function(done) { + Change.findOrCreateChange(this.modelName, this.modelId) + .then(function(ch) { + change = ch; + + done(); + }) + .catch(done); + }); + + it('should create a new change with the correct revision', function(done) { + var test = this; + change.rectify() + .then(function(ch) { + assert.equal(ch.rev, test.revisionForModel); + + done(); + }) + .catch(done); + }); + }); + describe('change.currentRevision(callback)', function() { it('should get the correct revision', function(done) { var test = this; @@ -229,11 +337,30 @@ describe('Change', function() { change.currentRevision(function(err, rev) { assert.equal(rev, test.revisionForModel); + done(); }); }); }); + describe('change.currentRevision - promise variant', function() { + it('should get the correct revision', function(done) { + var test = this; + var change = new Change({ + modelName: this.modelName, + modelId: this.modelId + }); + + change.currentRevision() + .then(function(rev) { + assert.equal(rev, test.revisionForModel); + + done(); + }) + .catch(done); + }); + }); + describe('Change.hash(str)', function() { // todo(ritch) test other hashing algorithms it('should hash the given string', function() { @@ -368,12 +495,34 @@ describe('Change', function() { Change.diff(this.modelName, 0, remoteChanges, function(err, diff) { if (err) return done(err); + assert.equal(diff.deltas.length, 1); assert.equal(diff.conflicts.length, 1); + done(); }); }); + it('should return delta and conflict lists - promise variant', function(done) { + var remoteChanges = [ + // an update => should result in a delta + {rev: 'foo2', prev: 'foo', modelName: this.modelName, modelId: 9, checkpoint: 1}, + // no change => should not result in a delta / conflict + {rev: 'bar', prev: 'bar', modelName: this.modelName, modelId: 10, checkpoint: 1}, + // a conflict => should result in a conflict + {rev: 'bat2', prev: 'bat0', modelName: this.modelName, modelId: 11, checkpoint: 1}, + ]; + + Change.diff(this.modelName, 0, remoteChanges) + .then(function(diff) { + assert.equal(diff.deltas.length, 1); + assert.equal(diff.conflicts.length, 1); + + done(); + }) + .catch(done); + }); + it('should set "prev" to local revision in non-conflicting delta', function(done) { var updateRecord = { rev: 'foo-new', @@ -384,6 +533,7 @@ describe('Change', function() { }; Change.diff(this.modelName, 0, [updateRecord], function(err, diff) { if (err) return done(err); + expect(diff.conflicts, 'conflicts').to.have.length(0); expect(diff.deltas, 'deltas').to.have.length(1); var actual = diff.deltas[0].toObject(); @@ -395,6 +545,7 @@ describe('Change', function() { prev: 'foo', // this is the current local revision rev: 'foo-new', }); + done(); }); }); @@ -411,6 +562,7 @@ describe('Change', function() { // with rev=foo CP=1 Change.diff(this.modelName, 2, [updateRecord], function(err, diff) { if (err) return done(err); + expect(diff.conflicts, 'conflicts').to.have.length(0); expect(diff.deltas, 'deltas').to.have.length(1); var actual = diff.deltas[0].toObject(); @@ -422,6 +574,7 @@ describe('Change', function() { prev: 'foo', // this is the current local revision rev: 'foo-new', }); + done(); }); }); @@ -437,6 +590,7 @@ describe('Change', function() { Change.diff(this.modelName, 0, [updateRecord], function(err, diff) { if (err) return done(err); + expect(diff.conflicts).to.have.length(0); expect(diff.deltas).to.have.length(1); var actual = diff.deltas[0].toObject(); @@ -448,6 +602,7 @@ describe('Change', function() { prev: null, // this is the current local revision rev: 'new-rev', }); + done(); }); }); diff --git a/test/checkpoint.test.js b/test/checkpoint.test.js index 98e248efb..b5b9093ae 100644 --- a/test/checkpoint.test.js +++ b/test/checkpoint.test.js @@ -1,28 +1,98 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var async = require('async'); var loopback = require('../'); +var expect = require('chai').expect; -// create a unique Checkpoint model var Checkpoint = loopback.Checkpoint.extend('TestCheckpoint'); -var memory = loopback.createDataSource({ - connector: loopback.Memory -}); -Checkpoint.attachTo(memory); - describe('Checkpoint', function() { - describe('current()', function() { + describe('bumpLastSeq() and current()', function() { + beforeEach(function() { + var memory = loopback.createDataSource({ + connector: loopback.Memory + }); + Checkpoint.attachTo(memory); + }); + it('returns the highest `seq` value', function(done) { async.series([ - Checkpoint.create.bind(Checkpoint), - Checkpoint.create.bind(Checkpoint), + Checkpoint.bumpLastSeq.bind(Checkpoint), + Checkpoint.bumpLastSeq.bind(Checkpoint), function(next) { Checkpoint.current(function(err, seq) { if (err) next(err); + expect(seq).to.equal(3); + next(); }); } ], done); }); + + it('Should be no race condition for current() when calling in parallel', function(done) { + async.parallel([ + function(next) { Checkpoint.current(next); }, + function(next) { Checkpoint.current(next); } + ], function(err, list) { + if (err) return done(err); + + Checkpoint.find(function(err, data) { + if (err) return done(err); + + expect(data).to.have.length(1); + + done(); + }); + }); + }); + + it('Should be no race condition for bumpLastSeq() when calling in parallel', function(done) { + async.parallel([ + function(next) { Checkpoint.bumpLastSeq(next); }, + function(next) { Checkpoint.bumpLastSeq(next); } + ], function(err, list) { + if (err) return done(err); + + Checkpoint.find(function(err, data) { + if (err) return done(err); + // The invariant "we have at most 1 checkpoint instance" is preserved + // even when multiple calls are made in parallel + expect(data).to.have.length(1); + // There is a race condition here, we could end up with both 2 or 3 as the "seq". + // The current implementation of the memory connector always yields 2 though. + expect(data[0].seq).to.equal(2); + // In this particular case, since the new last seq is always 2, both results + // should be 2. + expect(list.map(function(it) {return it.seq;})) + .to.eql([2, 2]); + + done(); + }); + }); + }); + + it('Checkpoint.current() for non existing checkpoint should initialize checkpoint', function(done) { + Checkpoint.current(function(err, seq) { + expect(seq).to.equal(1); + + done(err); + }); + }); + + it('bumpLastSeq() works when singleton instance does not exists yet', function(done) { + Checkpoint.bumpLastSeq(function(err, cp) { + // We expect `seq` to be 2 since `checkpoint` does not exist and + // `bumpLastSeq` for the first time not only initializes it to one, + // but also increments the initialized value by one. + expect(cp.seq).to.equal(2); + + done(err); + }); + }); }); }); diff --git a/test/data-source.test.js b/test/data-source.test.js index 662c07184..19ce7a87e 100644 --- a/test/data-source.test.js +++ b/test/data-source.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + describe('DataSource', function() { var memory; diff --git a/test/e2e/remote-connector.e2e.js b/test/e2e/remote-connector.e2e.js index 1a3424718..e24436929 100644 --- a/test/e2e/remote-connector.e2e.js +++ b/test/e2e/remote-connector.e2e.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var path = require('path'); var loopback = require('../../'); var models = require('../fixtures/e2e/models'); @@ -19,7 +24,9 @@ describe('RemoteConnector', function() { foo: 'bar' }, function(err, inst) { if (err) return done(err); + assert(inst.id); + done(); }); }); @@ -30,7 +37,9 @@ describe('RemoteConnector', function() { }); m.save(function(err, data) { if (err) return done(err); + assert(data.foo === 'bar'); + done(); }); }); diff --git a/test/e2e/replication.e2e.js b/test/e2e/replication.e2e.js index 24f6967e0..ad7d57363 100644 --- a/test/e2e/replication.e2e.js +++ b/test/e2e/replication.e2e.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var path = require('path'); var loopback = require('../../'); var models = require('../fixtures/e2e/models'); @@ -27,8 +32,10 @@ describe('Replication', function() { }, function(err, created) { LocalTestModel.replicate(0, TestModel, function() { if (err) return done(err); - TestModel.findOne({n: RANDOM}, function(err, found) { + + TestModel.findOne({ n: RANDOM }, function(err, found) { assert.equal(created.id, found.id); + done(); }); }); diff --git a/test/email.test.js b/test/email.test.js index 018f543ca..be12a3a63 100644 --- a/test/email.test.js +++ b/test/email.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../'); var MyEmail; var assert = require('assert'); @@ -61,6 +66,7 @@ describe('Email and SMTP', function() { assert(mail.response); assert(mail.envelope); assert(mail.messageId); + done(err); }); }); @@ -78,6 +84,7 @@ describe('Email and SMTP', function() { assert(mail.response); assert(mail.envelope); assert(mail.messageId); + done(err); }); }); diff --git a/test/error-handler.test.js b/test/error-handler.test.js index d19abf47e..273481240 100644 --- a/test/error-handler.test.js +++ b/test/error-handler.test.js @@ -1,3 +1,8 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + var loopback = require('../'); var app; var assert = require('assert'); @@ -17,6 +22,7 @@ describe('loopback.errorHandler(options)', function() { .get('/url-does-not-exist') .end(function(err, res) { assert.ok(res.error.text.match(/