Merge branch 'master' into 2.0
This commit is contained in:
commit
79f504a3c7
33
README.md
33
README.md
|
@ -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 |
|
@ -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 = [];
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -3,12 +3,7 @@
|
|||
*/
|
||||
var registry = require('../registry');
|
||||
var compat = require('../compat');
|
||||
var juggler = require('loopback-datasource-juggler');
|
||||
var ModelBuilder = juggler.ModelBuilder;
|
||||
var DataSource = juggler.DataSource;
|
||||
var modeler = new ModelBuilder();
|
||||
var assert = require('assert');
|
||||
var _ = require('underscore');
|
||||
var SharedClass = require('strong-remoting').SharedClass;
|
||||
|
||||
/**
|
||||
|
@ -80,7 +75,7 @@ var SharedClass = require('strong-remoting').SharedClass;
|
|||
* @property {DataSource} dataSource
|
||||
*/
|
||||
|
||||
var Model = module.exports = modeler.define('Model');
|
||||
var Model = module.exports = registry.modelBuilder.define('Model');
|
||||
|
||||
/*!
|
||||
* Called when a model is extended.
|
||||
|
@ -292,10 +287,8 @@ Model._getAccessTypeForMethod = function(method) {
|
|||
return ACL.WRITE;
|
||||
case 'count':
|
||||
return ACL.READ;
|
||||
break;
|
||||
default:
|
||||
return ACL.EXECUTE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -122,6 +122,8 @@ PersistedModel.findOrCreate = function findOrCreate(query, data, callback) {
|
|||
throwNotAttached(this.modelName, 'findOrCreate');
|
||||
};
|
||||
|
||||
PersistedModel.findOrCreate._delegate = true;
|
||||
|
||||
/**
|
||||
* Check whether a model instance exists in database
|
||||
*
|
||||
|
@ -493,7 +495,7 @@ PersistedModel.setupRemoting = function() {
|
|||
http: {verb: 'post', path: '/update'}
|
||||
});
|
||||
|
||||
setRemoting(PersistedModel, 'removeById', {
|
||||
setRemoting(PersistedModel, 'deleteById', {
|
||||
description: 'Delete a model instance by id from the data source',
|
||||
accepts: {arg: 'id', type: 'any', description: 'Model id', required: true,
|
||||
http: {source: 'path'}},
|
||||
|
|
|
@ -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
|
||||
|
@ -65,7 +54,7 @@ var options = {
|
|||
principalType: ACL.ROLE,
|
||||
principalId: Role.OWNER,
|
||||
permission: ACL.ALLOW,
|
||||
property: 'removeById'
|
||||
property: 'deleteById'
|
||||
},
|
||||
{
|
||||
principalType: ACL.ROLE,
|
||||
|
@ -114,7 +103,7 @@ var options = {
|
|||
*
|
||||
* - DENY EVERYONE `*`
|
||||
* - ALLOW EVERYONE `create`
|
||||
* - ALLOW OWNER `removeById`
|
||||
* - ALLOW OWNER `deleteById`
|
||||
* - ALLOW EVERYONE `login`
|
||||
* - ALLOW EVERYONE `logout`
|
||||
* - ALLOW EVERYONE `findById`
|
||||
|
@ -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,
|
||||
{
|
||||
|
|
|
@ -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.findModel(modelName)
|
||||
*/
|
||||
registry.findModel = function(modelName) {
|
||||
return this.Model.modelBuilder.models[modelName];
|
||||
return this.modelBuilder.models[modelName];
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -210,7 +214,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];
|
||||
|
@ -232,10 +236,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;
|
||||
};
|
||||
|
@ -309,7 +313,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) {
|
||||
|
@ -356,3 +360,5 @@ Object.defineProperty(registry, 'DataModel', {
|
|||
}
|
||||
});
|
||||
|
||||
// Set the default model base class. This is done after the Model class is defined.
|
||||
registry.modelBuilder.defaultModelBaseClass = registry.Model;
|
||||
|
|
38
package.json
38
package.json
|
@ -32,37 +32,36 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"async": "~0.9.0",
|
||||
"bcryptjs": "~1.0.4",
|
||||
"body-parser": "~1.2.2",
|
||||
"body-parser": "~1.4.3",
|
||||
"canonical-json": "0.0.4",
|
||||
"debug": "~1.0.2",
|
||||
"ejs": "~1.0.0",
|
||||
"express": "4.x",
|
||||
"inflection": "~1.3.7",
|
||||
"nodemailer": "~0.7.0",
|
||||
"strong-remoting": "~2.0.0-beta4",
|
||||
"strong-remoting": "~2.0.0-beta5",
|
||||
"bcryptjs": "~2.0.1",
|
||||
"debug": "~1.0.4",
|
||||
"inflection": "~1.3.8",
|
||||
"nodemailer": "~0.7.1",
|
||||
"uid2": "0.0.3",
|
||||
"underscore": "~1.6.0",
|
||||
"underscore.string": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"loopback-datasource-juggler": "~2.0.0-beta2"
|
||||
"loopback-datasource-juggler": "~2.0.0-beta3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "~4.1.11",
|
||||
"browserify": "~4.2.1",
|
||||
"chai": "~1.9.1",
|
||||
"cookie-parser": "~1.1.0",
|
||||
"errorhandler": "~1.0.1",
|
||||
"es5-shim": "^3.4.0",
|
||||
"cookie-parser": "~1.3.2",
|
||||
"errorhandler": "~1.1.1",
|
||||
"es5-shim": "^4.0.0",
|
||||
"grunt": "~0.4.5",
|
||||
"grunt-browserify": "~2.1.0",
|
||||
"grunt-browserify": "~2.1.3",
|
||||
"grunt-cli": "^0.1.13",
|
||||
"grunt-contrib-jshint": "~0.10.0",
|
||||
"grunt-contrib-uglify": "~0.4.0",
|
||||
"grunt-contrib-uglify": "~0.5.0",
|
||||
"grunt-contrib-watch": "~0.6.1",
|
||||
"grunt-karma": "~0.8.3",
|
||||
"grunt-mocha-test": "^0.11.0",
|
||||
"karma": "~0.12.16",
|
||||
"karma-browserify": "~0.2.1",
|
||||
"karma-chrome-launcher": "~0.1.4",
|
||||
"karma-firefox-launcher": "~0.1.3",
|
||||
|
@ -71,13 +70,14 @@
|
|||
"karma-mocha": "^0.1.4",
|
||||
"karma-phantomjs-launcher": "~0.1.4",
|
||||
"karma-script-launcher": "~0.1.0",
|
||||
"loopback-boot": "1.x >=1.1",
|
||||
"loopback-datasource-juggler": "~2.0.0-beta2",
|
||||
"loopback-boot": "^1.1.0",
|
||||
"loopback-datasource-juggler": "~2.0.0-beta3",
|
||||
"loopback-testing": "~0.2.0",
|
||||
"mocha": "~1.20.1",
|
||||
"serve-favicon": "~2.0.0",
|
||||
"serve-favicon": "~2.0.1",
|
||||
"strong-task-emitter": "0.0.x",
|
||||
"supertest": "~0.13.0"
|
||||
"supertest": "~0.13.0",
|
||||
"karma": "~0.12.17"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -86,8 +86,6 @@
|
|||
"browser": {
|
||||
"express": "./lib/browser-express.js",
|
||||
"connect": false,
|
||||
"passport": false,
|
||||
"passport-local": false,
|
||||
"nodemailer": false
|
||||
},
|
||||
"license": {
|
||||
|
|
|
@ -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) {
|
||||
|
@ -168,7 +200,7 @@ function createTestApp(testToken, settings, done) {
|
|||
principalId: "$everyone",
|
||||
accessType: ACL.ALL,
|
||||
permission: ACL.DENY,
|
||||
property: 'removeById'
|
||||
property: 'deleteById'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
|
@ -123,7 +123,7 @@
|
|||
"permission": "DENY",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$owner",
|
||||
"property": "removeById"
|
||||
"property": "deleteById"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -18,7 +18,7 @@ describe('remoting - integration', function () {
|
|||
it("should load remoting options", function () {
|
||||
var remotes = app.remotes();
|
||||
assert.deepEqual(remotes.options, {"json": {"limit": "1kb", "strict": false},
|
||||
"urlencoded": {"limit": "8kb"}});
|
||||
"urlencoded": {"limit": "8kb", "extended": true}});
|
||||
});
|
||||
|
||||
it("rest handler", function () {
|
||||
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -198,10 +198,10 @@ describe('Model Tests', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Model.removeById(id, [callback])', function () {
|
||||
describe('Model.deleteById(id, [callback])', function () {
|
||||
it("Delete a model instance from the attached data source", function (done) {
|
||||
User.create({first: 'joe', last: 'bob'}, function (err, user) {
|
||||
User.removeById(user.id, function (err) {
|
||||
User.deleteById(user.id, function (err) {
|
||||
User.findById(user.id, function (err, notFound) {
|
||||
assert.equal(notFound, null);
|
||||
done();
|
||||
|
|
Loading…
Reference in New Issue