Merge branch 'release/1.10.0' into production

This commit is contained in:
Raymond Feng 2014-07-16 09:27:54 -07:00
commit 010cc38ec0
18 changed files with 562 additions and 70 deletions

View File

@ -14,9 +14,9 @@ LoopBack consists of:
For more details, see http://loopback.io/.
## LoopBack modules
## LoopBack modules
In addition to the [main LoopBack module](https://github.com/strongloop/loopback), LoopBack consists of numerous other modules that implement specific functionality,
In addition to the [main LoopBack module](https://github.com/strongloop/loopback), LoopBack consists of numerous other modules that implement specific functionality,
as illustrated below:
![LoopBack modules](https://github.com/strongloop/loopback/raw/master/docs/assets/lb-modules.png "LoopBack modules")
@ -33,18 +33,21 @@ as illustrated below:
* [loopback-connector-mssql](https://github.com/strongloop/loopback-connector-mssql)
* [loopback-connector-postgresql](https://github.com/strongloop/loopback-connector-postgresql)
* [loopback-connector-rest](https://github.com/strongloop/loopback-connector-rest)
* [loopback-connector-soap](https://github.com/strongloop/loopback-connector-soap)
* [loopback-connector-soap](https://github.com/strongloop/loopback-connector-soap)
* Mobile services
* [loopback-push-notification](https://github.com/strongloop/loopback-push-notification)
* [loopback-storage-service](https://github.com/strongloop/loopback-storage-service)
* Mobile Components
* [loopback-component-push](https://github.com/strongloop/loopback-component-push)
* [loopback-component-storage](https://github.com/strongloop/loopback-component-storage)
* Security Components
* [loopback-component-passport](https://github.com/strongloop/loopback-component-passport)
* Clients
* [loopback-ios](https://github.com/strongloop/loopback-ios)
* [strong-remoting-ios](https://github.com/strongloop/strong-remoting-ios)
* [loopback-android](https://github.com/strongloop/loopback-android)
* [strong-remoting-android](https://github.com/strongloop/strong-remoting-android)
* [loopback-angular](https://github.com/strongloop/loopback-angular)
* [loopback-sdk-ios](https://github.com/strongloop/loopback-sdk-ios)
* [loopback-sdk-android](https://github.com/strongloop/loopback-sdk-android)
* [loopback-sdk-angular](https://github.com/strongloop/loopback-sdk-angular)
* [loopback-sdk-angular-cli](https://github.com/strongloop/loopback-sdk-angular-cli)
* [grunt-loopback-sdk-angular](https://github.com/strongloop/grunt-loopback-sdk-angular)
* Tools
* [loopback-explorer](https://github.com/strongloop/loopback-explorer)
@ -52,17 +55,17 @@ as illustrated below:
* [strong-cli](https://github.com/strongloop/strong-cli)
* Examples
* [loopback-example-database](https://github.com/strongloop/loopback-example-database)
* [loopback-example-datagraph](https://github.com/strongloop/loopback-example-datagraph)
* [loopback-example-full-stack](https://github.com/strongloop/loopback-example-full-stack)
* [loopback-example-office-supplies](https://github.com/strongloop/loopback-example-office-supplies)
* [loopback-example-todo](https://github.com/strongloop/loopback-example-todo)
* [loopback-example-access-control](https://github.com/strongloop/loopback-example-access-control)
* [loopback-example-proxy](https://github.com/strongloop/loopback-example-proxy)
* [strongloop-community/loopback-example-datagraph](https://github.com/strongloop-community/loopback-example-datagraph)
* [strongloop-community/loopback-example-database](https://github.com/strongloop-community/loopback-example-database)
* [strongloop-community/loopback-examples-ios](https://github.com/strongloop-community/loopback-examples-ios)
* [strongloop-community/loopback-example-ssl](https://github.com/strongloop-community/loopback-example-ssl)
* [loopback-example-ssl](https://github.com/strongloop/loopback-example-ssl)
## Resources
## Resources
* [Documentation](http://docs.strongloop.com/display/LB/LoopBack).
* [API documentation](http://apidocs.strongloop.com/loopback).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 309 KiB

View File

@ -879,6 +879,13 @@ app.listen = function(cb) {
server.on('listening', function() {
self.set('port', this.address().port);
if (!self.get('url')) {
// A better default host would be `0.0.0.0`,
// but such URL is not supported by Windows
var host = self.get('host') || '127.0.0.1';
var url = 'http://' + host + ':' + self.get('port') + '/';
self.set('url', url);
}
});
var useAppConfig =

View File

@ -18,7 +18,7 @@ module.exports = MailConnector;
*/
function MailConnector(settings) {
assert(typeof settings === 'object', 'cannot initiaze MailConnector without a settings object');
assert(typeof settings === 'object', 'cannot initialize MailConnector without a settings object');
var transports = settings.transports || [];
this.transportsIndex = {};
this.transports = [];

View File

@ -15,9 +15,9 @@ module.exports = token;
* Check for an access token in cookies, headers, and query string parameters.
* This function always checks for the following:
*
* - `access_token`
* - `X-Access-Token`
* - `authorization`
* - `access_token` (params only)
* - `X-Access-Token` (headers only)
* - `authorization` (headers and cookies)
*
* It checks for these values in cookies, headers, and query string parameters _in addition_ to the items
* specified in the options parameter.

View File

@ -209,12 +209,20 @@ function tokenIdForRequest(req, options) {
id = req.header(headers[i]);
if(typeof id === 'string') {
// Add support for oAuth 2.0 bearer token
// http://tools.ietf.org/html/rfc6750
if (id.indexOf('Bearer ') === 0) {
id = id.substring(7);
// Decode from base64
var buf = new Buffer(id, 'base64');
id = buf.toString('utf8');
}
return id;
}
}
if(req.signedCookies) {
for(i = 0, length = headers.length; i < length; i++) {
for(i = 0, length = cookies.length; i < length; i++) {
id = req.signedCookies[cookies[i]];
if(typeof id === 'string') {

View File

@ -221,24 +221,42 @@ setRemoting(DataModel.findOne, {
* @param {Function} [cb] - callback called with (err)
*/
DataModel.remove =
DataModel.deleteAll =
DataModel.destroyAll = function destroyAll(where, cb) {
throwNotAttached(this.modelName, 'destroyAll');
};
/**
* Alias for `destroyAll`
*/
DataModel.remove = DataModel.destroyAll;
/**
* Alias for `destroyAll`
*/
DataModel.deleteAll = DataModel.destroyAll;
/**
* Destroy a record by id
* @param {*} id The id value
* @param {Function} cb - callback called with (err)
*/
DataModel.removeById =
DataModel.deleteById =
DataModel.destroyById = function deleteById(id, cb) {
throwNotAttached(this.modelName, 'deleteById');
};
/**
* Alias for deleteById
*/
DataModel.deleteById = DataModel.destroyById;
/**
* Alias for deleteById
*/
DataModel.removeById = DataModel.destroyById;
// deleteById ~ remoting attributes
setRemoting(DataModel.deleteById, {
description: 'Delete a model instance by id from the data source',

View File

@ -3,8 +3,6 @@
*/
var registry = require('../registry');
var compat = require('../compat');
var ModelBuilder = require('loopback-datasource-juggler').ModelBuilder;
var modeler = new ModelBuilder();
var assert = require('assert');
/**
@ -14,7 +12,7 @@ var assert = require('assert');
* @param {Object} data
*/
var Model = module.exports = modeler.define('Model');
var Model = module.exports = registry.modelBuilder.define('Model');
Model.shared = true;

View File

@ -26,22 +26,11 @@ var properties = {
realm: {type: String},
username: {type: String},
password: {type: String, required: true},
credentials: Object, // deprecated, to be removed in 2.x
challenges: Object, // deprecated, to be removed in 2.x
email: {type: String, required: true},
emailVerified: Boolean,
verificationToken: String,
credentials: [
'UserCredential' // User credentials, private or public, such as private/public keys, Kerberos tickets, oAuth tokens, facebook, google, github ids
],
challenges: [
'Challenge' // Security questions/answers
],
// https://en.wikipedia.org/wiki/Multi-factor_authentication
/*
factors: [
'AuthenticationFactor'
],
*/
status: String,
created: Date,
lastUpdated: Date
@ -186,6 +175,15 @@ User.login = function (credentials, include, fn) {
debug('An error is reported from User.findOne: %j', err);
fn(defaultError);
} else if(user) {
if (self.settings.emailVerificationRequired) {
if (!user.emailVerified) {
// Fail to log in if email verification is not done yet
debug('User email has not been verified');
err = new Error('login failed as the email has not been verified');
err.statusCode = 401;
return fn(err);
}
}
user.hasPassword(credentials.password, function(err, isMatch) {
if(err) {
debug('An error is reported from User.hasPassword: %j', err);
@ -441,6 +439,15 @@ User.setup = function () {
this.$password = bcrypt.hashSync(plain, salt);
}
// Make sure emailVerified is not set by creation
UserModel.beforeRemote('create', function(ctx, user, next) {
var body = ctx.req.body;
if (body && body.emailVerified) {
body.emailVerified = false;
}
next();
});
loopback.remoteMethod(
UserModel.login,
{

View File

@ -9,12 +9,16 @@
var assert = require('assert');
var extend = require('util')._extend;
var DataSource = require('loopback-datasource-juggler').DataSource;
var juggler = require('loopback-datasource-juggler');
var DataSource = juggler.DataSource;
var ModelBuilder = juggler.ModelBuilder;
var registry = module.exports;
registry.defaultDataSources = {};
registry.modelBuilder = new ModelBuilder();
/**
* Create a named vanilla JavaScript class constructor with an attached
* set of properties and options.
@ -179,7 +183,7 @@ registry.configureModel = function(ModelCtor, config) {
* @header loopback.getModel(modelName)
*/
registry.getModel = function(modelName) {
return this.Model.modelBuilder.models[modelName];
return this.modelBuilder.models[modelName];
};
/**
@ -194,7 +198,7 @@ registry.getModel = function(modelName) {
registry.getModelByType = function(modelType) {
assert(typeof modelType === 'function',
'The model type must be a constructor');
var models = this.Model.modelBuilder.models;
var models = this.modelBuilder.models;
for(var m in models) {
if(models[m].prototype instanceof modelType) {
return models[m];
@ -216,10 +220,10 @@ registry.getModelByType = function(modelType) {
*/
registry.createDataSource = function (name, options) {
var loopback = this;
var ds = new DataSource(name, options, loopback.Model.modelBuilder);
var self = this;
var ds = new DataSource(name, options, self.modelBuilder);
ds.createModel = function (name, properties, settings) {
var ModelCtor = loopback.createModel(name, properties, settings);
var ModelCtor = self.createModel(name, properties, settings);
ModelCtor.attachTo(ds);
return ModelCtor;
};
@ -293,7 +297,7 @@ registry.getDefaultDataSourceForType = function(type) {
*/
registry.autoAttach = function() {
var models = this.Model.modelBuilder.models;
var models = this.modelBuilder.models;
assert.equal(typeof models, 'object', 'Cannot autoAttach without a models object');
Object.keys(models).forEach(function(modelName) {
@ -327,3 +331,6 @@ registry.DataSource = DataSource;
registry.Model = require('./models/model');
registry.DataModel = require('./models/data-model');
// Set the default model base class. This is done after the Model class is defined.
registry.modelBuilder.defaultModelBaseClass = registry.Model;

View File

@ -26,36 +26,36 @@
"mobile",
"mBaaS"
],
"version": "1.9.1",
"version": "1.10.0",
"scripts": {
"test": "mocha -R spec"
},
"dependencies": {
"debug": "~1.0.2",
"express": "~3.5.0",
"strong-remoting": "~1.5.0",
"inflection": "~1.3.7",
"nodemailer": "~0.7.0",
"async": "~0.9.0",
"bcryptjs": "~2.0.1",
"debug": "~1.0.3",
"ejs": "~1.0.0",
"bcryptjs": "~1.0.3",
"underscore.string": "~2.3.3",
"underscore": "~1.6.0",
"express": "3.x",
"inflection": "~1.3.8",
"nodemailer": "~0.7.1",
"strong-remoting": "~1.5.1",
"uid2": "0.0.3",
"async": "~0.9.0"
"underscore": "~1.6.0",
"underscore.string": "~2.3.3"
},
"peerDependencies": {
"loopback-datasource-juggler": ">=1.4.0 <1.7.0"
"loopback-datasource-juggler": "^1.7.0"
},
"devDependencies": {
"loopback-datasource-juggler": ">=1.4.0 <1.7.0",
"loopback-datasource-juggler": "^1.7.0",
"mocha": "~1.20.1",
"strong-task-emitter": "0.0.x",
"supertest": "~0.13.0",
"chai": "~1.9.1",
"loopback-testing": "~0.2.0",
"browserify": "~4.1.11",
"browserify": "~4.2.1",
"grunt": "~0.4.5",
"grunt-browserify": "~2.1.0",
"grunt-browserify": "~2.1.3",
"grunt-contrib-uglify": "~0.5.0",
"grunt-contrib-jshint": "~0.10.0",
"grunt-contrib-watch": "~0.6.1",
@ -64,7 +64,7 @@
"karma-firefox-launcher": "~0.1.3",
"karma-html2js-preprocessor": "~0.1.0",
"karma-phantomjs-launcher": "~0.1.4",
"karma": "~0.12.16",
"karma": "~0.12.17",
"karma-browserify": "~0.2.1",
"karma-mocha": "~0.1.4",
"grunt-karma": "~0.8.3"
@ -76,8 +76,6 @@
"browser": {
"express": "./lib/browser-express.js",
"connect": false,
"passport": false,
"passport-local": false,
"nodemailer": false
},
"license": {

View File

@ -12,7 +12,25 @@ describe('loopback.token(options)', function() {
.end(done);
});
it('should populate req.token from a header', function (done) {
it('should populate req.token from an authorization header', function (done) {
createTestAppAndRequest(this.token, done)
.get('/')
.set('authorization', this.token.id)
.expect(200)
.end(done);
});
it('should populate req.token from an X-Access-Token header', function (done) {
createTestAppAndRequest(this.token, done)
.get('/')
.set('X-Access-Token', this.token.id)
.expect(200)
.end(done);
});
it('should populate req.token from an authorization header with bearer token', function (done) {
var token = this.token.id;
token = 'Bearer '+ new Buffer(token).toString('base64');
createTestAppAndRequest(this.token, done)
.get('/')
.set('authorization', this.token.id)
@ -33,6 +51,20 @@ describe('loopback.token(options)', function() {
});
});
it('should populate req.token from a header or a secure cookie', function (done) {
var app = createTestApp(this.token, done);
var id = this.token.id;
request(app)
.get('/token')
.end(function(err, res) {
request(app)
.get('/')
.set('authorization', id)
.set('Cookie', res.header['set-cookie'])
.end(done);
});
});
it('should skip when req.token is already present', function(done) {
var tokenStub = { id: 'stub id' };
app.use(function(req, res, next) {

View File

@ -481,6 +481,18 @@ describe('app', function() {
});
});
it('updates "url" on "listening" event', function(done) {
var app = loopback();
app.set('port', 0);
app.set('host', undefined);
app.listen(function() {
expect(app.get('url'), 'url')
.to.equal('http://127.0.0.1:' + app.get('port') + '/');
done();
});
});
it('forwards to http.Server.listen on more than one arg', function(done) {
var app = loopback();
app.set('port', 1);

View File

@ -52,5 +52,60 @@
}
}
}
},
"physician": {
"dataSource": "db",
"public": true,
"properties": {
"name": "string"
},
"options": {
"relations": {
"patients": {
"model": "patient",
"type": "hasMany",
"through": "appointment",
"foreignKey": "patientId"
}
}
}
},
"patient": {
"dataSource": "db",
"public": true,
"properties": {
"name": "string"
},
"options": {
"relations": {
"physicians": {
"model": "physician",
"type": "hasMany",
"through": "appointment",
"foreignKey": "physicianId"
}
}
}
},
"appointment": {
"dataSource": "db",
"public": true,
"properties": {
"date": "date"
},
"options": {
"relations": {
"physician": {
"model": "physician",
"type": "belongsTo",
"foreignKey": "physicianId"
},
"patient": {
"model": "patient",
"type": "belongsTo",
"foreignKey": "patientId"
}
}
}
}
}

View File

@ -13,7 +13,7 @@ describe('loopback', function() {
});
});
describe('loopback.createDataSource(options)', function(){
describe('loopback.createDataSource(options)', function() {
it('Create a data source with a connector.', function() {
var dataSource = loopback.createDataSource({
connector: loopback.Memory
@ -22,6 +22,23 @@ describe('loopback', function() {
});
});
describe('data source created by loopback', function() {
it('should create model extending Model by default', function () {
var dataSource = loopback.createDataSource({
connector: loopback.Memory
});
var m1 = dataSource.createModel('m1', {});
assert(m1.prototype instanceof loopback.Model);
});
});
describe('model created by loopback', function() {
it('should extend from Model by default', function() {
var m1 = loopback.createModel('m1', {});
assert(m1.prototype instanceof loopback.Model);
});
});
describe('loopback.autoAttach', function () {
it('doesn\'t overwrite model with datasource configured', function () {
var ds1 = loopback.createDataSource('db1', {

View File

@ -6,6 +6,7 @@ var app = require(path.join(SIMPLE_APP, 'app.js'));
var assert = require('assert');
var expect = require('chai').expect;
var debug = require('debug')('loopback:test:relations.integration');
var async = require('async');
describe('relations - integration', function () {
@ -122,6 +123,218 @@ describe('relations - integration', function () {
});
});
describe('hasMany through', function() {
function setup(connecting, cb) {
var root = {};
async.series([
// Clean up models
function (done) {
app.models.physician.destroyAll(function (err) {
app.models.patient.destroyAll(function (err) {
app.models.appointment.destroyAll(function (err) {
done();
});
});
});
},
// Create a physician
function (done) {
app.models.physician.create({
name: 'ph1'
}, function (err, physician) {
root.physician = physician;
done();
});
},
// Create a patient
connecting ? function (done) {
root.physician.patients.create({
name: 'pa1'
}, function (err, patient) {
root.patient = patient;
root.relUrl = '/api/physicians/' + root.physician.id
+ '/patients/rel/' + root.patient.id;
done();
});
} : function (done) {
app.models.patient.create({
name: 'pa1'
}, function (err, patient) {
root.patient = patient;
root.relUrl = '/api/physicians/' + root.physician.id
+ '/patients/rel/' + root.patient.id;
done();
});
}], function (err, done) {
cb(err, root);
});
}
describe('PUT /physicians/:id/patients/rel/:fk', function () {
before(function (done) {
var self = this;
setup(false, function (err, root) {
self.url = root.relUrl;
self.patient = root.patient;
self.physician = root.physician;
done();
});
});
lt.describe.whenCalledRemotely('PUT', '/api/physicians/:id/patients/rel/:fk', function () {
it('should succeed with statusCode 200', function () {
assert.equal(this.res.statusCode, 200);
assert.equal(this.res.body.patientId, this.patient.id);
assert.equal(this.res.body.physicianId, this.physician.id);
});
it('should create a record in appointment', function (done) {
var self = this;
app.models.appointment.find(function (err, apps) {
assert.equal(apps.length, 1);
assert.equal(apps[0].patientId, self.patient.id);
done();
});
});
it('should connect physician to patient', function (done) {
var self = this;
self.physician.patients(function (err, patients) {
assert.equal(patients.length, 1);
assert.equal(patients[0].id, self.patient.id);
done();
});
});
});
});
describe('DELETE /physicians/:id/patients/rel/:fk', function () {
before(function (done) {
var self = this;
setup(true, function (err, root) {
self.url = root.relUrl;
self.patient = root.patient;
self.physician = root.physician;
done();
});
});
it('should create a record in appointment', function (done) {
var self = this;
app.models.appointment.find(function (err, apps) {
assert.equal(apps.length, 1);
assert.equal(apps[0].patientId, self.patient.id);
done();
});
});
it('should connect physician to patient', function (done) {
var self = this;
self.physician.patients(function (err, patients) {
assert.equal(patients.length, 1);
assert.equal(patients[0].id, self.patient.id);
done();
});
});
lt.describe.whenCalledRemotely('DELETE', '/api/physicians/:id/patients/rel/:fk', function () {
it('should succeed with statusCode 200', function () {
assert.equal(this.res.statusCode, 200);
});
it('should remove the record in appointment', function (done) {
var self = this;
app.models.appointment.find(function (err, apps) {
assert.equal(apps.length, 0);
done();
});
});
it('should remove the connection between physician and patient', function (done) {
var self = this;
// Need to refresh the cache
self.physician.patients(true, function (err, patients) {
assert.equal(patients.length, 0);
done();
});
});
});
});
describe('GET /physicians/:id/patients/:fk', function () {
before(function (done) {
var self = this;
setup(true, function (err, root) {
self.url = '/api/physicians/' + root.physician.id
+ '/patients/' + root.patient.id;
self.patient = root.patient;
self.physician = root.physician;
done();
});
});
lt.describe.whenCalledRemotely('GET', '/api/physicians/:id/patients/:fk', function () {
it('should succeed with statusCode 200', function () {
assert.equal(this.res.statusCode, 200);
assert.equal(this.res.body.id, this.physician.id);
});
});
});
describe('DELETE /physicians/:id/patients/:fk', function () {
before(function (done) {
var self = this;
setup(true, function (err, root) {
self.url = '/api/physicians/' + root.physician.id
+ '/patients/' + root.patient.id;
self.patient = root.patient;
self.physician = root.physician;
done();
});
});
lt.describe.whenCalledRemotely('DELETE', '/api/physicians/:id/patients/:fk', function () {
it('should succeed with statusCode 200', function () {
assert.equal(this.res.statusCode, 200);
});
it('should remove the record in appointment', function (done) {
var self = this;
app.models.appointment.find(function (err, apps) {
assert.equal(apps.length, 0);
done();
});
});
it('should remove the connection between physician and patient', function (done) {
var self = this;
// Need to refresh the cache
self.physician.patients(true, function (err, patients) {
assert.equal(patients.length, 0);
done();
});
});
it('should remove the record in patient', function (done) {
var self = this;
app.models.patient.find(function (err, patients) {
assert.equal(patients.length, 0);
done();
});
});
});
});
});
describe('hasAndBelongsToMany', function() {
beforeEach(function defineProductAndCategoryModels() {
var product = app.model(

View File

@ -66,4 +66,33 @@ describe('remoting - integration', function () {
});
});
describe('Model', function() {
it('has expected remote methods', function() {
var storeClass = app.handler('rest').adapter
.getClasses()
.filter(function(c) { return c.name === 'store'; })[0];
var methods = storeClass.methods
.map(function(m) {
return [
m.name + '()',
m.getHttpMethod(),
m.getFullPath()
].join(' ');
});
// The list of methods is from docs:
// http://docs.strongloop.com/display/LB/Exposing+models+over+a+REST+API
expect(methods).to.include.members([
'create() POST /stores',
'upsert() PUT /stores',
'exists() GET /stores/:id/exists',
'findById() GET /stores/:id',
'find() GET /stores',
'findOne() GET /stores/findOne',
'deleteById() DELETE /stores/:id',
'count() GET /stores/count',
'prototype.updateAttributes() PUT /stores/:id',
]);
});
});
});

View File

@ -8,6 +8,8 @@ var userMemory = loopback.createDataSource({
describe('User', function(){
var validCredentials = {email: 'foo@bar.com', password: 'bar'};
var validCredentialsEmailVerified = {email: 'foo1@bar.com', password: 'bar1', emailVerified: true};
var validCredentialsEmailVerifiedOverREST = {email: 'foo2@bar.com', password: 'bar2', emailVerified: true};
var validCredentialsWithTTL = {email: 'foo@bar.com', password: 'bar', ttl: 3600};
var invalidCredentials = {email: 'foo1@bar.com', password: 'bar1'};
var incompleteCredentials = {password: 'bar1'};
@ -30,7 +32,9 @@ describe('User', function(){
app.use(loopback.rest());
app.model(User);
User.create(validCredentials, done);
User.create(validCredentials, function(err, user) {
User.create(validCredentialsEmailVerified, done);
});
});
afterEach(function (done) {
@ -49,6 +53,22 @@ describe('User', function(){
});
});
it('credentials/challenges are object types', function (done) {
User.create({email: 'f1@b.com', password: 'bar1',
credentials: {cert: 'xxxxx', key: '111'},
challenges: {x: 'X', a: 1}
}, function (err, user) {
assert(!err);
User.findById(user.id, function (err, user) {
assert(user.id);
assert(user.email);
assert.deepEqual(user.credentials, {cert: 'xxxxx', key: '111'});
assert.deepEqual(user.challenges, {x: 'X', a: 1});
done();
});
});
});
it('Email is required', function (done) {
User.create({password: '123'}, function (err) {
assert(err);
@ -57,8 +77,7 @@ describe('User', function(){
assert.equal(err.details.context, "user");
assert.deepEqual(err.details.codes.email, [
'presence',
'format.blank',
'uniqueness'
'format.blank'
]);
done();
@ -105,6 +124,18 @@ describe('User', function(){
var u = new User({username: 'foo', password: 'bar'});
assert(u.password !== 'bar');
});
it('Create a user over REST should remove emailVerified property', function(done) {
request(app)
.post('/users')
.expect('Content-Type', /json/)
.expect(200)
.send(validCredentialsEmailVerifiedOverREST)
.end(function(err, res){
assert(!res.body.emailVerified);
done();
});
});
});
describe('User.login', function() {
@ -155,7 +186,7 @@ describe('User', function(){
});
});
});
it('Login a user over REST by providing credentials', function(done) {
request(app)
.post('/users/login')
@ -235,6 +266,63 @@ describe('User', function(){
});
});
});
describe('User.login requiring email verification', function() {
beforeEach(function() {
User.settings.emailVerificationRequired = true;
});
afterEach(function() {
User.settings.emailVerificationRequired = false;
});
it('Login a user by without email verification', function(done) {
User.login(validCredentials, function (err, accessToken) {
assert(err);
done();
});
});
it('Login a user by with email verification', function(done) {
User.login(validCredentialsEmailVerified, function (err, accessToken) {
assert(accessToken.userId);
assert(accessToken.id);
assert.equal(accessToken.id.length, 64);
done();
});
});
it('Login a user over REST when email verification is required', function(done) {
request(app)
.post('/users/login')
.expect('Content-Type', /json/)
.expect(200)
.send(validCredentialsEmailVerified)
.end(function(err, res){
if(err) return done(err);
var accessToken = res.body;
assert(accessToken.userId);
assert(accessToken.id);
assert.equal(accessToken.id.length, 64);
assert(accessToken.user === undefined);
done();
});
});
it('Login a user over REST without email verification when it is required', function(done) {
request(app)
.post('/users/login')
.expect('Content-Type', /json/)
.expect(401)
.send(validCredentials)
.end(function(err, res) {
done();
});
});
});
describe('User.logout', function() {
it('Logout a user by providing the current accessToken id (using node)', function(done) {