diff --git a/Gruntfile.js b/Gruntfile.js index 8953639..725f82f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -37,7 +37,8 @@ module.exports = function( grunt ) { unit: { options: { require: [ 'chai' ], - reporter: 'spec' + reporter: 'spec', + timeout: 5000 }, src: [ 'tests/unit/*.js' ].concat( getModulePaths( 'tests', 'unit', '*.js' ) ) }, diff --git a/lib/config/index.js b/lib/config/index.js index 8da4b2d..6e73c16 100644 --- a/lib/config/index.js +++ b/lib/config/index.js @@ -1,3 +1,3 @@ var path = require( 'path' ); -module.exports = require( path.resolve( [ __dirname, '..', 'config' ].join( path.sep ) ) + path.sep ); \ No newline at end of file +module.exports = require( path.resolve( [ __dirname, '..', '..', 'config' ].join( path.sep ) ) + path.sep ); \ No newline at end of file diff --git a/modules/auth/module.js b/modules/auth/module.js deleted file mode 100644 index d7b740c..0000000 --- a/modules/auth/module.js +++ /dev/null @@ -1,59 +0,0 @@ -var passport = require( 'passport' ) - , debug = require( 'debug' )( 'AuthModule' ) - , ModuleClass = require( 'classes' ).ModuleClass - , RedisStore = require( 'connect-redis' )( injector.getInstance( 'express' ) ) - , Module; - -Module = ModuleClass.extend({ - store: null, - - preResources: function() { - injector.instance( 'passport', passport ); - }, - - configureApp: function( app, express ) { - // Setup the redis store - this.store = new RedisStore({ - host: this.config.redis.host, - port: this.config.redis.port, - prefix: this.config.redis.prefix + process.env.NODE_ENV + "_", - password: this.config.redis.key - }); - - // Session management - app.use( express.cookieParser() ); - app.use( express.session({ - secret: this.config.secretKey, - cookie: { secure: false, maxAge: 86400000 }, - store: this.store - })); - - // Enable CORS - app.use( this.proxy( 'enableCors' ) ); - - // Initialize passport - app.use( passport.initialize() ); - app.use( passport.session() ); - }, - - enableCors: function( req, res, next ) { - res.header("Access-Control-Allow-Origin", req.headers.origin); - res.header("Access-Control-Allow-Headers", "x-requested-with, content-type"); - res.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS"); - res.header("Access-Control-Allow-Credentials", "true"); - res.header("Access-Control-Max-Age", "1000000000"); - // intercept OPTIONS method - if ('OPTIONS' == req.method) { - res.send(200); - } - else { - next(); - } - }, - - preShutdown: function() { - this.store.client.quit(); - } -}); - -module.exports = new Module( 'auth', injector ); \ No newline at end of file diff --git a/modules/auth/package.json b/modules/auth/package.json deleted file mode 100644 index 76a2591..0000000 --- a/modules/auth/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "auth", - "version": "0.0.1", - "private": true, - "dependencies": { - "passport": "~0.1.18", - "passport-facebook": "~1.0.2", - "passport-local": "~0.1.6", - "connect-redis": "~1.4.6", - "connect": "~2.12.0", - "redis": "~0.10.0" - }, - "author": { - "name": "Clevertech", - "email": "info@clevertech.biz", - "web": "http://www.clevertech.biz" - }, - "collaborators": [ - "Richard Gustin " - ], - "description": "Clevertech Authentication Module", - "keywords": [ - "" - ], - "main": "module.js", - "devDependencies": {} -} diff --git a/modules/clever-auth-facebook/config/default.json b/modules/clever-auth-facebook/config/default.json new file mode 100644 index 0000000..3c00b27 --- /dev/null +++ b/modules/clever-auth-facebook/config/default.json @@ -0,0 +1,21 @@ +{ + "clever-auth-facebook": { + "secretKey": "youMustChangeMe", + + "redis": { + "host": "localhost", + "port": "6379", + "prefix": "", + "key": "" + }, + + "facebook": { + "clientId": "247066242129783", + "clientSecret": "b9ee18a9c582994aeac022c2713256ee", + "redirectURIs": "http://localhost:8080/auth/facebook/return" + }, + + "frontendURL": "http://localhost:9000/" + + } +} \ No newline at end of file diff --git a/modules/clever-auth-facebook/controllers/UserFacebookController.js b/modules/clever-auth-facebook/controllers/UserFacebookController.js new file mode 100644 index 0000000..00960eb --- /dev/null +++ b/modules/clever-auth-facebook/controllers/UserFacebookController.js @@ -0,0 +1,121 @@ +var config = require ( 'config' )[ 'clever-auth-facebook' ] + , passport = require ( 'passport' ) + , qs = require ( 'qs' ) + , FacebookStrategy = require('passport-facebook').Strategy; + +var scope = 'email,user_about_me'; + +module.exports = function ( UserFacebookService ) { + + passport.serializeUser( function ( user, done ) { + done( null, user ); + } ); + + passport.deserializeUser( function ( user, done ) { + done( null, user ) + } ); + + passport.use( new FacebookStrategy( + { + clientID: config.facebook.clientId, + clientSecret: config.facebook.clientSecret, + callbackURL: config.facebook.redirectURIs, + scope: scope + }, + function ( accessToken, refreshToken, profile, done ) { + + UserFacebookService + .findOrCreate( profile, accessToken ) + .then( function( fbUser ) { + return UserFacebookService.authenticate ( fbUser, profile ) + }) + .then( UserFacebookService.updateAccessedDate ) + .then( done.bind( null, null ) ) + .fail( done ); + } + )); + + + return (require( 'classes' ).Controller).extend ( + { + service: UserFacebookService + }, + { + listAction: function () { + UserFacebookService.listUsers() + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, //tested + + getAction: function () { + var fbuId = this.req.params.id; + + UserFacebookService + .findUserById( fbuId ) + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, //tested + + deleteAction: function () { + var fbuId = this.req.params.id; + + UserFacebookService + .deleteUser( fbuId ) + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, //tested + + loginAction: function () { + var params = { + client_id: config.facebook.clientId, + redirect_uri: config.facebook.redirectURIs, + scope: scope + }; + + this.send( { url: 'https://www.facebook.com/dialog/oauth?' + qs.stringify( params ) }, 200 ); + + }, //tested + + returnAction: function () { + passport.authenticate( 'facebook', this.proxy( 'handleLocalUser' ) )( this.req, this.res, this.next ); + }, + + handleLocalUser: function ( err, user ) { + + if ( err ) return this.handleException( err ); + + if ( !user ) { + this.res.statusCode = 302; + this.res.setHeader( 'body', {} ); + this.res.setHeader( 'Location', config.frontendURL ); + this.res.end(); + } else { + this.loginUserJson( user ); + } + }, + + loginUserJson: function ( user ) { + this.req.login( user, this.proxy( 'handleLoginJson', user ) ); + }, + + handleLoginJson: function ( user, err ) { + if ( err ) return this.handleException( err ); + + this.res.statusCode = 302; + this.res.setHeader( 'body', user ); + this.res.setHeader( 'Location', config.frontendURL ); + this.res.end(); + }, + + handleServiceMessage: function ( obj ) { + + if ( obj.statuscode ) { + this.send( obj.message, obj.statuscode ); + return; + } + + this.send( obj, 200 ); + } + + } ); +}; \ No newline at end of file diff --git a/modules/clever-auth-facebook/models/orm/UserFacebookModel.js b/modules/clever-auth-facebook/models/orm/UserFacebookModel.js new file mode 100644 index 0000000..024b651 --- /dev/null +++ b/modules/clever-auth-facebook/models/orm/UserFacebookModel.js @@ -0,0 +1,70 @@ +module.exports = function ( sequelize, DataTypes ) { + return sequelize.define( "UserFacebook", + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + isEmail: true + } + }, + firstname: { + type: DataTypes.STRING, + allowNull: true + }, + lastname: { + type: DataTypes.STRING, + allowNull: true + }, + facebookid: { + type: DataTypes.STRING, + allowNull: false + }, + picture: { + type: DataTypes.STRING, + allowNull: true + }, + link: { + type: DataTypes.STRING, + allowNull: true + }, + locale: { + type: DataTypes.STRING, + allowNull: true + }, + token: { + type: DataTypes.STRING, + allowNull: false + }, + accessedAt: { + type: DataTypes.DATE + }, + UserId: { + type: DataTypes.INTEGER + } + }, + { + paranoid: true, + + getterMethods: { + fullName: function () { + return [ this.getDataValue( 'firstname' ), this.getDataValue( 'lastname' )].join( ' ' ); + } + }, + + instanceMethods: { + toJSON: function () { + var values = this.values; + values.fullName = this.fullName; + delete values.token; + return values; + } + } + } ); +}; \ No newline at end of file diff --git a/modules/clever-auth-facebook/module.js b/modules/clever-auth-facebook/module.js new file mode 100644 index 0000000..82373d4 --- /dev/null +++ b/modules/clever-auth-facebook/module.js @@ -0,0 +1,129 @@ +var passport = require( 'passport' ) + , debug = require( 'debug' )( 'CleverAuthFacebookModule' ) + , fs = require( 'fs' ) + , path = require( 'path' ) + , ModuleClass = require( 'classes' ).ModuleClass + , RedisStore = require( 'connect-redis' )( injector.getInstance( 'express' ) ) + , Module; + +Module = ModuleClass.extend( { + models: null, + store: null, + sequelize: null, + + preSetup: function () { + var self = this; + this.models = {}; + this.sequelize = injector.getInstance ( 'sequelize' ); + var dir = fs.readdirSync ( path.join ( __dirname, 'models', 'orm' ) ); + dir.forEach ( function ( model ) { + self.getModel ( path.join ( __dirname, 'models', 'orm', model ) ); + } ); + }, + + preResources: function () { + injector.instance ( 'passport', passport ); + }, + + modulesLoaded: function () { + this.defineModelsAssociations (); + }, + + defineModelsAssociations: function () { + if ( !this.config.hasOwnProperty ( 'modelAssociations' ) ) { + return true; + } + + debug ( 'Defining model assocations' ); + + Object.keys ( this.config.modelAssociations ).forEach ( this.proxy ( function ( modelName ) { + Object.keys ( this.config.modelAssociations[ modelName ] ).forEach ( this.proxy ( 'defineModelAssociations', modelName ) ); + } ) ); + }, + + defineModelAssociations: function ( modelName, assocType ) { + var associatedWith = this.config.modelAssociations[ modelName ][ assocType ]; + if ( !associatedWith instanceof Array ) { + associatedWith = [ associatedWith ]; + } + + associatedWith.forEach ( this.proxy ( 'associateModels', modelName, assocType ) ); + }, + + associateModels: function ( modelName, assocType, assocTo ) { + // Support second argument + if ( assocTo instanceof Array ) { + debug ( '%s %s %s with second argument of ', modelName, assocType, assocTo[0], assocTo[1] ); + this.models[ modelName ][ assocType ] ( this.models[ assocTo[0] ], assocTo[1] ); + } else { + debug ( '%s %s %s', modelName, assocType, assocTo ); + this.models[ modelName ][ assocType ] ( this.models[assocTo] ); + } + }, + + getModel: function ( modelPath ) { + var modelName = modelPath.split ( '/' ).pop ().split ( '.' ).shift (); + + if ( typeof this.models[ modelName ] === 'undefined' ) { + debug ( [ 'Loading model', modelName, 'from', modelPath ].join ( ' ' ) ); + + // Call on sequelizejs to load the model + this.models[ modelName ] = this.sequelize.import ( modelPath ); + + // Set a flat for tracking + this.models[ modelName ].ORM = true; + + // Add the model to the injector + injector.instance ( 'ORM' + modelName, this.models[ modelName ] ); + } + + return this.models[ modelName ]; + }, + + configureApp: function ( app, express ) { +// Setup the redis store + this.store = new RedisStore ( { + host: this.config.redis.host, + port: this.config.redis.port, + prefix: this.config.redis.prefix + process.env.NODE_ENV + "_", + password: this.config.redis.key + } ); + +// Session management + app.use ( express.cookieParser () ); + app.use ( express.session ( { + secret: this.config.secretKey, + cookie: { secure: false, maxAge: 86400000 }, + store: this.store + } ) ); + + // Enable CORS + app.use ( this.proxy ( 'enableCors' ) ); + + // Initialize passport + app.use ( passport.initialize () ); + app.use ( passport.session () ); + }, + + enableCors: function ( req, res, next ) { + + res.header ( 'Access-Control-Allow-Origin', req.headers.origin ); + res.header ( 'Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE' ); + res.header ( 'Access-Control-Allow-Headers', 'Content-Type, Authorization, x-requested-with' ); + res.header( "Access-Control-Allow-Credentials", "true" ); + res.header( "Access-Control-Max-Age", "1000000000" ); + // intercept OPTIONS method + if ( 'OPTIONS' == req.method ) { + res.send ( 200 ); + } + else { + next (); + } + }, + + preShutdown: function () { + this.store.client.quit (); + } +} ); + +module.exports = new Module( 'clever-auth-facebook', injector ); \ No newline at end of file diff --git a/modules/clever-auth-facebook/package.json b/modules/clever-auth-facebook/package.json new file mode 100644 index 0000000..d731035 --- /dev/null +++ b/modules/clever-auth-facebook/package.json @@ -0,0 +1,32 @@ +{ + "name": "clever-auth-facebook", + "version": "0.0.1", + "private": true, + "dependencies": { + "passport": "~0.1.18", + "connect-redis": "~1.4.6", + "q": "", + "sequelize": "1.7.0-beta8", + "moment": "", + "passport-facebook": "", + "qs": "*" + }, + "author": { + "name": "Clevertech", + "email": "info@clevertech.biz", + "web": "http://www.clevertech.biz" + }, + "collaborators": [ + "" + ], + "description": "", + "keywords": [ + "" + ], + "main": "module.js", + "devDependencies": { + "chai": "*", + "mocha": "*", + "sinon": "" + } +} \ No newline at end of file diff --git a/modules/clever-auth-facebook/routes.js b/modules/clever-auth-facebook/routes.js new file mode 100644 index 0000000..169b7d7 --- /dev/null +++ b/modules/clever-auth-facebook/routes.js @@ -0,0 +1,8 @@ +module.exports = function ( + app, + UserFacebookController ) +{ + + app.all('/auth/facebook/?:action?', UserFacebookController.attach() ); + +}; \ No newline at end of file diff --git a/modules/clever-auth-facebook/services/UserFacebookService.js b/modules/clever-auth-facebook/services/UserFacebookService.js new file mode 100644 index 0000000..2477b49 --- /dev/null +++ b/modules/clever-auth-facebook/services/UserFacebookService.js @@ -0,0 +1,234 @@ +var Q = require( 'q' ) + , crypto = require( 'crypto' ) + , moment = require( 'moment' ) + , Sequelize = require( 'sequelize' ) + , UserFacebookService = null; + +module.exports = function ( sequelize, + ORMUserFacebookModel ) { + + if ( UserFacebookService && UserFacebookService.instance ) { + return UserFacebookService.instance; + } + + var UserService = null; + + UserFacebookService = require( 'services' ).BaseService.extend( { + + formatData: function ( profile, accessToken ) { + var name = !!profile._json.name ? profile._json.name.split( ' ' ) : []; + return { + facebookid: profile._json.id, + username: profile._json.username, + firstname: profile._json.first_name, + lastname: profile._json.last_name, + email: profile._json.email, + picture: !!profile._json.picture + ? typeof profile._json.picture == 'object' && profile._json.picture.data + ? profile._json.picture.data.url + : profile._json.picture + : null, + link: profile._json.link, + locale: profile._json.locale || null, + token: accessToken || null + } + }, + + findOrCreate: function ( profile, accessToken ) { + var deferred = Q.defer() + , data = this.formatData ( profile, accessToken ); + + if ( data.email ) { + + ORMUserFacebookModel + .find ( { where: { email: data.email } } ) + .success ( function ( fbUser ) { + + if ( !fbUser ) { + + data.accessedAt = moment.utc ().format ( 'YYYY-MM-DD HH:ss:mm' ); + + ORMUserFacebookModel + .create ( data ) + .success ( deferred.resolve ) + .error ( deferred.reject ); + + } else { + + fbUser + .updateAttributes ( { accessedAt: moment.utc ().format ( 'YYYY-MM-DD HH:ss:mm' ), token: data.token } ) + .success ( deferred.resolve ) + .error ( deferred.reject ); + } + } ) + .error ( deferred.reject ); + } else { + deferred.resolve(); + } + + return deferred.promise; + }, //tested + + authenticate: function( fbUser, profile ) { + var deferred = Q.defer() + , data = this.formatData ( profile ); + + try { + UserService = injector.getInstance( 'UserService' ); + } catch ( err ) { + console.log( err ); + } + + if ( !!fbUser && !!UserService ) { + + if ( fbUser.UserId ) { + + UserService + .find( { where: { id: fbUser.UserId, email: fbUser.email } } ) + .then( function( users ) { + + var user = !!users && !!users.length + ? users[0] + : { statuscode: 403, message: 'invalid' }; + + deferred.resolve( user ); + }) + .fail( deferred.reject ); + + } else { + + UserService + .find( { where: { email: fbUser.email } } ) + .then( function( user ) { + + user = user[0]; + + if ( !!user && !!user.id ) { + + fbUser + .updateAttributes( { UserId: user.id } ) + .success ( function () { + deferred.resolve ( user ); + } ) + .error( deferred.reject ); + + } else { + + data.confirmed = true; + data.username = data.email; + data.password = crypto.createHash( 'sha1' ).update( Math.floor(Math.random() * 1e18) + '' ).digest( 'hex' ) + + UserService + .create( data ) + .then( function( user ) { + + fbUser + .updateAttributes ( { UserId: user.id } ) + .success ( function () { + deferred.resolve ( user ); + } ) + .error ( deferred.reject ); + }) + .fail( deferred.reject ); + } + }) + .fail( deferred.reject ); + } + + } else { + deferred.resolve( fbUser ); + } + + return deferred.promise; + }, //tested + + updateAccessedDate: function( user ){ + var deferred = Q.defer(); + + if ( !!user && !!user.id ) { + user + .updateAttributes( { accessedAt: moment.utc().format( 'YYYY-MM-DD HH:ss:mm' ) } ) + .success( function() { + deferred.resolve( user.toJSON() ); + }) + .error( deferred.reject ); + } else { + deferred.resolve( user ); + } + + return deferred.promise; + }, //tested + + listUsers: function() { + var deferred = Q.defer(); + + ORMUserFacebookModel + .findAll( { where: { deletedAt: null } } ) + .success( function( fbUsers ) { + if ( !!fbUsers && !!fbUsers.length ) { + deferred.resolve( fbUsers.map( function( u ) { return u.toJSON(); } ) ); + } else { + deferred.resolve( {} ); + } + }) + .error( deferred.reject ); + + return deferred.promise; + }, //tested + + findUserById: function( fbUserId ) { + var deferred = Q.defer(); + + ORMUserFacebookModel + .find( fbUserId ) + .success( function( fbUser ) { + + if ( !!fbUser && !!fbUser.id ){ + deferred.resolve( fbUser.toJSON() ); + } else { + deferred.resolve( { statuscode: 403, message: 'user do not exist' } ); + } + }) + .error( deferred.reject ); + + return deferred.promise; + }, //tested + + deleteUser: function( fbUserId ) { + var deferred = Q.defer(); + + ORMUserFacebookModel + .find( fbUserId ) + .success( function( fbUser ) { + + if ( !!fbUser && !!fbUser.id ) { + + fbUser + .destroy() + .success( function( result ) { + + if ( !result.deletedAt ) { + deferred.resolve( { statuscode: 500, message: 'error' } ); + } else { + deferred.resolve( { statuscode: 200, message: 'user is deleted' } ); + } + + }) + .error( deferred.reject ); + + } else { + deferred.resolve( { statuscode: 403, message: 'user do not exist' } ) + } + }) + .error( deferred.reject ); + + return deferred.promise; + } //tested + + } ); + + UserFacebookService.instance = new UserFacebookService( sequelize ); + UserFacebookService.Model = ORMUserFacebookModel; + + return UserFacebookService.instance; +}; \ No newline at end of file diff --git a/modules/clever-auth-facebook/tests/unit/test.controllers.UserFacebookController.js b/modules/clever-auth-facebook/tests/unit/test.controllers.UserFacebookController.js new file mode 100644 index 0000000..d601fe9 --- /dev/null +++ b/modules/clever-auth-facebook/tests/unit/test.controllers.UserFacebookController.js @@ -0,0 +1,229 @@ +// Bootstrap the testing environmen +var testEnv = require( 'utils' ).testEnv(); + +var expect = require( 'chai' ).expect + , Q = require ( 'q' ) + , sinon = require( 'sinon' ) + , Service; + +var new_user; + +describe( 'controllers.UserFacebookController', function () { + var Service, UserFacebookController, ctrl, fbUsers = []; + + before( function ( done ) { + testEnv( function ( _UserFacebookService_, _UserFacebookController_ ) { + var req = { + params: { action: 'fakeAction'}, + method: 'GET', + query: {} + }; + + var res = { + json: function () {} + }; + + var next = function () {}; + + Controller = _UserFacebookController_; + Service = _UserFacebookService_; + ctrl = new Controller( req, res, next ); + + var profile_1 = { + _json: { + id: '100001182151515151', + name: 'Volodumyr Denshchykov', + first_name: 'Volodumyr', + last_name: 'Denshchykov', + link: 'https://www.facebook.com/denshikov.vovan', + gender: 'male', + email: 'denshikov_vovan@mail.ru', + timezone: 2, + locale: 'ru_RU', + verified: true, + updated_time: '2014-02-10T16:21:17+0000', + username: 'denshikov.vovan', + picture: 'url_for_picture' + } + } + , profile_2 = { + _json: { + id: '100001182151515151', + name: 'Volodumyr Denshchykov', + first_name: 'Volodumyr', + last_name: 'Denshchykov', + link: 'https://www.facebook.com/denshikov.vovan', + gender: 'male', + email: 'denshikov_vovanok@mail.ru', + timezone: 2, + locale: 'ru_RU', + verified: true, + updated_time: '2014-02-10T16:21:17+0000', + username: 'denshikov.vovan', + picture: 'url_for_picture' + } + }; + + Service + .findOrCreate( profile_1, 'bhbhgvbljbhscvzkchvblzclvnzbkjxcv' ) + .then( function( fbUser ) { + + expect( fbUser ).to.be.an( 'object' ).and.be.ok; + expect( fbUser ).to.have.property( 'id' ).and.be.ok; + + fbUsers.push( fbUser ); + + Service + .findOrCreate( profile_2, 'kjajkvl zsdvbakhvckhabskhv' ) + .then( function( fbUser ) { + + expect( fbUser ).to.be.an( 'object' ).and.be.ok; + expect( fbUser ).to.have.property( 'id' ).and.be.ok; + + fbUsers.push( fbUser ); + + done(); + }, done ); + }, done ); + } ); + } ); + + afterEach( function ( done ) { + + ctrl.req = { + params: { action: 'fakeAction'}, + method: 'GET', + query: {}, + body: {} + }; + + ctrl.res = { + json: function () {} + }; + + done(); + }); + + describe( 'loginAction()', function () { + + it( 'should call this.send() with specify parameters', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'url' ).and.contain( 'https://www.facebook.com/dialog/oauth?' ); + + done(); + }; + + ctrl.loginAction(); + } ); + + } ); + + describe( 'listAction()', function () { + + it( 'should be able to get list of users', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'array' ).and.have.length.above( 1 ); + expect( result[0] ).to.be.an( 'object' ).and.be.ok; + expect( result[0] ).to.have.property( 'id' ).and.be.ok; + expect( result[0] ).to.have.property( 'firstname' ).and.be.ok; + expect( result[0] ).to.have.property( 'lastname' ).and.be.ok; + expect( result[0] ).to.have.property( 'email' ).and.be.ok; + expect( result[0] ).to.have.property( 'facebookid' ).and.be.ok; + expect( result[0] ).to.not.have.property( 'token' ); + + done(); + }; + + ctrl.listAction(); + } ); + + } ); + + describe( 'getAction()', function () { + + it( 'should be able to get user by id', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.be.ok; + expect( result ).to.have.property( 'firstname' ).and.be.ok; + expect( result ).to.have.property( 'lastname' ).and.be.ok; + expect( result ).to.have.property( 'email' ).and.be.ok; + expect( result ).to.have.property( 'facebookid' ).and.be.ok; + expect( result ).to.not.have.property( 'token' ); + + done(); + }; + + ctrl.req.params = { id: fbUsers[0].id }; + + ctrl.getAction(); + } ); + + it( 'should be able to get error if user do not exist', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: 15151515151151515 }; + + ctrl.getAction(); + } ); + + } ); + + describe( 'deleteAction()', function () { + + it( 'should be able to delete user by id', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: fbUsers[0].id }; + + ctrl.deleteAction(); + } ); + + it( 'should be able to get error if user do not exist', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: 15151515151151515 }; + + ctrl.deleteAction(); + } ); + + } ); + +} ); \ No newline at end of file diff --git a/modules/clever-auth-facebook/tests/unit/test.service.UserFacebookService.js b/modules/clever-auth-facebook/tests/unit/test.service.UserFacebookService.js new file mode 100644 index 0000000..67c1608 --- /dev/null +++ b/modules/clever-auth-facebook/tests/unit/test.service.UserFacebookService.js @@ -0,0 +1,802 @@ +var expect = require ( 'chai' ).expect + , request = require ( 'supertest' ) + , path = require( 'path' ) + , app = require ( path.resolve( __dirname + '/../../../../' ) + '/index.js' ) + , config = require( 'config' ) + , testEnv = require ( 'utils' ).testEnv() + , sinon = require( 'sinon' ) + , Q = require ( 'q' ); + +var UserService = null; + +var fbUserId_1, accessedAtDate, fbUser, user_1; + +describe( 'service.UserFacebookService', function () { + var Service, Model; + + before( function ( done ) { + this.timeout( 15000 ); + testEnv( function ( _UserFacebookService_, _ORMUserFacebookModel_ ) { + + Service = _UserFacebookService_; + Model = _ORMUserFacebookModel_; + + done(); + }, done ); + } ); + + describe( '.formatData( profile, accessToken )', function () { + + it( 'should return an object with filtered data', function ( done ) { + var accessToken = 'sdasdasdasdasdasdasdasda' + , profile = { id: '100001182151515151', + username: 'denshikov.vovan', + displayName: 'Volodumyr Denshchykov', + name: + { + familyName: 'Denshchykov', + givenName: 'Volodumyr', + middleName: undefined + }, + gender: 'male', + profileUrl: 'https://www.facebook.com/denshikov.vovan', + emails: [ { value: 'denshikov_vovan@mail.ru' } ], + provider: 'facebook', + _raw: '{"id":"100001182409330","name":"\\u0432\\u043b\\u0430\\u0434\\u0438\\u043c\\u0438\\u0440 \\u0434\\u0435\\u043d\\u0449\\u0438\\u043a\\u043e\\u0432","first_name":"\\u0432\\u043b\\u0430\\u0434\\u0438\\u043c\\u0438\\u0440","last_name":"\\u0434\\u0435\\u043d\\u0449\\u0438\\u043a\\u043e\\u0432","link":"https:\\/\\/www.facebook.com\\/denshikov.vovan","gender":"male","email":"denshikov_vovan\\u0040mail.ru","timezone":2,"locale":"ru_RU","verified":true,"updated_time":"2014-02-10T16:21:17+0000","username":"denshikov.vovan"}', + _json: + { + id: '100001182151515151', + name: 'Volodumyr Denshchykov', + first_name: 'Volodumyr', + last_name: 'Denshchykov', + link: 'https://www.facebook.com/denshikov.vovan', + gender: 'male', + email: 'denshikov_vovan@mail.ru', + timezone: 2, + locale: 'ru_RU', + verified: true, + updated_time: '2014-02-10T16:21:17+0000', + username: 'denshikov.vovan' + } + } + + + var data = Service.formatData( profile, accessToken ); + + expect( data ).to.be.an( 'object' ).and.be.ok; + + expect( data ).to.have.property( 'email' ).and.equal( profile._json.email ); + expect( data ).to.have.property( 'firstname' ).and.equal( profile._json.first_name ); + expect( data ).to.have.property( 'lastname' ).and.equal( profile._json.last_name ); + expect( data ).to.have.property( 'facebookid' ).and.equal( profile._json.id ); + expect( data ).to.have.property( 'picture' ).and.be.null; + expect( data ).to.have.property( 'link' ).and.equal( profile._json.link ); + expect( data ).to.have.property( 'locale' ).and.equal( profile._json.locale ); + expect( data ).to.have.property( 'token' ).and.equal( accessToken ); + + expect( data ).to.not.have.property( 'gender' ); + expect( data ).to.not.have.property( 'name' ); + expect( data ).to.not.have.property( 'plan' ); + + done(); + } ); + + it( 'should return an object with filtered data that contain field picture', function ( done ) { + var accessToken = 'sdasdasdasdasdasdasdasda' + , profile = { + _json: { + id: '100001182151515151', + name: 'Volodumyr Denshchykov', + first_name: 'Volodumyr', + last_name: 'Denshchykov', + link: 'https://www.facebook.com/denshikov.vovan', + gender: 'male', + email: 'denshikov_vovan@mail.ru', + timezone: 2, + locale: 'ru_RU', + verified: true, + updated_time: '2014-02-10T16:21:17+0000', + username: 'denshikov.vovan', + picture: { + data: { + url: 'url_for_picture' + } + } + } + }; + + + var data = Service.formatData( profile, accessToken ); + + expect( data ).to.be.an( 'object' ).and.be.ok; + + expect( data ).to.have.property( 'email' ).and.equal( profile._json.email ); + expect( data ).to.have.property( 'facebookid' ).and.equal( profile._json.id ); + expect( data ).to.have.property( 'picture' ).and.equal( profile._json.picture.data.url ); + + done(); + } ); + + it( 'should return an object with filtered data that contain field picture', function ( done ) { + var accessToken = 'sdasdasdasdasdasdasdasda' + , profile = { + _json: { + id: '100001182151515151', + name: 'Volodumyr Denshchykov', + first_name: 'Volodumyr', + last_name: 'Denshchykov', + link: 'https://www.facebook.com/denshikov.vovan', + gender: 'male', + email: 'denshikov_vovan@mail.ru', + timezone: 2, + locale: 'ru_RU', + verified: true, + updated_time: '2014-02-10T16:21:17+0000', + username: 'denshikov.vovan', + picture: 'url_for_picture' + } + }; + + + var data = Service.formatData( profile, accessToken ); + + expect( data ).to.be.an( 'object' ).and.be.ok; + + expect( data ).to.have.property( 'email' ).and.equal( profile._json.email ); + expect( data ).to.have.property( 'facebookid' ).and.equal( profile._json.id ); + expect( data ).to.have.property( 'picture' ).and.equal( profile._json.picture ); + + done(); + } ); + + } ); + + describe( '.findOrCreate( profile, accessToken )', function () { + + it( 'should not call ORMUserFacebookModel.find() if email is not define', function ( done ) { + var accessToken = 'sdasdasdasdasdasdasdasda' + , profile = { + _json: { + id: '100001182151515151', + name: 'Volodumyr Denshchykov', + first_name: 'Volodumyr', + last_name: 'Denshchykov', + link: 'https://www.facebook.com/denshikov.vovan', + gender: 'male', + email: null, + timezone: 2, + locale: 'ru_RU', + verified: true, + updated_time: '2014-02-10T16:21:17+0000', + username: 'denshikov.vovan', + picture: 'url_for_picture' + } + }; + + var spy = sinon.spy( Model, 'find' ); + + Service + .findOrCreate( profile, accessToken ) + .then( function ( result ) { + + expect( result ).to.not.be.ok; + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + }, done ) + } ); + + it( 'should call ORMUserFacebookModel.find(), ORMUserFacebookModel.create() and create fbUser if facebook account do not already exist', function ( done ) { + var accessToken = 'sdasdasdasdasdasdasdasda' + , profile = { + _json: { + id: '100001182151515151', + name: 'Volodumyr Denshchykov', + first_name: 'Volodumyr', + last_name: 'Denshchykov', + link: 'https://www.facebook.com/denshikov.vovan', + gender: 'male', + email: 'denshikov_vovan@mail.ru', + timezone: 2, + locale: 'ru_RU', + verified: true, + updated_time: '2014-02-10T16:21:17+0000', + username: 'denshikov.vovan', + picture: 'url_for_picture' + } + }; + + var spyFind = sinon.spy( Model, 'find' ); + var spyCreate = sinon.spy( Model, 'create' ); + + Service + .findOrCreate( profile, accessToken ) + .delay( 1000 ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.be.ok; + + expect( spyFind.called ).to.be.true; + expect( spyFind.calledOnce ).to.be.true; + + expect( spyCreate.called ).to.be.true; + expect( spyCreate.calledOnce ).to.be.true; + + spyFind.restore(); + spyCreate.restore(); + + Model + .find( result.id ) + .success( function( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( result.id ); + expect( user ).to.have.property( 'token' ).and.equal( accessToken ); + expect( user ).to.have.property( 'firstname' ).and.equal( profile._json.first_name ); + expect( user ).to.have.property( 'lastname' ).and.equal( profile._json.last_name ); + expect( user ).to.have.property( 'email' ).and.equal( profile._json.email ); + expect( user ).to.have.property( 'facebookid' ).and.equal( profile._json.id ); + expect( user ).to.have.property( 'accessedAt' ).and.be.ok; + + fbUserId_1 = user.id; + accessedAtDate = +new Date( user.accessedAt ); + fbUser = user; + + done(); + }) + .error( done ); + }, done ) + } ); + + it( 'should call ORMUserFacebookModel.find(), ORMUserFacebookModel.updateAttributes(), not call ORMUserFacebookModel.create() and update already existing fbUser', function ( done ) { + var accessToken = '15151515151515151' + , profile = { + _json: { + id: '100001182151515151', + name: 'Volodumyr Denshchykov', + first_name: 'Volodumyr', + last_name: 'Denshchykov', + link: 'https://www.facebook.com/denshikov.vovan', + gender: 'male', + email: 'denshikov_vovan@mail.ru', + timezone: 2, + locale: 'ru_RU', + verified: true, + updated_time: '2014-02-10T16:21:17+0000', + username: 'denshikov.vovan', + picture: 'url_for_picture' + } + }; + + var spyFind = sinon.spy( Model, 'find' ); + var spyCreate = sinon.spy( Model, 'create' ); + + Service + .findOrCreate( profile, accessToken ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( fbUserId_1 ); + expect( result ).to.have.property( 'token' ).and.equal( accessToken ); + + var newAccessedAtDate = +new Date( result.accessedAt ); + + expect( accessedAtDate ).to.not.equal( newAccessedAtDate ); + + expect( spyFind.called ).to.be.true; + expect( spyFind.calledOnce ).to.be.true; + + expect( spyCreate.called ).to.be.false; + + spyFind.restore(); + spyCreate.restore(); + + done(); + }, done ) + } ); + + } ); + + describe( '.authenticate( fbUser, profile )', function () { + + before( function( done ) { + var accessToken = '516516516+51+651' + , profile = { + _json: { + id: '100001182151515151', + name: 'Volodumyr Denshchykov', + first_name: 'Volodumyr', + last_name: 'Denshchykov', + link: 'https://www.facebook.com/denshikov.vovan', + gender: 'male', + email: 'denshikov_vovanok@mail.ru', + timezone: 2, + locale: 'ru_RU', + verified: true, + updated_time: '2014-02-10T16:21:17+0000', + username: 'denshikov.vovan', + picture: 'url_for_picture' + } + }; + + try { + UserService = injector.getInstance( 'UserService' ); + } catch ( err ) { + console.log( err ); + } + + Service + .findOrCreate( profile, accessToken ) + .then( function( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.be.ok; + + fbUser = user; + + done(); + }, done ); + }); + + it( 'should return fbUser if UserService is not defined', function ( done ) { + var profile = { + _json: { + id: '100001182151515151', + name: 'Volodumyr Denshchykov', + first_name: 'Volodumyr', + last_name: 'Denshchykov', + link: 'https://www.facebook.com/denshikov.vovan', + gender: 'male', + email: 'denshikov_vovanok@mail.ru', + timezone: 2, + locale: 'ru_RU', + verified: true, + updated_time: '2014-02-10T16:21:17+0000', + username: 'denshikov.vovan', + picture: 'url_for_picture' + } + }; + + if ( !UserService ) { + Service + .authenticate( fbUser, profile ) + .then( function ( user ) { + + expect( !UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( fbUser.id ); + expect( user ).to.have.property( 'token' ).and.be.ok; + expect( user ).to.have.property( 'firstname' ).and.equal( fbUser.firstname ); + expect( user ).to.have.property( 'lastname' ).and.equal( fbUser.lastname ); + expect( user ).to.have.property( 'email' ).and.equal( fbUser.email ); + expect( user ).to.have.property( 'facebookid' ).and.equal( fbUser.facebookid ); + expect( user ).to.have.property( 'accessedAt' ).and.be.ok; + + done(); + } ) + .fail( done ); + } else { + console.log( 'UserService is defined' ); + done(); + } + } ); + + it( 'should create new user and to associate it with qUser if UserService is defined', function ( done ) { + var profile = { + _json: { + id: '100001182151515151', + name: 'Volodumyr Denshchykov', + first_name: 'Volodumyr', + last_name: 'Denshchykov', + link: 'https://www.facebook.com/denshikov.vovan', + gender: 'male', + email: 'denshikov_vovanok@mail.ru', + timezone: 2, + locale: 'ru_RU', + verified: true, + updated_time: '2014-02-10T16:21:17+0000', + username: 'denshikov.vovan', + picture: 'url_for_picture' + } + }; + + if ( !!UserService ) { + + var spy = sinon.spy( UserService, 'create' ); + + Service + .authenticate( fbUser, profile ) + .then( function ( user ) { + + expect( !!UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.not.equal( fbUser.id ); + + expect( spy.called ).to.be.true; + expect( spy.calledOnce ).to.be.true; + + spy.restore(); + + UserService + .findById( user.id ) + .then( function( _user ) { + + expect( _user ).to.be.an( 'object' ).and.be.ok; + expect( _user ).to.have.property( 'id' ).and.equal( user.id ); + expect( _user ).to.have.property( 'email' ).and.equal( profile._json.email ); + expect( _user ).to.have.property( 'password' ).and.be.ok; + expect( _user ).to.have.property( 'firstname' ).and.equal( profile._json.first_name ); + expect( _user ).to.have.property( 'lastname' ).and.equal( profile._json.last_name ); + expect( _user ).to.have.property( 'confirmed' ).and.equal( true ); + expect( _user ).to.have.property( 'active' ).and.equal( true ); + + user_1 = _user; + + Service + .find( { where: { id: fbUser.id } } ) + .then( function( _fbUser ) { + + expect( _fbUser ).to.be.an( 'array' ).and.have.length( 1 ); + + _fbUser = _fbUser[0]; + + expect( _fbUser ).to.be.an( 'object' ).and.be.ok; + expect( _fbUser ).to.have.property( 'id' ).and.equal( fbUser.id ); + expect( _fbUser ).to.have.property( 'UserId' ).and.equal( user.id ); + + done(); + }, done ) + }, done ); + } ) + .fail( done ); + } else { + console.log( 'UserService is not defined' ); + done(); + } + + } ); + + it( 'should return existing user by fbUser.UserId if UserService is defined', function ( done ) { + var profile = { + _json: { + id: '100001182151515151', + name: 'Volodumyr Denshchykov', + first_name: 'Volodumyr', + last_name: 'Denshchykov', + link: 'https://www.facebook.com/denshikov.vovan', + gender: 'male', + email: 'denshikov_vovanok@mail.ru', + timezone: 2, + locale: 'ru_RU', + verified: true, + updated_time: '2014-02-10T16:21:17+0000', + username: 'denshikov.vovan', + picture: 'url_for_picture' + } + }; + + if ( !!UserService ) { + + var spy = sinon.spy( UserService, 'create' ); + + Service + .authenticate( fbUser, profile ) + .then( function ( user ) { + + expect( !!UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( user_1.id ); + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + } ) + .fail( done ); + } else { + console.log( 'UserService is not defined' ); + done(); + } + + } ); + + it( 'should return error if user with such fbUser.UserId do not exist and if UserService is defined' , function ( done ) { + var profile = { + _json: { + id: '100001182151515151', + name: 'Volodumyr Denshchykov', + first_name: 'Volodumyr', + last_name: 'Denshchykov', + link: 'https://www.facebook.com/denshikov.vovan', + gender: 'male', + email: 'denshikov_vovanok@mail.ru', + timezone: 2, + locale: 'ru_RU', + verified: true, + updated_time: '2014-02-10T16:21:17+0000', + username: 'denshikov.vovan', + picture: 'url_for_picture' + } + }; + + if ( !!UserService ) { + + var spy = sinon.spy( UserService, 'create' ); + + fbUser + .updateAttributes( { UserId: 1000 } ) + .success( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'UserId' ).and.equal( 1000 ); + + Service + .authenticate( fbUser, profile ) + .then( function ( user ) { + + expect( !!UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( user ).to.have.property( 'message' ).and.be.ok; + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + } ) + .fail( done ); + }) + .error( done ); + } else { + console.log( 'UserService is not defined' ); + done(); + } + + } ); + + it( 'should return existing user by fbUser.email if user with such email already exist', function ( done ) { + var profile = { + _json: { + id: '100001182151515151', + name: 'Volodumyr Denshchykov', + first_name: 'Volodumyr', + last_name: 'Denshchykov', + link: 'https://www.facebook.com/denshikov.vovan', + gender: 'male', + email: 'denshikov_vovanok@mail.ru', + timezone: 2, + locale: 'ru_RU', + verified: true, + updated_time: '2014-02-10T16:21:17+0000', + username: 'denshikov.vovan', + picture: 'url_for_picture' + } + }; + + if ( !!UserService ) { + + var spy = sinon.spy( UserService, 'create' ); + + fbUser + .updateAttributes( { UserId: null } ) + .success( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'UserId' ).and.equal( null ); + + Service + .authenticate( fbUser, profile ) + .then( function ( user ) { + + expect( !!UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( user_1.id ); + + expect( spy.called ).to.be.false; + + spy.restore(); + + Service + .find( { where: { id: fbUser.id } } ) + .then( function( _fbUser ) { + + expect( _fbUser ).to.be.an( 'array' ).and.have.length( 1 ); + + _fbUser = _fbUser[0]; + + expect( _fbUser ).to.be.an( 'object' ).and.be.ok; + expect( _fbUser ).to.have.property( 'id' ).and.equal( fbUser.id ); + expect( _fbUser ).to.have.property( 'UserId' ).and.equal( user.id ); + + done(); + }, done ) + } ) + .fail( done ); + }) + .error( done ); + } else { + console.log( 'UserService is not defined' ); + done(); + } + + } ); + + } ); + + describe( '.updateAccessedDate( user )', function () { + + it( 'should be able to update user', function ( done ) { + + if ( !!UserService ) { + + user_1 + .updateAttributes( { accessedAt: null } ) + .success( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( user_1.id ); + expect( result ).to.have.property( 'accessedAt' ).and.not.be.ok; + + Service + .updateAccessedDate( user_1 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( user_1.id ); + expect( result ).to.have.property( 'accessedAt' ).and.be.ok; + + done(); + }, done ); + }) + .error( done ); + + } else { + console.log( 'UserService is not defined' ); + done(); + } + } ); + + it( 'should be able to update fbUser', function ( done ) { + + fbUser + .updateAttributes( { accessedAt: null } ) + .success( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( fbUser.id ); + expect( result ).to.have.property( 'accessedAt' ).and.not.be.ok; + + Service + .updateAccessedDate( fbUser ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( fbUser.id ); + expect( result ).to.have.property( 'accessedAt' ).and.be.ok; + + done(); + }, done ); + }) + .error( done ); + } ); + + it( 'should be able to get object if it is not a user', function ( done ) { + + Service + .updateAccessedDate ( { statuscode: 403, message: 'invalid' } ) + .then ( function ( result ) { + + expect ( result ).to.be.an ( 'object' ).and.be.ok; + expect ( result ).to.have.property ( 'statuscode' ).and.equal ( 403 ); + expect ( result ).to.have.property ( 'message' ).and.be.ok; + + done (); + }, done ); + } ); + + } ); + + describe( '.listUsers()', function () { + + it( 'should be able to get list of all fbUsers', function ( done ) { + + Service + .listUsers() + .then( function ( users ) { + + expect( users ).to.be.an( 'array' ).and.have.length.above( 1 ); + expect( users[0] ).to.be.an( 'object' ).and.be.ok; + expect( users[0] ).to.have.property( 'id' ).and.be.ok; + expect( users[0] ).to.have.property( 'facebookid' ).and.be.ok; + expect( users[0] ).to.have.property( 'email' ).and.be.ok; + expect( users[0] ).to.have.property( 'locale' ).and.be.ok; + expect( users[0] ).to.not.have.property( 'token' ); + + done(); + } ) + .fail( done ); + } ); + + } ); + + describe( '.findUserById()', function () { + + it( 'should be able to get fbUser by Id', function ( done ) { + + Service + .findUserById( fbUser.id ) + .then( function ( fbUser ) { + + expect( fbUser ).to.be.an( 'object' ).and.be.ok; + expect( fbUser ).to.have.property( 'id' ).and.equal( fbUser.id ); + expect( fbUser ).to.have.property( 'facebookid' ).and.be.ok; + expect( fbUser ).to.have.property( 'email' ).and.be.ok; + expect( fbUser ).to.have.property( 'locale' ).and.be.ok; + expect( fbUser ).to.not.have.property( 'token' ); + + done(); + } ) + .fail( done ); + } ); + + it( 'should be able to get the error if the fbUser does not exist', function ( done ) { + + Service + .findUserById( 151515115151515151 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + }, done ) + } ); + + } ); + + describe( '.deleteUser( fbUserId )', function () { + + it( 'should be able to get the error if the fbUser does not exist', function ( done ) { + + Service + .deleteUser( 151515115151515151 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + }, done ) + } ); + + it( 'should be able to delete fbUser', function ( done ) { + + Service + .deleteUser( fbUser.id ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 200 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + } ) + .fail( done ); + } ); + + } ); + +} ); \ No newline at end of file diff --git a/modules/clever-auth-github/config/default.json b/modules/clever-auth-github/config/default.json new file mode 100644 index 0000000..d3fdc11 --- /dev/null +++ b/modules/clever-auth-github/config/default.json @@ -0,0 +1,21 @@ +{ + "clever-auth-github": { + "secretKey": "youMustChangeMe", + + "redis": { + "host": "localhost", + "port": "6379", + "prefix": "", + "key": "" + }, + + "github": { + "clientId": "486f044b3383cdc29388", + "clientSecret": "f28246b75d34625decc9d835a15107a51f1e8225", + "redirectURIs": "http://localhost:8080/auth/github/return" + }, + + "frontendURL": "http://localhost:9000/" + + } +} \ No newline at end of file diff --git a/modules/clever-auth-github/controllers/UserGithubController.js b/modules/clever-auth-github/controllers/UserGithubController.js new file mode 100644 index 0000000..be03dfd --- /dev/null +++ b/modules/clever-auth-github/controllers/UserGithubController.js @@ -0,0 +1,123 @@ +var config = require ( 'config' )[ 'clever-auth-github' ] + , passport = require ( 'passport' ) + , qs = require ( 'qs' ) + , GitHubStrategy = require('passport-github').Strategy; + +var state = +new Date() + ''; + +module.exports = function ( UserGithubService ) { + + passport.serializeUser( function ( user, done ) { + done( null, user ); + } ); + + passport.deserializeUser( function ( user, done ) { + done( null, user ) + } ); + + passport.use( new GitHubStrategy( + { + clientID: config.github.clientId, + clientSecret: config.github.clientSecret, + callbackURL: config.github.redirectURIs, + state: state, + scope: 'user' + }, + function ( accessToken, refreshToken, profile, done ) { + + UserGithubService + .findOrCreate( profile, accessToken ) + .then( function( gUser ) { + return UserGithubService.authenticate ( gUser, profile ) + }) + .then( UserGithubService.updateAccessedDate ) + .then( done.bind( null, null ) ) + .fail( done ); + } + )); + + + return (require( 'classes' ).Controller).extend ( + { + service: UserGithubService + }, + { + listAction: function () { + UserGithubService.listUsers() + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, //tested + + getAction: function () { + var guId = this.req.params.id; + + UserGithubService + .findUserById( guId ) + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, //tested + + deleteAction: function () { + var guId = this.req.params.id; + + UserGithubService + .deleteUser( guId ) + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, //tested + + loginAction: function () { + var params = { + client_id: config.github.clientId, + redirect_uri: config.github.redirectURIs, + scope: 'user', + state: state + }; + + this.send( { url: 'https://github.com/login/oauth/authorize?' + qs.stringify( params ) }, 200 ); + + }, //tested + + returnAction: function () { + passport.authenticate( 'github', this.proxy( 'handleLocalUser' ) )( this.req, this.res, this.next ); + }, + + handleLocalUser: function ( err, user ) { + + if ( err ) return this.handleException( err ); + + if ( !user ) { + this.res.statusCode = 302; + this.res.setHeader( 'body', {} ); + this.res.setHeader( 'Location', config.frontendURL ); + this.res.end(); + } else { + this.loginUserJson( user ); + } + }, + + loginUserJson: function ( user ) { + this.req.login( user, this.proxy( 'handleLoginJson', user ) ); + }, + + handleLoginJson: function ( user, err ) { + if ( err ) return this.handleException( err ); + + this.res.statusCode = 302; + this.res.setHeader( 'body', user ); + this.res.setHeader( 'Location', config.frontendURL ); + this.res.end(); + }, + + handleServiceMessage: function ( obj ) { + + if ( obj.statuscode ) { + this.send( obj.message, obj.statuscode ); + return; + } + + this.send( obj, 200 ); + } + + } ); +}; \ No newline at end of file diff --git a/modules/clever-auth-github/models/orm/UserGithubModel.js b/modules/clever-auth-github/models/orm/UserGithubModel.js new file mode 100644 index 0000000..dd92589 --- /dev/null +++ b/modules/clever-auth-github/models/orm/UserGithubModel.js @@ -0,0 +1,70 @@ +module.exports = function ( sequelize, DataTypes ) { + return sequelize.define( "UserGithub", + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + isEmail: true + } + }, + firstname: { + type: DataTypes.STRING, + allowNull: true + }, + lastname: { + type: DataTypes.STRING, + allowNull: true + }, + githubid: { + type: DataTypes.STRING, + allowNull: false + }, + picture: { + type: DataTypes.STRING, + allowNull: true + }, + link: { + type: DataTypes.STRING, + allowNull: true + }, + locale: { + type: DataTypes.STRING, + allowNull: true + }, + token: { + type: DataTypes.STRING, + allowNull: false + }, + accessedAt: { + type: DataTypes.DATE + }, + UserId: { + type: DataTypes.INTEGER + } + }, + { + paranoid: true, + + getterMethods: { + fullName: function () { + return [ this.getDataValue( 'firstname' ), this.getDataValue( 'lastname' )].join( ' ' ); + } + }, + + instanceMethods: { + toJSON: function () { + var values = this.values; + values.fullName = this.fullName; + delete values.token; + return values; + } + } + } ); +}; \ No newline at end of file diff --git a/modules/clever-auth-github/module.js b/modules/clever-auth-github/module.js new file mode 100644 index 0000000..f705326 --- /dev/null +++ b/modules/clever-auth-github/module.js @@ -0,0 +1,129 @@ +var passport = require( 'passport' ) + , debug = require( 'debug' )( 'CleverAuthGithubModule' ) + , fs = require( 'fs' ) + , path = require( 'path' ) + , ModuleClass = require( 'classes' ).ModuleClass + , RedisStore = require( 'connect-redis' )( injector.getInstance( 'express' ) ) + , Module; + +Module = ModuleClass.extend( { + models: null, + store: null, + sequelize: null, + + preSetup: function () { + var self = this; + this.models = {}; + this.sequelize = injector.getInstance ( 'sequelize' ); + var dir = fs.readdirSync ( path.join ( __dirname, 'models', 'orm' ) ); + dir.forEach ( function ( model ) { + self.getModel ( path.join ( __dirname, 'models', 'orm', model ) ); + } ); + }, + + preResources: function () { + injector.instance ( 'passport', passport ); + }, + + modulesLoaded: function () { + this.defineModelsAssociations (); + }, + + defineModelsAssociations: function () { + if ( !this.config.hasOwnProperty ( 'modelAssociations' ) ) { + return true; + } + + debug ( 'Defining model assocations' ); + + Object.keys ( this.config.modelAssociations ).forEach ( this.proxy ( function ( modelName ) { + Object.keys ( this.config.modelAssociations[ modelName ] ).forEach ( this.proxy ( 'defineModelAssociations', modelName ) ); + } ) ); + }, + + defineModelAssociations: function ( modelName, assocType ) { + var associatedWith = this.config.modelAssociations[ modelName ][ assocType ]; + if ( !associatedWith instanceof Array ) { + associatedWith = [ associatedWith ]; + } + + associatedWith.forEach ( this.proxy ( 'associateModels', modelName, assocType ) ); + }, + + associateModels: function ( modelName, assocType, assocTo ) { + // Support second argument + if ( assocTo instanceof Array ) { + debug ( '%s %s %s with second argument of ', modelName, assocType, assocTo[0], assocTo[1] ); + this.models[ modelName ][ assocType ] ( this.models[ assocTo[0] ], assocTo[1] ); + } else { + debug ( '%s %s %s', modelName, assocType, assocTo ); + this.models[ modelName ][ assocType ] ( this.models[assocTo] ); + } + }, + + getModel: function ( modelPath ) { + var modelName = modelPath.split ( '/' ).pop ().split ( '.' ).shift (); + + if ( typeof this.models[ modelName ] === 'undefined' ) { + debug ( [ 'Loading model', modelName, 'from', modelPath ].join ( ' ' ) ); + + // Call on sequelizejs to load the model + this.models[ modelName ] = this.sequelize.import ( modelPath ); + + // Set a flat for tracking + this.models[ modelName ].ORM = true; + + // Add the model to the injector + injector.instance ( 'ORM' + modelName, this.models[ modelName ] ); + } + + return this.models[ modelName ]; + }, + + configureApp: function ( app, express ) { +// Setup the redis store + this.store = new RedisStore ( { + host: this.config.redis.host, + port: this.config.redis.port, + prefix: this.config.redis.prefix + process.env.NODE_ENV + "_", + password: this.config.redis.key + } ); + +// Session management + app.use ( express.cookieParser () ); + app.use ( express.session ( { + secret: this.config.secretKey, + cookie: { secure: false, maxAge: 86400000 }, + store: this.store + } ) ); + + // Enable CORS + app.use ( this.proxy ( 'enableCors' ) ); + + // Initialize passport + app.use ( passport.initialize () ); + app.use ( passport.session () ); + }, + + enableCors: function ( req, res, next ) { + + res.header ( 'Access-Control-Allow-Origin', req.headers.origin ); + res.header ( 'Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE' ); + res.header ( 'Access-Control-Allow-Headers', 'Content-Type, Authorization, x-requested-with' ); + res.header( "Access-Control-Allow-Credentials", "true" ); + res.header( "Access-Control-Max-Age", "1000000000" ); + // intercept OPTIONS method + if ( 'OPTIONS' == req.method ) { + res.send ( 200 ); + } + else { + next (); + } + }, + + preShutdown: function () { + this.store.client.quit (); + } +} ); + +module.exports = new Module( 'clever-auth-github', injector ); \ No newline at end of file diff --git a/modules/clever-auth-github/package.json b/modules/clever-auth-github/package.json new file mode 100644 index 0000000..3c3c3ae --- /dev/null +++ b/modules/clever-auth-github/package.json @@ -0,0 +1,32 @@ +{ + "name": "clever-auth-github", + "version": "0.0.1", + "private": true, + "dependencies": { + "passport": "~0.1.18", + "connect-redis": "~1.4.6", + "q": "", + "sequelize": "1.7.0-beta8", + "moment": "", + "passport-github": "", + "qs": "*" + }, + "author": { + "name": "Clevertech", + "email": "info@clevertech.biz", + "web": "http://www.clevertech.biz" + }, + "collaborators": [ + "" + ], + "description": "", + "keywords": [ + "" + ], + "main": "module.js", + "devDependencies": { + "chai": "*", + "mocha": "*", + "sinon": "" + } +} \ No newline at end of file diff --git a/modules/clever-auth-github/routes.js b/modules/clever-auth-github/routes.js new file mode 100644 index 0000000..aca9fa8 --- /dev/null +++ b/modules/clever-auth-github/routes.js @@ -0,0 +1,8 @@ +module.exports = function ( + app, + UserGithubController ) +{ + + app.all('/auth/github/?:action?', UserGithubController.attach() ); + +}; \ No newline at end of file diff --git a/modules/clever-auth-github/services/UserGithubService.js b/modules/clever-auth-github/services/UserGithubService.js new file mode 100644 index 0000000..fa138d2 --- /dev/null +++ b/modules/clever-auth-github/services/UserGithubService.js @@ -0,0 +1,232 @@ +var Q = require( 'q' ) + , crypto = require( 'crypto' ) + , moment = require( 'moment' ) + , Sequelize = require( 'sequelize' ) + , UserGithubService = null; + +module.exports = function ( sequelize, + ORMUserGithubModel ) { + + if ( UserGithubService && UserGithubService.instance ) { + return UserGithubService.instance; + } + + var UserService = null; + + UserGithubService = require( 'services' ).BaseService.extend( { + + formatData: function ( profile, accessToken ) { + var name = !!profile._json.name ? profile._json.name.split( ' ' ) : []; + return { + githubid: profile._json.id, + username: profile._json.login, + firstname: !!name.length ? name[ 0 ] : null, + lastname: !!name.length && name.length > 1 ? name[ name.length - 1 ] : null, + email: profile._json.email, + picture: profile._json.avatar_url || null, + link: profile._json.html_url, + locale: profile._json.location || null, + token: accessToken || null + } + }, //tested + + findOrCreate: function ( profile, accessToken ) { + var deferred = Q.defer() + , data = this.formatData ( profile, accessToken ); + + if ( data.email ) { + + ORMUserGithubModel + .find ( { where: { email: data.email } } ) + .success ( function ( gUser ) { + + if ( !gUser ) { + + data.accessedAt = moment.utc ().format ( 'YYYY-MM-DD HH:ss:mm' ); + + ORMUserGithubModel + .create ( data ) + .success ( deferred.resolve ) + .error ( deferred.reject ); + + } else { + + gUser + .updateAttributes ( { accessedAt: moment.utc ().format ( 'YYYY-MM-DD HH:ss:mm' ), token: data.token } ) + .success ( deferred.resolve ) + .error ( deferred.reject ); + } + } ) + .error ( deferred.reject ); + } else { + deferred.resolve(); + } + + + + return deferred.promise; + }, //tested + + authenticate: function( gUser, profile ) { + var deferred = Q.defer() + , data = this.formatData ( profile ); + + try { + UserService = injector.getInstance( 'UserService' ); + } catch ( err ) { + console.log( err ); + } + + if ( !!gUser && !!UserService ) { + + if ( gUser.UserId ) { + + UserService + .find( { where: { id: gUser.UserId, email: gUser.email } } ) + .then( function( users ) { + + var user = !!users && !!users.length + ? users[0] + : { statuscode: 403, message: 'invalid' }; + + deferred.resolve( user ); + }) + .fail( deferred.reject ); + + } else { + + UserService + .find( { where: { email: gUser.email } } ) + .then( function( user ) { + + user = user[0]; + + if ( !!user && !!user.id ) { + + gUser + .updateAttributes( { UserId: user.id } ) + .success ( function () { + deferred.resolve ( user ); + } ) + .error( deferred.reject ); + + } else { + + data.confirmed = true; + data.username = data.email; + data.password = crypto.createHash( 'sha1' ).update( Math.floor(Math.random() * 1e18) + '' ).digest( 'hex' ) + + UserService + .create( data ) + .then( function( user ) { + + gUser + .updateAttributes ( { UserId: user.id } ) + .success ( function () { + deferred.resolve ( user ); + } ) + .error ( deferred.reject ); + }) + .fail( deferred.reject ); + } + }) + .fail( deferred.reject ); + } + + } else { + deferred.resolve( gUser ); + } + + return deferred.promise; + }, //tested + + updateAccessedDate: function( user ){ + var deferred = Q.defer(); + + if ( !!user && !!user.id ) { + user + .updateAttributes( { accessedAt: moment.utc().format( 'YYYY-MM-DD HH:ss:mm' ) } ) + .success( function() { + deferred.resolve( user.toJSON() ); + }) + .error( deferred.reject ); + } else { + deferred.resolve( user ); + } + + return deferred.promise; + }, //tested + + listUsers: function() { + var deferred = Q.defer(); + + ORMUserGithubModel + .findAll( { where: { deletedAt: null } } ) + .success( function( gUsers ) { + if ( !!gUsers && !!gUsers.length ) { + deferred.resolve( gUsers.map( function( u ) { return u.toJSON(); } ) ); + } else { + deferred.resolve( {} ); + } + }) + .error( deferred.reject ); + + return deferred.promise; + }, //tested + + findUserById: function( gUserId ) { + var deferred = Q.defer(); + + ORMUserGithubModel + .find( gUserId ) + .success( function( gUser ) { + + if ( !!gUser && !!gUser.id ){ + deferred.resolve( gUser.toJSON() ); + } else { + deferred.resolve( { statuscode: 403, message: 'user do not exist' } ); + } + }) + .error( deferred.reject ); + + return deferred.promise; + }, //tested + + deleteUser: function( gUserId ) { + var deferred = Q.defer(); + + ORMUserGithubModel + .find( gUserId ) + .success( function( gUser ) { + + if ( !!gUser && !!gUser.id ) { + + gUser + .destroy() + .success( function( result ) { + + if ( !result.deletedAt ) { + deferred.resolve( { statuscode: 500, message: 'error' } ); + } else { + deferred.resolve( { statuscode: 200, message: 'user is deleted' } ); + } + + }) + .error( deferred.reject ); + + } else { + deferred.resolve( { statuscode: 403, message: 'user do not exist' } ) + } + }) + .error( deferred.reject ); + + return deferred.promise; + } //tested + + } ); + + UserGithubService.instance = new UserGithubService( sequelize ); + UserGithubService.Model = ORMUserGithubModel; + + return UserGithubService.instance; +}; \ No newline at end of file diff --git a/modules/clever-auth-github/tests/unit/test.controllers.UserGithubController.js b/modules/clever-auth-github/tests/unit/test.controllers.UserGithubController.js new file mode 100644 index 0000000..62cd8cb --- /dev/null +++ b/modules/clever-auth-github/tests/unit/test.controllers.UserGithubController.js @@ -0,0 +1,215 @@ +// Bootstrap the testing environmen +var testEnv = require( 'utils' ).testEnv(); + +var expect = require( 'chai' ).expect + , Q = require ( 'q' ) + , sinon = require( 'sinon' ) + , Service; + +var new_user; + +describe( 'controllers.UserGithubController', function () { + var Service, UserGithubController, ctrl, gUsers = []; + + before( function ( done ) { + testEnv( function ( _UserGithubService_, _UserGithubController_ ) { + var req = { + params: { action: 'fakeAction'}, + method: 'GET', + query: {} + }; + + var res = { + json: function () {} + }; + + var next = function () {}; + + Controller = _UserGithubController_; + Service = _UserGithubService_; + ctrl = new Controller( req, res, next ); + + var profile_1 = { + _json: { + id: '112064034597570891032', + email: 'volodymyrm@clevertech.biz', + name: 'Volodymyr Denshchykov', + html_url: 'https://github.com/denshikov-vovan', + location: 'Ukraine', + avatar_url: 'https://gravatar.com/avatar/e98ef168c69fd2a812a8dd46a775072a?d=https%3A%2F%2Fidenticons.github.com%2F814919104a88497c37d2154d918bba97.png&r=x' + } + } + , profile_2 = { + _json: { + id: '112064034597570891032', + email: 'volodymyrm1@clevertech.biz', + name: 'Volodymyr Petrov', + html_url: 'https://github.com/denshikov-vovan', + location: 'Ukraine', + avatar_url: 'https://gravatar.com/avatar/e98ef168c69fd2a812a8dd46a775072a?d=https%3A%2F%2Fidenticons.github.com%2F814919104a88497c37d2154d918bba97.png&r=x' + } + }; + + Service + .findOrCreate( profile_1, 'bhbhgvbljbhscvzkchvblzclvnzbkjxcv' ) + .then( function( gUser ) { + + expect( gUser ).to.be.an( 'object' ).and.be.ok; + expect( gUser ).to.have.property( 'id' ).and.be.ok; + + gUsers.push( gUser ); + + Service + .findOrCreate( profile_2, 'kjajkvl zsdvbakhvckhabskhv' ) + .then( function( gUser ) { + + expect( gUser ).to.be.an( 'object' ).and.be.ok; + expect( gUser ).to.have.property( 'id' ).and.be.ok; + + gUsers.push( gUser ); + + done(); + }, done ); + }, done ); + } ); + } ); + + afterEach( function ( done ) { + + ctrl.req = { + params: { action: 'fakeAction'}, + method: 'GET', + query: {}, + body: {} + }; + + ctrl.res = { + json: function () {} + }; + + done(); + }); + + describe( 'loginAction()', function () { + + it( 'should call this.send() with specify parameters', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'url' ).and.contain( 'https://github.com/login/oauth/authorize?' ); + + done(); + }; + + ctrl.loginAction(); + } ); + + } ); + + describe( 'listAction()', function () { + + it( 'should be able to get list of users', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'array' ).and.have.length.above( 1 ); + expect( result[0] ).to.be.an( 'object' ).and.be.ok; + expect( result[0] ).to.have.property( 'id' ).and.be.ok; + expect( result[0] ).to.have.property( 'firstname' ).and.be.ok; + expect( result[0] ).to.have.property( 'lastname' ).and.be.ok; + expect( result[0] ).to.have.property( 'email' ).and.be.ok; + expect( result[0] ).to.have.property( 'githubid' ).and.be.ok; + expect( result[0] ).to.not.have.property( 'token' ); + + done(); + }; + + ctrl.listAction(); + } ); + + } ); + + describe( 'getAction()', function () { + + it( 'should be able to get user by id', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.be.ok; + expect( result ).to.have.property( 'firstname' ).and.be.ok; + expect( result ).to.have.property( 'lastname' ).and.be.ok; + expect( result ).to.have.property( 'email' ).and.be.ok; + expect( result ).to.have.property( 'githubid' ).and.be.ok; + expect( result ).to.not.have.property( 'token' ); + + done(); + }; + + ctrl.req.params = { id: gUsers[0].id }; + + ctrl.getAction(); + } ); + + it( 'should be able to get error if user do not exist', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: 15151515151151515 }; + + ctrl.getAction(); + } ); + + } ); + + describe( 'deleteAction()', function () { + + it( 'should be able to delete user by id', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: gUsers[0].id }; + + ctrl.deleteAction(); + } ); + + it( 'should be able to get error if user do not exist', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: 15151515151151515 }; + + ctrl.deleteAction(); + } ); + + } ); + +} ); \ No newline at end of file diff --git a/modules/clever-auth-github/tests/unit/test.service.UserGithubService.js b/modules/clever-auth-github/tests/unit/test.service.UserGithubService.js new file mode 100644 index 0000000..0e167e3 --- /dev/null +++ b/modules/clever-auth-github/tests/unit/test.service.UserGithubService.js @@ -0,0 +1,682 @@ +var expect = require ( 'chai' ).expect + , request = require ( 'supertest' ) + , path = require( 'path' ) + , app = require ( path.resolve( __dirname + '/../../../../' ) + '/index.js' ) + , config = require( 'config' ) + , testEnv = require ( 'utils' ).testEnv() + , sinon = require( 'sinon' ) + , Q = require ( 'q' ); + +var UserService = null; + +var gUserId_1, accessedAtDate, gUser, user_1; + +describe( 'service.UserGithubService', function () { + var Service, Model; + + before( function ( done ) { + this.timeout( 15000 ); + testEnv( function ( _UserGithubService_, _ORMUserGithubModel_ ) { + + Service = _UserGithubService_; + Model = _ORMUserGithubModel_; + + done(); + }, done ); + } ); + + describe( '.formatData( profile, accessToken )', function () { + + it( 'should return an object with filtered data', function ( done ) { + var accessToken = 'sdasdasdasdasdasdasdasda' + , profile = { + _json: { login: 'denshikov-vovan', + id: 45454545, + avatar_url: 'https://gravatar.com/avatar/e98ef168c69fd2a812a8dd46a775072a?d=https%3A%2F%2Fidenticons.github.com%2F814919104a88497c37d2154d918bba97.png&r=x', + gravatar_id: 'e98ef168c69fd2a812a8dd46a775072a', + url: 'https://api.github.com/users/denshikov-vovan', + html_url: 'https://github.com/denshikov-vovan', + followers_url: 'https://api.github.com/users/denshikov-vovan/followers', + following_url: 'https://api.github.com/users/denshikov-vovan/following{/other_user}', + gists_url: 'https://api.github.com/users/denshikov-vovan/gists{/gist_id}', + starred_url: 'https://api.github.com/users/denshikov-vovan/starred{/owner}{/repo}', + subscriptions_url: 'https://api.github.com/users/denshikov-vovan/subscriptions', + organizations_url: 'https://api.github.com/users/denshikov-vovan/orgs', + repos_url: 'https://api.github.com/users/denshikov-vovan/repos', + events_url: 'https://api.github.com/users/denshikov-vovan/events{/privacy}', + received_events_url: 'https://api.github.com/users/denshikov-vovan/received_events', + type: 'User', + site_admin: false, + name: 'Volodymyr', + company: 'Clevertech', + blog: '', + location: 'Ukraine', + email: 'volodymyrm@clevertech.biz', + hireable: false, + bio: null, + public_repos: 0, + public_gists: 0, + followers: 1, + following: 0, + created_at: '2013-08-27T09:35:10Z', + updated_at: '2014-02-07T16:18:12Z', + private_gists: 0, + total_private_repos: 0, + owned_private_repos: 0, + disk_usage: 0, + collaborators: 0, + plan: + { name: 'free', + space: 307200, + collaborators: 0, + private_repos: 0 } } + }; + + var data = Service.formatData( profile, accessToken ); + + expect( data ).to.be.an( 'object' ).and.be.ok; + + expect( data ).to.have.property( 'email' ).and.equal( profile._json.email ); + expect( data ).to.have.property( 'firstname' ).and.equal( 'Volodymyr' ); + expect( data ).to.have.property( 'lastname' ).and.be.null; + expect( data ).to.have.property( 'githubid' ).and.equal( profile._json.id ); + expect( data ).to.have.property( 'picture' ).and.equal( profile._json.avatar_url ); + expect( data ).to.have.property( 'link' ).and.equal( profile._json.html_url ); + expect( data ).to.have.property( 'locale' ).and.equal( profile._json.location ); + expect( data ).to.have.property( 'token' ).and.equal( accessToken ); + + expect( data ).to.not.have.property( 'bio' ); + expect( data ).to.not.have.property( 'name' ); + expect( data ).to.not.have.property( 'plan' ); + + done(); + } ); + + } ); + + describe( '.findOrCreate( profile, accessToken )', function () { + + it( 'should not call ORMUserGithubModel.find() if email is not define', function ( done ) { + var accessToken = 'sdasdasdasdasdasdasdasda' + , profile = { + _json: { + id: '112064034597570891032', + email: null, + name: 'Volodymyr', + html_url: 'https://github.com/denshikov-vovan', + location: 'Ukraine', + avatar_url: 'https://gravatar.com/avatar/e98ef168c69fd2a812a8dd46a775072a?d=https%3A%2F%2Fidenticons.github.com%2F814919104a88497c37d2154d918bba97.png&r=x' + } + }; + + var spy = sinon.spy( Model, 'find' ); + + Service + .findOrCreate( profile, accessToken ) + .then( function ( result ) { + + expect( result ).to.not.be.ok; + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + }, done ) + } ); + + it( 'should call ORMUserGithubModel.find(), ORMUserGithubModel.create() and create gUser if github account do not already exist', function ( done ) { + var accessToken = 'sdasdasdasdasdasdasdasda' + , profile = { + _json: { + id: '112064034597570891032', + email: 'volodymyrm@clevertech.biz', + name: 'Volodymyr', + html_url: 'https://github.com/denshikov-vovan', + location: 'Ukraine', + avatar_url: 'https://gravatar.com/avatar/e98ef168c69fd2a812a8dd46a775072a?d=https%3A%2F%2Fidenticons.github.com%2F814919104a88497c37d2154d918bba97.png&r=x' + } + }; + + var spyFind = sinon.spy( Model, 'find' ); + var spyCreate = sinon.spy( Model, 'create' ); + + Service + .findOrCreate( profile, accessToken ) + .delay( 1000 ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.be.ok; + + expect( spyFind.called ).to.be.true; + expect( spyFind.calledOnce ).to.be.true; + + expect( spyCreate.called ).to.be.true; + expect( spyCreate.calledOnce ).to.be.true; + + spyFind.restore(); + spyCreate.restore(); + + Model + .find( result.id ) + .success( function( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( result.id ); + expect( user ).to.have.property( 'token' ).and.equal( accessToken ); + expect( user ).to.have.property( 'firstname' ).and.equal( profile._json.name ); + expect( user ).to.have.property( 'lastname' ).and.be.null; + expect( user ).to.have.property( 'email' ).and.equal( profile._json.email ); + expect( user ).to.have.property( 'githubid' ).and.equal( profile._json.id ); + expect( user ).to.have.property( 'accessedAt' ).and.be.ok; + + gUserId_1 = user.id; + accessedAtDate = +new Date( user.accessedAt ); + gUser = user; + + done(); + }) + .error( done ); + }, done ) + } ); + + it( 'should call ORMUserGithubModel.find(), ORMUserGithubModel.updateAttributes(), not call ORMUserGithubModel.create() and update already existing gUser', function ( done ) { + var accessToken = '15151515151515151' + , profile = { + _json: { + id: '112064034597570891032', + email: 'volodymyrm@clevertech.biz', + name: 'Volodymyr', + html_url: 'https://github.com/denshikov-vovan', + location: 'Ukraine', + avatar_url: 'https://gravatar.com/avatar/e98ef168c69fd2a812a8dd46a775072a?d=https%3A%2F%2Fidenticons.github.com%2F814919104a88497c37d2154d918bba97.png&r=x' + } + }; + + var spyFind = sinon.spy( Model, 'find' ); + var spyCreate = sinon.spy( Model, 'create' ); + + Service + .findOrCreate( profile, accessToken ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( gUserId_1 ); + expect( result ).to.have.property( 'token' ).and.equal( accessToken ); + + var newAccessedAtDate = +new Date( result.accessedAt ); + + expect( accessedAtDate ).to.not.equal( newAccessedAtDate ); + + expect( spyFind.called ).to.be.true; + expect( spyFind.calledOnce ).to.be.true; + + expect( spyCreate.called ).to.be.false; + + spyFind.restore(); + spyCreate.restore(); + + done(); + }, done ) + } ); + + } ); + + describe( '.authenticate( gUser, profile )', function () { + + before( function( done ) { + var accessToken = '516516516+51+651' + , profile = { + _json: { + id: '151151515151515', + email: 'volodymyr1m@clevertech.biz', + name: 'Volodymyr Denshchykov', + html_url: 'https://github.com/denshikov-vovan', + location: 'Ukraine', + avatar_url: 'https://gravatar.com/avatar/e98ef168c69fd2a812a8dd46a775072a?d=https%3A%2F%2Fidenticons.github.com%2F814919104a88497c37d2154d918bba97.png&r=x' + } + }; + + try { + UserService = injector.getInstance( 'UserService' ); + } catch ( err ) { + console.log( err ); + } + + Service + .findOrCreate( profile, accessToken ) + .then( function( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.be.ok; + + gUser = user; + + done(); + }, done ); + }); + + it( 'should return gUser if UserService is not defined', function ( done ) { + var profile = { + _json: { + id: '112064034597570891032', + email: 'volodymyr2m@clevertech.biz', + name: 'Volodymyr Denokov', + html_url: 'https://github.com/denshikov-vovan', + location: 'Ukraine', + avatar_url: 'https://gravatar.com/avatar/e98ef168c69fd2a812a8dd46a775072a?d=https%3A%2F%2Fidenticons.github.com%2F814919104a88497c37d2154d918bba97.png&r=x' + } + }; + + if ( !UserService ) { + Service + .authenticate( gUser, profile ) + .then( function ( user ) { + + expect( !UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( user ).to.have.property( 'token' ).and.be.ok; + expect( user ).to.have.property( 'firstname' ).and.equal( gUser.firstname ); + expect( user ).to.have.property( 'lastname' ).and.equal( gUser.lastname ); + expect( user ).to.have.property( 'email' ).and.equal( gUser.email ); + expect( user ).to.have.property( 'githubid' ).and.equal( gUser.githubid ); + expect( user ).to.have.property( 'accessedAt' ).and.be.ok; + + done(); + } ) + .fail( done ); + } else { + console.log( 'UserService is defined' ); + done(); + } + } ); + + it( 'should create new user and to associate it with qUser if UserService is defined', function ( done ) { + var profile = { + _json: { + id: '112064034597570891032', + email: 'volodymyr1m@clevertech.biz', + name: 'Volodymyr', + html_url: 'https://github.com/denshikov-vovan', + location: 'Ukraine', + avatar_url: 'https://gravatar.com/avatar/e98ef168c69fd2a812a8dd46a775072a?d=https%3A%2F%2Fidenticons.github.com%2F814919104a88497c37d2154d918bba97.png&r=x' + } + }; + + if ( !!UserService ) { + + var spy = sinon.spy( UserService, 'create' ); + + Service + .authenticate( gUser, profile ) + .then( function ( user ) { + + expect( !!UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.not.equal( gUser.id ); + + expect( spy.called ).to.be.true; + expect( spy.calledOnce ).to.be.true; + + spy.restore(); + + UserService + .findById( user.id ) + .then( function( _user ) { + + expect( _user ).to.be.an( 'object' ).and.be.ok; + expect( _user ).to.have.property( 'id' ).and.equal( user.id ); + expect( _user ).to.have.property( 'email' ).and.equal( profile._json.email ); + expect( _user ).to.have.property( 'password' ).and.be.ok; + expect( _user ).to.have.property( 'firstname' ).and.equal( 'Volodymyr' ); + expect( _user ).to.have.property( 'lastname' ).and.be.null; + expect( _user ).to.have.property( 'confirmed' ).and.equal( true ); + expect( _user ).to.have.property( 'active' ).and.equal( true ); + + user_1 = _user; + + Service + .find( { where: { id: gUser.id } } ) + .then( function( _gUser ) { + + expect( _gUser ).to.be.an( 'array' ).and.have.length( 1 ); + + _gUser = _gUser[0]; + + expect( _gUser ).to.be.an( 'object' ).and.be.ok; + expect( _gUser ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( _gUser ).to.have.property( 'UserId' ).and.equal( user.id ); + + done(); + }, done ) + }, done ); + } ) + .fail( done ); + } else { + console.log( 'UserService is not defined' ); + done(); + } + + } ); + + it( 'should return existing user by gUser.UserId if UserService is defined', function ( done ) { + var profile = { + _json: { + id: '112064034597570891032', + email: 'volodymyr21m@clevertech.biz', + name: 'Volodymyr', + html_url: 'https://github.com/denshikov-vovan', + location: 'Ukraine', + avatar_url: 'https://gravatar.com/avatar/e98ef168c69fd2a812a8dd46a775072a?d=https%3A%2F%2Fidenticons.github.com%2F814919104a88497c37d2154d918bba97.png&r=x' + } + }; + + if ( !!UserService ) { + + var spy = sinon.spy( UserService, 'create' ); + + Service + .authenticate( gUser, profile ) + .then( function ( user ) { + + expect( !!UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( user_1.id ); + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + } ) + .fail( done ); + } else { + console.log( 'UserService is not defined' ); + done(); + } + + } ); + + it( 'should return error if user with such gUser.UserId do not exist and if UserService is defined' , function ( done ) { + var profile = { + _json: { + id: '112064034597570891032', + email: 'volodymyrm@clevertech.biz', + name: 'Volodymyr', + html_url: 'https://github.com/denshikov-vovan', + location: 'Ukraine', + avatar_url: 'https://gravatar.com/avatar/e98ef168c69fd2a812a8dd46a775072a?d=https%3A%2F%2Fidenticons.github.com%2F814919104a88497c37d2154d918bba97.png&r=x' + } + }; + + if ( !!UserService ) { + + var spy = sinon.spy( UserService, 'create' ); + + gUser + .updateAttributes( { UserId: 1000 } ) + .success( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'UserId' ).and.equal( 1000 ); + + Service + .authenticate( gUser, profile ) + .then( function ( user ) { + + expect( !!UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( user ).to.have.property( 'message' ).and.be.ok; + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + } ) + .fail( done ); + }) + .error( done ); + } else { + console.log( 'UserService is not defined' ); + done(); + } + + } ); + + it( 'should return existing user by gUser.email if user with such email already exist', function ( done ) { + var profile = { + _json: { + id: '112064034597570891032', + email: 'volodymyrm@clevertech.biz', + name: 'Volodymyr', + html_url: 'https://github.com/denshikov-vovan', + location: 'Ukraine', + avatar_url: 'https://gravatar.com/avatar/e98ef168c69fd2a812a8dd46a775072a?d=https%3A%2F%2Fidenticons.github.com%2F814919104a88497c37d2154d918bba97.png&r=x' + } + }; + + if ( !!UserService ) { + + var spy = sinon.spy( UserService, 'create' ); + + gUser + .updateAttributes( { UserId: null } ) + .success( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'UserId' ).and.equal( null ); + + Service + .authenticate( gUser, profile ) + .then( function ( user ) { + + expect( !!UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( user_1.id ); + + expect( spy.called ).to.be.false; + + spy.restore(); + + Service + .find( { where: { id: gUser.id } } ) + .then( function( _gUser ) { + + expect( _gUser ).to.be.an( 'array' ).and.have.length( 1 ); + + _gUser = _gUser[0]; + + expect( _gUser ).to.be.an( 'object' ).and.be.ok; + expect( _gUser ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( _gUser ).to.have.property( 'UserId' ).and.equal( user.id ); + + done(); + }, done ) + } ) + .fail( done ); + }) + .error( done ); + } else { + console.log( 'UserService is not defined' ); + done(); + } + + } ); + + } ); + + describe( '.updateAccessedDate( user )', function () { + + it( 'should be able to update user', function ( done ) { + + if ( !!UserService ) { + + user_1 + .updateAttributes( { accessedAt: null } ) + .success( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( user_1.id ); + expect( result ).to.have.property( 'accessedAt' ).and.not.be.ok; + + Service + .updateAccessedDate( user_1 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( user_1.id ); + expect( result ).to.have.property( 'accessedAt' ).and.be.ok; + + done(); + }, done ); + }) + .error( done ); + + } else { + console.log( 'UserService is not defined' ); + done(); + } + } ); + + it( 'should be able to update gUser', function ( done ) { + + gUser + .updateAttributes( { accessedAt: null } ) + .success( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( result ).to.have.property( 'accessedAt' ).and.not.be.ok; + + Service + .updateAccessedDate( gUser ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( result ).to.have.property( 'accessedAt' ).and.be.ok; + + done(); + }, done ); + }) + .error( done ); + } ); + + it( 'should be able to get object if it is not a user', function ( done ) { + + Service + .updateAccessedDate ( { statuscode: 403, message: 'invalid' } ) + .then ( function ( result ) { + + expect ( result ).to.be.an ( 'object' ).and.be.ok; + expect ( result ).to.have.property ( 'statuscode' ).and.equal ( 403 ); + expect ( result ).to.have.property ( 'message' ).and.be.ok; + + done (); + }, done ); + } ); + + } ); + + describe( '.listUsers()', function () { + + it( 'should be able to get list of all gUsers', function ( done ) { + + Service + .listUsers() + .then( function ( users ) { + + expect( users ).to.be.an( 'array' ).and.have.length.above( 1 ); + expect( users[0] ).to.be.an( 'object' ).and.be.ok; + expect( users[0] ).to.have.property( 'id' ).and.be.ok; + expect( users[0] ).to.have.property( 'githubid' ).and.be.ok; + expect( users[0] ).to.have.property( 'email' ).and.be.ok; + expect( users[0] ).to.have.property( 'locale' ).and.be.ok; + expect( users[0] ).to.not.have.property( 'token' ); + + done(); + } ) + .fail( done ); + } ); + + } ); + + describe( '.findUserById()', function () { + + it( 'should be able to get gUser by Id', function ( done ) { + + Service + .findUserById( gUser.id ) + .then( function ( gUser ) { + + expect( gUser ).to.be.an( 'object' ).and.be.ok; + expect( gUser ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( gUser ).to.have.property( 'githubid' ).and.be.ok; + expect( gUser ).to.have.property( 'email' ).and.be.ok; + expect( gUser ).to.have.property( 'locale' ).and.be.ok; + expect( gUser ).to.not.have.property( 'token' ); + + done(); + } ) + .fail( done ); + } ); + + it( 'should be able to get the error if the gUser does not exist', function ( done ) { + + Service + .findUserById( 151515115151515151 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + }, done ) + } ); + + } ); + + describe( '.deleteUser( gUserId )', function () { + + it( 'should be able to get the error if the gUser does not exist', function ( done ) { + + Service + .deleteUser( 151515115151515151 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + }, done ) + } ); + + it( 'should be able to delete gUser', function ( done ) { + + Service + .deleteUser( gUser.id ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 200 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + } ) + .fail( done ); + } ); + + } ); + +} ); \ No newline at end of file diff --git a/modules/clever-auth-google/config/default.json b/modules/clever-auth-google/config/default.json new file mode 100644 index 0000000..6c5ce26 --- /dev/null +++ b/modules/clever-auth-google/config/default.json @@ -0,0 +1,25 @@ +{ + "clever-auth-google": { + "secretKey": "youMustChangeMe", + + "redis": { + "host": "localhost", + "port": "6379", + "prefix": "", + "key": "" + }, + + "google": { + "clientId": "117613345915-5ammdhvej2pkgbfuso2u2u1mo2akmfg7.apps.googleusercontent.com", + "clientSecret": "YkcZxpWEKr_Jijb_RaL0nNlC", + "clientEmail": "117613345915-5ammdhvej2pkgbfuso2u2u1mo2akmfg7@developer.gserviceaccount.com", + "redirectURIs": "http://localhost:8080/auth/google/return", + "javascriptOrigins": "http://localhost:8080", + "authURI": "http://accounts.google.com/o/oauth2/auth", + "tokenURI": "http://accounts.google.com/o/oauth2/token" + }, + + "frontendURL": "http://localhost:9000/" + + } +} \ No newline at end of file diff --git a/modules/clever-auth-google/controllers/UserGoogleController.js b/modules/clever-auth-google/controllers/UserGoogleController.js new file mode 100644 index 0000000..ec57006 --- /dev/null +++ b/modules/clever-auth-google/controllers/UserGoogleController.js @@ -0,0 +1,111 @@ +var config = require ( 'config' )[ 'clever-auth-google' ] + , passport = require ( 'passport' ) + , qs = require ( 'qs' ) + , GoogleStrategy = require ( 'passport-google-oauth' ).OAuth2Strategy; + +module.exports = function ( UserGoogleService ) { + + passport.serializeUser( function ( user, done ) { + done( null, user ); + } ); + + passport.deserializeUser( function ( user, done ) { + done( null, user ) + } ); + + passport.use( new GoogleStrategy( + { + clientID: config.google.clientId, + clientSecret: config.google.clientSecret, + callbackURL: config.google.redirectURIs + }, + function ( accessToken, refreshToken, profile, done ) { + + UserGoogleService + .findOrCreate( profile, accessToken ) + .then( function( gUser ) { + return UserGoogleService.authenticate ( gUser, profile ) + }) + .then( UserGoogleService.updateAccessedDate ) + .then( done.bind( null, null ) ) + .fail( done ); + } + )); + + + return (require( 'classes' ).Controller).extend ( + { + service: UserGoogleService + }, + { + listAction: function () { + UserGoogleService.listUsers() + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, //tested + + getAction: function () { + var guId = this.req.params.id; + + UserGoogleService + .findUserById( guId ) + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, //tested + + deleteAction: function () { + var guId = this.req.params.id; + + UserGoogleService.deleteUser( guId ) + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, //tested + + loginAction: function () { + var params = { + response_type: "code", + client_id: config.google.clientId, + redirect_uri: config.google.redirectURIs, + display: "popup", + scope: "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email" + }; + + this.send( { url: 'https://accounts.google.com/o/oauth2/auth?' + qs.stringify( params ) }, 200 ); + + }, //tested + + returnAction: function () { + passport.authenticate( 'google', this.proxy( 'handleLocalUser' ) )( this.req, this.res, this.next ); + }, + + handleLocalUser: function ( err, user ) { + if ( err ) return this.handleException( err ); + if ( !user ) return this.send( {}, 403 ); + this.loginUserJson( user ); + }, + + loginUserJson: function ( user ) { + this.req.login( user, this.proxy( 'handleLoginJson', user ) ); + }, + + handleLoginJson: function ( user, err ) { + if ( err ) return this.handleException( err ); + + this.res.statusCode = 302; + this.res.setHeader( 'body', user ); + this.res.setHeader( 'Location', config.frontendURL ); + this.res.end(); + }, + + handleServiceMessage: function ( obj ) { + + if ( obj.statuscode ) { + this.send( obj.message, obj.statuscode ); + return; + } + + this.send( obj, 200 ); + } + + } ); +}; \ No newline at end of file diff --git a/modules/clever-auth-google/models/orm/UserGoogleModel.js b/modules/clever-auth-google/models/orm/UserGoogleModel.js new file mode 100644 index 0000000..41b89c2 --- /dev/null +++ b/modules/clever-auth-google/models/orm/UserGoogleModel.js @@ -0,0 +1,80 @@ +module.exports = function ( sequelize, DataTypes ) { + return sequelize.define( "UserGoogle", + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + token: { + type: DataTypes.STRING, + allowNull: false + }, + firstname: { + type: DataTypes.STRING, + allowNull: true + }, + lastname: { + type: DataTypes.STRING, + allowNull: true + }, + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + isEmail: true + } + }, + googleid: { + type: DataTypes.STRING, + allowNull: false + }, + picture: { + type: DataTypes.STRING, + allowNull: true + }, + link: { + type: DataTypes.STRING, + allowNull: true + }, + gender: { + type: DataTypes.STRING, + allowNull: true, + values: [ 'male', 'female' ] + }, + locale: { + type: DataTypes.STRING, + allowNull: true + }, + verified: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: false + }, + accessedAt: { + type: DataTypes.DATE + }, + UserId: { + type: DataTypes.INTEGER + } + }, + { + paranoid: true, + + getterMethods: { + fullName: function () { + return [ this.getDataValue( 'firstname' ), this.getDataValue( 'lastname' )].join( ' ' ); + } + }, + + instanceMethods: { + toJSON: function () { + var values = this.values; + values.fullName = this.fullName; + delete values.token; + return values; + } + } + } ); +}; \ No newline at end of file diff --git a/modules/clever-auth-google/module.js b/modules/clever-auth-google/module.js new file mode 100644 index 0000000..164c70d --- /dev/null +++ b/modules/clever-auth-google/module.js @@ -0,0 +1,129 @@ +var passport = require( 'passport' ) + , debug = require( 'debug' )( 'CleverAuthGoogleModule' ) + , fs = require( 'fs' ) + , path = require( 'path' ) + , ModuleClass = require( 'classes' ).ModuleClass + , RedisStore = require( 'connect-redis' )( injector.getInstance( 'express' ) ) + , Module; + +Module = ModuleClass.extend( { + models: null, + store: null, + sequelize: null, + + preSetup: function () { + var self = this; + this.models = {}; + this.sequelize = injector.getInstance ( 'sequelize' ); + var dir = fs.readdirSync ( path.join ( __dirname, 'models', 'orm' ) ); + dir.forEach ( function ( model ) { + self.getModel ( path.join ( __dirname, 'models', 'orm', model ) ); + } ); + }, + + preResources: function () { + injector.instance ( 'passport', passport ); + }, + + modulesLoaded: function () { + this.defineModelsAssociations (); + }, + + defineModelsAssociations: function () { + if ( !this.config.hasOwnProperty ( 'modelAssociations' ) ) { + return true; + } + + debug ( 'Defining model assocations' ); + + Object.keys ( this.config.modelAssociations ).forEach ( this.proxy ( function ( modelName ) { + Object.keys ( this.config.modelAssociations[ modelName ] ).forEach ( this.proxy ( 'defineModelAssociations', modelName ) ); + } ) ); + }, + + defineModelAssociations: function ( modelName, assocType ) { + var associatedWith = this.config.modelAssociations[ modelName ][ assocType ]; + if ( !associatedWith instanceof Array ) { + associatedWith = [ associatedWith ]; + } + + associatedWith.forEach ( this.proxy ( 'associateModels', modelName, assocType ) ); + }, + + associateModels: function ( modelName, assocType, assocTo ) { + // Support second argument + if ( assocTo instanceof Array ) { + debug ( '%s %s %s with second argument of ', modelName, assocType, assocTo[0], assocTo[1] ); + this.models[ modelName ][ assocType ] ( this.models[ assocTo[0] ], assocTo[1] ); + } else { + debug ( '%s %s %s', modelName, assocType, assocTo ); + this.models[ modelName ][ assocType ] ( this.models[assocTo] ); + } + }, + + getModel: function ( modelPath ) { + var modelName = modelPath.split ( '/' ).pop ().split ( '.' ).shift (); + + if ( typeof this.models[ modelName ] === 'undefined' ) { + debug ( [ 'Loading model', modelName, 'from', modelPath ].join ( ' ' ) ); + + // Call on sequelizejs to load the model + this.models[ modelName ] = this.sequelize.import ( modelPath ); + + // Set a flat for tracking + this.models[ modelName ].ORM = true; + + // Add the model to the injector + injector.instance ( 'ORM' + modelName, this.models[ modelName ] ); + } + + return this.models[ modelName ]; + }, + + configureApp: function ( app, express ) { +// Setup the redis store + this.store = new RedisStore ( { + host: this.config.redis.host, + port: this.config.redis.port, + prefix: this.config.redis.prefix + process.env.NODE_ENV + "_", + password: this.config.redis.key + } ); + +// Session management + app.use ( express.cookieParser () ); + app.use ( express.session ( { + secret: this.config.secretKey, + cookie: { secure: false, maxAge: 86400000 }, + store: this.store + } ) ); + + // Enable CORS + app.use ( this.proxy ( 'enableCors' ) ); + + // Initialize passport + app.use ( passport.initialize () ); + app.use ( passport.session () ); + }, + + enableCors: function ( req, res, next ) { + + res.header ( 'Access-Control-Allow-Origin', req.headers.origin ); + res.header ( 'Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE' ); + res.header ( 'Access-Control-Allow-Headers', 'Content-Type, Authorization, x-requested-with' ); + res.header( "Access-Control-Allow-Credentials", "true" ); + res.header( "Access-Control-Max-Age", "1000000000" ); + // intercept OPTIONS method + if ( 'OPTIONS' == req.method ) { + res.send ( 200 ); + } + else { + next (); + } + }, + + preShutdown: function () { + this.store.client.quit (); + } +} ); + +module.exports = new Module( 'clever-auth-google', injector ); \ No newline at end of file diff --git a/modules/clever-auth-google/package.json b/modules/clever-auth-google/package.json new file mode 100644 index 0000000..ff6bf36 --- /dev/null +++ b/modules/clever-auth-google/package.json @@ -0,0 +1,32 @@ +{ + "name": "clever-auth-google", + "version": "0.0.1", + "private": true, + "dependencies": { + "passport": "~0.1.18", + "connect-redis": "~1.4.6", + "q": "", + "sequelize": "1.7.0-beta8", + "moment": "", + "passport-google-oauth": "~0.1.5", + "qs": "*" + }, + "author": { + "name": "Clevertech", + "email": "info@clevertech.biz", + "web": "http://www.clevertech.biz" + }, + "collaborators": [ + "" + ], + "description": "", + "keywords": [ + "" + ], + "main": "module.js", + "devDependencies": { + "chai": "*", + "mocha": "*", + "sinon": "" + } +} \ No newline at end of file diff --git a/modules/clever-auth-google/routes.js b/modules/clever-auth-google/routes.js new file mode 100644 index 0000000..7231a73 --- /dev/null +++ b/modules/clever-auth-google/routes.js @@ -0,0 +1,8 @@ +module.exports = function ( + app, + UserGoogleController ) +{ + + app.all('/auth/google/?:action?', UserGoogleController.attach() ); + +}; \ No newline at end of file diff --git a/modules/clever-auth-google/services/UserGoogleService.js b/modules/clever-auth-google/services/UserGoogleService.js new file mode 100644 index 0000000..55b8b52 --- /dev/null +++ b/modules/clever-auth-google/services/UserGoogleService.js @@ -0,0 +1,230 @@ +var Q = require( 'q' ) + , crypto = require( 'crypto' ) + , moment = require( 'moment' ) + , Sequelize = require( 'sequelize' ) + , UserGoogleService = null; + +module.exports = function ( sequelize, + ORMUserGoogleModel ) { + + if ( UserGoogleService && UserGoogleService.instance ) { + return UserGoogleService.instance; + } + + var UserService = null; + + UserGoogleService = require( 'services' ).BaseService.extend( { + + formatData: function ( profile, accessToken ) { + return { + googleid: profile._json.id, + email: profile._json.email, + firstname: profile._json.given_name, + lastname: profile._json.family_name, + picture: profile._json.picture || null, + verified: profile._json.verified_email, + link: profile._json.link, + gender: profile._json.gender, + locale: profile._json.locale, + token: accessToken || null + } + }, //tested + + findOrCreate: function ( profile, accessToken ) { + var deferred = Q.defer() + , data = this.formatData ( profile, accessToken ); + + if ( data.verified ) { + + ORMUserGoogleModel + .find( { where: { email: data.email } } ) + .success( function ( gUser ) { + + if ( !gUser ) { + + data.accessedAt = moment.utc().format( 'YYYY-MM-DD HH:ss:mm' ); + + ORMUserGoogleModel + .create( data ) + .success( deferred.resolve ) + .error( deferred.reject ); + + } else { + + gUser + .updateAttributes( { accessedAt: moment.utc().format( 'YYYY-MM-DD HH:ss:mm' ), token: data.token } ) + .success( deferred.resolve ) + .error( deferred.reject ); + } + } ) + .error( deferred.reject ); + } else { + deferred.resolve(); + } + + return deferred.promise; + }, //tested + + authenticate: function( gUser, profile ) { + var deferred = Q.defer() + , data = this.formatData ( profile ); + + try { + UserService = injector.getInstance( 'UserService' ); + } catch ( err ) { + console.log( err ); + } + + if ( !!gUser && !!UserService ) { + + if ( gUser.UserId ) { + + UserService + .find( { where: { id: gUser.UserId, email: gUser.email } } ) + .then( function( users ) { + + var user = !!users && !!users.length + ? users[0] + : { statuscode: 403, message: 'invalid' }; + + deferred.resolve( user ); + }) + .fail( deferred.reject ); + + } else { + + UserService + .find( { where: { email: gUser.email } } ) + .then( function( user ) { + + user = user[0]; + + if ( !!user && !!user.id ) { + + gUser + .updateAttributes( { UserId: user.id } ) + .success ( function () { + deferred.resolve ( user ); + } ) + .error( deferred.reject ); + + } else { + + data.confirmed = true; + data.username = data.email; + data.password = crypto.createHash( 'sha1' ).update( Math.floor(Math.random() * 1e18) + '' ).digest( 'hex' ) + + UserService + .create( data ) + .then( function( user ) { + + gUser + .updateAttributes ( { UserId: user.id } ) + .success ( function () { + deferred.resolve ( user ); + } ) + .error ( deferred.reject ); + }) + .fail( deferred.reject ); + } + }) + .fail( deferred.reject ); + } + + } else { + deferred.resolve( gUser ); + } + + return deferred.promise; + }, //tested + + updateAccessedDate: function( user ){ + var deferred = Q.defer(); + + if ( !!user && !!user.id ) { + user + .updateAttributes( { accessedAt: moment.utc().format( 'YYYY-MM-DD HH:ss:mm' ) } ) + .success( function() { + deferred.resolve( user.toJSON() ); + }) + .error( deferred.reject ); + } else { + deferred.resolve( user ); + } + + return deferred.promise; + }, //tested + + listUsers: function() { + var deferred = Q.defer(); + + ORMUserGoogleModel + .findAll( { where: { deletedAt: null } } ) + .success( function( gUsers ) { + if ( !!gUsers && !!gUsers.length ) { + deferred.resolve( gUsers.map( function( u ) { return u.toJSON(); } ) ); + } else { + deferred.resolve( {} ); + } + }) + .error( deferred.reject ); + + return deferred.promise; + }, //tested + + findUserById: function( gUserId ) { + var deferred = Q.defer(); + + ORMUserGoogleModel + .find( gUserId ) + .success( function( gUser ) { + + if ( !!gUser && !!gUser.id ){ + deferred.resolve( gUser.toJSON() ); + } else { + deferred.resolve( { statuscode: 403, message: 'user do not exist' } ); + } + }) + .error( deferred.reject ); + + return deferred.promise; + }, //tested + + deleteUser: function( gUserId ) { + var deferred = Q.defer(); + + ORMUserGoogleModel + .find( gUserId ) + .success( function( gUser ) { + + if ( !!gUser && !!gUser.id ) { + + gUser + .destroy() + .success( function( result ) { + + if ( !result.deletedAt ) { + deferred.resolve( { statuscode: 500, message: 'error' } ); + } else { + deferred.resolve( { statuscode: 200, message: 'user is deleted' } ); + } + + }) + .error( deferred.reject ); + + } else { + deferred.resolve( { statuscode: 403, message: 'user do not exist' } ) + } + }) + .error( deferred.reject ); + + return deferred.promise; + } //tested + + } ); + + UserGoogleService.instance = new UserGoogleService( sequelize ); + UserGoogleService.Model = ORMUserGoogleModel; + + return UserGoogleService.instance; +}; \ No newline at end of file diff --git a/modules/clever-auth-google/tests/unit/test.controllers.UserGoogleController.js b/modules/clever-auth-google/tests/unit/test.controllers.UserGoogleController.js new file mode 100644 index 0000000..798b45f --- /dev/null +++ b/modules/clever-auth-google/tests/unit/test.controllers.UserGoogleController.js @@ -0,0 +1,221 @@ +// Bootstrap the testing environmen +var testEnv = require( 'utils' ).testEnv(); + +var expect = require( 'chai' ).expect + , Q = require ( 'q' ) + , sinon = require( 'sinon' ) + , Service; + +var new_user; + +describe( 'controllers.UserGoogleController', function () { + var Service, UserGoogleController, ctrl, gUsers = []; + + before( function ( done ) { + testEnv( function ( _UserGoogleService_, _UserGoogleController_ ) { + var req = { + params: { action: 'fakeAction'}, + method: 'GET', + query: {} + }; + + var res = { + json: function () {} + }; + + var next = function () {}; + + Controller = _UserGoogleController_; + Service = _UserGoogleService_; + ctrl = new Controller( req, res, next ); + + var profile_1 = { + _json: { + id: '121212121212121', + email: 'volodymyrs@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'asasasd', + family_name: 'Denssdfsdfhchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru' + } + } + , profile_2 = { + _json: { + id: '45454454545454', + email: 'volodymyr@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'asasasd', + family_name: 'Denssdfsdfhchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru' + } + }; + + Service + .findOrCreate( profile_1, 'bhbhgvbljbhscvzkchvblzclvnzbkjxcv' ) + .then( function( gUser ) { + + expect( gUser ).to.be.an( 'object' ).and.be.ok; + expect( gUser ).to.have.property( 'id' ).and.be.ok; + + gUsers.push( gUser ) + + Service + .findOrCreate( profile_2, 'kjajkvl zsdvbakhvckhabskhv' ) + .then( function( gUser ) { + + expect( gUser ).to.be.an( 'object' ).and.be.ok; + expect( gUser ).to.have.property( 'id' ).and.be.ok; + + gUsers.push( gUser ) + + done(); + }, done ); + }, done ); + } ); + } ); + + afterEach( function ( done ) { + + ctrl.req = { + params: { action: 'fakeAction'}, + method: 'GET', + query: {}, + body: {} + }; + + ctrl.res = { + json: function () {} + }; + + done(); + }); + + describe( 'loginAction()', function () { + + it( 'should call this.send() with specify parameters', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'url' ).and.contain( 'https://accounts.google.com/o/oauth2/auth?' ); + + done(); + }; + + ctrl.loginAction(); + } ); + + } ); + + describe( 'listAction()', function () { + + it( 'should be able to get list of users', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'array' ).and.have.length.above( 1 ); + expect( result[0] ).to.be.an( 'object' ).and.be.ok; + expect( result[0] ).to.have.property( 'id' ).and.be.ok; + expect( result[0] ).to.have.property( 'firstname' ).and.be.ok; + expect( result[0] ).to.have.property( 'lastname' ).and.be.ok; + expect( result[0] ).to.have.property( 'email' ).and.be.ok; + expect( result[0] ).to.have.property( 'googleid' ).and.be.ok; + expect( result[0] ).to.not.have.property( 'token' ); + + done(); + }; + + ctrl.listAction(); + } ); + + } ); + + describe( 'getAction()', function () { + + it( 'should be able to get user by id', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.be.ok; + expect( result ).to.have.property( 'firstname' ).and.be.ok; + expect( result ).to.have.property( 'lastname' ).and.be.ok; + expect( result ).to.have.property( 'email' ).and.be.ok; + expect( result ).to.have.property( 'googleid' ).and.be.ok; + expect( result ).to.not.have.property( 'token' ); + + done(); + }; + + ctrl.req.params = { id: gUsers[0].id }; + + ctrl.getAction(); + } ); + + it( 'should be able to get error if user do not exist', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: 15151515151151515 }; + + ctrl.getAction(); + } ); + + } ); + + describe( 'deleteAction()', function () { + + it( 'should be able to delete user by id', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: gUsers[0].id }; + + ctrl.deleteAction(); + } ); + + it( 'should be able to get error if user do not exist', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: 15151515151151515 }; + + ctrl.deleteAction(); + } ); + + } ); + +} ); \ No newline at end of file diff --git a/modules/clever-auth-google/tests/unit/test.service.UserGoogleService.js b/modules/clever-auth-google/tests/unit/test.service.UserGoogleService.js new file mode 100644 index 0000000..266c6b0 --- /dev/null +++ b/modules/clever-auth-google/tests/unit/test.service.UserGoogleService.js @@ -0,0 +1,683 @@ +var expect = require ( 'chai' ).expect + , request = require ( 'supertest' ) + , path = require( 'path' ) + , app = require ( path.resolve( __dirname + '/../../../../' ) + '/index.js' ) + , config = require( 'config' ) + , testEnv = require ( 'utils' ).testEnv() + , sinon = require( 'sinon' ) + , Q = require ( 'q' ); + +var UserService = null; + +var gUserId_1, accessedAtDate, gUser, user_1; + +describe( 'service.UserGoogleService', function () { + var Service, Model; + + before( function ( done ) { + this.timeout( 15000 ); + testEnv( function ( _UserGoogleService_, _ORMUserGoogleModel_ ) { + + Service = _UserGoogleService_; + Model = _ORMUserGoogleModel_; + + done(); + }, done ); + } ); + + describe( '.formatData( profile, accessToken )', function () { + + it( 'should return an object with filtered data', function ( done ) { + var accessToken = 'sdasdasdasdasdasdasdasda' + , profile = { + provider: 'google', + id: '112064034597570891452', + displayName: 'Volodymyr Denshchykov', + name: { familyName: 'Denshchykov', givenName: 'Volodymyr' }, + emails: [ + { value: 'volodymyr@clevertech.biz' } + ], + _raw: '{\n "id": "112064034597570891032",\n "email": "volodymyr@clevertech.biz",\n "verified_email": true,\n "name": "Volodymyr Denshchykov",\n "given_name": "Volodymyr",\n "family_name": "Denshchykov",\n "link": "https://plus.google.com/112064034597570891032",\n "gender": "male",\n "locale": "ru",\n "hd": "clevertech.biz"\n}\n', + _json: { + id: '112064034597570891032', + email: 'volodymyr@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'Volodymyr', + family_name: 'Denshchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru', + hd: 'clevertech.biz' } + }; + + var data = Service.formatData( profile, accessToken ); + + expect( data ).to.be.an( 'object' ).and.be.ok; + + expect( data ).to.have.property( 'token' ).and.equal( accessToken ); + expect( data ).to.have.property( 'firstname' ).and.equal( profile._json.given_name ); + expect( data ).to.have.property( 'lastname' ).and.equal( profile._json.family_name ); + expect( data ).to.have.property( 'email' ).and.equal( profile._json.email ); + expect( data ).to.have.property( 'googleid' ).and.equal( profile._json.id ); + expect( data ).to.have.property( 'picture' ).and.not.be.ok; + expect( data ).to.have.property( 'link' ).and.equal( profile._json.link ); + expect( data ).to.have.property( 'gender' ).and.equal( profile._json.gender ); + expect( data ).to.have.property( 'locale' ).and.equal( profile._json.locale ); + expect( data ).to.have.property( 'verified' ).and.equal( profile._json.verified_email ); + + expect( data ).to.not.have.property( 'hd' ); + expect( data ).to.not.have.property( '_raw' ); + expect( data ).to.not.have.property( 'provider' ); + + done(); + } ); + + } ); + + describe( '.findOrCreate( profile, accessToken )', function () { + + it( 'should not call ORMUserGoogleModel.find() if google account is not verify', function ( done ) { + var accessToken = 'sdasdasdasdasdasdasdasda' + , profile = { + _json: { + id: '112064034597570891032', + email: 'volodymyr@clevertech.biz', + verified_email: false, + name: 'Volodymyr Denshchykov', + given_name: 'Volodymyr', + family_name: 'Denshchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru', + hd: 'clevertech.biz' } + }; + + var spy = sinon.spy( Model, 'find' ); + + Service + .findOrCreate( profile, accessToken ) + .then( function ( result ) { + + expect( result ).to.not.be.ok; + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + }, done ) + } ); + + it( 'should call ORMUserGoogleModel.find(), ORMUserGoogleModel.create() and create gUser if google account is verify and do not already exist', function ( done ) { + var accessToken = 'sdasdasdasdasdasdasdasda' + , profile = { + _json: { + id: '112064034597570891032', + email: 'volodymyr@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'Volodymyr', + family_name: 'Denshchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru', + hd: 'clevertech.biz' } + }; + + var spyFind = sinon.spy( Model, 'find' ); + var spyCreate = sinon.spy( Model, 'create' ); + + Service + .findOrCreate( profile, accessToken ) + .delay( 1000 ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.be.ok; + + expect( spyFind.called ).to.be.true; + expect( spyFind.calledOnce ).to.be.true; + + expect( spyCreate.called ).to.be.true; + expect( spyCreate.calledOnce ).to.be.true; + + spyFind.restore(); + spyCreate.restore(); + + Model + .find( result.id ) + .success( function( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( result.id ); + expect( user ).to.have.property( 'token' ).and.equal( accessToken ); + expect( user ).to.have.property( 'firstname' ).and.equal( profile._json.given_name ); + expect( user ).to.have.property( 'lastname' ).and.equal( profile._json.family_name ); + expect( user ).to.have.property( 'email' ).and.equal( profile._json.email ); + expect( user ).to.have.property( 'googleid' ).and.equal( profile._json.id ); + expect( user ).to.have.property( 'accessedAt' ).and.be.ok; + + gUserId_1 = user.id; + accessedAtDate = new Date( user.accessedAt ) + ''; + gUser = user; + + done(); + }) + .error( done ); + }, done ) + } ); + + it( 'should call ORMUserGoogleModel.find(), ORMUserGoogleModel.updateAttributes(), not call ORMUserGoogleModel.create() and update already existing gUser', function ( done ) { + var accessToken = '15151515151515151' + , profile = { + _json: { + id: '112064034597570891032', + email: 'volodymyr@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'Volodymyr', + family_name: 'Denshchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru', + hd: 'clevertech.biz' } + }; + + var spyFind = sinon.spy( Model, 'find' ); + var spyCreate = sinon.spy( Model, 'create' ); + + Service + .findOrCreate( profile, accessToken ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( gUserId_1 ); + expect( result ).to.have.property( 'token' ).and.equal( accessToken ); + + var newAccessedAtDate = new Date( result.accessedAt ) + ''; + + expect( accessedAtDate ).to.not.equal( newAccessedAtDate ); + + expect( spyFind.called ).to.be.true; + expect( spyFind.calledOnce ).to.be.true; + + expect( spyCreate.called ).to.be.false; + + spyFind.restore(); + spyCreate.restore(); + + done(); + }, done ) + } ); + + } ); + + describe( '.authenticate( gUser, profile )', function () { + + before( function( done ) { + var accessToken = '151216562162351626' + , profile = { + _json: { + id: '121212121212121', + email: 'volodymyrs@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'asasasd', + family_name: 'Denssdfsdfhchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru' + } + }; + + try { + UserService = injector.getInstance( 'UserService' ); + } catch ( err ) { + console.log( err ); + } + + Service + .findOrCreate( profile, accessToken ) + .then( function( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.be.ok; + + gUser = user; + + done(); + }, done ); + }); + + it( 'should return gUser if UserService is not defined', function ( done ) { + var profile = { + _json: { + id: '121212121212121', + email: 'volodymyrs@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'asasasd', + family_name: 'Denssdfsdfhchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru' + } + }; + + if ( !UserService ) { + Service + .authenticate( gUser, profile ) + .then( function ( user ) { + + expect( !UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( user ).to.not.have.property( 'token' ); + expect( user ).to.have.property( 'firstname' ).and.equal( gUser.firstname ); + expect( user ).to.have.property( 'lastname' ).and.equal( gUser.lastname ); + expect( user ).to.have.property( 'email' ).and.equal( gUser.email ); + expect( user ).to.have.property( 'googleid' ).and.equal( gUser.googleid ); + expect( user ).to.have.property( 'accessedAt' ).and.be.ok; + + done(); + } ) + .fail( done ); + } else { + console.log( 'UserService is not defined' ); + done(); + } + } ); + + it( 'should create new user and to associate it with qUser if UserService is defined', function ( done ) { + var profile = { + _json: { + id: '121212121212121', + email: 'volodymyrs@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'asasasd', + family_name: 'Denssdfsdfhchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru' + } + }; + + if ( !!UserService ) { + + var spy = sinon.spy( UserService, 'create' ); + + Service + .authenticate( gUser, profile ) + .then( function ( user ) { + + expect( !!UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.not.equal( gUser.id ); + + expect( spy.called ).to.be.true; + expect( spy.calledOnce ).to.be.true; + + spy.restore(); + + UserService + .findById( user.id ) + .then( function( _user ) { + + expect( _user ).to.be.an( 'object' ).and.be.ok; + expect( _user ).to.have.property( 'id' ).and.equal( user.id ); + expect( _user ).to.have.property( 'email' ).and.equal( profile._json.email ); + expect( _user ).to.have.property( 'password' ).and.be.ok; + expect( _user ).to.have.property( 'firstname' ).and.equal( profile._json.given_name ); + expect( _user ).to.have.property( 'lastname' ).and.equal( profile._json.family_name ); + expect( _user ).to.have.property( 'confirmed' ).and.equal( true ); + expect( _user ).to.have.property( 'active' ).and.equal( true ); + + user_1 = _user; + + Service + .find( { where: { id: gUser.id } } ) + .then( function( _gUser ) { + + expect( _gUser ).to.be.an( 'array' ).and.have.length( 1 ); + + _gUser = _gUser[0]; + + expect( _gUser ).to.be.an( 'object' ).and.be.ok; + expect( _gUser ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( _gUser ).to.have.property( 'UserId' ).and.equal( user.id ); + + done(); + }, done ) + }, done ); + } ) + .fail( done ); + } else { + console.log( 'UserService is defined' ); + done(); + } + + } ); + + it( 'should return existing user by gUser.UserId if UserService is defined', function ( done ) { + var profile = { + _json: { + id: '121212121212121', + email: 'volodymyrs@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'asasasd', + family_name: 'Denssdfsdfhchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru' + } + }; + + if ( !!UserService ) { + + var spy = sinon.spy( UserService, 'create' ); + + Service + .authenticate( gUser, profile ) + .then( function ( user ) { + + expect( !!UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( user_1.id ); + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + } ) + .fail( done ); + } else { + console.log( 'UserService is defined' ); + done(); + } + + } ); + + it( 'should return error if user with such gUser.UserId do not exist and if UserService is defined' , function ( done ) { + var profile = { + _json: { + id: '121212121212121', + email: 'volodymyrs@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'asasasd', + family_name: 'Denssdfsdfhchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru' + } + }; + + if ( !!UserService ) { + + var spy = sinon.spy( UserService, 'create' ); + + gUser + .updateAttributes( { UserId: 1000 } ) + .success( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'UserId' ).and.equal( 1000 ); + + Service + .authenticate( gUser, profile ) + .then( function ( user ) { + + expect( !!UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( user ).to.have.property( 'message' ).and.be.ok; + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + } ) + .fail( done ); + }) + .error( done ); + } else { + console.log( 'UserService is defined' ); + done(); + } + + } ); + + it( 'should return existing user by gUser.email if user with such email already exist', function ( done ) { + var profile = { + _json: { + id: '121212121212121', + email: 'volodymyrs@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'asasasd', + family_name: 'Denssdfsdfhchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru' + } + }; + + if ( !!UserService ) { + + var spy = sinon.spy( UserService, 'create' ); + + gUser + .updateAttributes( { UserId: null } ) + .success( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'UserId' ).and.equal( null ); + + Service + .authenticate( gUser, profile ) + .then( function ( user ) { + + expect( !!UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( user_1.id ); + + expect( spy.called ).to.be.false; + + spy.restore(); + + Service + .find( { where: { id: gUser.id } } ) + .then( function( _gUser ) { + + expect( _gUser ).to.be.an( 'array' ).and.have.length( 1 ); + + _gUser = _gUser[0]; + + expect( _gUser ).to.be.an( 'object' ).and.be.ok; + expect( _gUser ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( _gUser ).to.have.property( 'UserId' ).and.equal( user.id ); + + done(); + }, done ) + } ) + .fail( done ); + }) + .error( done ); + } else { + console.log( 'UserService is defined' ); + done(); + } + + } ); + + } ); + + describe( '.updateAccessedDate( user )', function () { + + it( 'should be able to update user', function ( done ) { + + user_1 + .updateAttributes( { accessedAt: null } ) + .success( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( user_1.id ); + expect( result ).to.have.property( 'accessedAt' ).and.not.be.ok; + + Service + .updateAccessedDate( user_1 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( user_1.id ); + expect( result ).to.have.property( 'accessedAt' ).and.be.ok; + + done(); + }, done ); + }) + .error( done ); + } ); + + it( 'should be able to update gUser', function ( done ) { + + gUser + .updateAttributes( { accessedAt: null } ) + .success( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( result ).to.have.property( 'accessedAt' ).and.not.be.ok; + + Service + .updateAccessedDate( gUser ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( result ).to.have.property( 'accessedAt' ).and.be.ok; + + done(); + }, done ); + }) + .error( done ); + } ); + + it( 'should be able to get object if it is not a user', function ( done ) { + + Service + .updateAccessedDate ( { statuscode: 403, message: 'invalid' } ) + .then ( function ( result ) { + + expect ( result ).to.be.an ( 'object' ).and.be.ok; + expect ( result ).to.have.property ( 'statuscode' ).and.equal ( 403 ); + expect ( result ).to.have.property ( 'message' ).and.be.ok; + + done (); + }, done ); + } ); + + } ); + + describe( '.listUsers()', function () { + + it( 'should be able to get list of all gUsers', function ( done ) { + + Service + .listUsers() + .then( function ( users ) { + + expect( users ).to.be.an( 'array' ).and.have.length.above( 1 ); + expect( users[0] ).to.be.an( 'object' ).and.be.ok; + expect( users[0] ).to.have.property( 'id' ).and.be.ok; + expect( users[0] ).to.have.property( 'googleid' ).and.be.ok; + expect( users[0] ).to.have.property( 'email' ).and.be.ok; + expect( users[0] ).to.have.property( 'gender' ).and.be.ok; + expect( users[0] ).to.not.have.property( 'token' ); + + done(); + } ) + .fail( done ); + } ); + + } ); + + describe( '.findUserById()', function () { + + it( 'should be able to get gUser by Id', function ( done ) { + + Service + .findUserById( gUser.id ) + .then( function ( gUser ) { + + expect( gUser ).to.be.an( 'object' ).and.be.ok; + expect( gUser ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( gUser ).to.have.property( 'googleid' ).and.be.ok; + expect( gUser ).to.have.property( 'email' ).and.be.ok; + expect( gUser ).to.have.property( 'gender' ).and.be.ok; + expect( gUser ).to.not.have.property( 'token' ); + + done(); + } ) + .fail( done ); + } ); + + it( 'should be able to get the error if the gUser does not exist', function ( done ) { + + Service + .findUserById( 151515115151515151 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + }, done ) + } ); + + } ); + + describe( '.deleteUser( gUserId )', function () { + + it( 'should be able to get the error if the gUser does not exist', function ( done ) { + + Service + .deleteUser( 151515115151515151 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + }, done ) + } ); + + it( 'should be able to delete gUser', function ( done ) { + + Service + .deleteUser( gUser.id ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 200 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + } ) + .fail( done ); + } ); + + } ); + +} ); \ No newline at end of file diff --git a/modules/clever-auth-twitter/config/default.json b/modules/clever-auth-twitter/config/default.json new file mode 100644 index 0000000..cd4b1bd --- /dev/null +++ b/modules/clever-auth-twitter/config/default.json @@ -0,0 +1,21 @@ +{ + "clever-auth-twitter": { + "secretKey": "youMustChangeMe", + + "redis": { + "host": "localhost", + "port": "6379", + "prefix": "", + "key": "" + }, + + "twitter": { + "APIKey": "dNFtIYkruK6ZET8T8QobLQ", + "APISecret": "EN8YSnv9aLNYgCh9MUbQt2Py7U8AlMHmUCU7iQ4ig", + "redirectURIs": "http://127.0.0.1:8080/auth/twitter/return" + }, + + "frontendURL": "http://localhost:9000/" + + } +} \ No newline at end of file diff --git a/modules/clever-auth-twitter/controllers/UserTwitterController.js b/modules/clever-auth-twitter/controllers/UserTwitterController.js new file mode 100644 index 0000000..7f69dd4 --- /dev/null +++ b/modules/clever-auth-twitter/controllers/UserTwitterController.js @@ -0,0 +1,124 @@ +var config = require ( 'config' )[ 'clever-auth-twitter' ] + , passport = require ( 'passport' ) + , qs = require ( 'qs' ) + , request = require ( 'request' ) + , TwitterStrategy = require('passport-twitter').Strategy; + +module.exports = function ( UserTwitterService ) { + + passport.serializeUser( function ( user, done ) { + done( null, user ); + } ); + + passport.deserializeUser( function ( user, done ) { + done( null, user ) + } ); + + passport.use( new TwitterStrategy( + { + consumerKey: config.twitter.APIKey, + consumerSecret: config.twitter.APISecret, + callbackURL: config.twitter.redirectURIs + }, + function ( token, tokenSecret, profile, done ) { + + UserTwitterService + .findOrCreate( profile, accessToken ) + .then( function( twUser ) { + return UserTwitterService.authenticate ( twUser, profile ) + }) + .then( UserTwitterService.updateAccessedDate ) + .then( done.bind( null, null ) ) + .fail( done ); + } + )); + + + return (require( 'classes' ).Controller).extend ( + { + service: UserTwitterService + }, + { + listAction: function () { + UserTwitterService.listUsers() + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, + + getAction: function () { + var twuId = this.req.params.id; + + UserTwitterService + .findUserById( twuId ) + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, + + deleteAction: function () { + var twuId = this.req.params.id; + + UserTwitterService.deleteUser( twuId ) + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, + + loginAction: function () { + var self = this + + var params = { + callback: config.twitter.redirectURIs, + consumer_key: config.twitter.APIKey, + consumer_secret: config.twitter.APISecret + } + , url = 'https://api.twitter.com/oauth/request_token'; + + request.post ( { url: url, oauth: params }, function ( err, req, body ) { + + var token = qs.parse( body ) + , url = 'https://api.twitter.com/oauth/authenticate?'; + + var params = { + oauth_token: token.oauth_token + }; + + self.send( { url: url + qs.stringify( params ) }, 200 ); + + }); + + }, + + returnAction: function () { + passport.authenticate( 'twitter', this.proxy( 'handleLocalUser' ) )( this.req, this.res, this.next ); + }, + + handleLocalUser: function ( err, user ) { + if ( err ) return this.handleException( err ); + if ( !user ) return this.send( {}, 403 ); + this.loginUserJson( user ); + }, + + loginUserJson: function ( user ) { + this.req.login( user, this.proxy( 'handleLoginJson', user ) ); + }, + + handleLoginJson: function ( user, err ) { + if ( err ) return this.handleException( err ); + + this.res.statusCode = 302; + this.res.setHeader( 'body', user ); + this.res.setHeader( 'Location', config.frontendURL ); + this.res.end(); + }, + + handleServiceMessage: function ( obj ) { + + if ( obj.statuscode ) { + this.send( obj.message, obj.statuscode ); + return; + } + + this.send( obj, 200 ); + } + + } ); +}; \ No newline at end of file diff --git a/modules/clever-auth-twitter/models/orm/UserTwitterModel.js b/modules/clever-auth-twitter/models/orm/UserTwitterModel.js new file mode 100644 index 0000000..a5490cd --- /dev/null +++ b/modules/clever-auth-twitter/models/orm/UserTwitterModel.js @@ -0,0 +1,70 @@ +module.exports = function ( sequelize, DataTypes ) { + return sequelize.define( "UserTwitter", + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + isEmail: true + } + }, + firstname: { + type: DataTypes.STRING, + allowNull: true + }, + lastname: { + type: DataTypes.STRING, + allowNull: true + }, + twitterid: { + type: DataTypes.STRING, + allowNull: false + }, + picture: { + type: DataTypes.STRING, + allowNull: true + }, + link: { + type: DataTypes.STRING, + allowNull: true + }, + locale: { + type: DataTypes.STRING, + allowNull: true + }, + token: { + type: DataTypes.STRING, + allowNull: false + }, + accessedAt: { + type: DataTypes.DATE + }, + UserId: { + type: DataTypes.INTEGER + } + }, + { + paranoid: true, + + getterMethods: { + fullName: function () { + return [ this.getDataValue( 'firstname' ), this.getDataValue( 'lastname' )].join( ' ' ); + } + }, + + instanceMethods: { + toJSON: function () { + var values = this.values; + values.fullName = this.fullName; + delete values.token; + return values; + } + } + } ); +}; \ No newline at end of file diff --git a/modules/clever-auth-twitter/module.js b/modules/clever-auth-twitter/module.js new file mode 100644 index 0000000..85c20bb --- /dev/null +++ b/modules/clever-auth-twitter/module.js @@ -0,0 +1,129 @@ +var passport = require( 'passport' ) + , debug = require( 'debug' )( 'CleverAuthGoogleModule' ) + , fs = require( 'fs' ) + , path = require( 'path' ) + , ModuleClass = require( 'classes' ).ModuleClass + , RedisStore = require( 'connect-redis' )( injector.getInstance( 'express' ) ) + , Module; + +Module = ModuleClass.extend( { + models: null, + store: null, + sequelize: null, + + preSetup: function () { + var self = this; + this.models = {}; + this.sequelize = injector.getInstance ( 'sequelize' ); + var dir = fs.readdirSync ( path.join ( __dirname, 'models', 'orm' ) ); + dir.forEach ( function ( model ) { + self.getModel ( path.join ( __dirname, 'models', 'orm', model ) ); + } ); + }, + + preResources: function () { + injector.instance ( 'passport', passport ); + }, + + modulesLoaded: function () { + this.defineModelsAssociations (); + }, + + defineModelsAssociations: function () { + if ( !this.config.hasOwnProperty ( 'modelAssociations' ) ) { + return true; + } + + debug ( 'Defining model assocations' ); + + Object.keys ( this.config.modelAssociations ).forEach ( this.proxy ( function ( modelName ) { + Object.keys ( this.config.modelAssociations[ modelName ] ).forEach ( this.proxy ( 'defineModelAssociations', modelName ) ); + } ) ); + }, + + defineModelAssociations: function ( modelName, assocType ) { + var associatedWith = this.config.modelAssociations[ modelName ][ assocType ]; + if ( !associatedWith instanceof Array ) { + associatedWith = [ associatedWith ]; + } + + associatedWith.forEach ( this.proxy ( 'associateModels', modelName, assocType ) ); + }, + + associateModels: function ( modelName, assocType, assocTo ) { + // Support second argument + if ( assocTo instanceof Array ) { + debug ( '%s %s %s with second argument of ', modelName, assocType, assocTo[0], assocTo[1] ); + this.models[ modelName ][ assocType ] ( this.models[ assocTo[0] ], assocTo[1] ); + } else { + debug ( '%s %s %s', modelName, assocType, assocTo ); + this.models[ modelName ][ assocType ] ( this.models[assocTo] ); + } + }, + + getModel: function ( modelPath ) { + var modelName = modelPath.split ( '/' ).pop ().split ( '.' ).shift (); + + if ( typeof this.models[ modelName ] === 'undefined' ) { + debug ( [ 'Loading model', modelName, 'from', modelPath ].join ( ' ' ) ); + + // Call on sequelizejs to load the model + this.models[ modelName ] = this.sequelize.import ( modelPath ); + + // Set a flat for tracking + this.models[ modelName ].ORM = true; + + // Add the model to the injector + injector.instance ( 'ORM' + modelName, this.models[ modelName ] ); + } + + return this.models[ modelName ]; + }, + + configureApp: function ( app, express ) { +// Setup the redis store + this.store = new RedisStore ( { + host: this.config.redis.host, + port: this.config.redis.port, + prefix: this.config.redis.prefix + process.env.NODE_ENV + "_", + password: this.config.redis.key + } ); + +// Session management + app.use ( express.cookieParser () ); + app.use ( express.session ( { + secret: this.config.secretKey, + cookie: { secure: false, maxAge: 86400000 }, + store: this.store + } ) ); + + // Enable CORS + app.use ( this.proxy ( 'enableCors' ) ); + + // Initialize passport + app.use ( passport.initialize () ); + app.use ( passport.session () ); + }, + + enableCors: function ( req, res, next ) { + + res.header ( 'Access-Control-Allow-Origin', req.headers.origin ); + res.header ( 'Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE' ); + res.header ( 'Access-Control-Allow-Headers', 'Content-Type, Authorization, x-requested-with' ); + res.header( "Access-Control-Allow-Credentials", "true" ); + res.header( "Access-Control-Max-Age", "1000000000" ); + // intercept OPTIONS method + if ( 'OPTIONS' == req.method ) { + res.send ( 200 ); + } + else { + next (); + } + }, + + preShutdown: function () { + this.store.client.quit (); + } +} ); + +module.exports = new Module( 'clever-auth-twitter', injector ); \ No newline at end of file diff --git a/modules/clever-auth-twitter/package.json b/modules/clever-auth-twitter/package.json new file mode 100644 index 0000000..4c82835 --- /dev/null +++ b/modules/clever-auth-twitter/package.json @@ -0,0 +1,33 @@ +{ + "name": "clever-auth-twitter", + "version": "0.0.1", + "private": true, + "dependencies": { + "passport": "~0.1.18", + "connect-redis": "~1.4.6", + "q": "", + "sequelize": "1.7.0-beta8", + "moment": "", + "passport-twitter": "", + "qs": "*", + "request": "~2.33.0" + }, + "author": { + "name": "Clevertech", + "email": "info@clevertech.biz", + "web": "http://www.clevertech.biz" + }, + "collaborators": [ + "" + ], + "description": "", + "keywords": [ + "" + ], + "main": "module.js", + "devDependencies": { + "chai": "*", + "mocha": "*", + "sinon": "" + } +} \ No newline at end of file diff --git a/modules/clever-auth-twitter/routes.js b/modules/clever-auth-twitter/routes.js new file mode 100644 index 0000000..40453c8 --- /dev/null +++ b/modules/clever-auth-twitter/routes.js @@ -0,0 +1,8 @@ +module.exports = function ( + app, + UserTwitterController ) +{ + + app.all('/auth/twitter/?:action?', UserTwitterController.attach() ); + +}; \ No newline at end of file diff --git a/modules/clever-auth-twitter/services/UserTwitterService.js b/modules/clever-auth-twitter/services/UserTwitterService.js new file mode 100644 index 0000000..b65da31 --- /dev/null +++ b/modules/clever-auth-twitter/services/UserTwitterService.js @@ -0,0 +1,230 @@ +var Q = require( 'q' ) + , crypto = require( 'crypto' ) + , moment = require( 'moment' ) + , Sequelize = require( 'sequelize' ) + , UserTwitterService = null; + +module.exports = function ( sequelize, + ORMUserTwitterModel ) { + + if ( UserTwitterService && UserTwitterService.instance ) { + return UserTwitterService.instance; + } + + var UserService = null; + + UserTwitterService = require( 'services' ).BaseService.extend( { + + formatData: function ( profile, accessToken ) { + return { + twitterid: profile._json.id, + email: profile._json.email, + firstname: profile._json.given_name, + lastname: profile._json.family_name, + picture: profile._json.picture || null, + verified: profile._json.verified_email, + link: profile._json.link, + gender: profile._json.gender, + locale: profile._json.locale, + token: accessToken || null + } + }, //tested + + findOrCreate: function ( profile, accessToken ) { + var deferred = Q.defer() + , data = this.formatData ( profile, accessToken ); + + if ( data.verified ) { + + ORMUserTwitterModel + .find( { where: { email: data.email } } ) + .success( function ( twUser ) { + + if ( !twUser ) { + + data.accessedAt = moment.utc().format( 'YYYY-MM-DD HH:ss:mm' ); + + ORMUserTwitterModel + .create( data ) + .success( deferred.resolve ) + .error( deferred.reject ); + + } else { + + twUser + .updateAttributes( { accessedAt: moment.utc().format( 'YYYY-MM-DD HH:ss:mm' ), token: data.token } ) + .success( deferred.resolve ) + .error( deferred.reject ); + } + } ) + .error( deferred.reject ); + } else { + deferred.resolve(); + } + + return deferred.promise; + }, //tested + + authenticate: function( twUser, profile ) { + var deferred = Q.defer() + , data = this.formatData ( profile ); + + try { + UserService = injector.getInstance( 'UserService' ); + } catch ( err ) { + console.log( err ); + } + + if ( !!twUser && !!UserService ) { + + if ( twUser.UserId ) { + + UserService + .find( { where: { id: twUser.UserId, email: twUser.email } } ) + .then( function( users ) { + + var user = !!users && !!users.length + ? users[0] + : { statuscode: 403, message: 'invalid' }; + + deferred.resolve( user ); + }) + .fail( deferred.reject ); + + } else { + + UserService + .find( { where: { email: twUser.email } } ) + .then( function( user ) { + + user = user[0]; + + if ( !!user && !!user.id ) { + + twUser + .updateAttributes( { UserId: user.id } ) + .success ( function () { + deferred.resolve ( user ); + } ) + .error( deferred.reject ); + + } else { + + data.confirmed = true; + data.username = data.email; + data.password = crypto.createHash( 'sha1' ).update( Math.floor(Math.random() * 1e18) + '' ).digest( 'hex' ) + + UserService + .create( data ) + .then( function( user ) { + + twUser + .updateAttributes ( { UserId: user.id } ) + .success ( function () { + deferred.resolve ( user ); + } ) + .error ( deferred.reject ); + }) + .fail( deferred.reject ); + } + }) + .fail( deferred.reject ); + } + + } else { + deferred.resolve( twUser ); + } + + return deferred.promise; + }, //tested + + updateAccessedDate: function( user ){ + var deferred = Q.defer(); + + if ( !!user && !!user.id ) { + user + .updateAttributes( { accessedAt: moment.utc().format( 'YYYY-MM-DD HH:ss:mm' ) } ) + .success( function() { + deferred.resolve( user.toJSON() ); + }) + .error( deferred.reject ); + } else { + deferred.resolve( user ); + } + + return deferred.promise; + }, //tested + + listUsers: function() { + var deferred = Q.defer(); + + ORMUserTwitterModel + .findAll( { where: { deletedAt: null } } ) + .success( function( twUsers ) { + if ( !!twUsers && !!twUsers.length ) { + deferred.resolve( twUsers.map( function( u ) { return u.toJSON(); } ) ); + } else { + deferred.resolve( {} ); + } + }) + .error( deferred.reject ); + + return deferred.promise; + }, //tested + + findUserById: function( twUserId ) { + var deferred = Q.defer(); + + ORMUserTwitterModel + .find( twUserId ) + .success( function( twUser ) { + + if ( !!twUser && !!twUser.id ){ + deferred.resolve( twUser.toJSON() ); + } else { + deferred.resolve( { statuscode: 403, message: 'user do not exist' } ); + } + }) + .error( deferred.reject ); + + return deferred.promise; + }, //tested + + deleteUser: function( twUserId ) { + var deferred = Q.defer(); + + ORMUserTwitterModel + .find( twUserId ) + .success( function( twUser ) { + + if ( !!twUser && !!twUser.id ) { + + twUser + .destroy() + .success( function( result ) { + + if ( !result.deletedAt ) { + deferred.resolve( { statuscode: 500, message: 'error' } ); + } else { + deferred.resolve( { statuscode: 200, message: 'user is deleted' } ); + } + + }) + .error( deferred.reject ); + + } else { + deferred.resolve( { statuscode: 403, message: 'user do not exist' } ) + } + }) + .error( deferred.reject ); + + return deferred.promise; + } //tested + + } ); + + UserTwitterService.instance = new UserTwitterService( sequelize ); + UserTwitterService.Model = ORMUserTwitterModel; + + return UserTwitterService.instance; +}; \ No newline at end of file diff --git a/modules/clever-auth-twitter/tests/unit/test.controllers.UserGoogleController.js b/modules/clever-auth-twitter/tests/unit/test.controllers.UserGoogleController.js new file mode 100644 index 0000000..798b45f --- /dev/null +++ b/modules/clever-auth-twitter/tests/unit/test.controllers.UserGoogleController.js @@ -0,0 +1,221 @@ +// Bootstrap the testing environmen +var testEnv = require( 'utils' ).testEnv(); + +var expect = require( 'chai' ).expect + , Q = require ( 'q' ) + , sinon = require( 'sinon' ) + , Service; + +var new_user; + +describe( 'controllers.UserGoogleController', function () { + var Service, UserGoogleController, ctrl, gUsers = []; + + before( function ( done ) { + testEnv( function ( _UserGoogleService_, _UserGoogleController_ ) { + var req = { + params: { action: 'fakeAction'}, + method: 'GET', + query: {} + }; + + var res = { + json: function () {} + }; + + var next = function () {}; + + Controller = _UserGoogleController_; + Service = _UserGoogleService_; + ctrl = new Controller( req, res, next ); + + var profile_1 = { + _json: { + id: '121212121212121', + email: 'volodymyrs@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'asasasd', + family_name: 'Denssdfsdfhchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru' + } + } + , profile_2 = { + _json: { + id: '45454454545454', + email: 'volodymyr@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'asasasd', + family_name: 'Denssdfsdfhchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru' + } + }; + + Service + .findOrCreate( profile_1, 'bhbhgvbljbhscvzkchvblzclvnzbkjxcv' ) + .then( function( gUser ) { + + expect( gUser ).to.be.an( 'object' ).and.be.ok; + expect( gUser ).to.have.property( 'id' ).and.be.ok; + + gUsers.push( gUser ) + + Service + .findOrCreate( profile_2, 'kjajkvl zsdvbakhvckhabskhv' ) + .then( function( gUser ) { + + expect( gUser ).to.be.an( 'object' ).and.be.ok; + expect( gUser ).to.have.property( 'id' ).and.be.ok; + + gUsers.push( gUser ) + + done(); + }, done ); + }, done ); + } ); + } ); + + afterEach( function ( done ) { + + ctrl.req = { + params: { action: 'fakeAction'}, + method: 'GET', + query: {}, + body: {} + }; + + ctrl.res = { + json: function () {} + }; + + done(); + }); + + describe( 'loginAction()', function () { + + it( 'should call this.send() with specify parameters', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'url' ).and.contain( 'https://accounts.google.com/o/oauth2/auth?' ); + + done(); + }; + + ctrl.loginAction(); + } ); + + } ); + + describe( 'listAction()', function () { + + it( 'should be able to get list of users', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'array' ).and.have.length.above( 1 ); + expect( result[0] ).to.be.an( 'object' ).and.be.ok; + expect( result[0] ).to.have.property( 'id' ).and.be.ok; + expect( result[0] ).to.have.property( 'firstname' ).and.be.ok; + expect( result[0] ).to.have.property( 'lastname' ).and.be.ok; + expect( result[0] ).to.have.property( 'email' ).and.be.ok; + expect( result[0] ).to.have.property( 'googleid' ).and.be.ok; + expect( result[0] ).to.not.have.property( 'token' ); + + done(); + }; + + ctrl.listAction(); + } ); + + } ); + + describe( 'getAction()', function () { + + it( 'should be able to get user by id', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.be.ok; + expect( result ).to.have.property( 'firstname' ).and.be.ok; + expect( result ).to.have.property( 'lastname' ).and.be.ok; + expect( result ).to.have.property( 'email' ).and.be.ok; + expect( result ).to.have.property( 'googleid' ).and.be.ok; + expect( result ).to.not.have.property( 'token' ); + + done(); + }; + + ctrl.req.params = { id: gUsers[0].id }; + + ctrl.getAction(); + } ); + + it( 'should be able to get error if user do not exist', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: 15151515151151515 }; + + ctrl.getAction(); + } ); + + } ); + + describe( 'deleteAction()', function () { + + it( 'should be able to delete user by id', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: gUsers[0].id }; + + ctrl.deleteAction(); + } ); + + it( 'should be able to get error if user do not exist', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: 15151515151151515 }; + + ctrl.deleteAction(); + } ); + + } ); + +} ); \ No newline at end of file diff --git a/modules/clever-auth-twitter/tests/unit/test.service.UserGoogleService.js b/modules/clever-auth-twitter/tests/unit/test.service.UserGoogleService.js new file mode 100644 index 0000000..266c6b0 --- /dev/null +++ b/modules/clever-auth-twitter/tests/unit/test.service.UserGoogleService.js @@ -0,0 +1,683 @@ +var expect = require ( 'chai' ).expect + , request = require ( 'supertest' ) + , path = require( 'path' ) + , app = require ( path.resolve( __dirname + '/../../../../' ) + '/index.js' ) + , config = require( 'config' ) + , testEnv = require ( 'utils' ).testEnv() + , sinon = require( 'sinon' ) + , Q = require ( 'q' ); + +var UserService = null; + +var gUserId_1, accessedAtDate, gUser, user_1; + +describe( 'service.UserGoogleService', function () { + var Service, Model; + + before( function ( done ) { + this.timeout( 15000 ); + testEnv( function ( _UserGoogleService_, _ORMUserGoogleModel_ ) { + + Service = _UserGoogleService_; + Model = _ORMUserGoogleModel_; + + done(); + }, done ); + } ); + + describe( '.formatData( profile, accessToken )', function () { + + it( 'should return an object with filtered data', function ( done ) { + var accessToken = 'sdasdasdasdasdasdasdasda' + , profile = { + provider: 'google', + id: '112064034597570891452', + displayName: 'Volodymyr Denshchykov', + name: { familyName: 'Denshchykov', givenName: 'Volodymyr' }, + emails: [ + { value: 'volodymyr@clevertech.biz' } + ], + _raw: '{\n "id": "112064034597570891032",\n "email": "volodymyr@clevertech.biz",\n "verified_email": true,\n "name": "Volodymyr Denshchykov",\n "given_name": "Volodymyr",\n "family_name": "Denshchykov",\n "link": "https://plus.google.com/112064034597570891032",\n "gender": "male",\n "locale": "ru",\n "hd": "clevertech.biz"\n}\n', + _json: { + id: '112064034597570891032', + email: 'volodymyr@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'Volodymyr', + family_name: 'Denshchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru', + hd: 'clevertech.biz' } + }; + + var data = Service.formatData( profile, accessToken ); + + expect( data ).to.be.an( 'object' ).and.be.ok; + + expect( data ).to.have.property( 'token' ).and.equal( accessToken ); + expect( data ).to.have.property( 'firstname' ).and.equal( profile._json.given_name ); + expect( data ).to.have.property( 'lastname' ).and.equal( profile._json.family_name ); + expect( data ).to.have.property( 'email' ).and.equal( profile._json.email ); + expect( data ).to.have.property( 'googleid' ).and.equal( profile._json.id ); + expect( data ).to.have.property( 'picture' ).and.not.be.ok; + expect( data ).to.have.property( 'link' ).and.equal( profile._json.link ); + expect( data ).to.have.property( 'gender' ).and.equal( profile._json.gender ); + expect( data ).to.have.property( 'locale' ).and.equal( profile._json.locale ); + expect( data ).to.have.property( 'verified' ).and.equal( profile._json.verified_email ); + + expect( data ).to.not.have.property( 'hd' ); + expect( data ).to.not.have.property( '_raw' ); + expect( data ).to.not.have.property( 'provider' ); + + done(); + } ); + + } ); + + describe( '.findOrCreate( profile, accessToken )', function () { + + it( 'should not call ORMUserGoogleModel.find() if google account is not verify', function ( done ) { + var accessToken = 'sdasdasdasdasdasdasdasda' + , profile = { + _json: { + id: '112064034597570891032', + email: 'volodymyr@clevertech.biz', + verified_email: false, + name: 'Volodymyr Denshchykov', + given_name: 'Volodymyr', + family_name: 'Denshchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru', + hd: 'clevertech.biz' } + }; + + var spy = sinon.spy( Model, 'find' ); + + Service + .findOrCreate( profile, accessToken ) + .then( function ( result ) { + + expect( result ).to.not.be.ok; + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + }, done ) + } ); + + it( 'should call ORMUserGoogleModel.find(), ORMUserGoogleModel.create() and create gUser if google account is verify and do not already exist', function ( done ) { + var accessToken = 'sdasdasdasdasdasdasdasda' + , profile = { + _json: { + id: '112064034597570891032', + email: 'volodymyr@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'Volodymyr', + family_name: 'Denshchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru', + hd: 'clevertech.biz' } + }; + + var spyFind = sinon.spy( Model, 'find' ); + var spyCreate = sinon.spy( Model, 'create' ); + + Service + .findOrCreate( profile, accessToken ) + .delay( 1000 ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.be.ok; + + expect( spyFind.called ).to.be.true; + expect( spyFind.calledOnce ).to.be.true; + + expect( spyCreate.called ).to.be.true; + expect( spyCreate.calledOnce ).to.be.true; + + spyFind.restore(); + spyCreate.restore(); + + Model + .find( result.id ) + .success( function( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( result.id ); + expect( user ).to.have.property( 'token' ).and.equal( accessToken ); + expect( user ).to.have.property( 'firstname' ).and.equal( profile._json.given_name ); + expect( user ).to.have.property( 'lastname' ).and.equal( profile._json.family_name ); + expect( user ).to.have.property( 'email' ).and.equal( profile._json.email ); + expect( user ).to.have.property( 'googleid' ).and.equal( profile._json.id ); + expect( user ).to.have.property( 'accessedAt' ).and.be.ok; + + gUserId_1 = user.id; + accessedAtDate = new Date( user.accessedAt ) + ''; + gUser = user; + + done(); + }) + .error( done ); + }, done ) + } ); + + it( 'should call ORMUserGoogleModel.find(), ORMUserGoogleModel.updateAttributes(), not call ORMUserGoogleModel.create() and update already existing gUser', function ( done ) { + var accessToken = '15151515151515151' + , profile = { + _json: { + id: '112064034597570891032', + email: 'volodymyr@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'Volodymyr', + family_name: 'Denshchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru', + hd: 'clevertech.biz' } + }; + + var spyFind = sinon.spy( Model, 'find' ); + var spyCreate = sinon.spy( Model, 'create' ); + + Service + .findOrCreate( profile, accessToken ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( gUserId_1 ); + expect( result ).to.have.property( 'token' ).and.equal( accessToken ); + + var newAccessedAtDate = new Date( result.accessedAt ) + ''; + + expect( accessedAtDate ).to.not.equal( newAccessedAtDate ); + + expect( spyFind.called ).to.be.true; + expect( spyFind.calledOnce ).to.be.true; + + expect( spyCreate.called ).to.be.false; + + spyFind.restore(); + spyCreate.restore(); + + done(); + }, done ) + } ); + + } ); + + describe( '.authenticate( gUser, profile )', function () { + + before( function( done ) { + var accessToken = '151216562162351626' + , profile = { + _json: { + id: '121212121212121', + email: 'volodymyrs@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'asasasd', + family_name: 'Denssdfsdfhchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru' + } + }; + + try { + UserService = injector.getInstance( 'UserService' ); + } catch ( err ) { + console.log( err ); + } + + Service + .findOrCreate( profile, accessToken ) + .then( function( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.be.ok; + + gUser = user; + + done(); + }, done ); + }); + + it( 'should return gUser if UserService is not defined', function ( done ) { + var profile = { + _json: { + id: '121212121212121', + email: 'volodymyrs@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'asasasd', + family_name: 'Denssdfsdfhchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru' + } + }; + + if ( !UserService ) { + Service + .authenticate( gUser, profile ) + .then( function ( user ) { + + expect( !UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( user ).to.not.have.property( 'token' ); + expect( user ).to.have.property( 'firstname' ).and.equal( gUser.firstname ); + expect( user ).to.have.property( 'lastname' ).and.equal( gUser.lastname ); + expect( user ).to.have.property( 'email' ).and.equal( gUser.email ); + expect( user ).to.have.property( 'googleid' ).and.equal( gUser.googleid ); + expect( user ).to.have.property( 'accessedAt' ).and.be.ok; + + done(); + } ) + .fail( done ); + } else { + console.log( 'UserService is not defined' ); + done(); + } + } ); + + it( 'should create new user and to associate it with qUser if UserService is defined', function ( done ) { + var profile = { + _json: { + id: '121212121212121', + email: 'volodymyrs@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'asasasd', + family_name: 'Denssdfsdfhchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru' + } + }; + + if ( !!UserService ) { + + var spy = sinon.spy( UserService, 'create' ); + + Service + .authenticate( gUser, profile ) + .then( function ( user ) { + + expect( !!UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.not.equal( gUser.id ); + + expect( spy.called ).to.be.true; + expect( spy.calledOnce ).to.be.true; + + spy.restore(); + + UserService + .findById( user.id ) + .then( function( _user ) { + + expect( _user ).to.be.an( 'object' ).and.be.ok; + expect( _user ).to.have.property( 'id' ).and.equal( user.id ); + expect( _user ).to.have.property( 'email' ).and.equal( profile._json.email ); + expect( _user ).to.have.property( 'password' ).and.be.ok; + expect( _user ).to.have.property( 'firstname' ).and.equal( profile._json.given_name ); + expect( _user ).to.have.property( 'lastname' ).and.equal( profile._json.family_name ); + expect( _user ).to.have.property( 'confirmed' ).and.equal( true ); + expect( _user ).to.have.property( 'active' ).and.equal( true ); + + user_1 = _user; + + Service + .find( { where: { id: gUser.id } } ) + .then( function( _gUser ) { + + expect( _gUser ).to.be.an( 'array' ).and.have.length( 1 ); + + _gUser = _gUser[0]; + + expect( _gUser ).to.be.an( 'object' ).and.be.ok; + expect( _gUser ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( _gUser ).to.have.property( 'UserId' ).and.equal( user.id ); + + done(); + }, done ) + }, done ); + } ) + .fail( done ); + } else { + console.log( 'UserService is defined' ); + done(); + } + + } ); + + it( 'should return existing user by gUser.UserId if UserService is defined', function ( done ) { + var profile = { + _json: { + id: '121212121212121', + email: 'volodymyrs@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'asasasd', + family_name: 'Denssdfsdfhchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru' + } + }; + + if ( !!UserService ) { + + var spy = sinon.spy( UserService, 'create' ); + + Service + .authenticate( gUser, profile ) + .then( function ( user ) { + + expect( !!UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( user_1.id ); + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + } ) + .fail( done ); + } else { + console.log( 'UserService is defined' ); + done(); + } + + } ); + + it( 'should return error if user with such gUser.UserId do not exist and if UserService is defined' , function ( done ) { + var profile = { + _json: { + id: '121212121212121', + email: 'volodymyrs@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'asasasd', + family_name: 'Denssdfsdfhchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru' + } + }; + + if ( !!UserService ) { + + var spy = sinon.spy( UserService, 'create' ); + + gUser + .updateAttributes( { UserId: 1000 } ) + .success( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'UserId' ).and.equal( 1000 ); + + Service + .authenticate( gUser, profile ) + .then( function ( user ) { + + expect( !!UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( user ).to.have.property( 'message' ).and.be.ok; + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + } ) + .fail( done ); + }) + .error( done ); + } else { + console.log( 'UserService is defined' ); + done(); + } + + } ); + + it( 'should return existing user by gUser.email if user with such email already exist', function ( done ) { + var profile = { + _json: { + id: '121212121212121', + email: 'volodymyrs@clevertech.biz', + verified_email: true, + name: 'Volodymyr Denshchykov', + given_name: 'asasasd', + family_name: 'Denssdfsdfhchykov', + link: 'https://plus.google.com/112064034597570891032', + gender: 'male', + locale: 'ru' + } + }; + + if ( !!UserService ) { + + var spy = sinon.spy( UserService, 'create' ); + + gUser + .updateAttributes( { UserId: null } ) + .success( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'UserId' ).and.equal( null ); + + Service + .authenticate( gUser, profile ) + .then( function ( user ) { + + expect( !!UserService ).to.be.true; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( user_1.id ); + + expect( spy.called ).to.be.false; + + spy.restore(); + + Service + .find( { where: { id: gUser.id } } ) + .then( function( _gUser ) { + + expect( _gUser ).to.be.an( 'array' ).and.have.length( 1 ); + + _gUser = _gUser[0]; + + expect( _gUser ).to.be.an( 'object' ).and.be.ok; + expect( _gUser ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( _gUser ).to.have.property( 'UserId' ).and.equal( user.id ); + + done(); + }, done ) + } ) + .fail( done ); + }) + .error( done ); + } else { + console.log( 'UserService is defined' ); + done(); + } + + } ); + + } ); + + describe( '.updateAccessedDate( user )', function () { + + it( 'should be able to update user', function ( done ) { + + user_1 + .updateAttributes( { accessedAt: null } ) + .success( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( user_1.id ); + expect( result ).to.have.property( 'accessedAt' ).and.not.be.ok; + + Service + .updateAccessedDate( user_1 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( user_1.id ); + expect( result ).to.have.property( 'accessedAt' ).and.be.ok; + + done(); + }, done ); + }) + .error( done ); + } ); + + it( 'should be able to update gUser', function ( done ) { + + gUser + .updateAttributes( { accessedAt: null } ) + .success( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( result ).to.have.property( 'accessedAt' ).and.not.be.ok; + + Service + .updateAccessedDate( gUser ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( result ).to.have.property( 'accessedAt' ).and.be.ok; + + done(); + }, done ); + }) + .error( done ); + } ); + + it( 'should be able to get object if it is not a user', function ( done ) { + + Service + .updateAccessedDate ( { statuscode: 403, message: 'invalid' } ) + .then ( function ( result ) { + + expect ( result ).to.be.an ( 'object' ).and.be.ok; + expect ( result ).to.have.property ( 'statuscode' ).and.equal ( 403 ); + expect ( result ).to.have.property ( 'message' ).and.be.ok; + + done (); + }, done ); + } ); + + } ); + + describe( '.listUsers()', function () { + + it( 'should be able to get list of all gUsers', function ( done ) { + + Service + .listUsers() + .then( function ( users ) { + + expect( users ).to.be.an( 'array' ).and.have.length.above( 1 ); + expect( users[0] ).to.be.an( 'object' ).and.be.ok; + expect( users[0] ).to.have.property( 'id' ).and.be.ok; + expect( users[0] ).to.have.property( 'googleid' ).and.be.ok; + expect( users[0] ).to.have.property( 'email' ).and.be.ok; + expect( users[0] ).to.have.property( 'gender' ).and.be.ok; + expect( users[0] ).to.not.have.property( 'token' ); + + done(); + } ) + .fail( done ); + } ); + + } ); + + describe( '.findUserById()', function () { + + it( 'should be able to get gUser by Id', function ( done ) { + + Service + .findUserById( gUser.id ) + .then( function ( gUser ) { + + expect( gUser ).to.be.an( 'object' ).and.be.ok; + expect( gUser ).to.have.property( 'id' ).and.equal( gUser.id ); + expect( gUser ).to.have.property( 'googleid' ).and.be.ok; + expect( gUser ).to.have.property( 'email' ).and.be.ok; + expect( gUser ).to.have.property( 'gender' ).and.be.ok; + expect( gUser ).to.not.have.property( 'token' ); + + done(); + } ) + .fail( done ); + } ); + + it( 'should be able to get the error if the gUser does not exist', function ( done ) { + + Service + .findUserById( 151515115151515151 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + }, done ) + } ); + + } ); + + describe( '.deleteUser( gUserId )', function () { + + it( 'should be able to get the error if the gUser does not exist', function ( done ) { + + Service + .deleteUser( 151515115151515151 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + }, done ) + } ); + + it( 'should be able to delete gUser', function ( done ) { + + Service + .deleteUser( gUser.id ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 200 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + } ) + .fail( done ); + } ); + + } ); + +} ); \ No newline at end of file diff --git a/modules/auth/config/default.json b/modules/clever-auth/config/default.json similarity index 61% rename from modules/auth/config/default.json rename to modules/clever-auth/config/default.json index 55907c5..a99f475 100644 --- a/modules/auth/config/default.json +++ b/modules/clever-auth/config/default.json @@ -1,5 +1,5 @@ { - "auth": { + "clever-auth": { "secretKey": "youMustChangeMe", "redis": { @@ -7,6 +7,10 @@ "port": "6379", "prefix": "", "key": "" - } + }, + + "hostUrl": "http://localhost:", + + "email_confirmation": false } } \ No newline at end of file diff --git a/modules/clever-auth/controllers/UserController.js b/modules/clever-auth/controllers/UserController.js new file mode 100644 index 0000000..cc4d8ab --- /dev/null +++ b/modules/clever-auth/controllers/UserController.js @@ -0,0 +1,370 @@ +var crypto = require( 'crypto' ) + , moment = require( 'moment' ) + , passport = require( 'passport' ) + , LocalStrategy = require( 'passport-local' ).Strategy; + +module.exports = function ( UserService ) { + + passport.serializeUser( function ( user, done ) { + done( null, user ); + } ); + + passport.deserializeUser( function ( user, done ) { + done( null, user ) + } ); + + passport.use( new LocalStrategy( function ( username, password, done ) { + var credentials = { + email: username, + password: crypto.createHash( 'sha1' ).update( password ).digest( 'hex' ), + confirmed: true, + active: true + }; + + UserService.authenticate( credentials ) + .then( done.bind( null, null ) ) + .fail( done ); + } ) ); + + return (require( 'classes' ).Controller).extend( + { + service: UserService, + + requiresLogin: function ( req, res, next ) { + + if ( !req.isAuthenticated() ) { + return res.send( 401 ); + } + + next(); + }, //tested + + requiresAdminRights: function ( req, res, next ) { + + if ( !req.isAuthenticated() || !req.session.passport.user || !req.session.passport.user.hasAdminRight ) { + return res.send( 403 ); + } + + next(); + }, //tested + + checkPasswordRecoveryData: function ( req, res, next ) { + var userId = req.body.userId + , password = req.body.password + , token = req.body.token + + if ( !userId ) { + return res.send( 400, 'Invalid user Id.' ); + } + + if ( !token ) { + return res.send( 400, 'Invalid Token.' ); + } + + if ( !password ) { + return res.send( 400, 'Password does not much the requirements' ); + } + + next(); + } //tested + }, + { + listAction: function () { + UserService.listUsers() + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, //tested + + getAction: function () { + var uId = this.req.params.id; + + UserService.getUserFullDataJson( { id: uId } ) + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, //tested + + postAction: function () { + var data = this.req.body; + + if ( data.id ) { + return this.putAction(); + } + + if ( !data.email ) { + this.send( 'Email is mandatory', 400 ); + return; + } + + var tplData = { + firstName: data.firstname, + userEmail: data.email, + tplTitle: 'User Confirmation', + subject: data.firstname || data.email + ' wants to add you to their recruiting team!' + }; + + + UserService + .createUser( data, tplData ) + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, //tested without email confirmation + + putAction: function () { + var meId = this.req.user.id + , userId = this.req.params.id + , data = this.req.body; + + if ( !userId ) { + this.send( 'Bad Request', 400 ); + return; + } + + UserService + .handleUpdateUser( userId, data ) + .then( this.proxy( 'handleSessionUpdate', meId ) ) + .fail( this.proxy( 'handleException' ) ); + + }, //tested + + handleSessionUpdate: function ( meId, user ) { + if ( user.id && ( meId === user.id ) ) { + this.loginUserJson ( user ); + return; + } + + this.handleServiceMessage( user ); + }, //tested through putAction + + deleteAction: function () { + var uId = this.req.params.id; + + UserService.deleteUser( uId ) + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + + }, //tested + + loginAction: function () { + passport.authenticate( 'local', this.proxy( 'handleLocalUser' ) )( this.req, this.res, this.next ); + }, //tested + + handleLocalUser: function ( err, user ) { + if ( err ) return this.handleException( err ); + if ( !user ) return this.send( {}, 403 ); + this.loginUserJson( user ); + }, //tested through loginAction + + loginUserJson: function ( user ) { + this.req.login( user, this.proxy( 'handleLoginJson', user ) ); + }, //tested through loginAction, putAction, currentAction + + handleLoginJson: function ( user, err ) { + if ( err ) return this.handleException( err ); + this.send( user, 200 ); + }, //tested through loginAction, putAction + + currentAction: function () { + var user = this.req.user + , reload = this.req.query.reload || false; + + if ( !user ) { + this.send( {}, 404 ); + return; + } + + if ( !reload ) { + this.send( user, 200 ); + return; + } + + UserService + .getUserFullDataJson( { id: user.id } ) + .then( this.proxy( 'loginUserJson' ) ) + .fail( this.proxy( 'handleException' ) ); + + }, //tested + + logoutAction: function () { + this.req.logout(); + this.res.send( {}, 200 ); + }, //tested + + recoverAction: function () { + var email = this.req.body.email; + + if ( !email ) { + this.send( 'missing email', 400 ); + return; + } + + UserService + .find( { where: { email: email } } ) + .then( this.proxy( 'handlePasswordRecovery' ) ) + .fail( this.proxy( 'handleException' ) ); + }, + + handlePasswordRecovery: function ( user ) { + + if ( !user.length ) { + this.send( {}, 403 ); + return; + } + + UserService + .generatePasswordResetHash( user[0] ) + .then( this.proxy( 'handleMailRecoveryToken' ) ) + .fail( this.proxy( 'handleException' ) ); + + }, + + handleMailRecoveryToken: function ( recoverData ) { + + if ( !recoverData.hash && recoverData.statuscode ) { + this.handleServiceMessage( recoverData ); + return; + } + + UserService + .mailPasswordRecoveryToken( recoverData ) + .then( this.proxy( "handleServiceMessage" ) ) + .fail( this.proxy( "handleException" ) ); + }, + + resetAction: function () { + var userId = this.req.body.userId || this.req.body.user, + password = this.req.body.password, + token = this.req.body.token; + + UserService + .findById( userId ) + .then( this.proxy( 'handlePasswordReset', password, token ) ) + .fail( this.proxy( 'handleException' ) ); + + }, + + handlePasswordReset: function ( password, token, user ) { + + if ( !user ) { + this.send( {}, 403 ); + return; + } + + UserService + .generatePasswordResetHash( user ) + .then( this.proxy( 'verifyResetTokenValidity', user, password, token ) ) + .fail( this.proxy( 'handleException' ) ); + + }, + + verifyResetTokenValidity: function ( user, newPassword, token, resetData ) { + + if ( !resetData.hash && resetData.statuscode ) { + this.handleServiceMessage( resetData ); + return; + } + + var hash = resetData.hash; + if ( token != hash ) { + return this.send( 'Invalid token', 400 ); + } + + this.handleUpdatePassword( newPassword, [user] ); + + }, + + handleUpdatePassword: function ( newPassword, user ) { + + if ( user.length ) { + user = user[0]; + user.updateAttributes( { + password: crypto.createHash( 'sha1' ).update( newPassword ).digest( 'hex' ) + } ).success( function ( user ) { + this.send( {status: 200, results: user} ); + }.bind( this ) + ).fail( this.proxy( 'handleException' ) ); + } else { + this.send( {status: 400, error: "Incorrect old password!"} ); + } + }, + + confirmAction: function () { + var password = this.req.body.password + , token = this.req.body.token + , userId = this.req.body.userId; + + + UserService.findById( userId ) + .then( this.proxy( 'handleAccountConfirmation', password, token ) ) + .fail( this.proxy( 'handleException' ) ); + + }, + + handleAccountConfirmation: function ( pass, token, user ) { + if ( !user ) { + this.send( 'Invalid confirmation link', 403 ); + return; + } + if ( user.confirmed ) { + this.send( 'You have already activated your account', 400 ); + return; + } + + UserService.generatePasswordResetHash( user ) + .then( this.proxy( "confirmAccount", user, pass, token ) ) + .fail( this.proxy( "handleException" ) ); + + }, + + confirmAccount: function ( user, pass, token, hashobj ) { + + if ( !hashobj.hash && hashobj.statuscode ) { + this.handleServiceMessage( hashobj ); + return; + } + + if ( token !== hashobj.hash ) { + this.send( 'Invalid token', 400 ); + return; + } + + var newpass = crypto.createHash( 'sha1' ).update( pass ).digest( 'hex' ); + + user + .updateAttributes( { + active: true, + confirmed: true, + password: newpass + } ) + .success( this.proxy( 'send', 200 ) ) + .error( this.proxy( 'handleException' ) ); + + }, + + resendAction: function () { + var me = this.req.user + , userId = this.req.params.id + , data = this.req.body; + + var tplData = { + firstName: this.req.user.firstname, accountSubdomain: this.req.user.account.subdomain, userFirstName: '', userEmail: '', tplTitle: 'Account Confirmation', subject: this.req.user.firstname + ' wants to add you to their recruiting team!' + }; + + UserService + .resendAccountConfirmation( me.account.id, userId, tplData ) + .then( this.proxy( 'handleServiceMessage' ) ) + .then( this.proxy( 'handleException' ) ); + + }, + + handleServiceMessage: function ( obj ) { + + if ( obj.statuscode ) { + this.send( obj.message, obj.statuscode ); + return; + } + + this.send( obj, 200 ); + } + + } ); +}; \ No newline at end of file diff --git a/modules/clever-auth/models/orm/UserModel.js b/modules/clever-auth/models/orm/UserModel.js new file mode 100644 index 0000000..8462373 --- /dev/null +++ b/modules/clever-auth/models/orm/UserModel.js @@ -0,0 +1,77 @@ +module.exports = function ( sequelize, DataTypes ) { + return sequelize.define( "User", + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + title: { + type: DataTypes.STRING, + allowNull: true + }, + username: { + type: DataTypes.STRING, + allowNull: false, + unique: true + }, + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + isEmail: true + } + }, + password: { + type: DataTypes.STRING, + allowNull: false + }, + firstname: { + type: DataTypes.STRING, + allowNull: true + }, + lastname: { + type: DataTypes.STRING, + allowNull: true + }, + phone: { + type: DataTypes.STRING, + allowNull: true + }, + confirmed: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + }, + active: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true + }, + hasAdminRight: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + }, + accessedAt: { + type: DataTypes.DATE + } + }, + { + paranoid: true, + getterMethods: { + fullName: function () { + return [ this.getDataValue( 'firstname' ), this.getDataValue( 'lastname' )].join( ' ' ); + } + }, + instanceMethods: { + toJSON: function () { + var values = this.values; + values.fullName = this.fullName; + delete values.password; + return values; + } + } + } ); +}; \ No newline at end of file diff --git a/modules/clever-auth/module.js b/modules/clever-auth/module.js new file mode 100644 index 0000000..33955bf --- /dev/null +++ b/modules/clever-auth/module.js @@ -0,0 +1,128 @@ +var passport = require( 'passport' ) + , debug = require( 'debug' )( 'AuthModule' ) + , fs = require( 'fs' ) + , path = require( 'path' ) + , ModuleClass = require( 'classes' ).ModuleClass + , RedisStore = require( 'connect-redis' )( injector.getInstance( 'express' ) ) + , Module; + +Module = ModuleClass.extend( { + models: null, + store: null, + sequelize: null, + + preSetup: function() { + var self = this; + this.models = {}; + this.sequelize = injector.getInstance( 'sequelize' ); + var dir = fs.readdirSync( path.join( __dirname, 'models', 'orm' ) ); + dir.forEach( function ( model ) { + self.getModel( path.join( __dirname, 'models', 'orm', model ) ); + } ); + }, + + preResources: function () { + injector.instance( 'passport', passport ); + }, + + modulesLoaded: function() { + this.defineModelsAssociations(); + }, + + defineModelsAssociations: function() { + if (!this.config.hasOwnProperty( 'modelAssociations' )) { + return true; + } + + debug( 'Defining model assocations' ); + + Object.keys( this.config.modelAssociations ).forEach( this.proxy( function( modelName ) { + Object.keys( this.config.modelAssociations[ modelName ] ).forEach( this.proxy( 'defineModelAssociations', modelName ) ); + })); + }, + + defineModelAssociations: function( modelName, assocType ) { + var associatedWith = this.config.modelAssociations[ modelName ][ assocType ]; + if ( ! associatedWith instanceof Array ) { + associatedWith = [ associatedWith ]; + } + + associatedWith.forEach( this.proxy( 'associateModels', modelName, assocType ) ); + }, + + associateModels: function( modelName, assocType, assocTo ) { + // Support second argument + if ( assocTo instanceof Array ) { + debug( '%s %s %s with second argument of ', modelName, assocType, assocTo[0], assocTo[1] ); + this.models[ modelName ][ assocType ]( this.models[ assocTo[0] ], assocTo[1] ); + } else { + debug( '%s %s %s', modelName, assocType, assocTo ); + this.models[ modelName ][ assocType ]( this.models[assocTo] ); + } + }, + + getModel: function( modelPath ) { + var modelName = modelPath.split( '/' ).pop().split( '.' ).shift(); + + if ( typeof this.models[ modelName ] === 'undefined' ) { + debug( [ 'Loading model', modelName, 'from', modelPath ].join( ' ' ) ); + + // Call on sequelizejs to load the model + this.models[ modelName ] = this.sequelize.import( modelPath ); + + // Set a flat for tracking + this.models[ modelName ].ORM = true; + + // Add the model to the injector + injector.instance( 'ORM' + modelName, this.models[ modelName ] ); + } + + return this.models[ modelName ]; + }, + + configureApp: function ( app, express ) { + // Setup the redis store + this.store = new RedisStore( { + host: this.config.redis.host, + port: this.config.redis.port, + prefix: this.config.redis.prefix + process.env.NODE_ENV + "_", + password: this.config.redis.key + } ); + + // Session management + app.use( express.cookieParser() ); + app.use( express.session( { + secret: this.config.secretKey, + cookie: { secure: false, maxAge: 86400000 }, + store: this.store + } ) ); + + // Enable CORS + app.use( this.proxy( 'enableCors' ) ); + + // Initialize passport + app.use( passport.initialize() ); + app.use( passport.session() ); + }, + + enableCors: function ( req, res, next ) { + res.header( "Access-Control-Allow-Origin", req.headers.origin ); + res.header( "Access-Control-Allow-Headers", "x-requested-with, content-type" ); + res.header( "Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS" ); + res.header( "Access-Control-Allow-Credentials", "true" ); + res.header( "Access-Control-Max-Age", "1000000000" ); + // intercept OPTIONS method + if ( 'OPTIONS' == req.method ) { + res.send( 200 ); + } + else { + next(); + } + }, + + preShutdown: function () { + this.store.client.quit(); + } +} ); + +module.exports = new Module( 'clever-auth', injector ); \ No newline at end of file diff --git a/modules/clever-auth/package.json b/modules/clever-auth/package.json new file mode 100644 index 0000000..62eedd2 --- /dev/null +++ b/modules/clever-auth/package.json @@ -0,0 +1,37 @@ +{ + "name": "clever-auth", + "version": "0.0.1", + "private": true, + "dependencies": { + "passport": "~0.1.18", + "passport-facebook": "~1.0.2", + "passport-local": "~0.1.6", + "connect-redis": "~1.4.6", + "connect": "~2.12.0", + "redis": "~0.10.0", + "q": "", + "lodash": "2.4.1", + "sequelize": "1.7.0-beta8", + "shortid": "~2.0.0", + "moment": "", + "async": "" + }, + "author": { + "name": "Clevertech", + "email": "info@clevertech.biz", + "web": "http://www.clevertech.biz" + }, + "collaborators": [ + "Richard Gustin " + ], + "description": "Clevertech Authentication Module", + "keywords": [ + "" + ], + "main": "module.js", + "devDependencies": { + "chai": "*", + "mocha": "*", + "sinon": "" + } +} \ No newline at end of file diff --git a/modules/clever-auth/routes.js b/modules/clever-auth/routes.js new file mode 100644 index 0000000..d19ec56 --- /dev/null +++ b/modules/clever-auth/routes.js @@ -0,0 +1,47 @@ +module.exports = function ( + app, +// AccountController, + UserController ) +{ + + app.get('/users', UserController.requiresLogin, UserController.attach()); + app.all('/users/?:id?', UserController.requiresLogin, UserController.attach()); + + app.all('/user/?:action?', UserController.attach()); + + +// app.post('/users/confirm', UserController.attach()); +// app.get('/users', UserController.attach()); +// app.post('/users', UserController.attach()); +// app.get('/users/:id', UserController.attach()); +// app.put('/users/:id', UserController.attach()); +// app.post('/users/:id', UserController.attach()); + +// app.post('/users/confirm', UserController.checkPasswordRecoveryData, UserController.attach()); +// app.get('/users', UserController.requiresLogin, UserController.attach()); +// app.post('/users', UserController.requiresLogin, UserController.attach()); +// app.get('/users/:id', UserController.requiresLogin, UserController.isUserInTheSameAccount, UserController.attach()); +// app.put('/users/:id', UserController.requiresLogin, UserController.isUserInTheSameAccount, UserController.attach()); +// app.post('/users/:id', UserController.requiresLogin, UserController.isUserInTheSameAccount, UserController.attach()); + + + + +// app.post('/users/:id/resend', UserController.requiresAdminRights, UserController.attach()); +// app.get('/user/current', UserController.attach()); +// app.get('/user/logout', UserController.requiresLogin, UserController.attach()); +// app.post('/user/login', UserController.attach()); +// app.post('/user/recover', UserController.attach()); +// app.post('/user/reset', UserController.checkPasswordRecoveryData, UserController.attach()); +// +// app['delete']('/users/:id', UserController.requiresLogin, UserController.isUserInTheSameAccount, UserController.attach()); +// app.all('/user/:action/:id?', UserController.requiresLogin, UserController.attach()); +// app.all('/user/?:action?', UserController.requiresLogin, UserController.attach()); + +// app.get('/account', UserController.requiresLogin, AccountController.attach()); +// app.put('/account', UserController.requiresLogin, AccountController.formatData, AccountController.attach()); +// app.post('/account', AccountController.isValidEmailDomain, AccountController.requiresUniqueSubdomain, AccountController.requiresUniqueUser, AccountController.attach()); +// app.post('/account/confirm', AccountController.attach()); +// app.post('/account/resend', AccountController.attach()); + +}; \ No newline at end of file diff --git a/modules/clever-auth/schema/seedData.json b/modules/clever-auth/schema/seedData.json new file mode 100644 index 0000000..b990861 --- /dev/null +++ b/modules/clever-auth/schema/seedData.json @@ -0,0 +1,15 @@ +{ + "UserModel": [ + { + "username": "test", + "email": "test@clevertech.biz", + "password": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", + "firstname": "Test", + "lastname": "Account", + "phone": "+15551234", + "active": true, + "confirmed": true, + "hasAdminRight": true + } + ] +} \ No newline at end of file diff --git a/modules/clever-auth/services/UserService.js b/modules/clever-auth/services/UserService.js new file mode 100644 index 0000000..3648a23 --- /dev/null +++ b/modules/clever-auth/services/UserService.js @@ -0,0 +1,411 @@ +var Q = require( 'q' ) + , crypto = require( 'crypto' ) + , moment = require( 'moment' ) + , Sequelize = require( 'sequelize' ) + , config = require( 'config' ) + , UserService = null; + +module.exports = function ( sequelize, + ORMUserModel ) { + + if ( UserService && UserService.instance ) { + return UserService.instance; + } + + var EmailService = null; + + UserService = require( 'services' ).BaseService.extend( { + + authenticate: function ( credentials ) { + var deferred = Q.defer() + , service = this + , chainer = new Sequelize.Utils.QueryChainer(); + + ORMUserModel + .find( { where: credentials } ) + .success( function ( user ) { + + if ( !user || !user.active ) { + return deferred.resolve(); + } + + chainer.add( user.updateAttributes( { accessedAt: moment.utc().format( 'YYYY-MM-DD HH:ss:mm' ) } ) ); + + chainer + .runSerially() + .success( function ( result ) { + + var userJson = ( result[0] ) ? JSON.parse( JSON.stringify( result[0] ) ) : {}; + + deferred.resolve( userJson ); + } ) + .error( deferred.reject ); + } ) + .error( deferred.reject ); + + return deferred.promise; + }, //tested + + getUserFullDataJson: function ( options ) { + var deferred = Q.defer() + , service = this; + + ORMUserModel + .find( { where: options } ) + .success( function ( user ) { + + if ( !user ) { + return deferred.resolve( {} ); + } + + var userJson = JSON.parse( JSON.stringify( user ) ); + + deferred.resolve( userJson ); + } ) + .error( deferred.reject ); + + return deferred.promise; + }, //tested + + generatePasswordResetHash: function ( user, tplData ) { + var deferred = Q.defer() + , md5 = null + , hash = null + , expTime = null + , actionpath = ( !user.confirmed ) ? 'user/confirm' : 'password_reset_submit' + , mailsubject = ( !user.confirmed ) ? 'User Confirmation' : 'Password Recovery'; + + if ( !user || !user.createdAt || !user.updatedAt || !user.password || !user.email ) { + deferred.resolve( { statuscode: 403, message: 'Unauthorized' } ); + } else { + + md5 = crypto.createHash( 'md5' ); + md5.update( user.createdAt + user.updatedAt + user.password + user.email + 'recover', 'utf8' ); + hash = md5.digest( 'hex' ); + + expTime = moment.utc().add( 'hours', 8 ).valueOf(); + + deferred.resolve( { + hash: hash, + expTime: expTime, + user: user, + action: actionpath, + mailsubject: mailsubject, + tplData: tplData || null + } ); + } + return deferred.promise; + }, //tested + + mailPasswordRecoveryToken: function ( obj ) { + +// var hosturl = !!config.hosturl +// ? config.hosturl +// : [ config['clever-auth'].hostUrl, config.webPort ].join(''); +// +// var link = hosturl + '/' + obj.action + '?u=' + obj.user.id + '&t=' + obj.hash + '&n=' + encodeURIComponent( obj.user.fullName ); + + + // var payload = { to: obj.user.email, from: 'no-reply@CleverTech.biz' }; + + // payload.text = (obj.action === 'account_confirm') + // ? "Please click on the link below to activate your account\n " + link + // : "Please click on the link below to enter a new password\n " + link; + + // var info = { link: link, companyLogo: 'http://app.CleverTech.biz/images/logo.png', companyName: 'CleverTech' }; + + // info.tplName = (obj.action === 'account_confirm') + // ? 'userNew' + // : 'passwordRecovery'; + + // if ( !obj.tplData ) { + // payload.subject = 'CleverTech: ' + obj.mailsubject; + + // info.firstname = obj.user.firstname; + // info.username = obj.user.username; + // info.user = obj.user; + // info.tplTitle = 'CleverTech: Password Recovery'; + + // } else { + // payload.subject = obj.tplData.subject; + + // info.tplTitle = obj.tplData.tplTitle; + // info.firstName = obj.tplData.firstName; + // info.accountSubdomain = obj.tplData.accountSubdomain; + // info.userFirstName = obj.tplData.userFirstName; + // info.userEmail = obj.tplData.userEmail; + // } + + return Q.resolve( 'Init Promise Chaining' ) + .then( function () { + return { statuscode: 200, message: 'Message successfully sent' }; + } ) + // .then( function() { + // return bakeTemplate( info ); + // } ) + // .then( function ( html ) { + // payload.html = html; + + // return mailer( payload ); + // } ) + // .then( function () { + // return { statuscode: 200, message: 'Message successfully sent' }; + // } ) + .fail( function ( err ) { + console.log( "\n\nERRR: ", err ); + return { statuscode: 500, message: err }; + } ); + }, + + createUser: function ( data, tplData ) { + var deferred = Q.defer() + , service = this + , usr; + + ORMUserModel + .find( { where: { email: data.email } } ) + .success( function ( user ) { + + if ( user !== null ) { + deferred.resolve( { statuscode: 400, message: 'Email already exist' } ); + return; + } + + try { + EmailService = require( 'services' )['EmailService']; + } catch ( err ) { + console.log( err ); + } + + if ( EmailService === null || !config['clever-auth'].email_confirmation ) { + + data.confirmed = true; + + service + .saveNewUser( data ) + .then( deferred.resolve ) + .fail( deferred.reject ); + + } else { + + data.confirmed = false; + + service + .saveNewUser( data ) + .then( function ( user ) { + usr = user; + return service.generatePasswordResetHash( user, tplData ); + } ) + .then( service.mailPasswordRecoveryToken ) + .then( function () { + deferred.resolve( usr ); + } ) + .fail( function ( err ) { + console.log( err ); + deferred.reject(); + } ); + } + } ) + .error( deferred.reject ); + + return deferred.promise; + }, //tested + + saveNewUser: function ( data ) { + var deferred = Q.defer(); + + data.username = data.username || data.email; + data.active = true; + data.password = data.password + ? crypto.createHash( 'sha1' ).update( data.password ).digest( 'hex' ) + : Math.random().toString( 36 ).slice( -14 ); + + ORMUserModel + .create( data ) + .success( deferred.resolve ) + .error( deferred.reject ); + + return deferred.promise; + }, //tested + + resendAccountConfirmation: function ( userId, tplData ) { + var deferred = Q.defer() + , service = this; + + ORMUserModel + .find( userId ) + .success( function ( user ) { + + if ( !user ) { + deferred.resolve( { statuscode: 403, message: 'User doesn\'t exist' } ); + return; + } + + if ( user.confirmed ) { + deferred.resolve( { statuscode: 403, message: user.email + ' , has already confirmed the account' } ); + return; + } + + tplData.userFirstName = user.firstname; + + tplData.userEmail = user.email; + + service.generatePasswordResetHash( user, tplData ) + .then( service.mailPasswordRecoveryToken ) + .then( function () { + deferred.resolve( { statuscode: 200, message: 'A confirmation link has been resent' } ); + } ) + .fail( deferred.reject ); + + } ) + .error( deferred.resolve ); + + return deferred.promise; + }, + + handleUpdateUser: function ( userId, data ) { + var deferred = Q.defer(); + + ORMUserModel + .find( { where: { id: userId } } ) + .success( function ( user ) { + + if ( !user ) { + deferred.resolve( { statuscode: 403, message: 'invalid id' } ); + return; + } + + if ( data.password && data.new_password ) { + + if ( crypto.createHash( 'sha1' ).update( data.password ).digest( 'hex' ) !== user.password ) { + deferred.resolve( {statuscode: 403, message: 'Invalid password'} ); + return; + } + + data.hashedPassword = crypto.createHash( 'sha1' ).update( data.new_password ).digest( 'hex' ); + } + + this + .checkEmailAndUpdate( user, data ) + .then( deferred.resolve ) + .fail( deferred.reject ); + + }.bind( this ) ) + .error( deferred.reject ); + + return deferred.promise; + }, //tested + + checkEmailAndUpdate: function ( user, data ) { + var deferred = Q.defer(); + + if ( data.email && ( user.email != data.email ) ) { + + ORMUserModel + .find( { where: { email: data.email } } ) + .success( function ( chkUser ) { + + if ( chkUser ) { + deferred.resolve( { statuscode: 400, message: "email already exists" } ); + return; + } + + this + .updateUser( user, data ) + .then( deferred.resolve ) + .fail( deferred.reject ); + + }.bind( this ) ) + .error( deferred.reject ); + } else { + + this + .updateUser( user, data ) + .then( deferred.resolve ) + .fail( deferred.reject ); + } + + return deferred.promise; + }, //tested + + updateUser: function ( user, data ) { + var deferred = Q.defer(); + + user.firstname = data.firstname || user.firstname; + user.lastname = data.lastname || user.lastname; + user.email = data.email || user.email; + user.phone = data.phone || user.phone; + + if ( data.hashedPassword ) { + user.password = data.hashedPassword; + } + + user.save() + .success( function ( user ) { + + this + .getUserFullDataJson( { id: user.id } ) + .then( deferred.resolve ) + .fail( deferred.reject ); + + }.bind( this ) ) + .error( deferred.reject ); + + return deferred.promise; + }, //tested + + listUsers: function() { + var deferred = Q.defer(); + + ORMUserModel + .findAll( { where: { deletedAt: null } } ) + .success( function( users ) { + if ( !!users && !!users.length ) { + deferred.resolve( users.map( function( u ) { return u.toJSON(); } ) ); + } else { + deferred.resolve( {} ); + } + }) + .error( deferred.reject ); + + return deferred.promise; + }, //tested + + deleteUser: function( userId ) { + var deferred = Q.defer(); + + ORMUserModel + .find( userId ) + .success( function( user ) { + + if ( !!user && !!user.id ) { + + user + .destroy() + .success( function( result ) { + + if ( !result.deletedAt ) { + deferred.resolve( { statuscode: 500, message: 'error' } ); + } else { + deferred.resolve( { statuscode: 200, message: 'user is deleted' } ); + } + + }) + .error( deferred.reject ); + + } else { + deferred.resolve( { statuscode: 403, message: 'user do not exist' } ) + } + }) + .error( deferred.reject ); + + return deferred.promise; + } + + } ); + + UserService.instance = new UserService( sequelize ); + UserService.Model = ORMUserModel; + + return UserService.instance; +}; \ No newline at end of file diff --git a/modules/clever-auth/tests/unit/test.controllers.UserController.js b/modules/clever-auth/tests/unit/test.controllers.UserController.js new file mode 100644 index 0000000..d70e0a7 --- /dev/null +++ b/modules/clever-auth/tests/unit/test.controllers.UserController.js @@ -0,0 +1,1657 @@ +// Bootstrap the testing environmen +var testEnv = require( 'utils' ).testEnv(); + +var expect = require( 'chai' ).expect + , Q = require ( 'q' ) + , sinon = require( 'sinon' ) + , Service; + +var new_user; + +describe( 'controllers.UserController', function () { + var Service, UserController, ctrl, users = []; + + before( function ( done ) { + testEnv( function ( _UserService_, _UserController_ ) { + var req = { + params: { action: 'fakeAction'}, + method: 'GET', + query: {} + }; + + var res = { + json: function () {} + }; + + var next = function () {}; + + UserController = _UserController_; + Service = _UserService_; + ctrl = new UserController( req, res, next ); + + Service.create( { + firstName: 'Joeqwer', + username: 'joe@example.com', + email: 'joe@example.com', + password: '7110eda4d09e062aa5e4a390b0a572ac0d2c0220' + } ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.be.ok; + + users.push( user ); + + return Service.create( { + firstName: 'Racheller', + username: 'rachel@example.com', + email: 'rachel@example.com', + password: '7110eda4d09e062aa5e4a390b0a572ac0d2c0220' + } ); + } ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.be.ok; + + users.push( user ); + + done(); + } ) + .fail( done ); + } ); + } ); + + afterEach( function ( done ) { + + ctrl.req = { + params: { action: 'fakeAction'}, + method: 'GET', + query: {}, + body: {} + }; + + ctrl.res = { + json: function () {} + }; + + done(); + }); + + describe( 'static members', function () { + + describe( ' requiresLogin( req, res, next )', function () { + + it( 'should call next if req.isAuthenticated() returns true', function ( done ) { + var req = { + isAuthenticated: function () { return true; } + } + , res = {} + , next = sinon.spy(); + + UserController.requiresLogin( req, res, next ); + + expect( req.isAuthenticated() ).to.be.true; + + expect( next.called ).to.be.true; + + done(); + } ); + + it( 'should send 401 if req.isAuthenticated() returns false', function ( done ) { + var req = { + isAuthenticated: function () { return false; } + } + , res = { + send: sinon.spy() + } + , next = function () {}; + + UserController.requiresLogin( req, res, next ); + + expect( req.isAuthenticated() ).to.be.false; + + expect( res.send.called ).to.be.true; + expect( res.send.calledWith( 401 ) ).to.be.true; + + done(); + } ); + + } ); + + describe( ' requiresAdminRights( req, res, next )', function () { + + it( 'should call next if req.isAuthenticated() returns true and hasAdminRight is true', function ( done ) { + var req = { + isAuthenticated: function () { return true; }, + session: { + passport: { + user: { + hasAdminRight: true + } + } + } + } + , res = {} + , next = sinon.spy(); + + UserController.requiresAdminRights( req, res, next ); + + expect( req.isAuthenticated() ).to.be.true; + expect( req.session.passport.user.hasAdminRight ).to.be.true; + + expect( next.called ).to.be.true; + + done(); + } ); + + it( 'should send 403 if req.isAuthenticated() returns false and hasAdminRight is true', function ( done ) { + var req = { + isAuthenticated: function () { return false; }, + session: { + passport: { + user: { + hasAdminRight: true + } + } + } + } + , res = { + send: sinon.spy() + } + , next = function () {}; + + UserController.requiresAdminRights( req, res, next ); + + expect( req.isAuthenticated() ).to.be.false; + expect( req.session.passport.user.hasAdminRight ).to.be.true; + + expect( res.send.called ).to.be.true; + expect( res.send.calledWith( 403 ) ).to.be.true; + + done(); + } ); + + it( 'should send 403 if req.isAuthenticated() returns true and hasAdminRight is false', function ( done ) { + var req = { + isAuthenticated: function () { return true; }, + session: { + passport: { + user: { + hasAdminRight: false + } + } + } + } + , res = { + send: sinon.spy() + } + , next = function () {}; + + UserController.requiresAdminRights( req, res, next ); + + expect( req.isAuthenticated() ).to.be.true; + expect( req.session.passport.user.hasAdminRight ).to.be.false; + + expect( res.send.called ).to.be.true; + expect( res.send.calledWith( 403 ) ).to.be.true; + + done(); + } ); + + it( 'should send 403 if req.isAuthenticated() returns false and hasAdminRight is false', function ( done ) { + var req = { + isAuthenticated: function () { return false; }, + session: { + passport: { + user: { + hasAdminRight: false + } + } + } + } + , res = { + send: sinon.spy() + } + , next = function () {}; + + UserController.requiresAdminRights( req, res, next ); + + expect( req.isAuthenticated() ).to.be.false; + expect( req.session.passport.user.hasAdminRight ).to.be.false; + + expect( res.send.called ).to.be.true; + expect( res.send.calledWith( 403 ) ).to.be.true; + + done(); + } ); + + } ); + + describe( ' checkPasswordRecoveryData( req, res, next )', function () { + + it( 'should call next if is right', function ( done ) { + var req = { + body: { + userId: 1, + password: 'asasasasa', + token: '15151saAS5A1S51A51S' + } + } + , res = {} + , next = sinon.spy(); + + UserController.checkPasswordRecoveryData( req, res, next ); + + expect( req.body.userId ).to.be.ok; + expect( req.body.password ).to.be.ok; + expect( req.body.token ).to.be.ok; + + expect( next.called ).to.be.true; + + done(); + } ); + + it( 'should send 400 if insufficiently UserId', function ( done ) { + var req = { + body: { + password: 'asasasasa', + token: '15151saAS5A1S51A51S' + } + } + , res = { + send: sinon.spy() + } + , next = function () {}; + + UserController.checkPasswordRecoveryData( req, res, next ); + + expect( req.body.userId ).to.not.be.ok; + expect( req.body.password ).to.be.ok; + expect( req.body.token ).to.be.ok; + + expect( res.send.called ).to.be.true; + expect( res.send.calledWith( 400, 'Invalid user Id.' ) ).to.be.true; + + done(); + } ); + + it( 'should send 400 if insufficiently password', function ( done ) { + var req = { + body: { + userId: 1, + token: '15151saAS5A1S51A51S' + } + } + , res = { + send: sinon.spy() + } + , next = function () {}; + + UserController.checkPasswordRecoveryData( req, res, next ); + + expect( req.body.userId ).to.be.ok; + expect( req.body.password ).to.not.be.ok; + expect( req.body.token ).to.be.ok; + + expect( res.send.called ).to.be.true; + expect( res.send.calledWith( 400, 'Password does not much the requirements' ) ).to.be.true; + + done(); + } ); + + it( 'should send 400 if insufficiently token', function ( done ) { + var req = { + body: { + userId: 1, + password: '151515' + } + } + , res = { + send: sinon.spy() + } + , next = function () {}; + + UserController.checkPasswordRecoveryData( req, res, next ); + + expect( req.body.userId ).to.be.ok; + expect( req.body.password ).to.be.ok; + expect( req.body.token ).to.not.be.ok; + + expect( res.send.called ).to.be.true; + expect( res.send.calledWith( 400, 'Invalid Token.' ) ).to.be.true; + + done(); + } ); + + } ); + + } ); + + describe( 'postAction()', function () { + + it( 'should hash password and save user', function ( done ) { + var data = { + username: 'admin', + email: 'admin@example.com', + password: 'secret_password' + }; + + ctrl.send = function ( user, status ) { + + expect( status ).to.equal( 200 ); + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.be.ok; + + Service.find( { where: { email: 'admin@example.com' } } ) + .then( function ( users ) { + + expect( users ).to.be.an( 'array' ).and.have.length( 1 ); + + user = users[0]; + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.be.ok; + expect( user ).to.have.property( 'username' ).and.equal( data.username ); + expect( user ).to.have.property( 'email' ).and.equal( data.email ); + expect( user ).to.have.property( 'password' ).and.equal( '2394a9661a9089208c1c9c65ccac85a91da6a859' ); + + done(); + } ) + .fail( done ); + }; + + ctrl.req.body = data; + + ctrl.postAction(); + } ); + + it( 'should be able to get the error if user with such email already exist', function ( done ) { + var data = { + username: 'admin', + email: users[0].email, + password: 'secret_password' + }; + + ctrl.send = function ( result, status ) { + + expect ( status ).to.equal ( 400 ); + + expect ( result ).to.be.an ( 'string' ).and.be.ok; + + done (); + }; + + ctrl.req.body = data; + + ctrl.postAction (); + + } ); + + it( 'should be able to get the error if insufficiently email', function ( done ) { + var data = { + username: 'admin', + password: 'secret_password' + }; + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 400 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.body = data; + + ctrl.postAction(); + + } ); + + //TODO - email confirmation + + } ); + + describe( 'putAction()', function () { + + before( function( done ) { + + Service.createUser( { + firstName: 'cdxsasdf', + username: 'xcxcxc@example.com', + email: 'sasasas@example.com', + password: 'secret_password' + }, {} ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.be.ok; + + new_user = user; + + done(); + }) + .fail( done ); + }); + + it( 'should call UserService.handleUpdateUser( userId, data ) if the data is complete', function ( done ) { + var data = { + firstname: 'petrushka' + }; + + var spy = sinon.spy( Service, 'handleUpdateUser' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( new_user.id ); + expect( result ).to.have.property( 'firstname' ).and.equal( data.firstname ); + + expect( spy.called ).to.be.true; + expect( spy.calledOnce ).to.be.true; + + var spyCall = spy.getCall ( 0 ).args; + + expect( spyCall ).to.be.an( 'array' ); + + expect( spyCall[0] ).to.be.an( 'number' ).and.equal( new_user.id ); + + expect( spyCall[1] ).to.be.an( 'object' ).and.be.ok; + expect( spyCall[1] ).to.have.property( 'firstname' ).and.equal( data.firstname ); + + spy.restore(); + + done(); + }; + + ctrl.req.user = { id: new_user.id }; + ctrl.req.params = { id: new_user.id }; + ctrl.req.body = data; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.putAction(); + } ); + + it( 'should not call UserService.handleUpdateUser if insufficiently userId', function ( done ) { + var data = { + firstname: 'petrushka' + }; + + var spy = sinon.spy( Service, 'handleUpdateUser' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 400 ); + + expect( result ).to.be.an( 'string' ).and.equal( 'Bad Request' ); + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + }; + + ctrl.req.user = { id: new_user.id }; + ctrl.req.params = { id: null }; + ctrl.req.body = data; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.putAction(); + } ); + + it( 'should call UserService.checkEmailAndUpdate( user, data ) if the data is complete', function ( done ) { + var data = { + firstname: 'petrush' + }; + + var spy = sinon.spy( Service, 'checkEmailAndUpdate' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( new_user.id ); + expect( result ).to.have.property( 'firstname' ).and.equal( data.firstname ); + + expect( spy.called ).to.be.true; + expect( spy.calledOnce ).to.be.true; + + var spyCall = spy.getCall ( 0 ).args; + + expect( spyCall ).to.be.an( 'array' ); + + expect( spyCall[0] ).to.be.an( 'object' ).and.be.ok; + expect( spyCall[0] ).to.have.property( 'id' ).and.equal( new_user.id ); + + expect( spyCall[1] ).to.be.an( 'object' ).and.be.ok; + expect( spyCall[1] ).to.have.property( 'firstname' ).and.equal( data.firstname ); + + spy.restore(); + + done(); + }; + + ctrl.req.user = { id: new_user.id }; + ctrl.req.params = { id: new_user.id }; + ctrl.req.body = data; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.putAction(); + } ); + + it( 'should not call UserService.checkEmailAndUpdate if user with such id do not exist', function ( done ) { + var data = { + firstname: 'petrushka' + }; + + var spy = sinon.spy( Service, 'checkEmailAndUpdate' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.equal( 'invalid id' ); + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + }; + + ctrl.req.user = { id: new_user.id }; + ctrl.req.params = { id: new_user.id + 10000000 }; + ctrl.req.body = data; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.putAction(); + } ); + + it( 'should call UserService.checkEmailAndUpdate( user, data ) if old password correct', function ( done ) { + var data = { + password: 'secret_password', + new_password: 'secret_password_new' + }; + + var spy = sinon.spy( Service, 'checkEmailAndUpdate' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( new_user.id ); + + expect( spy.called ).to.be.true; + expect( spy.calledOnce ).to.be.true; + + var spyCall = spy.getCall ( 0 ).args; + + expect( spyCall ).to.be.an( 'array' ); + + expect( spyCall[0] ).to.be.an( 'object' ).and.be.ok; + expect( spyCall[0] ).to.have.property( 'id' ).and.equal( new_user.id ); + + expect( spyCall[1] ).to.be.an( 'object' ).and.be.ok; + expect( spyCall[1] ).to.have.property( 'password' ).and.equal( data.password ); + expect( spyCall[1] ).to.have.property( 'new_password' ).and.equal( data.new_password ); + + spy.restore(); + + done(); + }; + + ctrl.req.user = { id: new_user.id }; + ctrl.req.params = { id: new_user.id }; + ctrl.req.body = data; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.putAction(); + } ); + + it( 'should not call UserService.checkEmailAndUpdate if old password incorrect', function ( done ) { + var data = { + password: 'secret_password', + new_password: 'secret_password_new' + }; + + var spy = sinon.spy( Service, 'checkEmailAndUpdate' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.equal( 'Invalid password' ); + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + }; + + ctrl.req.user = { id: new_user.id }; + ctrl.req.params = { id: new_user.id }; + ctrl.req.body = data; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.putAction(); + } ); + + it( 'should call UserService.updateUser( user, data ) if the email does not change', function ( done ) { + var data = { + password: 'secret_password_new', + new_password: 'secret_password' + }; + + var spy = sinon.spy( Service, 'updateUser' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( new_user.id ); + + expect( spy.called ).to.be.true; + expect( spy.calledOnce ).to.be.true; + + var spyCall = spy.getCall ( 0 ).args; + + expect( spyCall ).to.be.an( 'array' ); + + expect( spyCall[0] ).to.be.an( 'object' ).and.be.ok; + expect( spyCall[0] ).to.have.property( 'id' ).and.equal( new_user.id ); + + expect( spyCall[1] ).to.be.an( 'object' ).and.be.ok; + expect( spyCall[1] ).to.have.property( 'password' ).and.equal( data.password ); + expect( spyCall[1] ).to.have.property( 'new_password' ).and.equal( data.new_password ); + + spy.restore(); + + done(); + }; + + ctrl.req.user = { id: new_user.id }; + ctrl.req.params = { id: new_user.id }; + ctrl.req.body = data; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.putAction(); + } ); + + it( 'should call UserService.updateUser( user, data ) if the email change and valid', function ( done ) { + var data = { + email: 'qqq@mail.ru' + }; + + var spy = sinon.spy( Service, 'updateUser' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( new_user.id ); + + expect( spy.called ).to.be.true; + expect( spy.calledOnce ).to.be.true; + + var spyCall = spy.getCall ( 0 ).args; + + expect( spyCall ).to.be.an( 'array' ); + + expect( spyCall[0] ).to.be.an( 'object' ).and.be.ok; + expect( spyCall[0] ).to.have.property( 'id' ).and.equal( new_user.id ); + + expect( spyCall[1] ).to.be.an( 'object' ).and.be.ok; + expect( spyCall[1] ).to.have.property( 'email' ).and.equal( data.email ); + + spy.restore(); + + done(); + }; + + ctrl.req.user = { id: new_user.id }; + ctrl.req.params = { id: new_user.id }; + ctrl.req.body = data; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.putAction(); + } ); + + it( 'should not call UserService.updateUser if the email change and already exist', function ( done ) { + var data = { + email: 'admin@example.com' + }; + + var spy = sinon.spy( Service, 'updateUser' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 400 ); + + expect( result ).to.be.an( 'string' ).and.equal( 'email already exists' ); + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + }; + + ctrl.req.user = { id: new_user.id }; + ctrl.req.params = { id: new_user.id }; + ctrl.req.body = data; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.putAction(); + } ); + + it( 'should call UserService.getUserFullDataJson( options ) If all previous conditions have been met', function ( done ) { + var data = { + email: 'qqq_new@mail.ru' + }; + + var spy = sinon.spy( Service, 'getUserFullDataJson' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( new_user.id ); + + expect( spy.called ).to.be.true; + expect( spy.calledOnce ).to.be.true; + + var spyCall = spy.getCall ( 0 ).args; + + expect( spyCall ).to.be.an( 'array' ).and.have.length( 1 ); + + expect( spyCall[0] ).to.be.an( 'object' ).and.be.ok; + expect( spyCall[0] ).to.have.property( 'id' ).and.equal( new_user.id ); + + spy.restore(); + + done(); + }; + + ctrl.req.user = { id: new_user.id }; + ctrl.req.params = { id: new_user.id }; + ctrl.req.body = data; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.putAction(); + } ); + + it( 'should be able to get the error if insufficiently userId', function ( done ) { + var data = { + username: 'admin', + password: 'secret_password' + }; + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 400 ); + + expect( result ).to.be.an( 'string' ).and.equal( 'Bad Request' ); + + done(); + }; + + ctrl.req.user = { id: users[0].id }; + ctrl.req.params = { id: null }; + ctrl.req.body = data; + + ctrl.putAction(); + + } ); + + it( 'should be able to get the error if new email already exist', function ( done ) { + var data = { + email: users[0].email, + username: 'admin', + password: 'secret_password' + }; + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 400 ); + + expect( result ).to.be.an( 'string' ).and.equal( 'email already exists' ); + + done(); + }; + + ctrl.req.user = { id: new_user.id }; + ctrl.req.params = { id: new_user.id }; + ctrl.req.body = data; + + ctrl.putAction(); + + } ); + + it( 'should be able to get the error if old password incorrect', function ( done ) { + var data = { + password: 'secret_password_incorrect', + new_password: 'secret_password_new' + }; + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.equal( 'Invalid password' ); + + done(); + }; + + ctrl.req.user = { id: new_user.id }; + ctrl.req.params = { id: new_user.id }; + ctrl.req.body = data; + + ctrl.putAction(); + + } ); + + it( 'should hash password and to update firstname, lastname, email, phone, password and do not update other', function ( done ) { + var data = { + password: 'secret_password', + new_password: 'secret_password_updated', + + firstname: 'mishka', + lastname: 'mikhajlov', + email: 'qwqwqw@mail.ru', + phone: '845848485', + + username: 'vasjok', + confirmed: false, + active: false + }; + + var old_password = new_user.password; + + ctrl.send = function ( user, status ) { + + expect( status ).to.equal( 200 ); + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( new_user.id ); + + Service.findById( new_user.id ) + .then( function ( newUser ) { + + expect( newUser ).to.be.an( 'object' ).and.be.ok; + expect( newUser ).to.have.property( 'id' ).and.equal( new_user.id ); + + expect( newUser ).to.have.property( 'email' ).and.equal( data.email ); + expect( newUser ).to.have.property( 'password' ).and.not.equal( old_password ); + expect( newUser ).to.have.property( 'firstname' ).and.equal( data.firstname ); + expect( newUser ).to.have.property( 'lastname' ).and.equal( data.lastname ); + expect( newUser ).to.have.property( 'email' ).and.equal( data.email ); + expect( newUser ).to.have.property( 'phone' ).and.equal( data.phone ); + + expect( newUser ).to.have.property( 'username' ).and.not.equal( data.username ); + expect( newUser ).to.have.property( 'confirmed' ).and.not.equal( data.confirmed ); + expect( newUser ).to.have.property( 'active' ).and.not.equal( data.active ); + + new_user = newUser; + + done(); + } ) + .fail( done ); + }; + + ctrl.req.user = { id: new_user.id }; + ctrl.req.params = { id: new_user.id }; + ctrl.req.body = data; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.putAction(); + } ); + + } ); + + describe( 'loginAction()', function () { + + it( 'should call UserService.authenticate() anytime', function ( done ) { + + var spy = sinon.spy( Service, 'authenticate' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( new_user.id ); + + expect( spy.called ).to.be.true; + expect( spy.calledOnce ).to.be.true; + + var spyCall = spy.getCall ( 0 ).args[0]; + + expect( spyCall ).to.be.an( 'object' ).and.be.ok; + expect( spyCall ).to.have.property( 'email' ).and.equal( new_user.email ); + expect( spyCall ).to.have.property( 'password' ).and.equal( '46c8df75e98d0eea89d8414b9bd997b13f33caae' ); + expect( spyCall ).to.have.property( 'confirmed' ).and.equal( true ); + expect( spyCall ).to.have.property( 'active' ).and.equal( true ); + + spy.restore(); + + done(); + }; + + ctrl.req.body = { + username: new_user.email, + password: 'secret_password_updated' + }; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.loginAction(); + } ); + + it( 'should call this.handleLocalUser( err, user )', function ( done ) { + + var spy = sinon.spy( ctrl, 'handleLocalUser' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( new_user.id ); + + expect( spy.called ).to.be.true; + expect( spy.calledOnce ).to.be.true; + + var spyCall_err = spy.getCall ( 0 ).args[0]; + var spyCall_user = spy.getCall ( 0 ).args[1]; + + expect( spyCall_err ).to.be.not.ok; + + expect( spyCall_user ).to.be.an( 'object' ).and.be.ok; + expect( spyCall_user ).to.have.property( 'id' ).and.equal( new_user.id ); + expect( spyCall_user ).to.have.property( 'email' ).and.equal( new_user.email ); + expect( spyCall_user ).to.not.have.property( 'password' ); + expect( spyCall_user ).to.have.property( 'confirmed' ).and.equal( true ); + expect( spyCall_user ).to.have.property( 'active' ).and.equal( true ); + + spy.restore(); + + done(); + }; + + ctrl.req.body = { + username: new_user.email, + password: 'secret_password_updated' + }; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.loginAction(); + } ); + + it( 'should call this.loginUserJson( user ) if user was found', function ( done ) { + + var spy = sinon.spy( ctrl, 'loginUserJson' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( new_user.id ); + + expect( spy.called ).to.be.true; + expect( spy.calledOnce ).to.be.true; + + var spyCall = spy.getCall ( 0 ).args[0]; + + expect( spyCall ).to.be.an( 'object' ).and.be.ok; + expect( spyCall ).to.have.property( 'id' ).and.equal( new_user.id ); + expect( spyCall ).to.have.property( 'email' ).and.equal( new_user.email ); + expect( spyCall ).to.not.have.property( 'password' ); + expect( spyCall ).to.have.property( 'confirmed' ).and.equal( true ); + expect( spyCall ).to.have.property( 'active' ).and.equal( true ); + + spy.restore(); + + done(); + }; + + ctrl.req.body = { + username: new_user.email, + password: 'secret_password_updated' + }; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.loginAction(); + } ); + + it( 'should not call this.loginUserJson( user ) if user is not found', function ( done ) { + + var spy = sinon.spy( ctrl, 'loginUserJson' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'object' ).and.be.empty; + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + }; + + ctrl.req.body = { + username: new_user.email + '1', + password: 'secret_password_updated' + }; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.loginAction(); + } ); + + it( 'should call this.req.login( user, cb ) if user was found', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( new_user.id ); + + expect( spy.called ).to.be.true; + expect( spy.calledOnce ).to.be.true; + + var spyCall = spy.getCall ( 0 ).args; + + expect( spyCall ).to.be.an( 'array' ).and.have.length( 2 ); + + expect( spyCall[0] ).to.be.an( 'object' ).and.be.ok; + expect( spyCall[0] ).to.have.property( 'id' ).and.equal( new_user.id ); + expect( spyCall[0] ).to.have.property( 'email' ).and.equal( new_user.email ); + expect( spyCall[0] ).to.not.have.property( 'password' ); + expect( spyCall[0] ).to.have.property( 'confirmed' ).and.equal( true ); + expect( spyCall[0] ).to.have.property( 'active' ).and.equal( true ); + + expect( spyCall[1] ).to.be.an( 'function' ).and.be.ok; + + spy.restore(); + + done(); + }; + + ctrl.req.body = { + username: new_user.email, + password: 'secret_password_updated' + }; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + var spy = sinon.spy( ctrl.req, 'login' ); + + ctrl.loginAction(); + } ); + + it( 'should not call this.req.login( user, cb ) if user is not found', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'object' ).and.be.empty; + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + }; + + ctrl.req.body = { + username: new_user.email + '1', + password: 'secret_password_updated' + }; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + var spy = sinon.spy( ctrl.req, 'login' ); + + ctrl.loginAction(); + } ); + + it( 'should call this.handleLoginJson( user, err ) if user was found', function ( done ) { + + var spy = sinon.spy( ctrl, 'handleLoginJson' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( new_user.id ); + + expect( spy.called ).to.be.true; + expect( spy.calledOnce ).to.be.true; + + var spyCall = spy.getCall ( 0 ).args; + + expect( spyCall ).to.be.an( 'array' ); + + expect( spyCall[0] ).to.be.an( 'object' ).and.be.ok; + expect( spyCall[0] ).to.have.property( 'id' ).and.equal( new_user.id ); + expect( spyCall[0] ).to.have.property( 'email' ).and.equal( new_user.email ); + expect( spyCall[0] ).to.not.have.property( 'password' ); + expect( spyCall[0] ).to.have.property( 'confirmed' ).and.equal( true ); + expect( spyCall[0] ).to.have.property( 'active' ).and.equal( true ); + + expect( spyCall[1] ).to.not.be.ok; + + spy.restore(); + + done(); + }; + + ctrl.req.body = { + username: new_user.email, + password: 'secret_password_updated' + }; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.loginAction(); + } ); + + it( 'should not call this.handleLoginJson( user, err ) if user is not found', function ( done ) { + + var spy = sinon.spy( ctrl, 'handleLoginJson' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'object' ).and.be.empty; + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + }; + + ctrl.req.body = { + username: new_user.email + '1', + password: 'secret_password_updated' + }; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.loginAction(); + } ); + + it( 'should be able to authenticate user', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( new_user.id ); + + done(); + }; + + ctrl.req.body = { + username: new_user.email, + password: 'secret_password_updated' + }; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.loginAction(); + } ); + + it( 'should be able to get the error and empty object as result if user is not found', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'object' ).and.be.empty; + + done(); + }; + + ctrl.req.body = { + username: new_user.email, + password: 'secret_password_updated' + '15' + }; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.loginAction(); + } ); + + } ); + + describe( 'logoutAction()', function () { + + it( 'should call req.logout() and res.send( {}, 200 )', function ( done ) { + ctrl.req.logout = sinon.spy(); + ctrl.res.send = sinon.spy(); + ctrl.logoutAction(); + + expect( ctrl.req.logout.called ).to.be.true; + expect( ctrl.req.logout.calledOnce ).to.be.true; + + expect( ctrl.res.send.called ).to.be.true; + expect( ctrl.res.send.calledOnce ).to.be.true; + + var spyCall = ctrl.res.send.getCall ( 0 ).args; + + expect( spyCall ).to.be.an( 'array' ).and.have.length( 2 ) + expect( spyCall[0] ).to.be.an( 'object' ).and.be.empty; + expect( spyCall[1] ).to.be.a( 'number' ).and.equal( 200 ); + + done(); + } ); + + } ); + + describe( 'currentAction()', function () { + + it( 'should not call UserService.getUserFullDataJson( options ) if user is not login', function ( done ) { + + var spy = sinon.spy( Service, 'getUserFullDataJson' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 404 ); + + expect( result ).to.be.an( 'object' ).and.be.empty; + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + }; + + ctrl.req.user = null; + + ctrl.currentAction(); + } ); + + it( 'should not call UserService.getUserFullDataJson( options ) if user is login and do not require reload', function ( done ) { + + var spy = sinon.spy( Service, 'getUserFullDataJson' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.be.ok; + + expect( spy.called ).to.be.false; + + spy.restore(); + + done(); + }; + + ctrl.req.user = { id: 4 }; + ctrl.req.query = {}; + + ctrl.currentAction(); + } ); + + it( 'should call UserService.getUserFullDataJson( options ) if user is login and require reload', function ( done ) { + var spy = sinon.spy( Service, 'getUserFullDataJson' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( new_user.id ); + + expect( spy.called ).to.be.true; + expect( spy.calledOnce ).to.be.true; + + var spyCall = spy.getCall ( 0 ).args; + + expect( spyCall ).to.be.an( 'array' ).and.have.length( 1 ); + + expect( spyCall[0] ).to.be.an( 'object' ).and.be.ok; + expect( spyCall[0] ).to.have.property( 'id' ).and.equal( new_user.id ); + + spy.restore(); + + done(); + }; + + ctrl.req.user = { id: new_user.id }; + + ctrl.req.query = { + reload: true + }; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.currentAction(); + } ); + + it( 'should call this.loginUserJson( user ) if user is login and require reload', function ( done ) { + + var spy = sinon.spy( ctrl, 'loginUserJson' ); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( new_user.id ); + + expect( spy.called ).to.be.true; + expect( spy.calledOnce ).to.be.true; + + var spyCall = spy.getCall ( 0 ).args[0]; + + expect( spyCall ).to.be.an( 'object' ).and.be.ok; + expect( spyCall ).to.have.property( 'id' ).and.equal( new_user.id ); + expect( spyCall ).to.have.property( 'email' ).and.equal( new_user.email ); + expect( spyCall ).to.not.have.property( 'password' ); + expect( spyCall ).to.have.property( 'confirmed' ).and.equal( true ); + expect( spyCall ).to.have.property( 'active' ).and.equal( true ); + + spy.restore(); + + done(); + }; + + ctrl.req.user = { id: new_user.id }; + + ctrl.req.query = { + reload: true + }; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.currentAction(); + } ); + + it( 'should be able to get user if user is login and require reload', function ( done ) { + + var user = new_user.toJSON(); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( user.id ); + expect( result ).to.have.property( 'username' ).and.equal( user.username ); + expect( result ).to.have.property( 'email' ).and.equal( user.email ); + expect( result ).to.have.property( 'firstname' ).and.equal( user.firstname ); + expect( result ).to.have.property( 'phone' ).and.equal( user.phone ); + expect( result ).to.have.property( 'confirmed' ).and.equal( true ); + expect( result ).to.have.property( 'active' ).and.equal( true ); + + done(); + }; + + ctrl.req.user = { id: new_user.id }; + + ctrl.req.query = { + reload: true + }; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.currentAction(); + } ); + + it( 'should be able to get user if user is login and not require reload', function ( done ) { + + var user = new_user.toJSON(); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( user.id ); + expect( result ).to.have.property( 'username' ).and.equal( user.username ); + expect( result ).to.have.property( 'email' ).and.equal( user.email ); + expect( result ).to.have.property( 'firstname' ).and.equal( user.firstname ); + expect( result ).to.have.property( 'phone' ).and.equal( user.phone ); + expect( result ).to.have.property( 'confirmed' ).and.equal( true ); + expect( result ).to.have.property( 'active' ).and.equal( true ); + + done(); + }; + + ctrl.req.user = user; + + ctrl.req.query = { + reload: false + }; + + ctrl.req.login = function( user, callback ) { + if ( !!user && !!user.id ) { + callback( null, user ); + } + }; + + ctrl.currentAction(); + } ); + + it( 'should be able to get the error if user is not login', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect ( status ).to.equal ( 404 ); + + expect ( result ).to.be.an ( 'object' ).and.be.empty; + + done (); + }; + + ctrl.req.body = {}; + + ctrl.req.user = null; + + ctrl.currentAction(); + } ); + + } ); + + describe( 'listAction()', function () { + + it( 'should be able to get list of users', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'array' ).and.have.length.above( 1 ); + expect( result[0] ).to.be.an( 'object' ).and.be.ok; + expect( result[0] ).to.have.property( 'id' ).and.be.ok; + expect( result[0] ).to.have.property( 'username' ).and.be.ok; + expect( result[0] ).to.have.property( 'email' ).and.be.ok; + expect( result[0] ).to.have.property( 'active' ).and.equal( true ); + expect( result[0] ).to.not.have.property( 'password' ); + + done(); + }; + + ctrl.listAction(); + } ); + + } ); + + describe( 'getAction()', function () { + + it( 'should be able to get user by id', function ( done ) { + + var user = new_user.toJSON(); + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( user.id ); + expect( result ).to.have.property( 'username' ).and.equal( user.username ); + expect( result ).to.have.property( 'email' ).and.equal( user.email ); + expect( result ).to.have.property( 'active' ).and.equal( user.active ); + expect( result ).to.not.have.property( 'password' ); + + done(); + }; + + ctrl.req.params = { id: new_user.id }; + + ctrl.getAction(); + } ); + + it( 'should be able to get empty object if user do not exist', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ).and. be.empty; + + done(); + }; + + ctrl.req.params = { id: 15151515151151515 }; + + ctrl.getAction(); + } ); + + } ); + +} ); \ No newline at end of file diff --git a/modules/clever-auth/tests/unit/test.service.UserService.js b/modules/clever-auth/tests/unit/test.service.UserService.js new file mode 100644 index 0000000..f4dd9f0 --- /dev/null +++ b/modules/clever-auth/tests/unit/test.service.UserService.js @@ -0,0 +1,1092 @@ +var expect = require ( 'chai' ).expect + , request = require ( 'supertest' ) + , path = require( 'path' ) + , app = require ( path.resolve( __dirname + '/../../../../' ) + '/index.js' ) + , config = require( 'config' ) + , testEnv = require ( 'utils' ).testEnv() + , sinon = require( 'sinon' ) + , Q = require ( 'q' ); + +var EmailService = null; + +var user_1, user_1_json, old_password; + +describe( 'service.UserService', function () { + var UserService; + + before( function ( done ) { + this.timeout( 15000 ); + testEnv( function ( _UserService_, _ORMUserModel_ ) { + + UserService = _UserService_; + UserModel = _ORMUserModel_; + + done(); + }, done ); + } ); + + describe( '.authenticate( credentials )', function () { + + it( 'should return User with specified credentials', function ( done ) { + var data1 = { + username: 'Joe', + email: 'joe@example.com', + password: '1234' + }; + var data2 = { + username: 'Rachel', + email: 'rachel@example.com', + password: '1234' + }; + + UserService.create( data1 ) + .then( function () { + return UserService.create( data2 ); + } ) + .then( function () { + return UserService.authenticate( { + email: 'rachel@example.com', + password: '1234' + } ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'username' ).and.equal( data2.username ); + expect( user ).to.have.property( 'email' ).and.equal( data2.email ); + expect( user ).to.not.have.property( 'password' ); + + done(); + } ); + } ) + .fail( done ); + } ); + + it( 'should not return user when he is not active', function ( done ) { + var data = { + username: 'Joe3', + email: 'joe3@example.com', + password: '1234', + active: false + }; + + UserService + .create( data ) + .then( function () { + return UserService.authenticate( { + email: data.email, + password: data.password + } ); + + } ) + .then( function ( user ) { + + expect( user ).to.not.be.ok; + + done(); + } ) + .fail( done ); + } ); + + it( 'should set "accessedAt" property after successfull login', function ( done ) { + + var lastLogin = null + , data = { + username: 'Joe5', + email: 'joe5@example.com', + password: '1234', + active: true + }; + + UserService + .create( data ) + .then( function () { + + return UserService.authenticate( { + email: data.email, + password: data.password + } ); + } ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'username' ).and.equal( data.username ); + expect( user ).to.have.property( 'email' ).and.equal( data.email ); + expect( user ).to.have.property( 'accessedAt' ).and.be.ok; + + lastLogin = user.accessedAt; + + return lastLogin; + } ) + .delay( 1000 ) + .then( function () { + return UserService.authenticate( { + email: data.email, + password: data.password + } ); + } ) + .then( function ( user ) { + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'username' ).and.equal( data.username ); + expect( user ).to.have.property( 'email' ).and.equal( data.email ); + expect( user ).to.have.property( 'accessedAt' ).and.not.equal( lastLogin ); + + done(); + } ) + .fail( done ); + } ); + + } ); + + describe( '.getUserFullDataJson( options )', function () { + + it( 'should return User with specified options', function ( done ) { + var data = { + username: 'Rachel8', + email: 'rachel8@example.com', + password: '1234' + }; + + UserService.create( data ) + .then( function () { + return UserService.getUserFullDataJson( { email: data.email } ); + } ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'username' ).and.equal( data.username ); + expect( user ).to.have.property( 'email' ).and.equal( data.email ); + + done(); + } ) + .fail( done ); + } ); + + it( 'should return empty object when options does not match in the db', function ( done ) { + var data = { + username: 'Rachel10', + email: 'rachel10@example.com', + password: '1234' + }; + + UserService.create( data ) + .then( function () { + return UserService.getUserFullDataJson( { email: 'noneExistedEmail2@somemail.com' } ); + } ) + .then( function ( user ) { + + expect( user ).to.be.empty; + + done(); + } ) + .fail( done ); + } ); + + } ); + + describe( '.generatePasswordResetHash( user )', function () { + + it( 'should return data for user confirmation', function ( done ) { + + var data = { + username: 'Rachel12', + email: 'rachel12@example.com', + password: '1234', + confirmed: false + }; + + UserService + .create( data ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + + //Properties needed for creating hash value + expect( user ).to.have.property( 'createdAt' ).and.be.ok; + expect( user ).to.have.property( 'updatedAt' ).and.be.ok; + expect( user ).to.have.property( 'password' ).and.be.ok; + expect( user ).to.have.property( 'email' ).and.equal( data.email ); + expect( user ).to.have.property( 'confirmed' ).and.equal( data.confirmed ); + + return UserService.generatePasswordResetHash( user ); + } ) + .then( function ( recoverydata ) { + + expect( recoverydata ).to.be.an( 'object' ).and.be.ok; + expect( recoverydata ).to.have.property( 'hash' ).and.be.ok; + expect( recoverydata ).to.have.property( 'expTime' ).and.be.ok; + expect( recoverydata ).to.have.property( 'user' ).and.be.ok; + expect( recoverydata.user ).to.be.an( 'object' ).and.be.ok; + expect( recoverydata.user ).to.have.property( 'id' ).and.be.ok; + expect( recoverydata.user ).to.have.property( 'fullName' ).and.be.ok; + expect( recoverydata ).to.have.property( 'action' ).and.be.ok; + expect( recoverydata.action ).to.be.an( 'string' ).and.include( 'confirm' ); + expect( recoverydata ).to.have.property( 'mailsubject' ).and.be.ok; + expect( recoverydata.mailsubject ).to.be.an( 'string' ).and.include( 'Confirmation' ); + + done(); + } ) + .fail( done ); + } ); + + it( 'should return data for password recovery', function ( done ) { + + var data = { + username: 'Rachel13', + email: 'rachel13@example.com', + password: '1234', + confirmed: true, + "AccountId": 1 + }; + + UserService + .create( data ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + + //Properties needed for creating hash value + expect( user ).to.have.property( 'createdAt' ).and.be.ok; + expect( user ).to.have.property( 'updatedAt' ).and.be.ok; + expect( user ).to.have.property( 'password' ).and.be.ok; + expect( user ).to.have.property( 'email' ).and.equal( data.email ); + expect( user ).to.have.property( 'confirmed' ).and.equal( data.confirmed ); + + return UserService.generatePasswordResetHash( user ); + } ) + .then( function ( recoverydata ) { + + expect( recoverydata ).to.be.an( 'object' ).and.be.ok; + expect( recoverydata ).to.have.property( 'hash' ).and.be.ok; + expect( recoverydata ).to.have.property( 'expTime' ).and.be.ok; + expect( recoverydata ).to.have.property( 'user' ).and.be.ok; + expect( recoverydata.user ).to.be.an( 'object' ).and.be.ok; + expect( recoverydata.user ).to.have.property( 'id' ).and.be.ok; + expect( recoverydata.user ).to.have.property( 'fullName' ).and.be.ok; + expect( recoverydata ).to.have.property( 'action' ).and.be.ok; + expect( recoverydata.action ).to.be.an( 'string' ).and.include( 'reset' ); + expect( recoverydata ).to.have.property( 'mailsubject' ).and.be.ok; + expect( recoverydata.mailsubject ).to.be.an( 'string' ).and.include( 'Recovery' ); + + done(); + } ) + .fail( done ); + } ); + + it( 'should return status 403 and message when user missing fields', function ( done ) { + var data = { + username: 'Rachel13', + email: 'rachel13@example.com', + password: '1234' + }; + + UserService + .generatePasswordResetHash( data ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.an( 'string' ).and.be.ok; + + done(); + } ) + .fail( done ); + } ); + + } ); + + describe.skip( '.mailPasswordRecoveryToken( obj )', function () { + + it( 'should return status 200 and a message for account confirmation action ', function ( done ) { + var data = + { + user: { + email: "email@mail.com", + id: "id", + fullName: "Jim Ioak" + }, + hash: "some_valid_hash", + mailsubject: "some_valid_subject", + action: "account_confirm" + }; + + UserService + .mailPasswordRecoveryToken( data ) + .then( function ( result ) { + + should.exist( result ); + result.should.be.a( 'object' ); + + result.should.have.property( 'statuscode', 200 ); + result.should.have.property( 'message' ); + done(); + } ) + .fail( done ); + + } ); + + it.skip( 'should return status 200 and a message for password recovery action ', function ( done ) { + var data = { + user: { email: "email@mail.com", id: "id", fullName: "Jim Ioak" }, hash: "some_valid_hash", mailsubject: "some_valid_subject", action: "password_reset_submit" + }; + + UserService + .mailPasswordRecoveryToken( data ) + .then( function ( result ) { + + should.exist( result ); + result.should.be.a( 'object' ); + + result.should.have.property( 'statuscode', 200 ); + result.should.have.property( 'message' ); + done(); + } ) + .fail( done ); + } ); + + it.skip( 'should return status 500 and a message for unrecognized action ', function ( done ) { + var data = { + user: { email: "email@mail.com", id: "id", fullName: "Jim Ioak" }, hash: "some_valid_hash", mailsubject: "some_valid_subject", action: "unrecognized_action" + }; + + UserService + .mailPasswordRecoveryToken( data ) + .then( function ( result ) { + should.exist( result ); + result.should.be.a( 'object' ); + + result.should.have.property( 'statuscode', 500 ); + result.should.have.property( 'message' ); + + done(); + } ) + .fail( done ); + } ); + } ); + + describe( '.createUser( data )', function () { + + before( function( done ) { + try { + + EmailService = require( 'services' )['EmailService']; + + expect( EmailService ).to.be.ok; + + } catch ( err ) { + + expect( EmailService ).to.not.be.ok; + + } + + done(); + }); + + it( 'should return status 400 and a message when user with email exists', function ( done ) { + + var data = { + username: 'Rachel18', + email: 'rachel18@example.com', + password: '1234' + }; + + UserService + .create( data ) + .then( function () { + return UserService.createUser( data ); + } ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'statuscode' ).and.equal( 400 ); + expect( result ).to.have.property( 'message' ).and.be.an( 'string' ).and.be.ok; + + done(); + } ) + .fail( done ); + } ); + + it( 'should call .savedNewUser method anytime', function ( done ) { + sinon.spy( UserService, "saveNewUser" ); + + var data = { + username: 'Rachel20', + email: 'rachel20@example.com', + password: '1234', + "AccountId": 1 + }; + + UserService + .createUser( data ) + .then( function () { + + expect( UserService.saveNewUser.calledOnce ).to.be.true; + + done(); + } ) + .fail( done ); + } ); + + it( 'should call .generatePasswordResetHash method if EmailService exist and do not call otherwise', function ( done ) { + sinon.spy( UserService, "generatePasswordResetHash" ); + + var data = { + username: 'Rachel21', + email: 'rachel21@example.com', + password: '1234' + }; + + UserService + .createUser( data ) + .then( function () { + + if ( EmailService !== null && config['clever-auth'].email_confirmation ) { + + expect( UserService.generatePasswordResetHash.calledOnce ).to.be.true; + + UserService + .generatePasswordResetHash + .restore(); + } else { + + expect( UserService.generatePasswordResetHash.calledOnce ).to.be.false; + + } + done(); + } ) + .fail( done ); + } ); + + it( 'should call .mailPasswordRecoveryToken method if EmailService exist and do not call otherwise', function ( done ) { + this.timeout( 5000 ); + sinon.spy( UserService, "mailPasswordRecoveryToken" ); + + var data = { + username: 'Rachel22', + email: 'rachel22@example.com', + password: '1234' + }; + + UserService + .createUser( data ) + .then( function () { + + if ( EmailService !== null && config['clever-auth'].email_confirmation ) { + + expect( UserService.mailPasswordRecoveryToken.calledOnce ).to.be.true; + + UserService + .mailPasswordRecoveryToken + .restore(); + } else { + + expect( UserService.mailPasswordRecoveryToken.calledOnce ).to.be.false; + + } + done(); + } ) + .fail( done ); + } ); + + it( 'should return user object', function ( done ) { + + var data = { + username: 'Rachel23', + email: 'rachel23@example.com', + password: '1234' + }; + + UserService + .createUser( data ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.be.ok; + expect( user ).to.have.property( 'username' ).and.equal( data.username ); + expect( user ).to.have.property( 'email' ).and.equal( data.email ); + expect( user ).to.have.property( 'password' ).and.be.ok; + + done(); + } ) + .fail( done ); + } ); + + } ); + + describe( '.saveNewUser( data )', function () { + + it( 'should auto generate random password when password is not given', function ( done ) { + var data = { + username: 'rachel32@example.com', + email: 'rachel32@example.com' + }; + + UserService + .saveNewUser( data ) + .then( function ( newUser ) { + + expect( newUser ).to.be.an( 'object' ).and.be.ok; + expect( newUser ).to.have.property( 'id' ).and.be.ok; + expect( newUser ).to.have.property( 'username' ).and.equal( data.username ); + expect( newUser ).to.have.property( 'email' ).and.equal( data.email ); + expect( newUser ).to.have.property( 'password' ).and.be.ok; + + return UserModel.find( { where: { email: data.email } } ); + } ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.be.ok; + expect( user ).to.have.property( 'username' ).and.equal( data.username ); + expect( user ).to.have.property( 'email' ).and.equal( data.email ); + expect( user ).to.have.property( 'password' ).and.be.ok; + + done(); + } ) + .fail( done ); + } ); + + it( 'should hash password when password is given', function ( done ) { + + var data = { + username: 'rachel33@example.com', + email: 'rachel33@example.com', + password: '123' + }; + + UserService + .saveNewUser( data ) + .then( function ( newUser ) { + + expect( newUser ).to.be.an( 'object' ).and.be.ok; + expect( newUser ).to.have.property( 'id' ).and.be.ok; + expect( newUser ).to.have.property( 'username' ).and.equal( data.username ); + expect( newUser ).to.have.property( 'email' ).and.equal( data.email ); + expect( newUser ).to.have.property( 'password' ).and.be.ok; + + return UserModel.find( { where: { email: data.email } } ); + } ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.be.ok; + expect( user ).to.have.property( 'username' ).and.equal( data.username ); + expect( user ).to.have.property( 'email' ).and.equal( data.email ); + expect( user ).to.have.property( 'password' ).and.not.equal( '123' ); + expect( user.password.length ).to.be.ok.and.be.above( '123'.length ); + + done(); + } ) + .fail( done ); + } ); + + it( 'should return a new user object', function ( done ) { + var data = { + username: 'rachel34@example.com', + email: 'rachel34@example.com', + password: '123' + }; + + UserService + .saveNewUser( data ) + .then( function ( newUser ) { + + expect( newUser ).to.be.an( 'object' ).and.be.ok; + expect( newUser ).to.have.property( 'id' ).and.be.ok; + expect( newUser ).to.have.property( 'username' ).and.equal( data.username ); + expect( newUser ).to.have.property( 'email' ).and.equal( data.email ); + expect( newUser ).to.have.property( 'password' ).and.be.ok; + + return UserModel.find( { where: { email: data.email } } ); + } ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.be.ok; + expect( user ).to.have.property( 'username' ).and.equal( data.username ); + expect( user ).to.have.property( 'email' ).and.equal( data.email ); + expect( user ).to.have.property( 'password' ).and.be.ok; + + user_1 = user; + user_1_json = user.toJSON(); + + done(); + } ) + .fail( done ); + + } ); + + } ); + + describe( '.updateUser( user, data )', function () { + + it( 'should be able to update firstname, lastname, email, phone and do not update other', function ( done ) { + var data = { + firstname: 'mishka', + lastname: 'mikhajlov', + email: 'qwqwqw@mail.ru', + phone: '845848485', + + username: 'vasjok', + confirmed: true, + active: false + }; + + UserService + .updateUser( user_1, data ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( user_1_json.id ); + + return UserModel.find( user_1.id ); + } ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( user_1_json.id ); + expect( user ).to.have.property( 'email' ).and.equal( data.email ); + expect( user ).to.have.property( 'firstname' ).and.equal( data.firstname ); + expect( user ).to.have.property( 'lastname' ).and.equal( data.lastname ); + expect( user ).to.have.property( 'phone' ).and.equal( data.phone ); + + expect( user ).to.have.property( 'username' ).and.not.equal( data.username ); + expect( user.username ).to.equal( user_1_json.username ); + expect( user ).to.have.property( 'confirmed' ).and.not.equal( data.confirmed ); + expect( user.confirmed ).to.equal( user_1_json.confirmed ); + expect( user ).to.have.property( 'active' ).and.not.equal( data.active ); + expect( user.active ).to.equal( user_1_json.active ); + + user_1_json = user.toJSON(); + + done(); + } ) + .fail( done ); + } ); + + } ); + + describe( '.checkEmailAndUpdate( user, data )', function () { + + it( 'should return status 400 and a message when user with email exists', function ( done ) { + var data = { + email: 'rachel32@example.com' + }; + + UserService + .checkEmailAndUpdate( user_1, data ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'statuscode' ).and.equal( 400 ); + expect( result ).to.have.property( 'message' ).and.be.an( 'string' ).and.be.ok; + + return UserModel.find( user_1.id ); + } ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( user_1.id ); + expect( user ).to.have.property( 'email' ).and.equal( user_1.email ); + + done(); + } ) + .fail( done ); + } ); + + it( 'should be able to update email ', function ( done ) { + var data = { + email: 'rachel152@example.com' + }; + + UserService + .checkEmailAndUpdate( user_1, data ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( user_1.id ); + + return UserModel.find( user_1.id ); + } ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( user_1_json.id ); + expect( user ).to.have.property( 'username' ).and.equal( user_1_json.username ); + expect( user ).to.have.property( 'email' ).and.not.equal( user_1_json.email ); + expect( user.email ).to.equal( data.email ); + + user_1_json = user.toJSON(); + + done(); + } ) + .fail( done ); + } ); + + it( 'should be able to update firstname, lastname, email, phone and do not update other', function ( done ) { + var data = { + firstname: 'firstname', + lastname: 'lastname', + email: 'qqq@mail.ru', + phone: '09548848', + + username: 'vasjok', + confirmed: true, + active: false + }; + + UserService + .checkEmailAndUpdate( user_1, data ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( user_1_json.id ); + + return UserModel.find( user_1.id ); + } ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( user_1_json.id ); + expect( user ).to.have.property( 'email' ).and.equal( data.email ); + expect( user ).to.have.property( 'firstname' ).and.equal( data.firstname ); + expect( user ).to.have.property( 'lastname' ).and.equal( data.lastname ); + expect( user ).to.have.property( 'phone' ).and.equal( data.phone ); + + expect( user ).to.have.property( 'username' ).and.not.equal( data.username ); + expect( user ).to.have.property( 'confirmed' ).and.not.equal( data.confirmed ); + expect( user ).to.have.property( 'active' ).and.not.equal( data.active ); + + done(); + } ) + .fail( done ); + } ); + + } ); + + describe( '.handleUpdateUser( userId, data )', function () { + + it( 'should return status 403 and a message when user with userId do not exists', function ( done ) { + var data = { + firstname: 'qwqwqw', + lastname: 'ererere' + }; + + UserService + .handleUpdateUser( 1515151515, data ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.an( 'string' ).and.be.ok; + + done(); + } ) + .fail( done ); + } ); + + it( 'should return status 403 and a message if old password incorrect', function ( done ) { + var data = { + firstname: 'qwqwqw', + lastname: 'ererere', + email: 'xcxcxcxcx@mail.ru', + phone: '545454545', + password: '1223345', + new_password: '15151515' + }; + + UserService + .handleUpdateUser( user_1.id, data ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.an( 'string' ).and.be.ok; + + return UserModel.find( user_1.id ); + } ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( user_1.id ); + expect( user ).to.have.property( 'firstname' ).and.not.equal( data.firstname ); + expect( user ).to.have.property( 'lastname' ).and.not.equal( data.lastname ); + expect( user ).to.have.property( 'email' ).and.not.equal( data.email ); + expect( user ).to.have.property( 'phone' ).and.not.equal( data.phone ); + + old_password = user.password; + + done(); + } ) + .fail( done ); + } ); + + it( 'should hash password and update it', function ( done ) { + var data = { + firstname: 'qwqwqw', + lastname: 'ererere', + email: 'xcxcxcxcx@mail.ru', + phone: '545454545', + password: '123', + new_password: '321' + }; + + expect( user_1.password ).to.equal( old_password ); + + UserService + .handleUpdateUser( user_1.id, data ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ).and.be.ok; + expect( result ).to.have.property( 'id' ).and.equal( user_1_json.id ); + + return UserModel.find( user_1.id ); + } ) + .then( function ( user ) { + + expect( user ).to.be.an( 'object' ).and.be.ok; + expect( user ).to.have.property( 'id' ).and.equal( user_1.id ); + expect( user ).to.have.property( 'firstname' ).and.equal( data.firstname ); + expect( user ).to.have.property( 'lastname' ).and.equal( data.lastname ); + expect( user ).to.have.property( 'email' ).and.equal( data.email ); + expect( user ).to.have.property( 'phone' ).and.equal( data.phone ); + expect( user ).to.have.property( 'password' ).and.not.equal( old_password ); + + done(); + } ) + .fail( done ); + } ); + + } ); + + describe( '.listUsers()', function () { + + it( 'should be able to get list of all users', function ( done ) { + + UserService + .listUsers() + .then( function ( users ) { + + expect( users ).to.be.an( 'array' ).and.have.length.above( 1 ); + expect( users[0] ).to.be.an( 'object' ).and.be.ok; + expect( users[0] ).to.have.property( 'id' ).and.be.ok; + expect( users[0] ).to.have.property( 'username' ).and.be.ok; + expect( users[0] ).to.have.property( 'email' ).and.be.ok; + expect( users[0] ).to.have.property( 'active' ).and.equal( true ); + expect( users[0] ).to.not.have.property( 'password' ); + + done(); + } ) + .fail( done ); + } ); + + } ); + + describe( '.deleteUser( userId )', function () { + + it( 'should be able to get the error if the user does not exist', function ( done ) { + + UserService + .deleteUser( 151515115151515151 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + }, done ) + } ); + + it( 'should be able to delete user', function ( done ) { + + UserService + .deleteUser( user_1.id ) + .then( function ( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 200 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + } ) + .fail( done ); + } ); + + } ); + + describe.skip( '.resendAccountConfirmation( me, userId )', function () { + it( 'should return status code 403 and message when user id does not exist', function ( done ) { + var userId = 'noneExistedId' + , accId = 1; + + UserService + .resendAccountConfirmation( accId, userId ) + .then( function ( data ) { + should.exist( data ); + + data.should.be.a( 'object' ); + data.should.have.property( 'statuscode', 403 ); + data.should.have.property( 'message' ).and.not.be.empty; + done(); + } ) + + } ); + + it( 'should return status code 403 and message when account ids do not match', function ( done ) { + var newuser = { + username: 'rachel35@example.com', + email: 'rachel35@example.com', + password: '123', + "AccountId": 1 + } + , accId = 2; + + + UserService + .create( newuser ) + .then( function ( user ) { + return UserService.resendAccountConfirmation( accId, user ); + } ) + .then( function ( data ) { + should.exist( data ); + + data.should.be.a( 'object' ); + data.should.have.property( 'statuscode', 403 ); + data.should.have.property( 'message' ).and.not.be.empty; + done(); + } ) + .fail( done ); + } ); + + it( 'should return status code 403 and message when account has been confirmed', function ( done ) { + var newuser = { + username: 'rachel36@example.com', + email: 'rachel36@example.com', + password: '123', + confirmed: true, + "AccountId": 1 + } + , accId = 1; + + + UserService + .create( newuser ) + .then( function ( user ) { + return UserService.resendAccountConfirmation( accId, user ); + } ) + .then( function ( data ) { + should.exist( data ); + + data.should.be.a( 'object' ); + data.should.have.property( 'statuscode', 403 ); + data.should.have.property( 'message' ).and.not.be.empty; + done(); + } ) + .fail( done ); + } ); + + it( 'should call .generatePasswordResetHash method ', function ( done ) { + sinon.spy( UserService, "generatePasswordResetHash" ); + + var newuser = { + username: 'rachel363@example.com', + email: 'rachel363@example.com', + password: '123', + confirmed: false, + "AccountId": 1 + } + , accId = 1; + + + UserService + .create( newuser ) + .then( function ( user ) { + return UserService.resendAccountConfirmation( accId, user.id ); + } ) + .then( function ( data ) { + + UserService + .generatePasswordResetHash + .calledOnce + .should + .be + .true; + + UserService + .generatePasswordResetHash + .restore(); + + done(); + } ) + .fail( done ); + } ); + + it( 'should call .mailPasswordRecoveryToken method ', function ( done ) { + sinon.spy( UserService, "mailPasswordRecoveryToken" ); + + var newuser = { + username: 'rachel37@example.com', + email: 'rachel37@example.com', + password: '123', + confirmed: false, + "AccountId": 1 + } + , accId = 1; + + + UserService + .create( newuser ) + .then( function ( user ) { + return UserService.resendAccountConfirmation( accId, user.id ); + } ) + .then( function ( data ) { + + UserService + .mailPasswordRecoveryToken + .calledOnce + .should + .be + .true; + + UserService + .mailPasswordRecoveryToken + .restore(); + + done(); + } ) + .fail( done ); + } ); + + it( 'should return statuscode 200 and a message', function ( done ) { + + var newuser = { + username: 'rachel38@example.com', + email: 'rachel38@example.com', + password: '123', + confirmed: false, + "AccountId": 1 + } + , accId = 1; + + + UserService + .create( newuser ) + .then( function ( user ) { + return UserService.resendAccountConfirmation( accId, user.id ); + } ) + .then( function ( data ) { + + should.exist( data ); + + data.should.have.property( 'statuscode', 200 ); + data.should.have.property( 'message' ).and.not.be.empty; + + done(); + } ) + .fail( done ); + + + } ); + + } ); +} ); \ No newline at end of file diff --git a/modules/clever-email/config/default.json b/modules/clever-email/config/default.json new file mode 100644 index 0000000..d87484f --- /dev/null +++ b/modules/clever-email/config/default.json @@ -0,0 +1,34 @@ +{ + "clever-email": { + "cc": true, + "bcc": true, + "text": false, + + "systems": { + "Mandrill": { + "isActive": true, + "apiKey": "anyaWGoRNAFaQQW79MwqAA", + "async": false + }, + "SendGrid": { + "isActive": false, + "apiUser" : "", + "apiKey" : "" + }, + "MailGun": { + "isActive": false, + "domain" : "mytestdomain.com", + "apiKey" : "key-97zzcleh7dgz0kp4jfy1qp0fn38lml73" + } + }, + + "default": { + "noReply": "true", + "from": "no-reply@app.bolthr.com", + "fromName": "BoltHR", + "subject": "BoltHR: Autogenerated Notification", + "tplName": "default", + "logo": "http://app.bolthr.com/images/logo.png" + } + } +} \ No newline at end of file diff --git a/modules/clever-email/controllers/EmailController.js b/modules/clever-email/controllers/EmailController.js new file mode 100644 index 0000000..133298f --- /dev/null +++ b/modules/clever-email/controllers/EmailController.js @@ -0,0 +1,91 @@ +module.exports = function ( EmailService ) { + + return (require( 'classes' ).Controller).extend( + { + service: null + }, + /* @Prototype */ + { + + listAction: function () { + var userId = this.req.user.id; + + EmailService + .listEmails( userId ) + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, + + getAction: function () { + var userId = this.req.user.id + , emailId = this.req.params.id; + + EmailService + .getEmailByIds( userId, emailId ) + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, + + postAction: function () { + var userId = this.req.user.id + , accId = this.req.user.account.id + , accLogo = this.req.user.account.logo + , accName = this.req.user.account.name + , userFirstName = this.req.user.firstname + , userLastName = this.req.user.lastname + , data = this.req.body; + + data = data.map( function ( x ) { + x.userId = userId; + x.accId = accId; + x.userFirstName = userFirstName; + x.userLastName = userLastName; + x.accLogo = accLogo; + x.accName = accName; + return x; + } ); + + EmailService + .handleEmailCreation( data ) + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, + + putAction: function () { + this.send( 'invalid', 403 ); + }, + + deleteAction: function () { + var userId = this.req.user.id + , emailId = this.req.params.id; + + EmailService + .deleteEmail( userId, emailId ) + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + }, + + sendAction: function () { + var userId = this.req.user.id + , emailId = this.req.params.id + , type = this.req.body.type; + + EmailService + .handleEmailSending( userId, emailId, type ) + .then( this.proxy( 'handleServiceMessage' ) ) + .fail( this.proxy( 'handleException' ) ); + + }, + + handleServiceMessage: function ( obj ) { + + if ( obj.statuscode ) { + this.send( obj.message, obj.statuscode ); + return; + } + + this.send( obj, 200 ); + } + + } ); +}; \ No newline at end of file diff --git a/modules/clever-email/lib/mailer.js b/modules/clever-email/lib/mailer.js new file mode 100644 index 0000000..a35a7e4 --- /dev/null +++ b/modules/clever-email/lib/mailer.js @@ -0,0 +1,17 @@ +module.exports = function ( config ) { + + var mandrill = require( './mandrill' ) + , sendgrid = require( './sendgrid' ) + , mailgun = require( './mailgun' ) + , confs = config.systems; + + var mailer = confs.Mandrill.isActive + ? mandrill + : confs.SendGrid.isActive + ? sendgrid + : confs.MailGun.isActive + ? mailgun + : mandrill; + + return mailer( config ); +}; \ No newline at end of file diff --git a/modules/clever-email/lib/mailgun.js b/modules/clever-email/lib/mailgun.js new file mode 100644 index 0000000..f91c1fe --- /dev/null +++ b/modules/clever-email/lib/mailgun.js @@ -0,0 +1,72 @@ +module.exports = function ( config ) { + + var Q = require( 'q' ) + , _ = require( 'lodash' ) + , defConf = config.default + , confMailgun = config.systems.MailGun + , mailgun = require( 'mailgun-js' )( confMailgun.apiKey, confMailgun.domain ); + + return { + send: function ( email, body, type ) { + var deferred = Q.defer(); + + var message = this.createMessage( email, body, type ); + + mailgun + .messages + .send( message, function ( err, res, body ) { + + if ( err ) { + console.log( "MailGun Error: ", err.toString() ); + deferred.reject( err ); + return; + } + + deferred.resolve( _.map( res, function( x ) { return { status: x.message, id: x.id }; }) ); + } ); + + return deferred.promise; + }, + + createMessage: function( email, body, type ){ + var fromMail = defConf.from + , fromName = defConf.fromName; + + if ( email.dump.companyName ) { + fromName = email.dump.fromName; + fromMail = email.dump.fromMail; + } + + var message = { + from: [ fromName, ' <', fromMail, '>' ].join( '' ), + to: [ '<', email.dump.toMail, '>' ].join( '' ), + subject: email.subject || defConf.subject, + emailId: email.id + }; + + if ( config.text && type === "text" ){ + message.text = body; + } else { + message.html = body; + } + + if ( config.cc && email.dump.usersCC && email.dump.usersCC.length ) { + var cc = []; + email.dump.usersCC.forEach( function ( userEmail ) { + cc.push( [ '<', userEmail, '>' ].join( '' ) ); + } ); + message.cc = cc.join( ', ' ) + } + + if ( config.bcc && email.dump.usersBCC && email.dump.usersBCC.length ) { + var bcc = []; + email.dump.usersBCC.forEach( function ( userEmail ) { + bcc.push( [ '<', userEmail, '>' ].join( '' ) ); + } ); + message.bcc = bcc.join( ', ' ) + } + + return message; + } + }; +}; \ No newline at end of file diff --git a/modules/clever-email/lib/mandrill.js b/modules/clever-email/lib/mandrill.js new file mode 100644 index 0000000..f56c2e6 --- /dev/null +++ b/modules/clever-email/lib/mandrill.js @@ -0,0 +1,72 @@ +module.exports = function ( config ) { + + var Q = require( 'q' ) + , _ = require( 'lodash' ) + , mandrill = require( 'mandrill-api/mandrill' ) + , defConf = config.default + , confMandrill = config.systems.Mandrill + , mandrill_client = new mandrill.Mandrill( confMandrill.apiKey ); + + return { + send: function ( email, body, type ) { + var deferred = Q.defer(); + + var message = this.createMessage( email, body, type ) + , async = confMandrill.async; + + mandrill_client + .messages + .send( { "message": message, "async": async }, function ( result ) { + + deferred.resolve( _.map( result, function( x ) { return { status: x.status, id: x._id }; }) ); + + }, function ( err ) { + + console.log( 'A mandrill error occurred: ' + err.name + ' - ' + err.message ); + + deferred.reject( err ); + + } ); + + return deferred.promise; + }, + + createMessage: function( email, body, type ){ + var fromMail = defConf.from + , fromName = defConf.fromName; + + if ( email.dump.companyName ) { + fromName = email.dump.fromName; + fromMail = email.dump.fromMail; + } + + var message = { + to: [ { email: email.dump.toMail, type: 'to' } ], + subject: email.subject || defConf.subject, + from_email: fromMail, + from_name: fromName, + emailId: email.id + }; + + if ( config.text && type === "text" ){ + message.text = body; + } else { + message.html = body; + } + + if ( config.cc && email.dump.usersCC && email.dump.usersCC.length ) { + email.dump.usersCC.forEach( function ( userEmail ) { + message.to.push( { email: userEmail, type: 'cc' } ); + } ); + } + + if ( config.bcc && email.dump.usersBCC && email.dump.usersBCC.length ) { + email.dump.usersBCC.forEach( function ( userEmail ) { + message.to.push( { email: userEmail, type: 'bcc' } ); + } ); + } + + return message; + } + }; +}; \ No newline at end of file diff --git a/modules/clever-email/lib/sendgrid.js b/modules/clever-email/lib/sendgrid.js new file mode 100644 index 0000000..cf25727 --- /dev/null +++ b/modules/clever-email/lib/sendgrid.js @@ -0,0 +1,70 @@ +module.exports = function ( config ) { + + var Q = require( 'q' ) + , _ = require( 'lodash' ) + , defConf = config.default + , confSendGrid = config.systems.SendGrid + , sendgrid = require( 'sendgrid' )( confSendGrid.apiUser, confSendGrid.apiKey ) + , Email = sendgrid.Email; + + return { + send: function ( email, body, type ) { + var deferred = Q.defer() + , message = this.createMessage( email, body, type ) + , email = new Email( message ); + + if ( message.emailId ) { + email.setUniqueArgs( { email_id: message.emailId } ); + } + + sendgrid.send( email, function ( err, response ) { + + if ( err ) { + console.log( "SendGrid Error: ", err.toString() ); + deferred.reject( err ); + return; + } + + deferred.resolve( response ); + } ); + + return deferred.promise; + }, + + createMessage: function( email, body, type ){ + var fromMail = defConf.from + , fromName = defConf.fromName; + + if ( email.dump.companyName ) { + fromName = email.dump.fromName; + fromMail = email.dump.fromMail; + } + + var message = { + to: [ email.dump.toMail ], + subject: email.subject || defConf.subject, + from: fromMail, + fromname: fromName, + emailId: emailId + }; + + if ( config.text && type === "text" ){ + message.text = body; + } else { + message.html = body; + } + + if ( config.cc && email.dump.usersCC && email.dump.usersCC.length ) { + email.dump.usersCC.forEach( function ( userEmail ) { + message.to.push( userEmail ); + } ); + } + + if ( config.bcc && email.dump.usersBCC && email.dump.usersBCC.length ) { + message.bcc = email.dump.usersBCC; + } + + return message; + } + } +}; \ No newline at end of file diff --git a/modules/clever-email/models/orm/EmailAttachmentModel.js b/modules/clever-email/models/orm/EmailAttachmentModel.js new file mode 100644 index 0000000..f4526d3 --- /dev/null +++ b/modules/clever-email/models/orm/EmailAttachmentModel.js @@ -0,0 +1,33 @@ +module.exports = function ( sequelize, DataTypes ) { + return sequelize.define( "EmailAttachment", + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + fileName: { + type: DataTypes.STRING, + allowNull: true + }, + filePath: { + type: DataTypes.STRING, + allowNull: false + }, + mimeType: { + type: DataTypes.STRING, + allowNull: true + }, + EmailId: { + type: DataTypes.INTEGER + } + }, + { + paranoid: true, + instanceMethods: { + toJSON: function () { + return this.values; + } + } + } ); +}; \ No newline at end of file diff --git a/modules/clever-email/models/orm/EmailModel.js b/modules/clever-email/models/orm/EmailModel.js new file mode 100644 index 0000000..3391123 --- /dev/null +++ b/modules/clever-email/models/orm/EmailModel.js @@ -0,0 +1,79 @@ +module.exports = function ( sequelize, DataTypes ) { + return sequelize.define( "Email", + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + subject: { + type: DataTypes.STRING, + allowNull: false + }, + body: { + type: DataTypes.TEXT, + allowNull: true + }, + isDelivered: { + type: DataTypes.BOOLEAN, + allowNull: false, + default: true + }, + sentAttemps: { + type: DataTypes.INTEGER, + allowNull: false, + default: 0 + }, + isOpened: { + type: DataTypes.BOOLEAN, + allowNull: false, + default: false + }, + token: { + type: DataTypes.STRING, + allowNull: true + }, + dump: { + type: DataTypes.TEXT, + allowNull: true + }, + EmailTemplateId : { + type: DataTypes.INTEGER + }, + UserId : { + type: DataTypes.INTEGER + }, + AccountId : { + type: DataTypes.INTEGER + } + + }, + { + paranoid: true, + instanceMethods: { + toJSON: function () { + var values = this.values + , userData = {}; + + if ( values.user && values.user.id ) { + userData = { + id: values.user.id, + firstName: values.user.firstname, + lastName: values.user.lastname, + fullName: values.user.fullName, + email: values.user.email, + source: values.user.source + }; + + values.user = userData; + } + + if ( values.dump ) { + values.dump = JSON.parse( JSON.stringify( values.dump ) ); + } + + return values; + } + } + } ); +}; \ No newline at end of file diff --git a/modules/clever-email/models/orm/EmailUserModel.js b/modules/clever-email/models/orm/EmailUserModel.js new file mode 100644 index 0000000..1df4ec1 --- /dev/null +++ b/modules/clever-email/models/orm/EmailUserModel.js @@ -0,0 +1,29 @@ +module.exports = function ( sequelize, DataTypes ) { + return sequelize.define( "EmailUser", + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + status: { + type: DataTypes.ENUM, + values: ['cc', 'bcc'], + allowNull: false + }, + EmailId: { + type: DataTypes.INTEGER + }, + UserId: { + type: DataTypes.INTEGER + } + }, + { + paranoid: true, + instanceMethods: { + toJSON: function () { + return this.values; + } + } + } ); +}; \ No newline at end of file diff --git a/modules/clever-email/module.js b/modules/clever-email/module.js new file mode 100644 index 0000000..a5f6986 --- /dev/null +++ b/modules/clever-email/module.js @@ -0,0 +1,5 @@ +var Module = require( 'classes' ).ModuleClass.extend( { + +} ); + +module.exports = new Module( 'clever-email', injector ); \ No newline at end of file diff --git a/modules/clever-email/package.json b/modules/clever-email/package.json new file mode 100644 index 0000000..0e86feb --- /dev/null +++ b/modules/clever-email/package.json @@ -0,0 +1,25 @@ +{ + "name": "clever-email", + "version": "0.0.1", + "private": true, + "dependencies": { + "q": "", + "lodash": "2.4.1", + "mandrill-api": "*", + "sendgrid": "~0.4.2", + "mailgun-js": "*", + "shortid": "~2.0.0", + "sequelize": "1.7.0-beta8" + }, + "author": { + "name": "Clevertech", + "email": "info@clevertech.biz", + "web": "http://www.clevertech.biz" + }, + "main": "module.js", + "description": "", + "devDependencies": { + "chai": "*", + "mocha": "*" + } +} \ No newline at end of file diff --git a/modules/clever-email/routes.js b/modules/clever-email/routes.js new file mode 100644 index 0000000..7b61587 --- /dev/null +++ b/modules/clever-email/routes.js @@ -0,0 +1,13 @@ +module.exports = function ( + app, + EmailController, + UserController ) +{ + + app.get( '/emails', UserController.requiresLogin, EmailController.attach() ); + app.get( '/emails/:id', UserController.requiresLogin, EmailController.attach() ); + app.post( '/emails', UserController.requiresLogin, EmailController.attach() ); + app['delete']( '/emails/:id', UserController.requiresLogin, EmailController.attach() ); + app.post('/emails/:id/send' , UserController.requiresLogin, EmailController.attach()); + +}; \ No newline at end of file diff --git a/modules/clever-email/schema/seedData.json b/modules/clever-email/schema/seedData.json new file mode 100644 index 0000000..26325bc --- /dev/null +++ b/modules/clever-email/schema/seedData.json @@ -0,0 +1,13 @@ +{ + "EmailModel": [ + { + "subject": "custom subject", + "body": "custom body for testing", + "isDelivered": false, + "isOpened": false, + "token": "45sada45asd", + "dump": "{'companyLogo': 'LOGO','companyName': 'name','fromName': 'from name','fromMail': 'custom@mail.ru','toMail': 'denshikov_vovan@mail.ru'}", + "UserId": 1 + } + ] +} \ No newline at end of file diff --git a/modules/clever-email/services/EmailService.js b/modules/clever-email/services/EmailService.js new file mode 100644 index 0000000..ea40175 --- /dev/null +++ b/modules/clever-email/services/EmailService.js @@ -0,0 +1,329 @@ +var Q = require( 'q' ) + , Sequelize = require( 'sequelize' ) + , shortid = require( 'shortid' ) + , config = require( 'config' ) + , mailer = require( '../lib/mailer' )( config['clever-email'] ) + , _ = require( 'lodash' ) + , EmailService = null; + +module.exports = function ( sequelize, + ORMEmailModel, + ORMEmailAttachmentModel, + ORMUserModel, + ORMEmailUserModel ) { + + if ( EmailService && EmailService.instance ) { + return EmailService.instance; + } + + EmailService = require( 'services' ).BaseService.extend( { + + formatReplyAddress: function ( emailToken ) { + var addr = '' + , envName = ''; + + envName = ( config.environmentName == 'DEV' ) + ? 'dev' + : ( config.environmentName == 'PROD' ) + ? 'prod' + : ( config.environmentName == 'STAGE' ) + ? 'stage' + : 'local'; + + addr = ( envName != 'prod' ) + ? 'reply_' + emailToken + '@' + envName + '.bolthr.clevertech.biz' + : 'reply_' + emailToken + '@app-mail.bolthr.com'; + + return addr; + }, + + formatData: function ( data ) { + var o = { + email: {}, + usersCC: [], + usersBCC: [], + attachments: [], + sender: {} + }; + + var hasTemplate = (/false|true/.test( data.hasTemplate )) ? data.hasTemplate : true + , fullName = data.userFirstName || data.userLastName + ? [ data.userFirstName, data.userLastName ].join( ' ' ) + : config['clever-email'].default.fromName; + + //Email Object + o.email.subject = data.subject || null; + o.email.body = data.body || null; + o.email.token = data.userId + data.to.id + shortid.seed( 10000 ).generate(); + o.email.UserId = data.userId; + o.email.AccountId = data.accId; + o.email.EmailTemplateId = data.EmailTemplateId || null; + o.email.sentAttemps = 0; + o.email.isDelivered = false; + o.email.isOpened = false; + o.email.id = null; + + var emailURL = this.formatReplyAddress( o.email.token ); + + //Dump email dependency data + var dataDump = { + companyLogo: data.accLogo, + companyName: data.accName, + fromName: fullName, + fromMail: emailURL, + toMail: data.to.email, + usersCC: ( data.cc && data.cc.length ) ? data.cc.map( function ( x ) { return x.email } ) : [], + usersBCC: ( data.bcc && data.bcc.length) ? data.bcc.map( function ( x ) { return x.email } ) : [], + tplName: config['clever-email'].default.tplName, + tplTitle: data.subject || config['clever-email'].default.subject, + hasTemplate: hasTemplate + }; + + o.email.dump = JSON.stringify( dataDump ); + + //EmailUsers Object + o.usersCC = ( data.cc && data.cc.length ) ? data.cc : []; + o.usersBCC = ( data.bcc && data.bcc.length) ? data.bcc : []; + + //EmailAttachements Object + o.attachments = ( data.attachments && data.attachments.length ) ? data.attachments : []; + + //EmailSender Object + o.sender.fullName = fullName; + o.sender.email = emailURL; + + //EmailSurvey Object + o.survey = ( data.survey ) ? data.survey : null; + + o.hasTemplate = hasTemplate; + + return o; + }, + + listEmails: function ( userId ) { + var deferred = Q.defer(); + + this + .find( { where: { UserId: userId }, include: [ ORMEmailAttachmentModel ] } ) + .then( deferred.resolve ) + .fail( deferred.reject ); + + return deferred.promise; + }, + + getEmailByIds: function ( userId, emailId ) { + var deferred = Q.defer() + , service = this + , chainer = new Sequelize.Utils.QueryChainer(); + + chainer.add( + ORMEmailModel.find( { + where: { id: emailId, UserId: userId, 'deletedAt': null }, include: [ ORMEmailAttachmentModel ] + } ) + ); + + chainer.add( + ORMEmailUserModel.findAll( { + where: { EmailId: emailId, 'deletedAt': null }, include: [ ORMUserModel ] + } ) + ); + + chainer + .run() + .success( function ( results ) { + + if ( !results[0] ) { + deferred.resolve( {statuscode: 403, message: 'invalid'} ); + return; + } + + var emailJson = JSON.parse( JSON.stringify( results[ 0 ] ) ); + var emailUsers = results[ 1 ]; + + emailJson.users = emailUsers; + deferred.resolve( emailJson ); + + } ) + .error( deferred.reject ); + + return deferred.promise; + }, + + handleEmailCreation: function ( data ) { + var deferred = Q.defer() + , promises = [] + , service = this; + + data.forEach( function ( item ) { + promises.push( service.processEmailCreation( item ) ); + } ); + + Q.all( promises ) + .then( function() { + deferred.resolve( {statuscode: 200, message: 'email is created'} ); + }) + .fail( deferred.reject ); + + return deferred.promise; + }, + + processEmailCreation: function ( emailItem ) { + var deferred = Q.defer() + , service = this + , fData = this.formatData( emailItem ); + + service + .create( fData.email ) + .then( function ( savedEmail ) { + service + .saveEmailAssociation( savedEmail, fData ) + .then( function () { + deferred.resolve(); + } ) + .then( deferred.resolve ) + .fail( deferred.reject ); + } ) + .fail( deferred.reject ); + + return deferred.promise; + }, + + saveEmailAssociation: function ( savedEmail, fData ) { + var deferred = Q.defer() + , chainer = new Sequelize.Utils.QueryChainer(); + + //Users: CC + if ( fData.usersCC.length ) { + var l = fData.usersCC.length + , item + , cc = []; + while ( l-- ) { + item = fData.usersCC[l]; + cc.push( { + EmailId: savedEmail.id, UserId: item.id, status: 'cc' + } ); + } + + chainer.add( ORMEmailUserModel.bulkCreate( cc ) ); + } + + //Users: BCC + if ( fData.usersBCC.length ) { + var l = fData.usersBCC.length + , itm + , bcc = []; + + while ( l-- ) { + itm = fData.usersBCC[l]; + bcc.push( { + EmailId: savedEmail.id, UserId: itm.id, status: 'bcc' + } ); + } + + chainer.add( ORMEmailUserModel.bulkCreate( bcc ) ); + } + + //Attachments + if ( fData.attachments.length ) { + var l = fData.attachments.length + , attch + , emailDocs = []; + + while ( l-- ) { + attch = fData.attachments[l]; + emailDocs.push( { + id: null, + filePath: attch.filePath, + fileName: attch.fileName, + mimeType: attch.mimeType, + EmailId: savedEmail.id + } ); + } + + chainer.add( ORMEmailAttachmentModel.bulkCreate( emailDocs ) ); + } + + chainer + .run() + .success( function ( result ) { + deferred.resolve(result); + } ) + .error( function ( err ) { + deferred.reject( err ); + } ); + + return deferred.promise; + }, + + handleEmailSending: function ( userId, emailId, type ) { + var deferred = Q.defer() + , service = this; + + service + .getEmailByIds( userId, emailId ) + .then( function( result ) { + + if ( !!result && !!result.id && !!result.body ) { + + service + .sendEmail( result, result.body, type ) + .then( deferred.resolve ) + .fail( deferred.reject ); + + } else { + deferred.resolve( result ) + } + }) + .fail( deferred.reject ); + + return deferred.promise; + }, + + sendEmail: function ( email, body, type ) { + var deferred = Q.defer(); + + email.dump = _.isPlainObject( email.dump ) + ? email.dump + : JSON.parse( email.dump ); + + mailer.send( email, body, type ) + .then( deferred.resolve ) + .fail( deferred.reject ); + + return deferred.promise; + }, + + deleteEmail: function( userId, emailId ) { + var deferred = Q.defer() + , service = this; + + service + .getEmailByIds( userId, emailId ) + .then( function( result ) { + + if ( !!result && !!result.id ) { + + service + .destroy( emailId ) + .then( function() { + deferred.resolve( { statuscode: 200, message: 'email is deleted'} ) + }) + .fail( deferred.reject ); + + } else { + deferred.resolve( result ) + } + }) + .fail( deferred.reject ); + + return deferred.promise; + + } + + } ); + + EmailService.instance = new EmailService( sequelize ); + EmailService.Model = ORMEmailModel; + + return EmailService.instance; +}; \ No newline at end of file diff --git a/modules/clever-email/tests/unit/test.controllers.EmailController.js b/modules/clever-email/tests/unit/test.controllers.EmailController.js new file mode 100644 index 0000000..bffbef1 --- /dev/null +++ b/modules/clever-email/tests/unit/test.controllers.EmailController.js @@ -0,0 +1,481 @@ +// Bootstrap the testing environmen +var testEnv = require( 'utils' ).testEnv(); + +var expect = require( 'chai' ).expect + , Q = require ( 'q' ) + , Service; + +var userId = 1000, emailId; + +describe( 'controllers.EmailController', function () { + var ctrl; + + before( function ( done ) { + testEnv( function ( EmailController, EmailService ) { + var req = { + params: { action: 'fakeAction'}, + method: 'GET', + query: {} + }; + + var res = { + json: function () {} + }; + + var next = function () {}; + + ctrl = new EmailController( req, res, next ); + + Service = EmailService; + + done(); + } ); + } ); + + + describe( '.postAction()', function () { + + it( 'should be able to create emails with associations', function ( done ) { + + var email_1 = { + title: 'some title #2', + subject: 'some subject #2', + body: 'some body #2', + to: { + id: 15, + email: 'denshikov_vovan@mail.ru' + }, + hasTemplate: false, + cc: [ { id: 1500, email:'cc1@mail.ru' }, { id: userId, email: 'cc2@mail.ru' } ], + bcc: [ { id: 1600, email: 'bcc1@mail.com' }, { id: 1700, email: 'bcc2@mail.com' } ], + EmailTemplateId: 45 + }; + + var email_2 = { + title: 'some title #3', + subject: 'some subject #3', + body: 'some body #3', + to: { + id: 15, + email: 'to1@email.za' + }, + hasTemplate: false, + cc: [ { id: 1500, email:'cc1@mail.ru' }, { id: 1800, email: 'cc2@mail.ru' } ], + bcc: [ { id: 1600, email: 'bcc1@mail.com' }, { id: 1700, email: 'bcc2@mail.com' } ], + EmailTemplateId: 45 + }; + + + ctrl.send = function ( data, status ) { + + expect( status ).to.equal( 200 ); + + var promise = [ + Service.find( { where: { subject: email_1.subject, body: email_1.body } } ), + Service.find( { where: { subject: email_2.subject, body: email_2.body } } ) + ]; + + Q.all( promise ) + .then( function ( emails ) { + + emails[0] = emails[0][0]; + emails[1] = emails[1][0]; + + expect( emails ).to.be.an( 'array' ).and.have.length( 2 ); + + expect( emails[0] ).to.be.an( 'object' ); + expect( emails[0] ).to.have.property( 'id' ).and.be.ok; + expect( emails[0] ).to.have.property( 'subject' ).and.equal( email_1.subject ); + + expect( emails[1] ).to.be.an( 'object' ); + expect( emails[1] ).to.have.property( 'id' ).and.be.ok; + expect( emails[1] ).to.have.property( 'subject' ).and.equal( email_2.subject ); + + emailId = emails[0].id; + + done(); + + }, done ); + }; + ctrl.req.body = [ email_1, email_2 ]; + ctrl.req.user = { + id: userId, + firstname: 'Vitalis', + lastname: 'Popkin', + account: { + id:1, + logo: 'LOGO', + name: "AccountName" + } + }; + ctrl.postAction(); + } ); + + } ); + + describe( '.listAction()', function () { + + it( 'should be able to find all emails by userId', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'array' ).and.not.empty; + expect( result ).to.have.length.above( 1 ); + + expect( result[0] ).to.be.an( 'object' ); + expect( result[0] ).to.have.property( 'id' ).and.be.ok; + expect( result[0] ).to.have.property( 'subject' ).and.be.ok; + expect( result[0] ).to.have.property( 'body' ).and.be.ok; + expect( result[0] ).to.have.property( 'UserId' ).and.equal( userId ); + expect( result[0] ).to.have.property( 'emailAttachments' ).and.be.an( 'array' ).and.be.empty; + + done(); + }; + + ctrl.req.user = { + id: userId, + firstname: 'Vitalis', + lastname: 'Popkin', + account: { + id:1, + logo: 'LOGO', + name: "AccountName" + } + }; + + ctrl.listAction(); + } ); + + it( 'should be able to give us an empty array if no emails for userId', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'array' ).and.be.empty; + + done(); + }; + + ctrl.req.user = { + id: userId+1000, + firstname: 'Vitalis', + lastname: 'Popkin', + account: { + id:1, + logo: 'LOGO', + name: "AccountName" + } + }; + + ctrl.listAction(); + } ); + + } ); + + describe( '.getAction()', function () { + + it( 'should be able to find email bu emailId for user with userId', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'object' ); + expect( result ).to.contain.keys( 'emailAttachments' ); + + expect( result ).to.have.property( 'id' ).and.equal( emailId ); + expect( result ).to.have.property( 'subject' ).and.equal( 'some subject #2' ); + expect( result ).to.have.property( 'body' ).and.equal( 'some body #2' ); + expect( result ).to.have.property( 'isDelivered' ).and.equal( false ); + expect( result ).to.have.property( 'sentAttemps' ).and.equal( 0 ); + expect( result ).to.have.property( 'isOpened' ).and.equal( false ); + expect( result ).to.have.property( 'token' ).and.be.ok; + expect( result ).to.have.property( 'EmailTemplateId' ).and.equal( 45 ); + expect( result ).to.have.property( 'UserId' ).and.equal( userId ); + expect( result ).to.have.property( 'AccountId' ).and.equal( 1 ); + expect( result ).to.have.property( 'dump' ).and.be.ok; + expect( result.emailAttachments ).to.be.an( 'array' ).and.be.empty; + + expect( result ).to.have.property( 'users' ).to.be.an( 'array' ).and.have.length( 4 ); + expect( result.users[0] ).to.have.property( 'id' ).and.be.ok; + expect( result.users[0] ).to.have.property( 'status' ).and.be.ok; + expect( result.users[0] ).to.have.property( 'UserId' ).and.be.ok; + expect( result.users[0] ).to.have.property( 'EmailId' ).and.equal( emailId ); + + done(); + }; + + ctrl.req.params = { id: emailId }; + + ctrl.req.user = { + id: userId, + firstname: 'Vitalis', + lastname: 'Popkin', + account: { + id:1, + logo: 'LOGO', + name: "AccountName" + } + }; + + ctrl.getAction(); + } ); + + it( 'should be able to get the error if the emailId does not exist', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: 151515 }; + + ctrl.req.user = { + id: userId, + firstname: 'Vitalis', + lastname: 'Popkin', + account: { + id:1, + logo: 'LOGO', + name: "AccountName" + } + }; + + ctrl.getAction(); + } ); + + it( 'should be able to get the error if email does not belong to the user', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: emailId }; + + ctrl.req.user = { + id: 1212, + firstname: 'Vitalis', + lastname: 'Popkin', + account: { + id:1, + logo: 'LOGO', + name: "AccountName" + } + }; + + ctrl.getAction(); + } ); + + } ); + + describe( '.putAction()', function () { + + it( 'should be able to get the error if we call the putAction', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.putAction(); + } ); + + } ); + + describe( '.sendAction()', function () { + + it.skip( 'should be able to send email', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'array' ).and.is.not.empty; + expect( result ).to.have.length.above( 1 ); + expect( result[0] ).to.have.property( 'status' ).and.equal( 'sent' ); + expect( result[0] ).to.have.property( 'id' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: emailId }; + + ctrl.req.body = { type: 'html' }; + + ctrl.req.user = { + id: userId, + firstname: 'Vitalis', + lastname: 'Popkin', + account: { + id: 1, + logo: 'LOGO', + name: "AccountName" + } + }; + + ctrl.sendAction(); + } ); + + it( 'should be able to get the error if the emailId does not exist', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: 15151515151515 }; + + ctrl.req.body = { type: 'html' }; + + ctrl.req.user = { + id: userId, + firstname: 'Vitalis', + lastname: 'Popkin', + account: { + id: 1, + logo: 'LOGO', + name: "AccountName" + } + }; + + ctrl.sendAction(); + } ); + + it( 'should be able to give us an empty array if no emails for userId', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: emailId }; + + ctrl.req.body = { type: 'html' }; + + ctrl.req.user = { + id: userId + 10, + firstname: 'Vitalis', + lastname: 'Popkin', + account: { + id: 1, + logo: 'LOGO', + name: "AccountName" + } + }; + + ctrl.sendAction(); + } ); + + } ); + + describe( '.deleteAction()', function () { + + it( 'should be able to get the error if the emailId does not exist', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: 15151515515 }; + + ctrl.req.user = { + id: userId, + firstname: 'Vitalis', + lastname: 'Popkin', + account: { + id:1, + logo: 'LOGO', + name: "AccountName" + } + }; + + ctrl.deleteAction(); + } ); + + it( 'should be able to get the error if email does not belong to the user', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 403 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: emailId }; + + ctrl.req.user = { + id: userId + 10, + firstname: 'Vitalis', + lastname: 'Popkin', + account: { + id:1, + logo: 'LOGO', + name: "AccountName" + } + }; + + ctrl.deleteAction(); + } ); + + it( 'should be able to delete email by emailId for user with userId', function ( done ) { + + ctrl.send = function ( result, status ) { + + expect( status ).to.equal( 200 ); + + expect( result ).to.be.an( 'string' ).and.be.ok; + + done(); + }; + + ctrl.req.params = { id: emailId }; + + ctrl.req.user = { + id: userId, + firstname: 'Vitalis', + lastname: 'Popkin', + account: { + id:1, + logo: 'LOGO', + name: "AccountName" + } + }; + + ctrl.deleteAction(); + } ); + + } ); + + +} ); \ No newline at end of file diff --git a/modules/clever-email/tests/unit/test.service.EmailService.js b/modules/clever-email/tests/unit/test.service.EmailService.js new file mode 100644 index 0000000..849cf0e --- /dev/null +++ b/modules/clever-email/tests/unit/test.service.EmailService.js @@ -0,0 +1,673 @@ +var expect = require ( 'chai' ).expect + , request = require ( 'supertest' ) + , path = require( 'path' ) + , app = require ( path.resolve( __dirname + '/../../../../' ) + '/index.js' ) + , config = require( 'config' ) + , testEnv = require ( 'utils' ).testEnv() + , Q = require ( 'q' ); + +describe( 'service.EmailService', function () { + var Service, Model, UserModel, EmailUserModel; + + var userId, userId_1, userId_2, userId_3, userId_4 + , emailId_1, emailId_1_token, emailId_2; + + before( function ( done ) { + testEnv( function ( _EmailService_, _EmailModel_, _UserModel_, _EmailUserModel_ ) { + + Service = _EmailService_; + Model = _EmailModel_; + UserModel = _UserModel_; + EmailUserModel = _EmailUserModel_; + + var user = { username: 'sender', email: 'sender@mail.ru', password: '1234' }; + + UserModel + .create( user ) + .success( function( sender ) { + + expect( sender ).to.be.an( 'object' ); + expect( sender ).to.have.property( 'id' ).and.be.ok; + expect( sender ).to.have.property( 'username' ).and.equal( user.username ); + + userId = sender.id; + + done(); + }) + .error( done ); + + + }, done ); + } ); + + describe( '.formatReplyAddress( emailToken )', function () { + + it( 'should return an address according to environmentName', function ( done ) { + + var emailToken = '15da5AS15A1s'; + + var addr = Service.formatReplyAddress( emailToken ); + + if ( config.environmentName == 'DEV' ){ + expect( addr ).to.equal( 'reply_15da5AS15A1s@dev.bolthr.clevertech.biz' ); + } else if ( config.environmentName == 'PROD' ) { + expect( addr ).to.equal( 'reply_15da5AS15A1s@app-mail.bolthr.com' ); + } else if ( config.environmentName == 'STAGE' ) { + expect( addr ).to.equal( 'reply_15da5AS15A1s@stage.bolthr.clevertech.biz' ); + } else { + expect( addr ).to.equal( 'reply_15da5AS15A1s@local.bolthr.clevertech.biz' ); + } + + done(); + } ); + + } ); + + describe( '.formatData( data )', function () { + + it( 'should return an object with filtered data', function ( done ) { + + var data = { + title: 'some title', + subject: 'some subject', + body: 'some body', + userId: 5, + accId: 1, + to: { + id: 15, + email: 'to1@email.za' + }, + hasTemplate: false, + cc: [ { email:'cc1@mail.ru' }, { email: 'cc2@mail.ru' } ], + bcc: [ { email: 'bcc1@mail.com' }, { email: 'bcc2@mail.com' } ], + accLogo: 'LoGo', + accName: 'Default Name', + userFirstName: 'Dmitrij', + userLastName: 'Safronov', + EmailTemplateId: 45 + }; + + var emailData = Service.formatData( data ); + + expect( emailData ).to.be.an( 'object' ); + expect( emailData ).to.contain.keys( 'email', 'usersCC', 'usersBCC', 'attachments', 'survey', 'sender', 'hasTemplate' ); + + expect( emailData.email ).to.be.an( 'object' ); + expect( emailData.email ).to.have.property( 'subject' ).and.equal( data.subject ); + expect( emailData.email ).to.have.property( 'body' ).and.equal( data.body ); + expect( emailData.email ).to.have.property( 'token' ); + expect( emailData.email ).to.have.property( 'UserId' ).and.equal( data.userId ); + expect( emailData.email ).to.have.property( 'AccountId' ).and.equal( data.accId ); + expect( emailData.email ).to.have.property( 'EmailTemplateId' ).and.equal( data.EmailTemplateId ); + expect( emailData.email ).to.have.property( 'sentAttemps' ).and.equal( 0 ); + expect( emailData.email ).to.have.property( 'isDelivered' ).and.equal( false ); + expect( emailData.email ).to.have.property( 'isOpened' ).and.equal( false ); + expect( emailData.email ).to.have.property( 'id' ).and.not.ok; + + expect( emailData.email ).to.have.property( 'dump' ); + + var dump = JSON.parse( emailData.email.dump ); + + expect( dump ).to.have.property( 'companyLogo' ).and.equal( data.accLogo ); + expect( dump ).to.have.property( 'companyName' ).and.equal( data.accName ); + expect( dump ).to.have.property( 'fromName' ).and.equal( [ data.userFirstName, data.userLastName ].join( ' ' ) ); + expect( dump ).to.have.property( 'fromMail' ).and.be.ok; + expect( dump ).to.have.property( 'toMail' ).and.equal( data.to.email ); + expect( dump ).to.have.property( 'usersCC' ).that.that.is.an( 'array' ); + expect( dump ).to.have.property( 'usersBCC' ).that.that.is.an( 'array' ); + expect( dump ).to.have.property( 'tplName' ).and.equal( 'default' ); + expect( dump ).to.have.property( 'tplTitle' ).and.equal( data.subject ); + expect( dump ).to.have.property( 'hasTemplate' ).and.equal( data.hasTemplate ); + + expect( dump.usersCC ).to.have.length( data.cc.length ); + expect( dump.usersCC[0] ).to.equal( data.cc[0].email ); + expect( dump.usersCC[1] ).to.equal( data.cc[1].email ); + + expect( dump.usersBCC ).to.have.length( data.bcc.length ); + expect( dump.usersBCC[0] ).to.equal( data.bcc[0].email ); + expect( dump.usersBCC[1] ).to.equal( data.bcc[1].email ); + + expect( emailData.usersCC ).to.be.an( 'array' ); + expect( emailData.usersCC ).to.have.length( data.cc.length ); + expect( emailData.usersCC[0] ).to.be.an( 'object' ); + expect( emailData.usersCC[0] ).to.have.property( 'email' ).and.equal( data.cc[0].email ); + + expect( emailData.usersBCC ).to.be.an( 'array' ); + expect( emailData.usersBCC ).to.have.length( data.bcc.length ); + expect( emailData.usersBCC[0] ).to.be.an( 'object' ); + expect( emailData.usersBCC[0] ).to.have.property( 'email' ).and.equal( data.bcc[0].email ); + + expect( emailData.attachments ).to.be.an( 'array' ); + expect( emailData.attachments ).to.be.empty; + + expect( emailData.sender ).to.be.an( 'object' ); + expect( emailData.sender ).to.have.property( 'fullName' ).and.equal( [ data.userFirstName, data.userLastName ].join( ' ' ) ); + expect( emailData.sender ).to.have.property( 'email' ).and.be.ok; + + expect( emailData.survey ).to.not.be.ok; + + expect( emailData.hasTemplate ).to.equal( data.hasTemplate ); + + done(); + } ); + + } ); + + describe( '.saveEmailAssociation( savedEmail, fData )', function () { + + before( function ( done ) { + var users = [] + , promise = []; + users[0] = { username: 'pavlick', email: 'pavlicl@mail.ru', password: '1234' }; + users[1] = { username: 'mishka', email: 'mishka@mail.ru', password: '1234' }; + users[2] = { username: 'vovka', email: 'vovka@mail.ru', password: '1234' }; + users[3] = { username: 'petka', email: 'petka@mail.ru', password: '1234' }; + + users.forEach( function( user ) { + promise.push( UserModel.create( user ) ) + }); + + Q.all( promise ) + .then( function( users ) { + expect( users ).to.be.an( 'array' ).and.have.length( 4 ); + expect( users[0] ).to.be.an( 'object' ); + expect( users[0] ).to.have.property( 'id' ).and.be.ok; + expect( users[0] ).to.have.property( 'username' ).and.equal( users[0].username ); + expect( users[1] ).to.be.an( 'object' ); + expect( users[1] ).to.have.property( 'id' ).and.be.ok; + expect( users[1] ).to.have.property( 'username' ).and.equal( users[1].username ); + expect( users[2] ).to.be.an( 'object' ); + expect( users[2] ).to.have.property( 'id' ).and.be.ok; + expect( users[2] ).to.have.property( 'username' ).and.equal( users[2].username ); + expect( users[3] ).to.be.an( 'object' ); + expect( users[3] ).to.have.property( 'id' ).and.be.ok; + expect( users[3] ).to.have.property( 'username' ).and.equal( users[3].username ); + + userId_1 = users[0].id; + userId_2 = users[1].id; + userId_3 = users[2].id; + userId_4 = users[3].id; + + done(); + }) + .fail( done ); + + } ); + + it( 'should be able to save email associations', function ( done ) { + var data = { + title: 'some title', + subject: 'some subject', + body: 'some body', + userId: userId, + accId: 1, + to: { + id: 15, + email: 'to1@email.za' + }, + hasTemplate: false, + cc: [ { id: userId_1, email:'cc1@mail.ru' }, { id: userId_2, email: 'cc2@mail.ru' } ], + bcc: [ { id: userId_3, email: 'bcc1@mail.com' }, { id: userId_4, email: 'bcc2@mail.com' } ], + accLogo: 'LoGo', + accName: 'Default Name', + userFirstName: 'Dmitrij', + userLastName: 'Safronov', + EmailTemplateId: 45 + }; + + var emailData = Service.formatData( data ); + + Model + .create( emailData.email ) + .success( function( email ) { + + expect( email ).to.be.an( 'object' ); + expect( email ).to.have.property( 'id' ).and.be.ok; + expect( email ).to.have.property( 'subject' ).and.equal( data.subject ); + + emailId_1 = email.id; + emailId_1_token = email.token + + Service + .saveEmailAssociation( email, emailData ) + .then( function( res ) { + + EmailUserModel + .findAll( { where: { EmailId: email.id, UserId: userId_1 } } ) + .success( function( res ) { + + expect( res ).to.be.an( 'array' ).and.have.length( 1 ); + expect( res[0] ).to.be.an( 'object' ); + expect( res[0] ).to.have.property( 'id' ).and.be.ok; + expect( res[0] ).to.have.property( 'status' ).and.equal( 'cc' ); + + EmailUserModel + .findAll( { where: { EmailId: email.id, UserId: userId_4 } } ) + .success( function( res ) { + + expect( res ).to.be.an( 'array' ).and.have.length( 1 ); + expect( res[0] ).to.be.an( 'object' ); + expect( res[0] ).to.have.property( 'id' ).and.be.ok; + expect( res[0] ).to.have.property( 'status' ).and.equal( 'bcc' ); + + done(); + }) + .error( done ); + }) + .error( done ); + }) + .fail( done ) + + }) + } ); + + } ); + + describe( '.processEmailCreation( emailItem )', function () { + + it( 'should be able to create email with associations', function ( done ) { + var data = { + title: 'some title #1', + subject: 'some subject #1', + body: 'some body #1', + userId: userId, + accId: 1, + to: { + id: 15, + email: 'denshikov_vovan@mail.ru' + }, + hasTemplate: false, + cc: [ { id: userId_1, email:'cc1@mail.ru' }, { id: userId_2, email: 'cc2@mail.ru' } ], + bcc: [ { id: userId_3, email: 'bcc1@mail.com' }, { id: userId_4, email: 'bcc2@mail.com' } ], + accLogo: 'LoGo', + accName: 'Default Name', + userFirstName: 'Dmitrij', + userLastName: 'Safronov', + EmailTemplateId: 45 + }; + + Service + .processEmailCreation( data ) + .then( function() { + + Model + .find( { where: { subject: data.subject, body: data.body } } ) + .success( function( email ) { + + expect( email ).to.be.an( 'object' ); + expect( email ).to.have.property( 'id' ).and.be.ok; + expect( email ).to.have.property( 'subject' ).and.equal( data.subject ); + + emailId_2 = email.id; + + EmailUserModel + .findAll( { where: { EmailId: email.id, UserId: userId_1 } } ) + .success( function( res ) { + + expect( res ).to.be.an( 'array' ).and.have.length( 1 ); + expect( res[0] ).to.be.an( 'object' ); + expect( res[0] ).to.have.property( 'id' ).and.be.ok; + expect( res[0] ).to.have.property( 'status' ).and.equal( 'cc' ); + + EmailUserModel + .findAll( { where: { EmailId: email.id, UserId: userId_4 } } ) + .success( function( res ) { + + expect( res ).to.be.an( 'array' ).and.have.length( 1 ); + expect( res[0] ).to.be.an( 'object' ); + expect( res[0] ).to.have.property( 'id' ).and.be.ok; + expect( res[0] ).to.have.property( 'status' ).and.equal( 'bcc' ); + + done(); + }) + .error( done ); + }) + .error( done ); + }) + .error( done ); + }, done ) + + } ); + + } ); + + describe( '.handleEmailCreation( data )', function () { + + it( 'should be able to create emails with associations', function ( done ) { + var email_1 = { + title: 'some title #2', + subject: 'some subject #2', + body: 'some body #2', + userId: userId, + accId: 1, + to: { + id: 15, + email: 'to1@email.za' + }, + hasTemplate: false, + cc: [ { id: userId_1, email:'cc1@mail.ru' }, { id: userId, email: 'cc2@mail.ru' } ], + bcc: [ { id: userId_3, email: 'bcc1@mail.com' }, { id: userId_4, email: 'bcc2@mail.com' } ], + accLogo: 'LoGo', + accName: 'Default Name', + userFirstName: 'Dmitrij', + userLastName: 'Safronov', + EmailTemplateId: 45 + }; + + var email_2 = { + title: 'some title #3', + subject: 'some subject #3', + body: 'some body #3', + userId: 3, + accId: 1, + to: { + id: 15, + email: 'to1@email.za' + }, + hasTemplate: false, + cc: [ { id: userId_1, email:'cc1@mail.ru' }, { id: userId_2, email: 'cc2@mail.ru' } ], + bcc: [ { id: userId_3, email: 'bcc1@mail.com' }, { id: userId_4, email: 'bcc2@mail.com' } ], + accLogo: 'LoGo', + accName: 'Default Name', + userFirstName: 'Dmitrij', + userLastName: 'Safronov', + EmailTemplateId: 45 + }; + + Service + .handleEmailCreation( [ email_1, email_2 ] ) + .then( function() { + + var promise = [ + Model.find( { where: { subject: email_1.subject, body: email_1.body } } ), + Model.find( { where: { subject: email_2.subject, body: email_2.body } } ) + ]; + + Q.all( promise ) + .then( function( emails ) { + + expect( emails ).to.be.an( 'array' ).and.have.length( 2 ); + + expect( emails[0] ).to.be.an( 'object' ); + expect( emails[0] ).to.have.property( 'id' ).and.be.ok; + expect( emails[0] ).to.have.property( 'subject' ).and.equal( email_1.subject ); + + expect( emails[1] ).to.be.an( 'object' ); + expect( emails[1] ).to.have.property( 'id' ).and.be.ok; + expect( emails[1] ).to.have.property( 'subject' ).and.equal( email_2.subject ); + + done(); + + }, done ); + }, done ) + } ); + + } ); + + describe( '.getEmailByIds( userId, emailId )', function () { + + it( 'should be able to find email by emailId for user with userId', function ( done ) { + + Service + .getEmailByIds( userId, emailId_1 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.contain.keys( 'emailAttachments' ); + + expect( result ).to.have.property( 'id' ).and.equal( emailId_1 ); + expect( result ).to.have.property( 'subject' ).and.equal( 'some subject' ); + expect( result ).to.have.property( 'body' ).and.equal( 'some body' ); + expect( result ).to.have.property( 'isDelivered' ).and.equal( false ); + expect( result ).to.have.property( 'sentAttemps' ).and.equal( 0 ); + expect( result ).to.have.property( 'isOpened' ).and.equal( false ); + expect( result ).to.have.property( 'token' ).and.be.ok; + expect( result ).to.have.property( 'EmailTemplateId' ).and.equal( 45 ); + expect( result ).to.have.property( 'UserId' ).and.equal( userId ); + expect( result ).to.have.property( 'AccountId' ).and.equal( 1 ); + expect( result ).to.have.property( 'dump' ).and.be.ok; + expect( result.emailAttachments ).to.be.an( 'array' ).and.be.empty; + + expect( result ).to.have.property( 'users' ).to.be.an( 'array' ).and.have.length( 4 ); + expect( result.users[0] ).to.have.property( 'id' ).and.be.ok; + expect( result.users[0] ).to.have.property( 'status' ).and.be.ok; + expect( result.users[0] ).to.have.property( 'UserId' ).and.be.ok; + expect( result.users[0] ).to.have.property( 'EmailId' ).and.equal( emailId_1 ); + expect( result.users[0].user ).to.be.an( 'object' ).and.not.be.empty; + + done(); + }, done ) + } ); + + it( 'should be able to get the error if the emailId does not exist', function ( done ) { + + Service + .getEmailByIds( userId, 100000 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + }, done ) + } ); + + it( 'should be able to get the error if email does not belong to the user', function ( done ) { + + Service + .getEmailByIds( userId_1, emailId_1 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + }, done ) + } ); + + } ); + + describe( '.listEmails( userId )', function () { + + it( 'should be able to find all emails by userId', function ( done ) { + + Service + .listEmails( userId ) + .then( function( result ) { + + expect( result ).to.be.an( 'array' ).and.not.empty; + expect( result ).to.have.length.above( 1 ); + + expect( result[0] ).to.be.an( 'object' ); + expect( result[0] ).to.have.property( 'id' ).and.be.ok; + expect( result[0] ).to.have.property( 'subject' ).and.be.ok; + expect( result[0] ).to.have.property( 'body' ).and.be.ok; + expect( result[0] ).to.have.property( 'UserId' ).and.equal( userId ); + expect( result[0] ).to.have.property( 'emailAttachments' ).and.be.an( 'array' ).and.be.empty; + + done(); + }, done ) + } ); + + it( 'should be able to give us an empty array if no emails for userId', function ( done ) { + + Service + .listEmails( 10000 ) + .then( function( result ) { + + expect( result ).to.be.an( 'array' ).and.be.empty; + + done(); + }, done ) + } ); + + } ); + + describe.skip( '.sendEmail( email, body, type )', function () { + + it( 'should be able to send email', function ( done ) { + var data = { + title: 'some title', + subject: 'some subject', + body: 'this is the test mail', + userId: 1, + accId: 1, + to: { + id: 15, + email: 'denshikov_vovan@mail.ru' + }, + hasTemplate: false, + cc: [], + bcc: [], + accLogo: 'LoGo', + accName: 'Default Name', + userFirstName: 'Dmitrij', + userLastName: 'Safronov', + EmailTemplateId: 45 + }; + + var email = Service.formatData( data ).email; + + Service + .sendEmail( email, '
hi. it is the first email
', 'text' ) + .then( function( result ) { + + expect( result ).to.be.an( 'array' ).and.is.not.empty; + expect( result ).to.have.length( 1 ); + expect( result[0] ).to.have.property( 'status' ).and.equal( 'sent' ); + expect( result[0] ).to.have.property( 'id' ).and.be.ok; + + done(); + }, done ) + } ); + + } ); + + describe( '.handleEmailSending( userId, emailId, type )', function () { + + it.skip( 'should be able to send email', function ( done ) { + + var type = 'html'; + + Service + .handleEmailSending( userId, emailId_1, type ) + .then( function( result ) { + + expect( result ).to.be.an( 'array' ).and.is.not.empty; + expect( result ).to.have.length.above( 1 ); + expect( result[0] ).to.have.property( 'status' ).and.equal( 'sent' ); + expect( result[0] ).to.have.property( 'id' ).and.be.ok; + + done(); + }, done ) + } ); + + it( 'should be able to get the error if the emailId does not exist', function ( done ) { + + var type = 'html'; + + Service + .handleEmailSending( userId, 1000, type ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + }, done ) + } ); + + it( 'should be able to give us an empty array if no emails for userId', function ( done ) { + + var type = 'html'; + + Service + .handleEmailSending( userId_1, emailId_1, type ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + done(); + }, done ) + } ); + + } ); + + describe( '.deleteEmail( userId, emailId )', function () { + + it( 'should be able to get the error if the emailId does not exist', function ( done ) { + + Service + .deleteEmail( userId, 151515 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + Service + .findById( emailId_1 ) + .then( function( email ) { + + expect( email ).to.be.an( 'object' ).and.be.ok; + expect( email ).to.have.property( 'id' ).and.equal( emailId_1 ); + + done(); + }) + .fail( done ); + }, done ) + } ); + + it( 'should be able to get the error if email does not belong to the user', function ( done ) { + + Service + .deleteEmail( userId+15, emailId_1 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 403 ); + expect( result ).to.have.property( 'message' ).and.be.ok; + + Service + .findById( emailId_1 ) + .then( function( email ) { + + expect( email ).to.be.an( 'object' ).and.be.ok; + expect( email ).to.have.property( 'id' ).and.equal( emailId_1 ); + + done(); + }) + .fail( done ); + }, done ) + } ); + + it( 'should be able to delete email by emailId for user with userId', function ( done ) { + + Service + .deleteEmail( userId, emailId_1 ) + .then( function( result ) { + + expect( result ).to.be.an( 'object' ); + expect( result ).to.have.property( 'statuscode' ).and.equal( 200 ); + + Service + .findById( emailId_1 ) + .then( function( email ) { + + expect( email ).to.not.be.ok; + + done(); + }) + .fail( done ); + }, done ) + } ); + + } ); + + +} ); \ No newline at end of file diff --git a/modules/example-module/tests/integration/test.api.example.js b/modules/example-module/tests/integration/test.api.example.js index 968280a..3fc9704 100644 --- a/modules/example-module/tests/integration/test.api.example.js +++ b/modules/example-module/tests/integration/test.api.example.js @@ -2,7 +2,7 @@ var expect = require ( 'chai' ).expect , request = require ( 'supertest' ) , path = require( 'path' ) , app = require ( path.resolve( __dirname + '/../../../../' ) + '/index.js' ); -console.dir(app) +//console.dir(app) describe ( '/example', function () { describe ( 'POST /example', function () { it ( 'should return valid status', function ( done ) { @@ -11,7 +11,7 @@ describe ( '/example', function () { .expect ( 'Content-Type', /json/ ) .expect ( 200 ) .end ( function ( err, res ) { - console.dir( arguments) +// console.dir( arguments) if ( err ) return done ( err ); expect ( res.body ).to.eql ( { status: 'Created record!' diff --git a/modules/orm/bin/seedModels.js b/modules/orm/bin/seedModels.js index c1ca5ac..a32aadc 100644 --- a/modules/orm/bin/seedModels.js +++ b/modules/orm/bin/seedModels.js @@ -73,7 +73,7 @@ async.forEachSeries( // Handle hasOne if ( typeof model[funcName] !== 'function' ) { - funcName = 'set' + assocModelName; + funcName = 'set' + assocModelName.replace( "Model", "" ); associations = associations[0]; } diff --git a/modules/orm/config/default.json b/modules/orm/config/default.json index 20ea3de..69dbf11 100644 --- a/modules/orm/config/default.json +++ b/modules/orm/config/default.json @@ -1,9 +1,9 @@ { "orm": { "db": { - "username": "john", - "password": "secret", - "database": "nodeseed", + "username": "root", + "password": "qqq", + "database": "clevertech", "options": { "host": "localhost", "dialect": "mysql", @@ -11,7 +11,7 @@ } }, "modelAssociations": { - "Example": {} + } } } \ No newline at end of file diff --git a/package.json b/package.json index d053b0c..9fedc59 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,8 @@ "bundledDependencies": [ "orm", "odm", - "background-tasks", - "auth", - "example-module" + "clever-auth", + "clever-auth-twitter", + "clever-auth-facebook" ] }