From 102078dda98d2db9aacb9eb67d5d45ed0dbb1af9 Mon Sep 17 00:00:00 2001 From: Tyler Henkel Date: Mon, 25 Aug 2014 01:06:36 -0600 Subject: [PATCH 01/20] use more restrictive matching for serve-favicon version --- app/templates/_package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/_package.json b/app/templates/_package.json index 36256461c..e32849740 100644 --- a/app/templates/_package.json +++ b/app/templates/_package.json @@ -7,7 +7,7 @@ "morgan": "~1.0.0", "body-parser": "~1.5.0", "method-override": "~1.0.0", - "serve-favicon": "^2.0.1", + "serve-favicon": "~2.0.1", "cookie-parser": "~1.0.1", "express-session": "~1.0.2", "errorhandler": "~1.0.0", From 6aecdf7fc8929fe4853280d0e8ac878f3e2a20fd Mon Sep 17 00:00:00 2001 From: Patrick Baker Date: Tue, 26 Aug 2014 18:01:38 -0400 Subject: [PATCH 02/20] feat(auth): make crypto async - Update tests and add new test for changed password - User.authenticate() User.makeSalt() and User.encryptPassword() public API remains same - User.authenticate() User.makeSalt() and User.encryptPassword() callbacks are optional - Change User schema from hashedPassword to password - Remove unnecessary virtual attribute User.password, getters and setters are not async --- app/templates/server/.jshintrc | 1 + .../server/api/user(auth)/user.controller.js | 26 +++-- .../server/api/user(auth)/user.model.js | 104 +++++++++++++----- .../server/api/user(auth)/user.model.spec.js | 49 +++++++-- .../server/auth(auth)/local/passport.js | 14 ++- 5 files changed, 141 insertions(+), 53 deletions(-) diff --git a/app/templates/server/.jshintrc b/app/templates/server/.jshintrc index d7b958e7c..42fea558a 100644 --- a/app/templates/server/.jshintrc +++ b/app/templates/server/.jshintrc @@ -1,4 +1,5 @@ { + "expr": true, "node": true, "esnext": true, "bitwise": true, diff --git a/app/templates/server/api/user(auth)/user.controller.js b/app/templates/server/api/user(auth)/user.controller.js index f4cd10c29..17e6e0e04 100644 --- a/app/templates/server/api/user(auth)/user.controller.js +++ b/app/templates/server/api/user(auth)/user.controller.js @@ -14,7 +14,7 @@ var validationError = function(res, err) { * restriction: 'admin' */ exports.index = function(req, res) { - User.find({}, '-salt -hashedPassword', function (err, users) { + User.find({}, '-salt -password', function (err, users) { if(err) return res.send(500, err); res.json(200, users); }); @@ -67,15 +67,19 @@ exports.changePassword = function(req, res, next) { var newPass = String(req.body.newPassword); User.findById(userId, function (err, user) { - if(user.authenticate(oldPass)) { - user.password = newPass; - user.save(function(err) { - if (err) return validationError(res, err); - res.send(200); - }); - } else { - res.send(403); - } + user.authenticate(oldPass, function(authErr, authenticated) { + if (authErr) res.send(403); + + if (authenticated) { + user.password = newPass; + user.save(function(err) { + if (err) return validationError(res, err); + res.send(200); + }); + } else { + res.send(403); + } + }); }); }; @@ -86,7 +90,7 @@ exports.me = function(req, res, next) { var userId = req.user._id; User.findOne({ _id: userId - }, '-salt -hashedPassword', function(err, user) { // don't ever give out the password or salt + }, '-salt -password', function(err, user) { // don't ever give out the password or salt if (err) return next(err); if (!user) return res.json(401); res.json(user); diff --git a/app/templates/server/api/user(auth)/user.model.js b/app/templates/server/api/user(auth)/user.model.js index cc8d59263..b3497f859 100644 --- a/app/templates/server/api/user(auth)/user.model.js +++ b/app/templates/server/api/user(auth)/user.model.js @@ -12,7 +12,7 @@ var UserSchema = new Schema({ type: String, default: 'user' }, - hashedPassword: String, + password: String, provider: String, salt: String<% if (filters.oauth) { %>,<% if (filters.facebookAuth) { %> facebook: {},<% } %><% if (filters.twitterAuth) { %> @@ -24,16 +24,6 @@ var UserSchema = new Schema({ /** * Virtuals */ -UserSchema - .virtual('password') - .set(function(password) { - this._password = password; - this.salt = this.makeSalt(); - this.hashedPassword = this.encryptPassword(password); - }) - .get(function() { - return this._password; - }); // Public profile information UserSchema @@ -69,10 +59,10 @@ UserSchema // Validate empty password UserSchema - .path('hashedPassword') - .validate(function(hashedPassword) {<% if (filters.oauth) { %> + .path('password') + .validate(function(password) {<% if (filters.oauth) { %> if (authTypes.indexOf(this.provider) !== -1) return true;<% } %> - return hashedPassword.length; + return password.length; }, 'Password cannot be blank'); // Validate email is not taken @@ -99,12 +89,26 @@ var validatePresenceOf = function(value) { */ UserSchema .pre('save', function(next) { - if (!this.isNew) return next(); - - if (!validatePresenceOf(this.hashedPassword)<% if (filters.oauth) { %> && authTypes.indexOf(this.provider) === -1<% } %>) - next(new Error('Invalid password')); - else + // Handle new/update passwords + if (this.password) { + if (!validatePresenceOf(this.password)<% if (filters.oauth) { %> && authTypes.indexOf(this.provider) === -1<% } %>) + next(new Error('Invalid password')); + + // Make salt with a callback + var _this = this; + this.makeSalt(function(saltErr, salt) { + if (saltErr) next(saltErr); + _this.salt = salt; + // Async hash + _this.encryptPassword(_this.password, function(encryptErr, hashedPassword) { + if (encryptErr) next(encryptErr); + _this.password = hashedPassword; + next(); + }); + }); + } else { next(); + } }); /** @@ -115,34 +119,82 @@ UserSchema.methods = { * Authenticate - check if the passwords are the same * * @param {String} plainText + * @callback {callback} Optional callback * @return {Boolean} * @api public */ - authenticate: function(plainText) { - return this.encryptPassword(plainText) === this.hashedPassword; + authenticate: function(password, callback) { + if (!callback) + return this.password === this.encryptPassword(password); + + var _this = this; + this.encryptPassword(password, function(err, pwdGen) { + if (err) callback(err); + + if (_this.password === pwdGen) { + callback(null, true); + } else { + callback(null, false); + } + }); }, /** * Make salt * + * @param {Number} byteSize Optional salt byte size, default to 16 + * @callback {callback} Optional callback * @return {String} * @api public */ - makeSalt: function() { - return crypto.randomBytes(16).toString('base64'); + makeSalt: function(byteSize, callback) { + var defaultByteSize = 16; + + if (typeof arguments[0] === 'function') { + callback = arguments[0]; + byteSize = defaultByteSize; + } else if (typeof arguments[1] === 'function') { + callback = arguments[1]; + } + + if (!byteSize) { + byteSize = defaultByteSize; + } + + if (!callback) { + return crypto.randomBytes(byteSize).toString('base64'); + } + + return crypto.randomBytes(byteSize, function(err, salt) { + if (err) callback(err); + return callback(null, salt.toString('base64')); + }); }, /** * Encrypt password * * @param {String} password + * @callback {callback} Optional callback * @return {String} * @api public */ - encryptPassword: function(password) { - if (!password || !this.salt) return ''; + encryptPassword: function(password, callback) { + if (!password || !this.salt) { + return null; + } + + var defaultIterations = 10000; + var defaultKeyLength = 64; var salt = new Buffer(this.salt, 'base64'); - return crypto.pbkdf2Sync(password, salt, 10000, 64).toString('base64'); + + if (!callback) + return crypto.pbkdf2Sync(password, salt, defaultIterations, defaultKeyLength).toString('base64'); + + return crypto.pbkdf2(password, salt, defaultIterations, defaultKeyLength, function(err, key) { + if (err) callback(err); + return callback(null, key.toString('base64')); + }); } }; diff --git a/app/templates/server/api/user(auth)/user.model.spec.js b/app/templates/server/api/user(auth)/user.model.spec.js index 257c95b7c..95e8bfbd8 100644 --- a/app/templates/server/api/user(auth)/user.model.spec.js +++ b/app/templates/server/api/user(auth)/user.model.spec.js @@ -4,14 +4,9 @@ var should = require('should'); var app = require('../../app'); var User = require('./user.model'); -var user = new User({ - provider: 'local', - name: 'Fake User', - email: 'test@test.com', - password: 'password' -}); - describe('User Model', function() { + var user; + before(function(done) { // Clear users before testing User.remove().exec().then(function() { @@ -20,6 +15,15 @@ describe('User Model', function() { }); afterEach(function(done) { + // Start from scratch + user = new User({ + provider: 'local', + name: 'Fake User', + email: 'test@test.com', + password: 'password' + }); + + // Clear all users User.remove().exec().then(function() { done(); }); @@ -50,11 +54,34 @@ describe('User Model', function() { }); }); - it("should authenticate user if password is valid", function() { - return user.authenticate('password').should.be.true; + it("should authenticate user if password is valid", function(done) { + user.save(function(err, newUser) { + newUser.authenticate('password', function(authErr, authenticated) { + authenticated.should.be.true; + done(); + }); + }); + }); + + it("should not authenticate user if password is invalid", function(done) { + user.save(function(err, newUser) { + newUser.authenticate('invalidPassword', function(authErr, authenticated) { + authenticated.should.not.be.true; + done(); + }); + }); }); - it("should not authenticate user if password is invalid", function() { - return user.authenticate('blah').should.not.be.true; + it("should authenticate after updating password", function(done) { + user.save(function(err, newUser) { + newUser.password = 'newPassword'; + newUser.save(function() { + newUser.authenticate('newPassword', function(authErr, authenticated) { + authenticated.should.be.true; + done(); + }); + }); + }); }); + }); diff --git a/app/templates/server/auth(auth)/local/passport.js b/app/templates/server/auth(auth)/local/passport.js index ac82b42a2..4221f8786 100644 --- a/app/templates/server/auth(auth)/local/passport.js +++ b/app/templates/server/auth(auth)/local/passport.js @@ -15,11 +15,15 @@ exports.setup = function (User, config) { if (!user) { return done(null, false, { message: 'This email is not registered.' }); } - if (!user.authenticate(password)) { - return done(null, false, { message: 'This password is not correct.' }); - } - return done(null, user); + user.authenticate(password, function(authError, authenticated) { + if (authError) return done(authError); + if (!authenticated) { + return done(null, false, { message: 'This password is not correct.' }); + } else { + return done(null, user); + } + }); }); } )); -}; \ No newline at end of file +}; From de5c36ca3003ea9ef149eb26ca8393d6ab2d8a4a Mon Sep 17 00:00:00 2001 From: kingcody Date: Fri, 29 Aug 2014 11:05:05 -0400 Subject: [PATCH 03/20] refactor(server-tests): use sinon-chai and `mocha.conf.js` Changes: - add `mocha.conf.js` and use it as a `require` in `mochaTest` task - switch `should.js` for `mocha-chai` - change server-side test assertions to user chai assertions Breaking Changes: - should.js is no longer included, chai assertions should be used instead. --- app/templates/Gruntfile.js | 3 ++- app/templates/_package.json | 3 ++- app/templates/mocha.conf.js | 12 +++++++++++ app/templates/server/api/thing/thing.spec.js | 3 +-- .../server/api/user(auth)/user.model.spec.js | 20 ++++++++----------- endpoint/templates/name.spec.js | 5 ++--- test/fixtures/package.json | 3 ++- 7 files changed, 29 insertions(+), 20 deletions(-) create mode 100644 app/templates/mocha.conf.js diff --git a/app/templates/Gruntfile.js b/app/templates/Gruntfile.js index 998ca5620..d98992888 100644 --- a/app/templates/Gruntfile.js +++ b/app/templates/Gruntfile.js @@ -486,7 +486,8 @@ module.exports = function (grunt) { mochaTest: { options: { - reporter: 'spec' + reporter: 'spec', + require: 'mocha.conf.js' }, src: ['server/**/*.spec.js'] }, diff --git a/app/templates/_package.json b/app/templates/_package.json index e32849740..7b2cf48d5 100644 --- a/app/templates/_package.json +++ b/app/templates/_package.json @@ -30,6 +30,7 @@ "socketio-jwt": "^2.0.2"<% } %> }, "devDependencies": { + "chai-as-promised": "^4.1.1", "grunt": "~0.4.4", "grunt-autoprefixer": "~0.7.2", "grunt-wiredep": "~1.8.0", @@ -86,7 +87,7 @@ "karma": "~0.12.9", "karma-ng-html2js-preprocessor": "~0.1.0", "supertest": "~0.11.0", - "should": "~3.3.1" + "sinon-chai": "^2.5.0" }, "engines": { "node": ">=0.10.0" diff --git a/app/templates/mocha.conf.js b/app/templates/mocha.conf.js new file mode 100644 index 000000000..7bfaa573f --- /dev/null +++ b/app/templates/mocha.conf.js @@ -0,0 +1,12 @@ +'use strict'; + +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); +var chaiAsPromised = require('chai-as-promised'); + +global.expect = chai.expect; +global.assert = chai.assert; +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); diff --git a/app/templates/server/api/thing/thing.spec.js b/app/templates/server/api/thing/thing.spec.js index 17c8c6cd0..3f135445b 100644 --- a/app/templates/server/api/thing/thing.spec.js +++ b/app/templates/server/api/thing/thing.spec.js @@ -1,6 +1,5 @@ 'use strict'; -var should = require('should'); var app = require('../../app'); var request = require('supertest'); @@ -13,7 +12,7 @@ describe('GET /api/things', function() { .expect('Content-Type', /json/) .end(function(err, res) { if (err) return done(err); - res.body.should.be.instanceof(Array); + res.body.should.be.instanceOf(Array); done(); }); }); diff --git a/app/templates/server/api/user(auth)/user.model.spec.js b/app/templates/server/api/user(auth)/user.model.spec.js index 257c95b7c..8960ca859 100644 --- a/app/templates/server/api/user(auth)/user.model.spec.js +++ b/app/templates/server/api/user(auth)/user.model.spec.js @@ -1,6 +1,5 @@ 'use strict'; -var should = require('should'); var app = require('../../app'); var User = require('./user.model'); @@ -12,17 +11,14 @@ var user = new User({ }); describe('User Model', function() { - before(function(done) { - // Clear users before testing - User.remove().exec().then(function() { - done(); - }); + + // Clear users before testing + before(function() { + return User.remove().exec(); }); - afterEach(function(done) { - User.remove().exec().then(function() { - done(); - }); + afterEach(function() { + return User.remove().exec(); }); it('should begin with no users', function(done) { @@ -36,7 +32,7 @@ describe('User Model', function() { user.save(function() { var userDup = new User(user); userDup.save(function(err) { - should.exist(err); + err.should.be.instanceOf(Error); done(); }); }); @@ -45,7 +41,7 @@ describe('User Model', function() { it('should fail when saving without an email', function(done) { user.email = ''; user.save(function(err) { - should.exist(err); + err.should.be.instanceOf(Error); done(); }); }); diff --git a/endpoint/templates/name.spec.js b/endpoint/templates/name.spec.js index fcad73ebd..d287bafe5 100644 --- a/endpoint/templates/name.spec.js +++ b/endpoint/templates/name.spec.js @@ -1,6 +1,5 @@ 'use strict'; -var should = require('should'); var app = require('../../app'); var request = require('supertest'); @@ -13,8 +12,8 @@ describe('GET <%= route %>', function() { .expect('Content-Type', /json/) .end(function(err, res) { if (err) return done(err); - res.body.should.be.instanceof(Array); + res.body.should.be.instanceOf(Array); done(); }); }); -}); \ No newline at end of file +}); diff --git a/test/fixtures/package.json b/test/fixtures/package.json index ce4df22f9..9bdfd30ff 100644 --- a/test/fixtures/package.json +++ b/test/fixtures/package.json @@ -30,6 +30,7 @@ "socketio-jwt": "^2.0.2" }, "devDependencies": { + "chai-as-promised": "^4.1.1", "grunt": "~0.4.4", "grunt-autoprefixer": "~0.7.2", "grunt-wiredep": "~1.8.0", @@ -86,7 +87,7 @@ "karma": "~0.12.9", "karma-ng-html2js-preprocessor": "~0.1.0", "supertest": "~0.11.0", - "should": "~3.3.1" + "sinon-chai": "^2.5.0" }, "engines": { "node": ">=0.10.0" From dbbaa20b47629be60de8ce5f31a7c91abf9bdaef Mon Sep 17 00:00:00 2001 From: kingcody Date: Sat, 30 Aug 2014 07:53:03 -0400 Subject: [PATCH 04/20] feat(server-tests): code coverage and e2e Changes: - split server tests into `unit` and `e2e` type test - adjust `mochaTest` and `test:server` tasks to reflect the two types of tests - implement `grunt-mocha-istanbul` task for server-side code coverage - add `test:coverage` task - improve `mocha.conf.js` - add sinon, expect, and assert as globals to `server/.jshintrc-spec` - add `proxyquire` to the app for better stubbing in unit tests - improve test to reflect recent changes and to lay ground work for more coverage Grunt Task `test`: The grunt 'test' task now has a 'coverage' target. Used like so `grunt test:coverage`. This task will run istanbul for the server unit tests and e2e test separately. The resulting coverage reports will be placed in `coverage/(unit|e2e)`. There is also an option for the `test:coverage` task, possibilities for option are: - `unit` - `e2e` - `check` `test:coverage:check` will check the coverage reports for both `unit` and `e2e` and report whether or not they meet the required coverage. --- app/templates/Gruntfile.js | 94 +++++++++++- app/templates/_package.json | 4 +- app/templates/mocha.conf.js | 2 + app/templates/server/.jshintrc-spec | 5 +- app/templates/server/api/thing/index.js | 2 +- app/templates/server/api/thing/index.spec.js | 85 +++++++++++ .../server/api/thing/thing.controller.js | 2 +- app/templates/server/api/thing/thing.e2e.js | 135 ++++++++++++++++++ .../server/api/thing/thing.model(mongoose).js | 2 +- .../api/thing/thing.socket(socketio).js | 2 +- app/templates/server/api/thing/thing.spec.js | 19 --- app/templates/server/api/user(auth)/index.js | 1 - .../server/api/user(auth)/index.spec.js | 95 ++++++++++++ .../server/api/user(auth)/user.e2e.js | 68 +++++++++ .../server/api/user(auth)/user.model.spec.js | 66 +++++---- endpoint/templates/index.spec.js | 85 +++++++++++ endpoint/templates/name.e2e.js | 135 ++++++++++++++++++ endpoint/templates/name.spec.js | 19 --- test/fixtures/package.json | 2 + 19 files changed, 748 insertions(+), 75 deletions(-) create mode 100644 app/templates/server/api/thing/index.spec.js create mode 100644 app/templates/server/api/thing/thing.e2e.js delete mode 100644 app/templates/server/api/thing/thing.spec.js create mode 100644 app/templates/server/api/user(auth)/index.spec.js create mode 100644 app/templates/server/api/user(auth)/user.e2e.js create mode 100644 endpoint/templates/index.spec.js create mode 100644 endpoint/templates/name.e2e.js delete mode 100644 endpoint/templates/name.spec.js diff --git a/app/templates/Gruntfile.js b/app/templates/Gruntfile.js index d98992888..26866c21d 100644 --- a/app/templates/Gruntfile.js +++ b/app/templates/Gruntfile.js @@ -169,14 +169,14 @@ module.exports = function (grunt) { }, src: [ 'server/**/*.js', - '!server/**/*.spec.js' + '!server/**/*.{spec,e2e}.js' ] }, serverTest: { options: { jshintrc: 'server/.jshintrc-spec' }, - src: ['server/**/*.spec.js'] + src: ['server/**/*.{spec,e2e}.js'] }, all: [ '<%%= yeoman.client %>/{app,components}/**/*.js', @@ -489,7 +489,57 @@ module.exports = function (grunt) { reporter: 'spec', require: 'mocha.conf.js' }, - src: ['server/**/*.spec.js'] + unit: { + src: ['server/**/*.spec.js'] + }, + e2e: { + src: ['server/**/*.e2e.js'] + } + }, + + mocha_istanbul: { + unit: { + options: { + excludes: [ + '**/*.spec.js', + '**/*.mock.js', + '**/*.e2e.js' + ], + reporter: 'spec', + require: ['mocha.conf.js'], + mask: '**/*.spec.js', + coverageFolder: 'coverage/unit' + }, + src: 'server' + }, + e2e: { + options: { + excludes: [ + '**/*.spec.js', + '**/*.mock.js', + '**/*.e2e.js' + ], + reporter: 'spec', + require: ['mocha.conf.js'], + mask: '**/*.e2e.js', + coverageFolder: 'coverage/e2e' + }, + src: 'server' + } + }, + + istanbul_check_coverage: { + default: { + options: { + coverageFolder: 'coverage/*', + check: { + lines: 80, + statements: 80, + branches: 80, + functions: 80 + } + } + } }, protractor: { @@ -770,7 +820,8 @@ module.exports = function (grunt) { return grunt.task.run([ 'env:all', 'env:test', - 'mochaTest' + 'mochaTest:unit', + 'mochaTest:e2e' ]); } @@ -805,6 +856,41 @@ module.exports = function (grunt) { ]); } + else if (target === 'coverage') { + + if (option === 'unit') { + return grunt.task.run([ + 'env:all', + 'env:test', + 'mocha_istanbul:unit' + ]); + } + + else if (option === 'e2e') { + return grunt.task.run([ + 'env:all', + 'env:test', + 'mocha_istanbul:e2e' + ]); + } + + else if (option === 'check') { + return grunt.task.run([ + 'istanbul_check_coverage' + ]); + } + + else { + return grunt.task.run([ + 'env:all', + 'env:test', + 'mocha_istanbul', + 'istanbul_check_coverage' + ]); + } + + } + else grunt.task.run([ 'test:server', 'test:client' diff --git a/app/templates/_package.json b/app/templates/_package.json index 7b2cf48d5..9dfdadc5d 100644 --- a/app/templates/_package.json +++ b/app/templates/_package.json @@ -62,7 +62,8 @@ "grunt-asset-injector": "^0.1.0", "grunt-karma": "~0.8.2", "grunt-build-control": "DaftMonk/grunt-build-control", - "grunt-mocha-test": "~0.10.2",<% if(filters.sass) { %> + "grunt-mocha-test": "~0.10.2", + "grunt-mocha-istanbul": "^2.0.0",<% if(filters.sass) { %> "grunt-contrib-sass": "^0.7.3",<% } %><% if(filters.stylus) { %> "grunt-contrib-stylus": "latest",<% } %> "jit-grunt": "^0.5.0", @@ -86,6 +87,7 @@ "karma-phantomjs-launcher": "~0.1.4", "karma": "~0.12.9", "karma-ng-html2js-preprocessor": "~0.1.0", + "proxyquire": "^1.0.1", "supertest": "~0.11.0", "sinon-chai": "^2.5.0" }, diff --git a/app/templates/mocha.conf.js b/app/templates/mocha.conf.js index 7bfaa573f..497d43b2c 100644 --- a/app/templates/mocha.conf.js +++ b/app/templates/mocha.conf.js @@ -7,6 +7,8 @@ var chaiAsPromised = require('chai-as-promised'); global.expect = chai.expect; global.assert = chai.assert; +global.sinon = sinon; + chai.should(); chai.use(sinonChai); chai.use(chaiAsPromised); diff --git a/app/templates/server/.jshintrc-spec b/app/templates/server/.jshintrc-spec index b6b55cbf9..b9390c374 100644 --- a/app/templates/server/.jshintrc-spec +++ b/app/templates/server/.jshintrc-spec @@ -6,6 +6,9 @@ "before": true, "beforeEach": true, "after": true, - "afterEach": true + "afterEach": true, + "expect": true, + "assert": true, + "sinon": true } } diff --git a/app/templates/server/api/thing/index.js b/app/templates/server/api/thing/index.js index 242ed5901..e77e80c5b 100644 --- a/app/templates/server/api/thing/index.js +++ b/app/templates/server/api/thing/index.js @@ -12,4 +12,4 @@ router.put('/:id', controller.update); router.patch('/:id', controller.update); router.delete('/:id', controller.destroy);<% } %> -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/app/templates/server/api/thing/index.spec.js b/app/templates/server/api/thing/index.spec.js new file mode 100644 index 000000000..0fbfb1029 --- /dev/null +++ b/app/templates/server/api/thing/index.spec.js @@ -0,0 +1,85 @@ +'use strict'; + +var proxyquire = require('proxyquire').noPreserveCache(); + + /* thing.controller stub */ +var thingCtrl = { + index: 'thingCtrl.index'<% if(filters.mongoose) { %>, + show: 'thingCtrl.show', + create: 'thingCtrl.create', + update: 'thingCtrl.update', + destroy: 'thingCtrl.destroy'<% } %> + }, + /* express.Router().router stub */ + router = { + get: sinon.spy()<% if(filters.mongoose) { %>, + put: sinon.spy(), + patch: sinon.spy(), + post: sinon.spy(), + delete: sinon.spy()<% } %> + }, + /* stubbed thing router */ + index = proxyquire('./index.js', { + 'express': { + Router: function() { + return router; + } + }, + './thing.controller': thingCtrl + }); + +describe('Thing API Router:', function() { + + it('should return an express router instance', function() { + index.should.equal(router); + }); + + describe('GET /api/things', function() { + + it('should route to thing.controller.index', function() { + return router.get.withArgs('/', 'thingCtrl.index').should.have.been.calledOnce; + }); + + });<% if(filters.mongoose) { %> + + describe('GET /api/things/:id', function() { + + it('should route to thing.controller.show', function() { + return router.get.withArgs('/:id', 'thingCtrl.show').should.have.been.calledOnce; + }); + + }); + + describe('POST /api/things', function() { + + it('should route to thing.controller.create', function() { + return router.post.withArgs('/', 'thingCtrl.create').should.have.been.calledOnce; + }); + + }); + + describe('PUT /api/things/:id', function() { + + it('should route to thing.controller.update', function() { + return router.put.withArgs('/:id', 'thingCtrl.update').should.have.been.calledOnce; + }); + + }); + + describe('PATCH /api/things/:id', function() { + + it('should route to thing.controller.update', function() { + return router.patch.withArgs('/:id', 'thingCtrl.update').should.have.been.calledOnce; + }); + + }); + + describe('DELETE /api/things/:id', function() { + + it('should route to thing.controller.destroy', function() { + return router.delete.withArgs('/:id', 'thingCtrl.destroy').should.have.been.calledOnce; + }); + + });<% } %> + +}); diff --git a/app/templates/server/api/thing/thing.controller.js b/app/templates/server/api/thing/thing.controller.js index ba84d6fc9..72a678cf4 100644 --- a/app/templates/server/api/thing/thing.controller.js +++ b/app/templates/server/api/thing/thing.controller.js @@ -86,4 +86,4 @@ exports.destroy = function(req, res) { function handleError(res, err) { return res.send(500, err); -}<% } %> \ No newline at end of file +}<% } %> diff --git a/app/templates/server/api/thing/thing.e2e.js b/app/templates/server/api/thing/thing.e2e.js new file mode 100644 index 000000000..1140df7e9 --- /dev/null +++ b/app/templates/server/api/thing/thing.e2e.js @@ -0,0 +1,135 @@ +'use strict'; + +var app = require('../../app'); +var request = require('supertest');<% if(filters.mongoose) { %> + +var newThing;<% } %> + +describe('Thing API:', function() { + + describe('GET /api/things', function() { + var things; + + beforeEach(function(done) { + request(app) + .get('/api/things') + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if (err) return done(err); + things = res.body; + done(); + }); + }); + + it('should respond with JSON array', function() { + things.should.be.instanceOf(Array); + }); + + });<% if(filters.mongoose) { %> + + describe('POST /api/things', function() { + beforeEach(function(done) { + request(app) + .post('/api/things') + .send({ + name: 'New Thing', + info: 'This is the brand new thing!!!' + }) + .expect(201) + .expect('Content-Type', /json/) + .end(function(err, res) { + if (err) return done(err); + newThing = res.body; + done(); + }); + }); + + it('should respond with the newly created thing', function() { + newThing.name.should.equal('New Thing'); + newThing.info.should.equal('This is the brand new thing!!!'); + }); + + }); + + describe('GET /api/things/:id', function() { + var thing; + + beforeEach(function(done) { + request(app) + .get('/api/things/' + newThing._id) + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if (err) return done(err); + thing = res.body; + done(); + }); + }); + + afterEach(function() { + thing = {}; + }); + + it('should respond with the requested thing', function() { + thing.name.should.equal('New Thing'); + thing.info.should.equal('This is the brand new thing!!!'); + }); + + }); + + describe('PUT /api/things/:id', function() { + var updatedThing + + beforeEach(function(done) { + request(app) + .put('/api/things/' + newThing._id) + .send({ + name: 'Updated Thing', + info: 'This is the updated thing!!!' + }) + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if (err) return done(err); + updatedThing = res.body; + done(); + }); + }); + + afterEach(function() { + updatedThing = {}; + }); + + it('should respond with the updated thing', function() { + updatedThing.name.should.equal('Updated Thing'); + updatedThing.info.should.equal('This is the updated thing!!!'); + }); + + }); + + describe('DELETE /api/things/:id', function() { + + it('should respond with 204 on successful removal', function(done) { + request(app) + .delete('/api/things/' + newThing._id) + .expect(204) + .end(function(err, res) { + if (err) return done(err); + done(); + }); + }); + + it('should respond with 404 when thing does not exsist', function(done) { + request(app) + .delete('/api/things/' + newThing._id) + .expect(404) + .end(function(err, res) { + if (err) return done(err); + done(); + }); + }); + + });<% } %> + +}); diff --git a/app/templates/server/api/thing/thing.model(mongoose).js b/app/templates/server/api/thing/thing.model(mongoose).js index ed857cd3b..92a791e70 100644 --- a/app/templates/server/api/thing/thing.model(mongoose).js +++ b/app/templates/server/api/thing/thing.model(mongoose).js @@ -9,4 +9,4 @@ var ThingSchema = new Schema({ active: Boolean }); -module.exports = mongoose.model('Thing', ThingSchema); \ No newline at end of file +module.exports = mongoose.model('Thing', ThingSchema); diff --git a/app/templates/server/api/thing/thing.socket(socketio).js b/app/templates/server/api/thing/thing.socket(socketio).js index 79d327695..dbf3e2fe7 100644 --- a/app/templates/server/api/thing/thing.socket(socketio).js +++ b/app/templates/server/api/thing/thing.socket(socketio).js @@ -21,4 +21,4 @@ function onSave(socket, doc, cb) { function onRemove(socket, doc, cb) { socket.emit('thing:remove', doc); -} \ No newline at end of file +} diff --git a/app/templates/server/api/thing/thing.spec.js b/app/templates/server/api/thing/thing.spec.js deleted file mode 100644 index 3f135445b..000000000 --- a/app/templates/server/api/thing/thing.spec.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -var app = require('../../app'); -var request = require('supertest'); - -describe('GET /api/things', function() { - - it('should respond with JSON array', function(done) { - request(app) - .get('/api/things') - .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) return done(err); - res.body.should.be.instanceOf(Array); - done(); - }); - }); -}); diff --git a/app/templates/server/api/user(auth)/index.js b/app/templates/server/api/user(auth)/index.js index 48567e485..be6fd3af3 100644 --- a/app/templates/server/api/user(auth)/index.js +++ b/app/templates/server/api/user(auth)/index.js @@ -2,7 +2,6 @@ var express = require('express'); var controller = require('./user.controller'); -var config = require('../../config/environment'); var auth = require('../../auth/auth.service'); var router = express.Router(); diff --git a/app/templates/server/api/user(auth)/index.spec.js b/app/templates/server/api/user(auth)/index.spec.js new file mode 100644 index 000000000..5bcd4c2c0 --- /dev/null +++ b/app/templates/server/api/user(auth)/index.spec.js @@ -0,0 +1,95 @@ +'use strict'; + +var proxyquire = require('proxyquire').noPreserveCache(); + + /* user.controller stub */ +var userCtrl = { + index: 'userCtrl.index', + destroy: 'userCtrl.destroy', + me: 'userCtrl.me', + changePassword: 'userCtrl.changePassword', + show: 'userCtrl.show', + create: 'userCtrl.create' + }, + /* auth.service stub */ + authService = { + isAuthenticated: function() { + return 'authService.isAuthenticated'; + }, + hasRole: function(role) { + return 'authService.hasRole.' + role; + } + }, + /* express.Router().router stub */ + router = { + get: sinon.spy(), + put: sinon.spy(), + post: sinon.spy(), + delete: sinon.spy() + }, + /* stubbed user router */ + index = proxyquire('./index', { + 'express': { + Router: function() { + return router; + } + }, + './user.controller': userCtrl, + '../../auth/auth.service': authService + }); + +describe('User API Router:', function() { + + it('should return an express router instance', function() { + index.should.equal(router); + }); + + describe('GET /api/users', function() { + + it('should verify admin role and route to user.controller.index', function() { + return router.get.withArgs('/', 'authService.hasRole.admin', 'userCtrl.index').should.have.been.calledOnce; + }); + + }); + + describe('DELETE /api/users/:id', function() { + + it('should verify admin role and route to user.controller.destroy', function() { + return router.delete.withArgs('/:id', 'authService.hasRole.admin', 'userCtrl.destroy').should.have.been.calledOnce; + }); + + }); + + describe('GET /api/users/me', function() { + + it('should be authenticated and route to user.controller.me', function() { + return router.get.withArgs('/me', 'authService.isAuthenticated', 'userCtrl.me').should.have.been.calledOnce; + }); + + }); + + describe('PUT /api/users/:id/password', function() { + + it('should be authenticated and route to user.controller.changePassword', function() { + return router.put.withArgs('/:id/password', 'authService.isAuthenticated', 'userCtrl.changePassword').should.have.been.calledOnce; + }); + + }); + + describe('GET /api/users/:id', function() { + + it('should be authenticated and route to user.controller.show', function() { + return router.get.withArgs('/:id', 'authService.isAuthenticated', 'userCtrl.show').should.have.been.calledOnce; + }); + + }); + + describe('POST /api/users', function() { + + it('should route to user.controller.create', function() { + return router.post.withArgs('/', 'userCtrl.create').should.have.been.calledOnce; + }); + + }); + +}); diff --git a/app/templates/server/api/user(auth)/user.e2e.js b/app/templates/server/api/user(auth)/user.e2e.js new file mode 100644 index 000000000..917acedd9 --- /dev/null +++ b/app/templates/server/api/user(auth)/user.e2e.js @@ -0,0 +1,68 @@ +'use strict'; + +var app = require('../../app'); +var User = require('./user.model'); +var request = require('supertest'); + +describe('User API:', function() { + var user; + + // Clear users before testing + before(function(done) { + User.remove(function() { + user = new User({ + name: 'Fake User', + email: 'test@test.com', + password: 'password' + }); + + user.save(function(err) { + if (err) return done(err); + done(); + }); + }); + }); + + // Clear users after testing + after(function() { + return User.remove().exec(); + }); + + describe('GET /api/users/me', function() { + var token; + + before(function(done) { + request(app) + .post('/auth/local') + .send({ + email: 'test@test.com', + password: 'password' + }) + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + token = res.body.token; + done(); + }); + }); + + it('should respond with a user profile when authenticated', function(done) { + request(app) + .get('/api/users/me') + .set('authorization', 'Bearer ' + token) + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + res.body._id.should.equal(user._id.toString()); + done(); + }); + }); + + it('should respond with a 401 when not authenticated', function(done) { + request(app) + .get('/api/users/me') + .expect(401) + .end(done); + }); + }); +}); diff --git a/app/templates/server/api/user(auth)/user.model.spec.js b/app/templates/server/api/user(auth)/user.model.spec.js index 8960ca859..0df9a2d5b 100644 --- a/app/templates/server/api/user(auth)/user.model.spec.js +++ b/app/templates/server/api/user(auth)/user.model.spec.js @@ -10,47 +10,61 @@ var user = new User({ password: 'password' }); -describe('User Model', function() { +describe('User Model:', function() { // Clear users before testing before(function() { return User.remove().exec(); }); - afterEach(function() { - return User.remove().exec(); - }); + describe('User (schema)', function() { - it('should begin with no users', function(done) { - User.find({}, function(err, users) { - users.should.have.length(0); - done(); + it('should begin with no users', function() { + return User.find({}).exec().should.eventually.have.length(0); }); + }); - it('should fail when saving a duplicate user', function(done) { - user.save(function() { - var userDup = new User(user); - userDup.save(function(err) { - err.should.be.instanceOf(Error); - done(); + describe('user (instance)', function() { + + describe('.save()', function() { + // Clear users after tests + afterEach(function() { + return User.remove().exec(); }); + + it('should fail when saving a duplicate user', function(done) { + user.save(function() { + var userDup = new User(user); + userDup.save(function(err) { + err.should.be.instanceOf(Error); + done(); + }); + }); + }); + + it('should fail when saving without an email', function(done) { + user.email = ''; + user.save(function(err) { + err.should.be.instanceOf(Error); + done(); + }); + }); + }); - }); - it('should fail when saving without an email', function(done) { - user.email = ''; - user.save(function(err) { - err.should.be.instanceOf(Error); - done(); + describe('.authenticate()', function() { + + it("should authenticate user if password is valid", function() { + return user.authenticate('password').should.be.true; + }); + + it("should not authenticate user if password is invalid", function() { + return user.authenticate('blah').should.not.be.true; + }); + }); - }); - it("should authenticate user if password is valid", function() { - return user.authenticate('password').should.be.true; }); - it("should not authenticate user if password is invalid", function() { - return user.authenticate('blah').should.not.be.true; - }); }); diff --git a/endpoint/templates/index.spec.js b/endpoint/templates/index.spec.js new file mode 100644 index 000000000..62caed5dc --- /dev/null +++ b/endpoint/templates/index.spec.js @@ -0,0 +1,85 @@ +'use strict'; + +var proxyquire = require('proxyquire').noPreserveCache(); + + /* <%= name %>.controller stub */ +var <%= name %>Ctrl = { + index: '<%= name %>Ctrl.index'<% if(filters.mongoose) { %>, + show: '<%= name %>Ctrl.show', + create: '<%= name %>Ctrl.create', + update: '<%= name %>Ctrl.update', + destroy: '<%= name %>Ctrl.destroy'<% } %> + }, + /* express.Router().router stub */ + router = { + get: sinon.spy()<% if(filters.mongoose) { %>, + put: sinon.spy(), + patch: sinon.spy(), + post: sinon.spy(), + delete: sinon.spy()<% } %> + }, + /* stubbed <%= name %> router */ + index = proxyquire('./index.js', { + 'express': { + Router: function() { + return router; + } + }, + './<%= name %>.controller': <%= name %>Ctrl + }); + +describe('<%= classedName %> API Router:', function() { + + it('should return an express router instance', function() { + index.should.equal(router); + }); + + describe('GET <%= route %>', function() { + + it('should route to <%= name %>.controller.index', function() { + return router.get.withArgs('/', '<%= name %>Ctrl.index').should.have.been.calledOnce; + }); + + });<% if(filters.mongoose) { %> + + describe('GET <%= route %>/:id', function() { + + it('should route to <%= name %>.controller.show', function() { + return router.get.withArgs('/:id', '<%= name %>Ctrl.show').should.have.been.calledOnce; + }); + + }); + + describe('POST <%= route %>', function() { + + it('should route to <%= name %>.controller.create', function() { + return router.post.withArgs('/', '<%= name %>Ctrl.create').should.have.been.calledOnce; + }); + + }); + + describe('PUT <%= route %>/:id', function() { + + it('should route to <%= name %>.controller.update', function() { + return router.put.withArgs('/:id', '<%= name %>Ctrl.update').should.have.been.calledOnce; + }); + + }); + + describe('PATCH <%= route %>/:id', function() { + + it('should route to <%= name %>.controller.update', function() { + return router.patch.withArgs('/:id', '<%= name %>Ctrl.update').should.have.been.calledOnce; + }); + + }); + + describe('DELETE <%= route %>/:id', function() { + + it('should route to <%= name %>.controller.destroy', function() { + return router.delete.withArgs('/:id', '<%= name %>Ctrl.destroy').should.have.been.calledOnce; + }); + + });<% } %> + +}); diff --git a/endpoint/templates/name.e2e.js b/endpoint/templates/name.e2e.js new file mode 100644 index 000000000..5960b40a2 --- /dev/null +++ b/endpoint/templates/name.e2e.js @@ -0,0 +1,135 @@ +'use strict'; + +var app = require('../../app'); +var request = require('supertest');<% if(filters.mongoose) { %> + +var new<%= classedName %>;<% } %> + +describe('<%= classedName %> API:', function() { + + describe('GET <%= route %>', function() { + var <%= name %>s; + + beforeEach(function(done) { + request(app) + .get('<%= route %>') + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if (err) return done(err); + <%= name %>s = res.body; + done(); + }); + }); + + it('should respond with JSON array', function() { + <%= name %>s.should.be.instanceOf(Array); + }); + + });<% if(filters.mongoose) { %> + + describe('POST <%= route %>', function() { + beforeEach(function(done) { + request(app) + .post('<%= route %>') + .send({ + name: 'New <%= classedName %>', + info: 'This is the brand new <%= name %>!!!' + }) + .expect(201) + .expect('Content-Type', /json/) + .end(function(err, res) { + if (err) return done(err); + new<%= classedName %> = res.body; + done(); + }); + }); + + it('should respond with the newly created <%= name %>', function() { + new<%= classedName %>.name.should.equal('New <%= classedName %>'); + new<%= classedName %>.info.should.equal('This is the brand new <%= name %>!!!'); + }); + + }); + + describe('GET <%= route %>/:id', function() { + var <%= name %>; + + beforeEach(function(done) { + request(app) + .get('<%= route %>/' + new<%= classedName %>._id) + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if (err) return done(err); + <%= name %> = res.body; + done(); + }); + }); + + afterEach(function() { + <%= name %> = {}; + }); + + it('should respond with the requested <%= name %>', function() { + <%= name %>.name.should.equal('New <%= classedName %>'); + <%= name %>.info.should.equal('This is the brand new <%= name %>!!!'); + }); + + }); + + describe('PUT <%= route %>/:id', function() { + var updated<%= classedName %> + + beforeEach(function(done) { + request(app) + .put('<%= route %>/' + new<%= classedName %>._id) + .send({ + name: 'Updated <%= classedName %>', + info: 'This is the updated <%= name %>!!!' + }) + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if (err) return done(err); + updated<%= classedName %> = res.body; + done(); + }); + }); + + afterEach(function() { + updated<%= classedName %> = {}; + }); + + it('should respond with the updated <%= name %>', function() { + updated<%= classedName %>.name.should.equal('Updated <%= classedName %>'); + updated<%= classedName %>.info.should.equal('This is the updated <%= name %>!!!'); + }); + + }); + + describe('DELETE <%= route %>/:id', function() { + + it('should respond with 204 on successful removal', function(done) { + request(app) + .delete('<%= route %>/' + new<%= classedName %>._id) + .expect(204) + .end(function(err, res) { + if (err) return done(err); + done(); + }); + }); + + it('should respond with 404 when <%= name %> does not exsist', function(done) { + request(app) + .delete('<%= route %>/' + new<%= classedName %>._id) + .expect(404) + .end(function(err, res) { + if (err) return done(err); + done(); + }); + }); + + });<% } %> + +}); diff --git a/endpoint/templates/name.spec.js b/endpoint/templates/name.spec.js deleted file mode 100644 index d287bafe5..000000000 --- a/endpoint/templates/name.spec.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -var app = require('../../app'); -var request = require('supertest'); - -describe('GET <%= route %>', function() { - - it('should respond with JSON array', function(done) { - request(app) - .get('<%= route %>') - .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) return done(err); - res.body.should.be.instanceOf(Array); - done(); - }); - }); -}); diff --git a/test/fixtures/package.json b/test/fixtures/package.json index 9bdfd30ff..2304c008e 100644 --- a/test/fixtures/package.json +++ b/test/fixtures/package.json @@ -63,6 +63,7 @@ "grunt-karma": "~0.8.2", "grunt-build-control": "DaftMonk/grunt-build-control", "grunt-mocha-test": "~0.10.2", + "grunt-mocha-istanbul": "^2.0.0", "grunt-contrib-sass": "^0.7.3", "grunt-contrib-stylus": "latest", "jit-grunt": "^0.5.0", @@ -86,6 +87,7 @@ "karma-phantomjs-launcher": "~0.1.4", "karma": "~0.12.9", "karma-ng-html2js-preprocessor": "~0.1.0", + "proxyquire": "^1.0.1", "supertest": "~0.11.0", "sinon-chai": "^2.5.0" }, From 5198685150001a138230c888691d52fcec3e7a6c Mon Sep 17 00:00:00 2001 From: kingcody Date: Sat, 30 Aug 2014 15:26:37 -0400 Subject: [PATCH 05/20] fix(server-tests): `test:coverage` task Changes: - add missing `option` argument from `test` task - define `istanbul_check_coverage` with `jit-grunt` - place server coverage reports in their own folder: `coverage/server` --- app/templates/Gruntfile.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/templates/Gruntfile.js b/app/templates/Gruntfile.js index 26866c21d..a72f42dae 100644 --- a/app/templates/Gruntfile.js +++ b/app/templates/Gruntfile.js @@ -17,7 +17,8 @@ module.exports = function (grunt) { cdnify: 'grunt-google-cdn', protractor: 'grunt-protractor-runner', injector: 'grunt-asset-injector', - buildcontrol: 'grunt-build-control' + buildcontrol: 'grunt-build-control', + istanbul_check_coverage: 'grunt-mocha-istanbul' }); // Time how long tasks take. Can help when optimizing build times @@ -508,7 +509,7 @@ module.exports = function (grunt) { reporter: 'spec', require: ['mocha.conf.js'], mask: '**/*.spec.js', - coverageFolder: 'coverage/unit' + coverageFolder: 'coverage/server/unit' }, src: 'server' }, @@ -522,7 +523,7 @@ module.exports = function (grunt) { reporter: 'spec', require: ['mocha.conf.js'], mask: '**/*.e2e.js', - coverageFolder: 'coverage/e2e' + coverageFolder: 'coverage/server/e2e' }, src: 'server' } @@ -531,7 +532,7 @@ module.exports = function (grunt) { istanbul_check_coverage: { default: { options: { - coverageFolder: 'coverage/*', + coverageFolder: 'coverage/**', check: { lines: 80, statements: 80, @@ -815,7 +816,7 @@ module.exports = function (grunt) { grunt.task.run(['serve']); }); - grunt.registerTask('test', function(target) { + grunt.registerTask('test', function(target, option) { if (target === 'server') { return grunt.task.run([ 'env:all', From 7045907c849427a16c3aacaf5fd70d14430be09d Mon Sep 17 00:00:00 2001 From: Tyler Henkel Date: Sun, 31 Aug 2014 20:48:46 -0600 Subject: [PATCH 06/20] fix user model test --- app/templates/server/api/user(auth)/user.model.spec.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/templates/server/api/user(auth)/user.model.spec.js b/app/templates/server/api/user(auth)/user.model.spec.js index 952fc5461..afe8af882 100644 --- a/app/templates/server/api/user(auth)/user.model.spec.js +++ b/app/templates/server/api/user(auth)/user.model.spec.js @@ -64,12 +64,5 @@ describe('User Model:', function() { }); }); - - it("should authenticate user if password is valid", function() { - return user.authenticate('password').should.be.true; - }); - - it("should not authenticate user if password is invalid", function() { - return user.authenticate('blah').should.not.be.true; }); }); From 1ca7448fc05c369180c667077918cc2156c308a9 Mon Sep 17 00:00:00 2001 From: Rafael Almeida Date: Mon, 25 Aug 2014 17:40:12 +1200 Subject: [PATCH 07/20] feat (mongoose): use mongoose-bird to promisify the mongoose API --- app/templates/_package.json | 6 +- app/templates/server/api/thing/index.spec.js | 12 +- .../server/api/thing/thing.controller.js | 114 ++++++++++++------ .../server/api/thing/thing.model(mongoose).js | 2 +- .../server/api/user(auth)/index.spec.js | 12 +- .../server/api/user(auth)/user.controller.js | 95 +++++++++------ .../server/api/user(auth)/user.model.js | 31 +++-- .../server/api/user(auth)/user.model.spec.js | 68 ++++------- app/templates/server/app.js | 4 +- .../server/auth(auth)/auth.service.js | 30 +++-- app/templates/server/config/express.js | 4 +- app/templates/server/config/seed(mongoose).js | 83 +++++++------ endpoint/templates/index.spec.js | 12 +- endpoint/templates/name.controller.js | 114 ++++++++++++------ endpoint/templates/name.model(mongoose).js | 4 +- 15 files changed, 342 insertions(+), 249 deletions(-) diff --git a/app/templates/_package.json b/app/templates/_package.json index 9dfdadc5d..2d8337b71 100644 --- a/app/templates/_package.json +++ b/app/templates/_package.json @@ -15,7 +15,9 @@ "lodash": "~2.4.1",<% if(filters.jade) { %> "jade": "~1.2.0",<% } %><% if(filters.html) { %> "ejs": "~0.8.4",<% } %><% if(filters.mongoose) { %> - "mongoose": "~3.8.8",<% } %><% if(filters.auth) { %> + "mongoose": "~3.8.8", + "mongoose-bird": "~0.0.1", + <% } %><% if(filters.auth) { %> "jsonwebtoken": "^0.3.0", "express-jwt": "^0.1.3", "passport": "~0.2.0", @@ -46,7 +48,7 @@ "grunt-contrib-watch": "~0.6.1",<% if(filters.coffee) { %> "grunt-contrib-coffee": "^0.10.1",<% } %><% if(filters.jade) { %> "grunt-contrib-jade": "^0.11.0",<% } %><% if(filters.less) { %> - "grunt-contrib-less": "^0.11.0",<% } %> + "grunt-contrib-less": "^0.11.4",<% } %> "grunt-google-cdn": "~0.4.0", "grunt-newer": "~0.7.0", "grunt-ng-annotate": "^0.2.3", diff --git a/app/templates/server/api/thing/index.spec.js b/app/templates/server/api/thing/index.spec.js index 0fbfb1029..e62feff60 100644 --- a/app/templates/server/api/thing/index.spec.js +++ b/app/templates/server/api/thing/index.spec.js @@ -37,7 +37,7 @@ describe('Thing API Router:', function() { describe('GET /api/things', function() { it('should route to thing.controller.index', function() { - return router.get.withArgs('/', 'thingCtrl.index').should.have.been.calledOnce; + router.get.withArgs('/', 'thingCtrl.index').should.have.been.calledOnce; }); });<% if(filters.mongoose) { %> @@ -45,7 +45,7 @@ describe('Thing API Router:', function() { describe('GET /api/things/:id', function() { it('should route to thing.controller.show', function() { - return router.get.withArgs('/:id', 'thingCtrl.show').should.have.been.calledOnce; + router.get.withArgs('/:id', 'thingCtrl.show').should.have.been.calledOnce; }); }); @@ -53,7 +53,7 @@ describe('Thing API Router:', function() { describe('POST /api/things', function() { it('should route to thing.controller.create', function() { - return router.post.withArgs('/', 'thingCtrl.create').should.have.been.calledOnce; + router.post.withArgs('/', 'thingCtrl.create').should.have.been.calledOnce; }); }); @@ -61,7 +61,7 @@ describe('Thing API Router:', function() { describe('PUT /api/things/:id', function() { it('should route to thing.controller.update', function() { - return router.put.withArgs('/:id', 'thingCtrl.update').should.have.been.calledOnce; + router.put.withArgs('/:id', 'thingCtrl.update').should.have.been.calledOnce; }); }); @@ -69,7 +69,7 @@ describe('Thing API Router:', function() { describe('PATCH /api/things/:id', function() { it('should route to thing.controller.update', function() { - return router.patch.withArgs('/:id', 'thingCtrl.update').should.have.been.calledOnce; + router.patch.withArgs('/:id', 'thingCtrl.update').should.have.been.calledOnce; }); }); @@ -77,7 +77,7 @@ describe('Thing API Router:', function() { describe('DELETE /api/things/:id', function() { it('should route to thing.controller.destroy', function() { - return router.delete.withArgs('/:id', 'thingCtrl.destroy').should.have.been.calledOnce; + router.delete.withArgs('/:id', 'thingCtrl.destroy').should.have.been.calledOnce; }); });<% } %> diff --git a/app/templates/server/api/thing/thing.controller.js b/app/templates/server/api/thing/thing.controller.js index 72a678cf4..4d5abca05 100644 --- a/app/templates/server/api/thing/thing.controller.js +++ b/app/templates/server/api/thing/thing.controller.js @@ -7,10 +7,57 @@ * DELETE /things/:id -> destroy */ -'use strict'; +'use strict';<% if (filters.mongoose) { %> -var _ = require('lodash');<% if (filters.mongoose) { %> -var Thing = require('./thing.model');<% } %> +var _ = require('lodash'); +var Thing = require('./thing.model'); + +function handleError(res, statusCode){ + statusCode = statusCode || 500; + return function(err){ + res.send(statusCode, err); + }; +} + +function responseWithResult(res, statusCode){ + statusCode = statusCode || 200; + return function(entity){ + if(entity){ + return res.json(statusCode, entity); + } + }; +} + +function handleEntityNotFound(res){ + return function(entity){ + if(!entity){ + res.send(404); + return null; + } + return entity; + }; +} + +function saveUpdates(updates){ + return function(entity){ + var updated = _.merge(entity, updates); + return updated.saveAsync() + .then(function () { + return updated; + }); + }; +} + +function removeEntity(res){ + return function (entity) { + if(entity){ + return entity.removeAsync() + .then(function() { + return res.send(204); + }); + } + }; +}<% } %> // Get list of things exports.index = function(req, res) {<% if (!filters.mongoose) { %> @@ -34,56 +81,43 @@ exports.index = function(req, res) {<% if (!filters.mongoose) { %> name : 'Deployment Ready', info : 'Easily deploy your app to Heroku or Openshift with the heroku and openshift subgenerators' } - ]);<% } %><% if (filters.mongoose) { %> - Thing.find(function (err, things) { - if(err) { return handleError(res, err); } - return res.json(200, things); - });<% } %> + ]);<% } if (filters.mongoose) { %> + Thing.findAsync() + .then(responseWithResult(res)) + .catch(handleError(res));<% } %> };<% if (filters.mongoose) { %> // Get a single thing exports.show = function(req, res) { - Thing.findById(req.params.id, function (err, thing) { - if(err) { return handleError(res, err); } - if(!thing) { return res.send(404); } - return res.json(thing); - }); + Thing.findByIdAsync(req.params.id) + .then(handleEntityNotFound(res)) + .then(responseWithResult(res)) + .catch(handleError(res)); }; // Creates a new thing in the DB. exports.create = function(req, res) { - Thing.create(req.body, function(err, thing) { - if(err) { return handleError(res, err); } - return res.json(201, thing); - }); + Thing.createAsync(req.body) + .then(responseWithResult(res, 201)) + .catch(handleError(res)); }; // Updates an existing thing in the DB. exports.update = function(req, res) { - if(req.body._id) { delete req.body._id; } - Thing.findById(req.params.id, function (err, thing) { - if (err) { return handleError(res, err); } - if(!thing) { return res.send(404); } - var updated = _.merge(thing, req.body); - updated.save(function (err) { - if (err) { return handleError(res, err); } - return res.json(200, thing); - }); - }); + if(req.body._id) { + delete req.body._id; + } + Thing.findByIdAsync(req.params.id) + .then(handleEntityNotFound(res)) + .then(saveUpdates(req.body)) + .then(responseWithResult(res)) + .catch(handleError(res)); }; // Deletes a thing from the DB. exports.destroy = function(req, res) { - Thing.findById(req.params.id, function (err, thing) { - if(err) { return handleError(res, err); } - if(!thing) { return res.send(404); } - thing.remove(function(err) { - if(err) { return handleError(res, err); } - return res.send(204); - }); - }); -}; - -function handleError(res, err) { - return res.send(500, err); -}<% } %> + Thing.findByIdAsync(req.params.id) + .then(handleEntityNotFound(res)) + .then(removeEntity(res)) + .catch(handleError(res)); +};<% } %> diff --git a/app/templates/server/api/thing/thing.model(mongoose).js b/app/templates/server/api/thing/thing.model(mongoose).js index 92a791e70..a44bc710e 100644 --- a/app/templates/server/api/thing/thing.model(mongoose).js +++ b/app/templates/server/api/thing/thing.model(mongoose).js @@ -1,6 +1,6 @@ 'use strict'; -var mongoose = require('mongoose'), +var mongoose = require('mongoose-bird')(), Schema = mongoose.Schema; var ThingSchema = new Schema({ diff --git a/app/templates/server/api/user(auth)/index.spec.js b/app/templates/server/api/user(auth)/index.spec.js index 5bcd4c2c0..30b786fb3 100644 --- a/app/templates/server/api/user(auth)/index.spec.js +++ b/app/templates/server/api/user(auth)/index.spec.js @@ -47,7 +47,7 @@ describe('User API Router:', function() { describe('GET /api/users', function() { it('should verify admin role and route to user.controller.index', function() { - return router.get.withArgs('/', 'authService.hasRole.admin', 'userCtrl.index').should.have.been.calledOnce; + router.get.withArgs('/', 'authService.hasRole.admin', 'userCtrl.index').should.have.been.calledOnce; }); }); @@ -55,7 +55,7 @@ describe('User API Router:', function() { describe('DELETE /api/users/:id', function() { it('should verify admin role and route to user.controller.destroy', function() { - return router.delete.withArgs('/:id', 'authService.hasRole.admin', 'userCtrl.destroy').should.have.been.calledOnce; + router.delete.withArgs('/:id', 'authService.hasRole.admin', 'userCtrl.destroy').should.have.been.calledOnce; }); }); @@ -63,7 +63,7 @@ describe('User API Router:', function() { describe('GET /api/users/me', function() { it('should be authenticated and route to user.controller.me', function() { - return router.get.withArgs('/me', 'authService.isAuthenticated', 'userCtrl.me').should.have.been.calledOnce; + router.get.withArgs('/me', 'authService.isAuthenticated', 'userCtrl.me').should.have.been.calledOnce; }); }); @@ -71,7 +71,7 @@ describe('User API Router:', function() { describe('PUT /api/users/:id/password', function() { it('should be authenticated and route to user.controller.changePassword', function() { - return router.put.withArgs('/:id/password', 'authService.isAuthenticated', 'userCtrl.changePassword').should.have.been.calledOnce; + router.put.withArgs('/:id/password', 'authService.isAuthenticated', 'userCtrl.changePassword').should.have.been.calledOnce; }); }); @@ -79,7 +79,7 @@ describe('User API Router:', function() { describe('GET /api/users/:id', function() { it('should be authenticated and route to user.controller.show', function() { - return router.get.withArgs('/:id', 'authService.isAuthenticated', 'userCtrl.show').should.have.been.calledOnce; + router.get.withArgs('/:id', 'authService.isAuthenticated', 'userCtrl.show').should.have.been.calledOnce; }); }); @@ -87,7 +87,7 @@ describe('User API Router:', function() { describe('POST /api/users', function() { it('should route to user.controller.create', function() { - return router.post.withArgs('/', 'userCtrl.create').should.have.been.calledOnce; + router.post.withArgs('/', 'userCtrl.create').should.have.been.calledOnce; }); }); diff --git a/app/templates/server/api/user(auth)/user.controller.js b/app/templates/server/api/user(auth)/user.controller.js index 17e6e0e04..28e30199a 100644 --- a/app/templates/server/api/user(auth)/user.controller.js +++ b/app/templates/server/api/user(auth)/user.controller.js @@ -5,19 +5,37 @@ var passport = require('passport'); var config = require('../../config/environment'); var jwt = require('jsonwebtoken'); -var validationError = function(res, err) { - return res.json(422, err); +var validationError = function(res, statusCode) { + statusCode = statusCode || 422; + return function(err){ + res.json(statusCode, err); + }; }; +function handleError(res, statusCode){ + statusCode = statusCode || 500; + return function(err){ + res.send(statusCode, err); + }; +} + +function respondWith(res, statusCode){ + statusCode = statusCode || 200; + return function(){ + res.send(statusCode); + }; +} + /** * Get list of users * restriction: 'admin' */ exports.index = function(req, res) { - User.find({}, '-salt -password', function (err, users) { - if(err) return res.send(500, err); - res.json(200, users); - }); + User.findAsync({}, '-salt -hashedPassword') + .then(function (users) { + res.json(200, users); + }) + .catch(handleError(res)); }; /** @@ -27,11 +45,12 @@ exports.create = function (req, res, next) { var newUser = new User(req.body); newUser.provider = 'local'; newUser.role = 'user'; - newUser.save(function(err, user) { - if (err) return validationError(res, err); - var token = jwt.sign({_id: user._id }, config.secrets.session, { expiresInMinutes: 60*5 }); - res.json({ token: token }); - }); + newUser.saveAsync() + .then(function(user) { + var token = jwt.sign({_id: user._id }, config.secrets.session, { expiresInMinutes: 60*5 }); + res.json({ token: token }); + }) + .catch(validationError(res)); }; /** @@ -40,11 +59,16 @@ exports.create = function (req, res, next) { exports.show = function (req, res, next) { var userId = req.params.id; - User.findById(userId, function (err, user) { - if (err) return next(err); - if (!user) return res.send(401); - res.json(user.profile); - }); + User.findByIdAsync(userId) + .then(function (user) { + if(!user) { + return res.send(401); + } + res.json(user.profile); + }) + .catch(function(err){ + return next(err); + }); }; /** @@ -52,10 +76,9 @@ exports.show = function (req, res, next) { * restriction: 'admin' */ exports.destroy = function(req, res) { - User.findByIdAndRemove(req.params.id, function(err, user) { - if(err) return res.send(500, err); - return res.send(204); - }); + User.findByIdAndRemoveAsync(req.params.id) + .then(respondWith(res, 204)) + .catch(handleError(res)); }; /** @@ -66,21 +89,17 @@ exports.changePassword = function(req, res, next) { var oldPass = String(req.body.oldPassword); var newPass = String(req.body.newPassword); - User.findById(userId, function (err, user) { - user.authenticate(oldPass, function(authErr, authenticated) { - if (authErr) res.send(403); - - if (authenticated) { + User.findByIdAsync(userId) + .then(function(user) { + if(user.authenticate(oldPass)) { user.password = newPass; - user.save(function(err) { - if (err) return validationError(res, err); - res.send(200); - }); + return user.saveAsync() + .then(respondWith(res, 200)) + .catch(validationError(res)); } else { - res.send(403); + return res.send(403); } }); - }); }; /** @@ -88,13 +107,13 @@ exports.changePassword = function(req, res, next) { */ exports.me = function(req, res, next) { var userId = req.user._id; - User.findOne({ - _id: userId - }, '-salt -password', function(err, user) { // don't ever give out the password or salt - if (err) return next(err); - if (!user) return res.json(401); - res.json(user); - }); + + User.findOneAsync({ _id: userId }, '-salt -hashedPassword') + .then(function(user) { // don't ever give out the password or salt + if (!user) { return res.json(401); } + res.json(user); + }) + .catch(function(err){ return next(err); }); }; /** diff --git a/app/templates/server/api/user(auth)/user.model.js b/app/templates/server/api/user(auth)/user.model.js index b3497f859..b90386886 100644 --- a/app/templates/server/api/user(auth)/user.model.js +++ b/app/templates/server/api/user(auth)/user.model.js @@ -1,6 +1,6 @@ 'use strict'; -var mongoose = require('mongoose'); +var mongoose = require('mongoose-bird')(); var Schema = mongoose.Schema; var crypto = require('crypto');<% if(filters.oauth) { %> var authTypes = ['github', 'twitter', 'facebook', 'google'];<% } %> @@ -53,7 +53,9 @@ UserSchema UserSchema .path('email') .validate(function(email) {<% if (filters.oauth) { %> - if (authTypes.indexOf(this.provider) !== -1) return true;<% } %> + if (authTypes.indexOf(this.provider) !== -1){ + return true; + } <% } %> return email.length; }, 'Email cannot be blank'); @@ -61,7 +63,9 @@ UserSchema UserSchema .path('password') .validate(function(password) {<% if (filters.oauth) { %> - if (authTypes.indexOf(this.provider) !== -1) return true;<% } %> + if (authTypes.indexOf(this.provider) !== -1){ + return true; + } <% } %> return password.length; }, 'Password cannot be blank'); @@ -70,14 +74,19 @@ UserSchema .path('email') .validate(function(value, respond) { var self = this; - this.constructor.findOne({email: value}, function(err, user) { - if(err) throw err; - if(user) { - if(self.id === user.id) return respond(true); - return respond(false); - } - respond(true); - }); + return this.constructor.findOneAsync({email: value}) + .then(function(user) { + if(user) { + if(self.id === user.id) { + return respond(true); + } + return respond(false); + } + return respond(true); + }) + .catch(function(err){ + throw err; + }); }, 'The specified email address is already in use.'); var validatePresenceOf = function(value) { diff --git a/app/templates/server/api/user(auth)/user.model.spec.js b/app/templates/server/api/user(auth)/user.model.spec.js index afe8af882..fe51ea2ea 100644 --- a/app/templates/server/api/user(auth)/user.model.spec.js +++ b/app/templates/server/api/user(auth)/user.model.spec.js @@ -10,59 +10,39 @@ var user = new User({ password: 'password' }); -describe('User Model:', function() { - - // Clear users before testing +describe('User Model', function() { before(function() { + // Clear users before testing return User.remove().exec(); }); - describe('User (schema)', function() { - - it('should begin with no users', function() { - return User.find({}).exec().should.eventually.have.length(0); - }); - + afterEach(function() { + return User.remove().exec(); }); - describe('user (instance)', function() { - - describe('.save()', function() { - // Clear users after tests - afterEach(function() { - return User.remove().exec(); - }); - - it('should fail when saving a duplicate user', function(done) { - user.save(function() { - var userDup = new User(user); - userDup.save(function(err) { - err.should.be.instanceOf(Error); - done(); - }); - }); - }); - - it('should fail when saving without an email', function(done) { - user.email = ''; - user.save(function(err) { - err.should.be.instanceOf(Error); - done(); - }); - }); - - }); + it('should begin with no users', function() { + return User.findAsync({}) + .should.eventually.have.length(0); + }); - describe('.authenticate()', function() { + it('should fail when saving a duplicate user', function() { + return user.saveAsync() + .then(function() { + var userDup = new User(user); + return userDup.saveAsync(); + }).should.be.rejected; + }); - it("should authenticate user if password is valid", function() { - return user.authenticate('password').should.be.true; - }); + it('should fail when saving without an email', function() { + user.email = ''; + return user.saveAsync().should.be.rejected; + }); - it("should not authenticate user if password is invalid", function() { - return user.authenticate('blah').should.not.be.true; - }); + it("should authenticate user if password is valid", function() { + user.authenticate('password').should.be.true; + }); - }); + it("should not authenticate user if password is invalid", function() { + user.authenticate('blah').should.not.be.true; }); }); diff --git a/app/templates/server/app.js b/app/templates/server/app.js index 08b942f43..5369e94ef 100644 --- a/app/templates/server/app.js +++ b/app/templates/server/app.js @@ -8,7 +8,7 @@ process.env.NODE_ENV = process.env.NODE_ENV || 'development'; var express = require('express');<% if (filters.mongoose) { %> -var mongoose = require('mongoose');<% } %> +var mongoose = require('mongoose-bird')();<% } %> var config = require('./config/environment'); <% if (filters.mongoose) { %> // Connect to database @@ -34,4 +34,4 @@ server.listen(config.port, config.ip, function () { }); // Expose app -exports = module.exports = app; \ No newline at end of file +exports = module.exports = app; diff --git a/app/templates/server/auth(auth)/auth.service.js b/app/templates/server/auth(auth)/auth.service.js index 38ec34302..101dcc5c5 100644 --- a/app/templates/server/auth(auth)/auth.service.js +++ b/app/templates/server/auth(auth)/auth.service.js @@ -1,6 +1,6 @@ 'use strict'; -var mongoose = require('mongoose'); +var mongoose = require('mongoose-bird')(); var passport = require('passport'); var config = require('../config/environment'); var jwt = require('jsonwebtoken'); @@ -25,13 +25,17 @@ function isAuthenticated() { }) // Attach user to request .use(function(req, res, next) { - User.findById(req.user._id, function (err, user) { - if (err) return next(err); - if (!user) return res.send(401); - - req.user = user; - next(); - }); + User.findByIdAsync(req.user._id) + .then(function (user) { + if (!user) { + return res.send(401); + } + req.user = user; + next(); + }) + .catch(function(err){ + return next(err); + }); }); } @@ -39,7 +43,9 @@ function isAuthenticated() { * Checks if the user role meets the minimum requirements of the route */ function hasRole(roleRequired) { - if (!roleRequired) throw new Error('Required role needs to be set'); + if (!roleRequired) { + throw new Error('Required role needs to be set'); + } return compose() .use(isAuthenticated()) @@ -64,7 +70,9 @@ function signToken(id) { * Set token cookie directly for oAuth strategies */ function setTokenCookie(req, res) { - if (!req.user) return res.json(404, { message: 'Something went wrong, please try again.'}); + if (!req.user) { + return res.json(404, { message: 'Something went wrong, please try again.'}); + } var token = signToken(req.user._id, req.user.role); res.cookie('token', JSON.stringify(token)); res.redirect('/'); @@ -73,4 +81,4 @@ function setTokenCookie(req, res) { exports.isAuthenticated = isAuthenticated; exports.hasRole = hasRole; exports.signToken = signToken; -exports.setTokenCookie = setTokenCookie; \ No newline at end of file +exports.setTokenCookie = setTokenCookie; diff --git a/app/templates/server/config/express.js b/app/templates/server/config/express.js index 2243a7779..2403780f1 100644 --- a/app/templates/server/config/express.js +++ b/app/templates/server/config/express.js @@ -17,7 +17,7 @@ var config = require('./environment');<% if (filters.auth) { %> var passport = require('passport');<% } %><% if (filters.twitterAuth) { %> var session = require('express-session'); var mongoStore = require('connect-mongo')(session); -var mongoose = require('mongoose');<% } %> +var mongoose = require('mongoose-bird')();<% } %> module.exports = function(app) { var env = app.get('env'); @@ -57,4 +57,4 @@ module.exports = function(app) { app.use(morgan('dev')); app.use(errorHandler()); // Error handler - has to be last } -}; \ No newline at end of file +}; diff --git a/app/templates/server/config/seed(mongoose).js b/app/templates/server/config/seed(mongoose).js index 27ab19417..6d56010b6 100644 --- a/app/templates/server/config/seed(mongoose).js +++ b/app/templates/server/config/seed(mongoose).js @@ -6,44 +6,51 @@ 'use strict'; var Thing = require('../api/thing/thing.model'); -<% if (filters.auth) { %>var User = require('../api/user/user.model');<% } %> +<% if (filters.auth) { %> +var User = require('../api/user/user.model'); +<% } %> -Thing.find({}).remove(function() { - Thing.create({ - name : 'Development Tools', - info : 'Integration with popular tools such as Bower, Grunt, Karma, Mocha, JSHint, Node Inspector, Livereload, Protractor, Jade, Stylus, Sass, CoffeeScript, and Less.' - }, { - name : 'Server and Client integration', - info : 'Built with a powerful and fun stack: MongoDB, Express, AngularJS, and Node.' - }, { - name : 'Smart Build System', - info : 'Build system ignores `spec` files, allowing you to keep tests alongside code. Automatic injection of scripts and styles into your index.html' - }, { - name : 'Modular Structure', - info : 'Best practice client and server structures allow for more code reusability and maximum scalability' - }, { - name : 'Optimized Build', - info : 'Build process packs up your templates as a single JavaScript payload, minifies your scripts/css/images, and rewrites asset names for caching.' - },{ - name : 'Deployment Ready', - info : 'Easily deploy your app to Heroku or Openshift with the heroku and openshift subgenerators' +Thing.find({}).removeAsync() + .then(function() { + Thing.create({ + name : 'Development Tools', + info : 'Integration with popular tools such as Bower, Grunt, Karma, Mocha, JSHint, Node Inspector, Livereload, Protractor, Jade, Stylus, Sass, CoffeeScript, and Less.' + }, { + name : 'Server and Client integration', + info : 'Built with a powerful and fun stack: MongoDB, Express, AngularJS, and Node.' + }, { + name : 'Smart Build System', + info : 'Build system ignores `spec` files, allowing you to keep tests alongside code. Automatic injection of scripts and styles into your index.html' + }, { + name : 'Modular Structure', + info : 'Best practice client and server structures allow for more code reusability and maximum scalability' + }, { + name : 'Optimized Build', + info : 'Build process packs up your templates as a single JavaScript payload, minifies your scripts/css/images, and rewrites asset names for caching.' + },{ + name : 'Deployment Ready', + info : 'Easily deploy your app to Heroku or Openshift with the heroku and openshift subgenerators' + }); }); -});<% if (filters.auth) { %> -User.find({}).remove(function() { - User.create({ - provider: 'local', - name: 'Test User', - email: 'test@test.com', - password: 'test' - }, { - provider: 'local', - role: 'admin', - name: 'Admin', - email: 'admin@admin.com', - password: 'admin' - }, function() { - console.log('finished populating users'); - } - ); -});<% } %> \ No newline at end of file +<% if (filters.auth) { %> + +User.find({}).removeAsync() + .then(function() { + User.create({ + provider: 'local', + name: 'Test User', + email: 'test@test.com', + password: 'test' + }, { + provider: 'local', + role: 'admin', + name: 'Admin', + email: 'admin@admin.com', + password: 'admin' + }, function() { + console.log('finished populating users'); + } + ); + }); +<% } %> diff --git a/endpoint/templates/index.spec.js b/endpoint/templates/index.spec.js index 62caed5dc..b5bfb3a41 100644 --- a/endpoint/templates/index.spec.js +++ b/endpoint/templates/index.spec.js @@ -37,7 +37,7 @@ describe('<%= classedName %> API Router:', function() { describe('GET <%= route %>', function() { it('should route to <%= name %>.controller.index', function() { - return router.get.withArgs('/', '<%= name %>Ctrl.index').should.have.been.calledOnce; + router.get.withArgs('/', '<%= name %>Ctrl.index').should.have.been.calledOnce; }); });<% if(filters.mongoose) { %> @@ -45,7 +45,7 @@ describe('<%= classedName %> API Router:', function() { describe('GET <%= route %>/:id', function() { it('should route to <%= name %>.controller.show', function() { - return router.get.withArgs('/:id', '<%= name %>Ctrl.show').should.have.been.calledOnce; + router.get.withArgs('/:id', '<%= name %>Ctrl.show').should.have.been.calledOnce; }); }); @@ -53,7 +53,7 @@ describe('<%= classedName %> API Router:', function() { describe('POST <%= route %>', function() { it('should route to <%= name %>.controller.create', function() { - return router.post.withArgs('/', '<%= name %>Ctrl.create').should.have.been.calledOnce; + router.post.withArgs('/', '<%= name %>Ctrl.create').should.have.been.calledOnce; }); }); @@ -61,7 +61,7 @@ describe('<%= classedName %> API Router:', function() { describe('PUT <%= route %>/:id', function() { it('should route to <%= name %>.controller.update', function() { - return router.put.withArgs('/:id', '<%= name %>Ctrl.update').should.have.been.calledOnce; + router.put.withArgs('/:id', '<%= name %>Ctrl.update').should.have.been.calledOnce; }); }); @@ -69,7 +69,7 @@ describe('<%= classedName %> API Router:', function() { describe('PATCH <%= route %>/:id', function() { it('should route to <%= name %>.controller.update', function() { - return router.patch.withArgs('/:id', '<%= name %>Ctrl.update').should.have.been.calledOnce; + router.patch.withArgs('/:id', '<%= name %>Ctrl.update').should.have.been.calledOnce; }); }); @@ -77,7 +77,7 @@ describe('<%= classedName %> API Router:', function() { describe('DELETE <%= route %>/:id', function() { it('should route to <%= name %>.controller.destroy', function() { - return router.delete.withArgs('/:id', '<%= name %>Ctrl.destroy').should.have.been.calledOnce; + router.delete.withArgs('/:id', '<%= name %>Ctrl.destroy').should.have.been.calledOnce; }); });<% } %> diff --git a/endpoint/templates/name.controller.js b/endpoint/templates/name.controller.js index 1d9da544e..5851a1338 100644 --- a/endpoint/templates/name.controller.js +++ b/endpoint/templates/name.controller.js @@ -1,60 +1,94 @@ -'use strict'; +'use strict';<% if (filters.mongoose) { %> -var _ = require('lodash');<% if (filters.mongoose) { %> -var <%= classedName %> = require('./<%= name %>.model');<% } %> +var _ = require('lodash'); +var <%= classedName %> = require('./<%= name %>.model'); + +function handleError(res, statusCode){ + statusCode = statusCode || 500; + return function(err){ + res.send(statusCode, err); + }; +} + +function responseWithResult(res, statusCode){ + statusCode = statusCode || 200; + return function(entity){ + if(entity){ + return res.json(statusCode, entity); + } + }; +} + +function handleEntityNotFound(res){ + return function(entity){ + if(!entity){ + res.send(404); + return null; + } + return entity; + }; +} + +function saveUpdates(updates){ + return function(entity){ + var updated = _.merge(entity, updates); + return updated.saveAsync() + .then(function () { + return updated; + }); + }; +} + +function removeEntity(res){ + return function (entity) { + if(entity){ + return entity.removeAsync() + .then(function() { + return res.send(204); + }); + } + }; +}<% } %> // Get list of <%= name %>s exports.index = function(req, res) {<% if (!filters.mongoose) { %> - res.json([]);<% } %><% if (filters.mongoose) { %> - <%= classedName %>.find(function (err, <%= name %>s) { - if(err) { return handleError(res, err); } - return res.json(200, <%= name %>s); - });<% } %> + res.json([]);<% } if (filters.mongoose) { %> + <%= classedName %>.findAsync() + .then(responseWithResult(res)) + .catch(handleError(res));<% } %> };<% if (filters.mongoose) { %> // Get a single <%= name %> exports.show = function(req, res) { - <%= classedName %>.findById(req.params.id, function (err, <%= name %>) { - if(err) { return handleError(res, err); } - if(!<%= name %>) { return res.send(404); } - return res.json(<%= name %>); - }); + <%= classedName %>.findByIdAsync(req.params.id) + .then(handleEntityNotFound(res)) + .then(responseWithResult(res)) + .catch(handleError(res)); }; // Creates a new <%= name %> in the DB. exports.create = function(req, res) { - <%= classedName %>.create(req.body, function(err, <%= name %>) { - if(err) { return handleError(res, err); } - return res.json(201, <%= name %>); - }); + <%= classedName %>.createAsync(req.body) + .then(responseWithResult(res, 201)) + .catch(handleError(res)); }; // Updates an existing <%= name %> in the DB. exports.update = function(req, res) { - if(req.body._id) { delete req.body._id; } - <%= classedName %>.findById(req.params.id, function (err, <%= name %>) { - if (err) { return handleError(res, err); } - if(!<%= name %>) { return res.send(404); } - var updated = _.merge(<%= name %>, req.body); - updated.save(function (err) { - if (err) { return handleError(res, err); } - return res.json(200, <%= name %>); - }); - }); + if(req.body._id) { + delete req.body._id; + } + <%= classedName %>.findByIdAsync(req.params.id) + .then(handleEntityNotFound(res)) + .then(saveUpdates(req.body)) + .then(responseWithResult(res)) + .catch(handleError(res)); }; // Deletes a <%= name %> from the DB. exports.destroy = function(req, res) { - <%= classedName %>.findById(req.params.id, function (err, <%= name %>) { - if(err) { return handleError(res, err); } - if(!<%= name %>) { return res.send(404); } - <%= name %>.remove(function(err) { - if(err) { return handleError(res, err); } - return res.send(204); - }); - }); -}; - -function handleError(res, err) { - return res.send(500, err); -}<% } %> \ No newline at end of file + <%= classedName %>.findByIdAsync(req.params.id) + .then(handleEntityNotFound(res)) + .then(removeEntity(res)) + .catch(handleError(res)); +};<% } %> diff --git a/endpoint/templates/name.model(mongoose).js b/endpoint/templates/name.model(mongoose).js index 89e0dfaa7..9b23d9d41 100644 --- a/endpoint/templates/name.model(mongoose).js +++ b/endpoint/templates/name.model(mongoose).js @@ -1,6 +1,6 @@ 'use strict'; -var mongoose = require('mongoose'), +var mongoose = require('mongoose-bird')(), Schema = mongoose.Schema; var <%= classedName %>Schema = new Schema({ @@ -9,4 +9,4 @@ var <%= classedName %>Schema = new Schema({ active: Boolean }); -module.exports = mongoose.model('<%= classedName %>', <%= classedName %>Schema); \ No newline at end of file +module.exports = mongoose.model('<%= classedName %>', <%= classedName %>Schema); From 49d5bbdf5c8a59812e8f72ad5b95309894bea5ca Mon Sep 17 00:00:00 2001 From: kingcody Date: Sat, 6 Sep 2014 22:11:53 -0400 Subject: [PATCH 08/20] fix(gen): filter `client/components/socket` js files Changes: - add `(js)` filter to the js files in `client/components/socket` Closes #530 --- .../socket(socketio)/{socket.mock.js => socket.mock(js).js} | 0 .../socket(socketio)/{socket.service.js => socket.service(js).js} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename app/templates/client/components/socket(socketio)/{socket.mock.js => socket.mock(js).js} (100%) rename app/templates/client/components/socket(socketio)/{socket.service.js => socket.service(js).js} (100%) diff --git a/app/templates/client/components/socket(socketio)/socket.mock.js b/app/templates/client/components/socket(socketio)/socket.mock(js).js similarity index 100% rename from app/templates/client/components/socket(socketio)/socket.mock.js rename to app/templates/client/components/socket(socketio)/socket.mock(js).js diff --git a/app/templates/client/components/socket(socketio)/socket.service.js b/app/templates/client/components/socket(socketio)/socket.service(js).js similarity index 100% rename from app/templates/client/components/socket(socketio)/socket.service.js rename to app/templates/client/components/socket(socketio)/socket.service(js).js From c2ab8ae922ec0f2e377e560e937d09091e596b07 Mon Sep 17 00:00:00 2001 From: kingcody Date: Sat, 6 Sep 2014 21:53:07 -0400 Subject: [PATCH 09/20] test(gen): make tests more strict and DRY Changes: - `genFiles(ops)` creates an array of expected files based on options given - test for generated files on every set of tests - test for unexpected files - use testExec to be more DRY Exposes a bug with `client/components/socket`: `socket.service.js` and `socket.mock.js` are not filtered. See #530 --- package.json | 1 + test/test-file-creation.js | 416 +++++++++++++++++++++++++------------ 2 files changed, 287 insertions(+), 130 deletions(-) diff --git a/package.json b/package.json index cab669526..394f5e6d1 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "marked": "~0.2.8", "mocha": "~1.21.0", "q": "^1.0.1", + "recursive-readdir": "^1.2.0", "semver": "~2.2.1", "shelljs": "^0.3.0", "underscore.string": "^2.3.3" diff --git a/test/test-file-creation.js b/test/test-file-creation.js index 2dc0ee548..2ea172b2e 100644 --- a/test/test-file-creation.js +++ b/test/test-file-creation.js @@ -1,11 +1,12 @@ /*global describe, beforeEach, it */ 'use strict'; var path = require('path'); +var fs = require('fs-extra'); +var exec = require('child_process').exec; var helpers = require('yeoman-generator').test; var chai = require('chai'); var expect = chai.expect; -var fs = require('fs-extra'); -var exec = require('child_process').exec; +var recursiveReadDir = require('recursive-readdir'); describe('angular-fullstack generator', function () { var gen, defaultOptions = { @@ -32,6 +33,201 @@ describe('angular-fullstack generator', function () { }); } + function assertOnlyFiles(expectedFiles, done, path, skip) { + path = path || './'; + skip = skip || ['e2e', 'node_modules', 'client/bower_components']; + + recursiveReadDir(path, skip, function(err, files) { + if (err) { return done(err); } + + for (var i = 0, expectedFilesLength = expectedFiles.length; i < expectedFilesLength; i++) { + var index = files.indexOf(expectedFiles[i]); + files.splice(index, 1); + } + + if (files.length !== 0) { + err = new Error('unexpected files found'); + err.expected = ''; + err.actual = files.join('\n'); + return done(err); + } + + done(); + }); + } + + function runTest(type, self, cb, timeout) { + self.timeout(timeout || 60000); + gen.run({}, function() { + exec(type, function(error, stdout, stderr) { + switch(type) { + case 'grunt test:client': + expect(stdout, 'Client tests failed \n' + stdout ).to.contain('Executed 1 of 1\u001b[32m SUCCESS\u001b'); + break; + case 'grunt jshint': + expect(stdout).to.contain('Done, without errors.'); + break; + case 'grunt test:server': + expect(stdout, 'Server tests failed (do you have mongoDB running?) \n' + stdout).to.contain('Done, without errors.'); + break; + default: + expect(stderr).to.be.empty; + } + + cb(); + }); + }); + } + + function genFiles(ops) { + var mapping = { + stylesheet: { + sass: 'scss', + stylus: 'styl', + less: 'less', + css: 'css' + }, + markup: { + jade: 'jade', + html: 'html' + }, + script: { + js: 'js', + coffee: 'coffee' + } + }, + files = []; + + var oauthFiles = function(type) { + return [ + 'server/auth/' + type + '/index.js', + 'server/auth/' + type + '/passport.js', + ]; + }; + + + var script = mapping.script[ops.script], + markup = mapping.markup[ops.markup], + stylesheet = mapping.stylesheet[ops.stylesheet]; + + files = files.concat([ + 'client/.htaccess', + 'client/.jshintrc', + 'client/favicon.ico', + 'client/robots.txt', + 'client/index.html', + 'client/app/app.' + script, + 'client/app/app.' + stylesheet, + 'client/app/main/main.' + script, + 'client/app/main/main.' + markup, + 'client/app/main/main.' + stylesheet, + 'client/app/main/main.controller.' + script, + 'client/app/main/main.controller.spec.' + script, + 'client/assets/images/yeoman.png', + 'client/components/navbar/navbar.' + markup, + 'client/components/navbar/navbar.controller.' + script, + 'server/.jshintrc', + 'server/.jshintrc-spec', + 'server/app.js', + 'server/routes.js', + 'server/api/thing/index.js', + 'server/api/thing/index.spec.js', + 'server/api/thing/thing.controller.js', + 'server/api/thing/thing.e2e.js', + 'server/components/errors/index.js', + 'server/config/local.env.js', + 'server/config/local.env.sample.js', + 'server/config/express.js', + 'server/config/environment/index.js', + 'server/config/environment/development.js', + 'server/config/environment/production.js', + 'server/config/environment/test.js', + 'server/views/404.' + markup, + '.bowerrc', + '.buildignore', + '.editorconfig', + '.gitattributes', + '.gitignore', + '.travis.yml', + '.yo-rc.json', + 'Gruntfile.js', + 'package.json', + 'bower.json', + 'karma.conf.js', + 'mocha.conf.js', + 'protractor.conf.js' + ]); + + if (ops.uibootstrap) { + files = files.concat([ + 'client/components/modal/modal.' + markup, + 'client/components/modal/modal.' + stylesheet, + 'client/components/modal/modal.service.' + script, + ]); + } + + if (ops.mongoose) { + files = files.concat([ + 'server/api/thing/thing.model.js', + 'server/config/seed.js' + ]); + } + + if (ops.auth) { + files = files.concat([ + 'client/app/account/account.' + script, + 'client/app/account/login/login.' + markup, + 'client/app/account/login/login.' + stylesheet, + 'client/app/account/login/login.controller.' + script, + 'client/app/account/settings/settings.' + markup, + 'client/app/account/settings/settings.controller.' + script, + 'client/app/account/signup/signup.' + markup, + 'client/app/account/signup/signup.controller.' + script, + 'client/app/admin/admin.' + markup, + 'client/app/admin/admin.' + stylesheet, + 'client/app/admin/admin.' + script, + 'client/app/admin/admin.controller.' + script, + 'client/components/auth/auth.service.' + script, + 'client/components/auth/user.service.' + script, + 'client/components/mongoose-error/mongoose-error.directive.' + script, + 'server/api/user/index.js', + 'server/api/user/index.spec.js', + 'server/api/user/user.controller.js', + 'server/api/user/user.e2e.js', + 'server/api/user/user.model.js', + 'server/api/user/user.model.spec.js', + 'server/auth/index.js', + 'server/auth/auth.service.js', + 'server/auth/local/index.js', + 'server/auth/local/passport.js' + ]); + } + + if (ops.oauth) { + var oauth = ops.oauth; + for (var i = 0, oauthLength = oauth.length; i < oauthLength; i++) { + files = files.concat(oauthFiles(oauth[i].replace('Auth', ''))); + } + } + + if (ops.socketio) { + files = files.concat([ + 'client/components/socket/socket.service.' + script, + 'client/components/socket/socket.mock.' + script, + 'server/api/thing/thing.socket.js', + 'server/config/socketio.js' + ]); + } + + return files; + } + + function everyFile(files, ops) { + ops = ops || { + skip: ['node_modules', 'client/bower_components', 'e2e'] + } + } + beforeEach(function (done) { this.timeout(10000); var deps = [ @@ -68,33 +264,15 @@ describe('angular-fullstack generator', function () { }); it('should run client tests successfully', function(done) { - this.timeout(60000); - gen.run({}, function () { - exec('grunt test:client', function (error, stdout, stderr) { - expect(stdout, 'Client tests failed \n' + stdout ).to.contain('Executed 1 of 1\u001b[32m SUCCESS\u001b'); - done(); - }); - }); + runTest('grunt test:client', this, done); }); it('should pass jshint', function(done) { - this.timeout(60000); - gen.run({}, function () { - exec('grunt jshint', function (error, stdout, stderr) { - expect(stdout).to.contain('Done, without errors.'); - done(); - }); - }); + runTest('grunt jshint', this, done); }); it('should run server tests successfully', function(done) { - this.timeout(60000); - gen.run({}, function () { - exec('grunt test:server', function (error, stdout, stderr) { - expect(stdout, 'Server tests failed (do you have mongoDB running?) \n' + stdout).to.contain('Done, without errors.'); - done(); - }); - }); + runTest('grunt test:server', this, done); }); it('should run server tests successfully with generated endpoint', function(done) { @@ -130,6 +308,19 @@ describe('angular-fullstack generator', function () { }); }); + it('should generate expected files', function (done) { + gen.run({}, function () { + helpers.assertFile(genFiles(defaultOptions)); + done(); + }); + }); + + it('should not generate unexpected files', function (done) { + gen.run({}, function () { + assertOnlyFiles(genFiles(defaultOptions), done); + }); + }); + // it('should run e2e tests successfully', function(done) { // this.timeout(80000); // gen.run({}, function () { @@ -144,166 +335,131 @@ describe('angular-fullstack generator', function () { }); describe('with other preprocessors and oauth', function() { + var testOptions = { + script: 'coffee', + markup: 'jade', + stylesheet: 'less', + router: 'uirouter', + mongoose: true, + auth: true, + oauth: ['twitterAuth', 'facebookAuth', 'googleAuth'], + socketio: true + }; + beforeEach(function() { - helpers.mockPrompt(gen, { - script: 'coffee', - markup: 'jade', - stylesheet: 'less', - router: 'uirouter', - mongoose: true, - auth: true, - oauth: ['twitterAuth', 'facebookAuth', 'googleAuth'], - socketio: true - }); + helpers.mockPrompt(gen, testOptions); }); it('should run client tests successfully', function(done) { - this.timeout(60000); - gen.run({}, function () { - exec('grunt test:client', function (error, stdout, stderr) { - expect(stdout, 'Client tests failed \n' + stdout ).to.contain('Executed 1 of 1\u001b[32m SUCCESS\u001b'); - done(); - }); - }); + runTest('grunt test:client', this, done); }); it('should pass jshint', function(done) { - this.timeout(60000); + runTest('grunt jshint', this, done); + }); + + it('should run server tests successfully', function(done) { + runTest('grunt test:server', this, done); + }); + + it('should generate expected files', function (done) { gen.run({}, function () { - exec('grunt jshint', function (error, stdout, stderr) { - expect(stdout).to.contain('Done, without errors.'); - done(); - }); + helpers.assertFile(genFiles(testOptions)); + done(); }); }); - it('should run server tests successfully', function(done) { - this.timeout(60000); + it('should not generate unexpected files', function (done) { gen.run({}, function () { - exec('grunt test:server', function (error, stdout, stderr) { - expect(stdout, 'Server tests failed (do you have mongoDB running?) \n' + stdout).to.contain('Done, without errors.'); - done(); - }); + assertOnlyFiles(genFiles(testOptions), done); }); }); }); describe('with other preprocessors and no server options', function() { + var testOptions = { + script: 'coffee', + markup: 'jade', + stylesheet: 'stylus', + router: 'ngroute', + mongoose: false, + auth: false, + oauth: [], + socketio: false + }; + beforeEach(function(done) { - helpers.mockPrompt(gen, { - script: 'coffee', - markup: 'jade', - stylesheet: 'stylus', - router: 'ngroute', - mongoose: false, - auth: false, - oauth: [], - socketio: false - }); + helpers.mockPrompt(gen, testOptions); done(); }); it('should run client tests successfully', function(done) { - this.timeout(60000); - gen.run({}, function () { - exec('grunt test:client', function (error, stdout, stderr) { - expect(stdout, 'Client tests failed \n' + stdout ).to.contain('Executed 1 of 1\u001b[32m SUCCESS\u001b'); - done(); - }); - }); + runTest('grunt test:client', this, done); }); it('should pass jshint', function(done) { - this.timeout(60000); + runTest('grunt jshint', this, done); + }); + + it('should run server tests successfully', function(done) { + runTest('grunt test:server', this, done); + }); + + it('should generate expected files', function (done) { gen.run({}, function () { - exec('grunt jshint', function (error, stdout, stderr) { - expect(stdout).to.contain('Done, without errors.'); - done(); - }); + helpers.assertFile(genFiles(testOptions)); + done(); }); }); - it('should run server tests successfully', function(done) { - this.timeout(60000); + it('should not generate unexpected files', function (done) { gen.run({}, function () { - exec('grunt test:server', function (error, stdout, stderr) { - expect(stdout, 'Server tests failed (do you have mongoDB running?) \n' + stdout).to.contain('Done, without errors.'); - done(); - }); + assertOnlyFiles(genFiles(testOptions), done); }); }); }); describe('with no preprocessors and no server options', function() { + var testOptions = { + script: 'js', + markup: 'html', + stylesheet: 'css', + router: 'ngroute', + mongoose: false, + auth: false, + oauth: [], + socketio: false + }; + beforeEach(function(done) { - helpers.mockPrompt(gen, { - script: 'js', - markup: 'html', - stylesheet: 'css', - router: 'ngroute', - mongoose: false, - auth: false, - oauth: [], - socketio: false - }); + helpers.mockPrompt(gen, testOptions); done(); }); it('should run client tests successfully', function(done) { - this.timeout(60000); - gen.run({}, function () { - exec('grunt test:client', function (error, stdout, stderr) { - expect(stdout, 'Client tests failed \n' + stdout ).to.contain('Executed 1 of 1\u001b[32m SUCCESS\u001b'); - done(); - }); - }); + runTest('grunt test:client', this, done); }); it('should pass jshint', function(done) { - this.timeout(60000); - gen.run({}, function () { - exec('grunt jshint', function (error, stdout, stderr) { - expect(stdout).to.contain('Done, without errors.'); - done(); - }); - }); + runTest('grunt jshint', this, done); }); it('should run server tests successfully', function(done) { - this.timeout(60000); - gen.run({}, function () { - exec('grunt test:server', function (error, stdout, stderr) { - expect(stdout, 'Server tests failed (do you have mongoDB running?) \n' + stdout).to.contain('Done, without errors.'); - done(); - }); - }); + runTest('grunt test:server', this, done); }); it('should generate expected files', function (done) { - helpers.mockPrompt(gen, defaultOptions); - gen.run({}, function () { - helpers.assertFile([ - 'client/.htaccess', - 'client/favicon.ico', - 'client/robots.txt', - 'client/app/main/main.scss', - 'client/app/main/main.html', - 'client/index.html', - 'client/.jshintrc', - 'client/assets/images/yeoman.png', - '.bowerrc', - '.editorconfig', - '.gitignore', - 'Gruntfile.js', - 'package.json', - 'bower.json', - 'server/app.js', - 'server/config/express.js', - 'server/api/thing/index.js']); + helpers.assertFile(genFiles(testOptions)); done(); }); }); + + it('should not generate unexpected files', function (done) { + gen.run({}, function () { + assertOnlyFiles(genFiles(testOptions), done); + }); + }); }); }); }); From 3536b4577821734dff86cf6d8c47e501abb28da4 Mon Sep 17 00:00:00 2001 From: Tyler Henkel Date: Tue, 9 Sep 2014 22:33:21 -0600 Subject: [PATCH 10/20] fix unexpected files created in tests --- test/test-file-creation.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/test-file-creation.js b/test/test-file-creation.js index c8d26917b..f5a753375 100644 --- a/test/test-file-creation.js +++ b/test/test-file-creation.js @@ -345,7 +345,9 @@ describe('angular-fullstack generator', function () { mongoose: true, auth: true, oauth: ['twitterAuth', 'facebookAuth', 'googleAuth'], - socketio: true + socketio: true, + bootstrap: true, + uibootstrap: true }; beforeEach(function() { @@ -387,7 +389,9 @@ describe('angular-fullstack generator', function () { mongoose: false, auth: false, oauth: [], - socketio: false + socketio: false, + bootstrap: false, + uibootstrap: false }; beforeEach(function(done) { @@ -430,7 +434,9 @@ describe('angular-fullstack generator', function () { mongoose: false, auth: false, oauth: [], - socketio: false + socketio: false, + bootstrap: true, + uibootstrap: true }; beforeEach(function(done) { From cf6ae006f5dfd318f8234910365fa13c69336509 Mon Sep 17 00:00:00 2001 From: kingcody Date: Wed, 10 Sep 2014 08:52:09 -0400 Subject: [PATCH 11/20] test(gen): clean up code and test more endpoint combinations Changes: - remove unneeded code - use runTest for endpoints as well - test endpoints with different options - test endpoints with various cased named Exposes Bug: See #540 --- test/test-file-creation.js | 94 ++++++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 30 deletions(-) diff --git a/test/test-file-creation.js b/test/test-file-creation.js index f5a753375..b6171fabd 100644 --- a/test/test-file-creation.js +++ b/test/test-file-creation.js @@ -39,13 +39,16 @@ describe('angular-fullstack generator', function () { path = path || './'; skip = skip || ['e2e', 'node_modules', 'client/bower_components']; - recursiveReadDir(path, skip, function(err, files) { + recursiveReadDir(path, skip, function(err, actualFiles) { if (err) { return done(err); } + var files = actualFiles.concat(); - for (var i = 0, expectedFilesLength = expectedFiles.length; i < expectedFilesLength; i++) { - var index = files.indexOf(expectedFiles[i]); - files.splice(index, 1); - } + expectedFiles.forEach(function(file, i) { + var index = files.indexOf(file); + if (index >= 0) { + files.splice(index, 1); + } + }); if (files.length !== 0) { err = new Error('unexpected files found'); @@ -58,11 +61,16 @@ describe('angular-fullstack generator', function () { }); } - function runTest(type, self, cb, timeout) { + function runTest(cmd, self, cb) { + var args = Array.prototype.slice.call(arguments), + endpoint = (args[3] && typeof args[3] === 'string') ? args.splice(3, 1)[0] : null, + timeout = (args[3] && typeof args[3] === 'number') ? args.splice(3, 1)[0] : null; + self.timeout(timeout || 60000); - gen.run({}, function() { - exec(type, function(error, stdout, stderr) { - switch(type) { + + var execFn = function() { + exec(cmd, function(error, stdout, stderr) { + switch(cmd) { case 'grunt test:client': expect(stdout, 'Client tests failed \n' + stdout ).to.contain('Executed 1 of 1\u001b[32m SUCCESS\u001b'); break; @@ -78,7 +86,13 @@ describe('angular-fullstack generator', function () { cb(); }); - }); + }; + + if (endpoint) { + generatorTest('endpoint', endpoint, {}, execFn); + } else { + gen.run({}, execFn); + } } function genFiles(ops) { @@ -206,10 +220,9 @@ describe('angular-fullstack generator', function () { } if (ops.oauth) { - var oauth = ops.oauth; - for (var i = 0, oauthLength = oauth.length; i < oauthLength; i++) { - files = files.concat(oauthFiles(oauth[i].replace('Auth', ''))); - } + ops.oauth.forEach(function(type, i) { + files = files.concat(oauthFiles(type.replace('Auth', ''))); + }); } if (ops.socketio) { @@ -224,11 +237,10 @@ describe('angular-fullstack generator', function () { return files; } - function everyFile(files, ops) { - ops = ops || { - skip: ['node_modules', 'client/bower_components', 'e2e'] - } - } + + /** + * Generator tests + */ beforeEach(function (done) { this.timeout(10000); @@ -269,7 +281,7 @@ describe('angular-fullstack generator', function () { runTest('grunt test:client', this, done); }); - it('should pass jshint', function(done) { + it('should pass lint', function(done) { runTest('grunt jshint', this, done); }); @@ -277,14 +289,20 @@ describe('angular-fullstack generator', function () { runTest('grunt test:server', this, done); }); + it('should pass lint with generated endpoint', function(done) { + runTest('grunt jshint', this, done, 'foo'); + }); + it('should run server tests successfully with generated endpoint', function(done) { - this.timeout(60000); - generatorTest('endpoint', 'foo', {}, function() { - exec('grunt test:server', function (error, stdout, stderr) { - expect(stdout, 'Server tests failed (do you have mongoDB running?) \n' + stdout).to.contain('Done, without errors.'); - done(); - }); - }); + runTest('grunt test:server', this, done, 'foo'); + }); + + it('should pass lint with generated capitalized endpoint', function(done) { + runTest('grunt jshint', this, done, 'Foo'); + }); + + it('should run server tests successfully with generated capitalized endpoint', function(done) { + runTest('grunt test:server', this, done, 'Foo'); }); it('should use existing config if available', function(done) { @@ -358,7 +376,7 @@ describe('angular-fullstack generator', function () { runTest('grunt test:client', this, done); }); - it('should pass jshint', function(done) { + it('should pass lint', function(done) { runTest('grunt jshint', this, done); }); @@ -366,6 +384,14 @@ describe('angular-fullstack generator', function () { runTest('grunt test:server', this, done); }); + it('should pass lint with generated snake-case endpoint', function(done) { + runTest('grunt jshint', this, done, 'foo-bar'); + }); + + it('should run server tests successfully with generated snake-case endpoint', function(done) { + runTest('grunt test:server', this, done, 'foo-bar'); + }); + it('should generate expected files', function (done) { gen.run({}, function () { helpers.assertFile(genFiles(testOptions)); @@ -403,7 +429,7 @@ describe('angular-fullstack generator', function () { runTest('grunt test:client', this, done); }); - it('should pass jshint', function(done) { + it('should pass lint', function(done) { runTest('grunt jshint', this, done); }); @@ -411,6 +437,14 @@ describe('angular-fullstack generator', function () { runTest('grunt test:server', this, done); }); + it('should pass lint with generated endpoint', function(done) { + runTest('grunt jshint', this, done, 'foo'); + }); + + it('should run server tests successfully with generated endpoint', function(done) { + runTest('grunt test:server', this, done, 'foo'); + }); + it('should generate expected files', function (done) { gen.run({}, function () { helpers.assertFile(genFiles(testOptions)); @@ -448,7 +482,7 @@ describe('angular-fullstack generator', function () { runTest('grunt test:client', this, done); }); - it('should pass jshint', function(done) { + it('should pass lint', function(done) { runTest('grunt jshint', this, done); }); From 73620809a83c2a7ab5c3d6a81f67f8bb56f8fdb7 Mon Sep 17 00:00:00 2001 From: kingcody Date: Wed, 10 Sep 2014 09:32:07 -0400 Subject: [PATCH 12/20] fix(gen): camelCase endpoint name when used in variable name Closes #540 --- endpoint/templates/index.spec.js | 4 ++-- endpoint/templates/name.controller.js | 30 +++++++++++++-------------- endpoint/templates/name.e2e.js | 16 +++++++------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/endpoint/templates/index.spec.js b/endpoint/templates/index.spec.js index 62caed5dc..74c17d539 100644 --- a/endpoint/templates/index.spec.js +++ b/endpoint/templates/index.spec.js @@ -3,7 +3,7 @@ var proxyquire = require('proxyquire').noPreserveCache(); /* <%= name %>.controller stub */ -var <%= name %>Ctrl = { +var <%= cameledName %>Ctrl = { index: '<%= name %>Ctrl.index'<% if(filters.mongoose) { %>, show: '<%= name %>Ctrl.show', create: '<%= name %>Ctrl.create', @@ -25,7 +25,7 @@ var <%= name %>Ctrl = { return router; } }, - './<%= name %>.controller': <%= name %>Ctrl + './<%= name %>.controller': <%= cameledName %>Ctrl }); describe('<%= classedName %> API Router:', function() { diff --git a/endpoint/templates/name.controller.js b/endpoint/templates/name.controller.js index 1d9da544e..3b2bbc918 100644 --- a/endpoint/templates/name.controller.js +++ b/endpoint/templates/name.controller.js @@ -6,49 +6,49 @@ var <%= classedName %> = require('./<%= name %>.model');<% } %> // Get list of <%= name %>s exports.index = function(req, res) {<% if (!filters.mongoose) { %> res.json([]);<% } %><% if (filters.mongoose) { %> - <%= classedName %>.find(function (err, <%= name %>s) { + <%= classedName %>.find(function (err, <%= cameledName %>s) { if(err) { return handleError(res, err); } - return res.json(200, <%= name %>s); + return res.json(200, <%= cameledName %>s); });<% } %> };<% if (filters.mongoose) { %> // Get a single <%= name %> exports.show = function(req, res) { - <%= classedName %>.findById(req.params.id, function (err, <%= name %>) { + <%= classedName %>.findById(req.params.id, function (err, <%= cameledName %>) { if(err) { return handleError(res, err); } - if(!<%= name %>) { return res.send(404); } - return res.json(<%= name %>); + if(!<%= cameledName %>) { return res.send(404); } + return res.json(<%= cameledName %>); }); }; // Creates a new <%= name %> in the DB. exports.create = function(req, res) { - <%= classedName %>.create(req.body, function(err, <%= name %>) { + <%= classedName %>.create(req.body, function(err, <%= cameledName %>) { if(err) { return handleError(res, err); } - return res.json(201, <%= name %>); + return res.json(201, <%= cameledName %>); }); }; // Updates an existing <%= name %> in the DB. exports.update = function(req, res) { if(req.body._id) { delete req.body._id; } - <%= classedName %>.findById(req.params.id, function (err, <%= name %>) { + <%= classedName %>.findById(req.params.id, function (err, <%= cameledName %>) { if (err) { return handleError(res, err); } - if(!<%= name %>) { return res.send(404); } - var updated = _.merge(<%= name %>, req.body); + if(!<%= cameledName %>) { return res.send(404); } + var updated = _.merge(<%= cameledName %>, req.body); updated.save(function (err) { if (err) { return handleError(res, err); } - return res.json(200, <%= name %>); + return res.json(200, <%= cameledName %>); }); }); }; // Deletes a <%= name %> from the DB. exports.destroy = function(req, res) { - <%= classedName %>.findById(req.params.id, function (err, <%= name %>) { + <%= classedName %>.findById(req.params.id, function (err, <%= cameledName %>) { if(err) { return handleError(res, err); } - if(!<%= name %>) { return res.send(404); } - <%= name %>.remove(function(err) { + if(!<%= cameledName %>) { return res.send(404); } + <%= cameledName %>.remove(function(err) { if(err) { return handleError(res, err); } return res.send(204); }); @@ -57,4 +57,4 @@ exports.destroy = function(req, res) { function handleError(res, err) { return res.send(500, err); -}<% } %> \ No newline at end of file +}<% } %> diff --git a/endpoint/templates/name.e2e.js b/endpoint/templates/name.e2e.js index 5960b40a2..f168f1ca9 100644 --- a/endpoint/templates/name.e2e.js +++ b/endpoint/templates/name.e2e.js @@ -8,7 +8,7 @@ var new<%= classedName %>;<% } %> describe('<%= classedName %> API:', function() { describe('GET <%= route %>', function() { - var <%= name %>s; + var <%= cameledName %>s; beforeEach(function(done) { request(app) @@ -17,13 +17,13 @@ describe('<%= classedName %> API:', function() { .expect('Content-Type', /json/) .end(function(err, res) { if (err) return done(err); - <%= name %>s = res.body; + <%= cameledName %>s = res.body; done(); }); }); it('should respond with JSON array', function() { - <%= name %>s.should.be.instanceOf(Array); + <%= cameledName %>s.should.be.instanceOf(Array); }); });<% if(filters.mongoose) { %> @@ -53,7 +53,7 @@ describe('<%= classedName %> API:', function() { }); describe('GET <%= route %>/:id', function() { - var <%= name %>; + var <%= cameledName %>; beforeEach(function(done) { request(app) @@ -62,18 +62,18 @@ describe('<%= classedName %> API:', function() { .expect('Content-Type', /json/) .end(function(err, res) { if (err) return done(err); - <%= name %> = res.body; + <%= cameledName %> = res.body; done(); }); }); afterEach(function() { - <%= name %> = {}; + <%= cameledName %> = {}; }); it('should respond with the requested <%= name %>', function() { - <%= name %>.name.should.equal('New <%= classedName %>'); - <%= name %>.info.should.equal('This is the brand new <%= name %>!!!'); + <%= cameledName %>.name.should.equal('New <%= classedName %>'); + <%= cameledName %>.info.should.equal('This is the brand new <%= name %>!!!'); }); }); From ac8f335922bd77aa0781ebd4281b8b1b9186f18d Mon Sep 17 00:00:00 2001 From: Tyler Henkel Date: Sun, 14 Sep 2014 18:23:01 -0600 Subject: [PATCH 13/20] style(package): remove unneeded line break --- app/templates/_package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/templates/_package.json b/app/templates/_package.json index 2d8337b71..1912903fa 100644 --- a/app/templates/_package.json +++ b/app/templates/_package.json @@ -16,8 +16,7 @@ "jade": "~1.2.0",<% } %><% if(filters.html) { %> "ejs": "~0.8.4",<% } %><% if(filters.mongoose) { %> "mongoose": "~3.8.8", - "mongoose-bird": "~0.0.1", - <% } %><% if(filters.auth) { %> + "mongoose-bird": "~0.0.1",<% } %><% if(filters.auth) { %> "jsonwebtoken": "^0.3.0", "express-jwt": "^0.1.3", "passport": "~0.2.0", From 502be54d092ff1312a7d065da47dbe3d5ec3490e Mon Sep 17 00:00:00 2001 From: Tyler Henkel Date: Sun, 14 Sep 2014 18:25:38 -0600 Subject: [PATCH 14/20] remove fixtures that should not be git ignored --- .gitignore | 4 ++-- test/fixtures/package.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 9e436b36f..78a6835b7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,5 @@ demo .idea .DS_Store release.txt -fixtures/bower.json -fixtures/package.json \ No newline at end of file +test/fixtures/bower.json +test/fixtures/package.json \ No newline at end of file diff --git a/test/fixtures/package.json b/test/fixtures/package.json index 2304c008e..48d48607e 100644 --- a/test/fixtures/package.json +++ b/test/fixtures/package.json @@ -16,6 +16,7 @@ "jade": "~1.2.0", "ejs": "~0.8.4", "mongoose": "~3.8.8", + "mongoose-bird": "~0.0.1", "jsonwebtoken": "^0.3.0", "express-jwt": "^0.1.3", "passport": "~0.2.0", @@ -46,7 +47,7 @@ "grunt-contrib-watch": "~0.6.1", "grunt-contrib-coffee": "^0.10.1", "grunt-contrib-jade": "^0.11.0", - "grunt-contrib-less": "^0.11.0", + "grunt-contrib-less": "^0.11.4", "grunt-google-cdn": "~0.4.0", "grunt-newer": "~0.7.0", "grunt-ng-annotate": "^0.2.3", From 65d03fc8a39590e09ef36740707fdc883739e135 Mon Sep 17 00:00:00 2001 From: kingcody Date: Sun, 17 Aug 2014 03:48:29 -0400 Subject: [PATCH 15/20] feat(app-auth): Improve client-side Auth service Changes: - getCurrentUser, isLoggedIn, and isAdmin are now sync if no arg and async with an arg - Use Error first callback signature where applicable - Remove unused arguments from Auth service - Remove isLoggedInAsync - Switch use of isLoggedInAsync to isLoggedIn - Add/Improve comments - Fix client/app/account(auth)/settings/settings.controller(js).js Breaking Changes: - Callbacks that return Errors, use 'Error first' signature Closes #456 --- app/templates/client/app/app(coffee).coffee | 4 +- app/templates/client/app/app(js).js | 4 +- .../auth(auth)/auth.service(coffee).coffee | 93 +++++++----- .../components/auth(auth)/auth.service(js).js | 140 ++++++++++-------- 4 files changed, 136 insertions(+), 105 deletions(-) diff --git a/app/templates/client/app/app(coffee).coffee b/app/templates/client/app/app(coffee).coffee index ea9ae3c95..9c0a87a0e 100644 --- a/app/templates/client/app/app(coffee).coffee +++ b/app/templates/client/app/app(coffee).coffee @@ -34,6 +34,6 @@ angular.module '<%= scriptAppName %>', [<%= angularModules %>] .run ($rootScope, $location, Auth) -> # Redirect to login if route requires auth and you're not logged in $rootScope.$on <% if(filters.ngroute) { %>'$routeChangeStart'<% } %><% if(filters.uirouter) { %>'$stateChangeStart'<% } %>, (event, next) -> - Auth.isLoggedInAsync (loggedIn) -> + Auth.isLoggedIn (loggedIn) -> $location.path "/login" if next.authenticate and not loggedIn -<% } %> \ No newline at end of file +<% } %> diff --git a/app/templates/client/app/app(js).js b/app/templates/client/app/app(js).js index eef485d7c..4e6adea0c 100644 --- a/app/templates/client/app/app(js).js +++ b/app/templates/client/app/app(js).js @@ -46,10 +46,10 @@ angular.module('<%= scriptAppName %>', [<%= angularModules %>]) .run(function ($rootScope, $location, Auth) { // Redirect to login if route requires auth and you're not logged in $rootScope.$on(<% if(filters.ngroute) { %>'$routeChangeStart'<% } %><% if(filters.uirouter) { %>'$stateChangeStart'<% } %>, function (event, next) { - Auth.isLoggedInAsync(function(loggedIn) { + Auth.isLoggedIn(function(loggedIn) { if (next.authenticate && !loggedIn) { $location.path('/login'); } }); }); - })<% } %>; \ No newline at end of file + })<% } %>; diff --git a/app/templates/client/components/auth(auth)/auth.service(coffee).coffee b/app/templates/client/components/auth(auth)/auth.service(coffee).coffee index ac503ed0b..4131efe53 100644 --- a/app/templates/client/components/auth(auth)/auth.service(coffee).coffee +++ b/app/templates/client/components/auth(auth)/auth.service(coffee).coffee @@ -1,40 +1,35 @@ 'use strict' angular.module '<%= scriptAppName %>' -.factory 'Auth', ($location, $rootScope, $http, User, $cookieStore, $q) -> +.factory 'Auth', ($http, User, $cookieStore, $q) -> currentUser = if $cookieStore.get 'token' then User.get() else {} ### Authenticate user and save token @param {Object} user - login info - @param {Function} callback - optional + @param {Function} callback - optional, function(error) @return {Promise} ### login: (user, callback) -> - deferred = $q.defer() $http.post '/auth/local', email: user.email password: user.password - .success (data) -> - $cookieStore.put 'token', data.token + .then (res) -> + $cookieStore.put 'token', res.data.token currentUser = User.get() - deferred.resolve data callback?() + res.data - .error (err) => + , (err) => @logout() - deferred.reject err - callback? err - - deferred.promise + callback? err.data + $q.reject err.data ### Delete access token and user info - - @param {Function} ### logout: -> $cookieStore.remove 'token' @@ -46,7 +41,7 @@ angular.module '<%= scriptAppName %>' Create a new user @param {Object} user - user info - @param {Function} callback - optional + @param {Function} callback - optional, function(error, user) @return {Promise} ### createUser: (user, callback) -> @@ -54,7 +49,7 @@ angular.module '<%= scriptAppName %>' (data) -> $cookieStore.put 'token', data.token currentUser = User.get() - callback? user + callback? null, user , (err) => @logout() @@ -68,7 +63,7 @@ angular.module '<%= scriptAppName %>' @param {String} oldPassword @param {String} newPassword - @param {Function} callback - optional + @param {Function} callback - optional, function(error, user) @return {Promise} ### changePassword: (oldPassword, newPassword, callback) -> @@ -79,7 +74,7 @@ angular.module '<%= scriptAppName %>' newPassword: newPassword , (user) -> - callback? user + callback? null, user , (err) -> callback? err @@ -88,45 +83,61 @@ angular.module '<%= scriptAppName %>' ### - Gets all available info on authenticated user + Gets all available info on a user + (synchronous|asynchronous) - @return {Object} user + @param {Function|*} callback - optional, funciton(user) + @return {Object|Promise} ### - getCurrentUser: -> - currentUser + getCurrentUser: (callback) -> + return currentUser if arguments.length is 0 + value = if (currentUser.hasOwnProperty("$promise")) then currentUser.$promise else currentUser + $q.when value - ### - Check if a user is logged in synchronously + .then (user) -> + callback? user + user - @return {Boolean} - ### - isLoggedIn: -> - currentUser.hasOwnProperty 'role' + , -> + callback? {} + {} ### - Waits for currentUser to resolve before checking if user is logged in + Check if a user is logged in + (synchronous|asynchronous) + + @param {Function|*} callback - optional, function(is) + @return {Bool|Promise} ### - isLoggedInAsync: (callback) -> - if currentUser.hasOwnProperty '$promise' - currentUser.$promise.then -> - callback? true - return - .catch -> - callback? false - return + isLoggedIn: (callback) -> + return currentUser.hasOwnProperty("role") if arguments.length is 0 + + @getCurrentUser null + + .then (user) -> + is_ = user.hasOwnProperty("role") + callback? is_ + is_ - else - callback? currentUser.hasOwnProperty 'role' ### Check if a user is an admin + (synchronous|asynchronous) - @return {Boolean} + @param {Function|*} callback - optional, function(is) + @return {Bool|Promise} ### - isAdmin: -> - currentUser.role is 'admin' + isAdmin: (callback) -> + return currentUser.role is "admin" if arguments_.length is 0 + + @getCurrentUser null + + .then (user) -> + is_ = user.role is "admin" + callback? is_ + is_ ### diff --git a/app/templates/client/components/auth(auth)/auth.service(js).js b/app/templates/client/components/auth(auth)/auth.service(js).js index 9afb12da9..5baf0e0b4 100644 --- a/app/templates/client/components/auth(auth)/auth.service(js).js +++ b/app/templates/client/components/auth(auth)/auth.service(js).js @@ -1,9 +1,20 @@ 'use strict'; angular.module('<%= scriptAppName %>') - .factory('Auth', function Auth($location, $rootScope, $http, User, $cookieStore, $q) { - var currentUser = {}; - if($cookieStore.get('token')) { + .factory('Auth', function Auth($http, User, $cookieStore, $q) { + /** + * Return a callback or noop function + * + * @param {Function|*} cb - a 'potential' function + * @return {Function} + */ + var safeCb = function(cb) { + return (angular.isFunction(cb)) ? cb : angular.noop; + }, + + currentUser = {}; + + if ($cookieStore.get('token')) { currentUser = User.get(); } @@ -13,36 +24,28 @@ angular.module('<%= scriptAppName %>') * Authenticate user and save token * * @param {Object} user - login info - * @param {Function} callback - optional + * @param {Function} callback - optional, function(error) * @return {Promise} */ login: function(user, callback) { - var cb = callback || angular.noop; - var deferred = $q.defer(); - - $http.post('/auth/local', { + return $http.post('/auth/local', { email: user.email, password: user.password - }). - success(function(data) { - $cookieStore.put('token', data.token); + }) + .then(function(res) { + $cookieStore.put('token', res.data.token); currentUser = User.get(); - deferred.resolve(data); - return cb(); - }). - error(function(err) { + safeCb(callback)(); + return res.data; + }, function(err) { this.logout(); - deferred.reject(err); - return cb(err); + safeCb(callback)(err.data); + return $q.reject(err.data); }.bind(this)); - - return deferred.promise; }, /** * Delete access token and user info - * - * @param {Function} */ logout: function() { $cookieStore.remove('token'); @@ -53,21 +56,19 @@ angular.module('<%= scriptAppName %>') * Create a new user * * @param {Object} user - user info - * @param {Function} callback - optional + * @param {Function} callback - optional, function(error, user) * @return {Promise} */ createUser: function(user, callback) { - var cb = callback || angular.noop; - return User.save(user, function(data) { $cookieStore.put('token', data.token); currentUser = User.get(); - return cb(user); + return safeCb(callback)(null, user); }, function(err) { this.logout(); - return cb(err); + return safeCb(callback)(err); }.bind(this)).$promise; }, @@ -76,68 +77,87 @@ angular.module('<%= scriptAppName %>') * * @param {String} oldPassword * @param {String} newPassword - * @param {Function} callback - optional + * @param {Function} callback - optional, function(error, user) * @return {Promise} */ changePassword: function(oldPassword, newPassword, callback) { - var cb = callback || angular.noop; - return User.changePassword({ id: currentUser._id }, { oldPassword: oldPassword, newPassword: newPassword }, function(user) { - return cb(user); + return safeCb(callback)(null, user); }, function(err) { - return cb(err); + return safeCb(callback)(err); }).$promise; }, /** - * Gets all available info on authenticated user + * Gets all available info on a user + * (synchronous|asynchronous) * - * @return {Object} user + * @param {Function|*} callback - optional, funciton(user) + * @return {Object|Promise} */ - getCurrentUser: function() { - return currentUser; + getCurrentUser: function(callback) { + if (arguments.length === 0) { + return currentUser; + } + + var value = (currentUser.hasOwnProperty('$promise')) ? currentUser.$promise : currentUser; + return $q.when(value) + .then(function(user) { + safeCb(callback)(user); + return user; + }, function() { + safeCb(callback)({}); + return {}; + }); }, /** * Check if a user is logged in + * (synchronous|asynchronous) * - * @return {Boolean} + * @param {Function|*} callback - optional, function(is) + * @return {Bool|Promise} */ - isLoggedIn: function() { - return currentUser.hasOwnProperty('role'); - }, + isLoggedIn: function(callback) { + if (arguments.length === 0) { + return currentUser.hasOwnProperty('role'); + } - /** - * Waits for currentUser to resolve before checking if user is logged in - */ - isLoggedInAsync: function(cb) { - if(currentUser.hasOwnProperty('$promise')) { - currentUser.$promise.then(function() { - cb(true); - }).catch(function() { - cb(false); + return this.getCurrentUser(null) + .then(function(user) { + var is = user.hasOwnProperty('role'); + safeCb(callback)(is); + return is; }); - } else if(currentUser.hasOwnProperty('role')) { - cb(true); - } else { - cb(false); - } }, - /** - * Check if a user is an admin - * - * @return {Boolean} - */ - isAdmin: function() { - return currentUser.role === 'admin'; + /** + * Check if a user is an admin + * (synchronous|asynchronous) + * + * @param {Function|*} callback - optional, function(is) + * @return {Bool|Promise} + */ + isAdmin: function(callback) { + if (arguments.length === 0) { + return currentUser.role === 'admin'; + } + + return this.getCurrentUser(null) + .then(function(user) { + var is = user.role === 'admin'; + safeCb(callback)(is); + return is; + }); }, /** * Get auth token + * + * @return {String} - a token string used for authenticating */ getToken: function() { return $cookieStore.get('token'); From 2aaff12d3559770ee2e9bdd8a856ef808e713010 Mon Sep 17 00:00:00 2001 From: Tyler Henkel Date: Tue, 16 Sep 2014 23:41:46 -0600 Subject: [PATCH 16/20] fix typo --- .../client/components/auth(auth)/auth.service(coffee).coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/client/components/auth(auth)/auth.service(coffee).coffee b/app/templates/client/components/auth(auth)/auth.service(coffee).coffee index 4131efe53..1d4b29544 100644 --- a/app/templates/client/components/auth(auth)/auth.service(coffee).coffee +++ b/app/templates/client/components/auth(auth)/auth.service(coffee).coffee @@ -130,7 +130,7 @@ angular.module '<%= scriptAppName %>' @return {Bool|Promise} ### isAdmin: (callback) -> - return currentUser.role is "admin" if arguments_.length is 0 + return currentUser.role is "admin" if arguments.length is 0 @getCurrentUser null From 6aadee6d956cefb9a787eb0851d287654dfca111 Mon Sep 17 00:00:00 2001 From: kingcody Date: Fri, 15 Aug 2014 02:50:09 -0400 Subject: [PATCH 17/20] feat(app-routing): improve app routing Changes: - Use `ui-sref` instead of `href` or `ng-href` when ui-router is chosen - Use `ui-sref-active` instead of `ng-class='{active: isActive()}'` when ui-router is chosen - Use `$state.go()` where applicable, when ui-router is chosen - Use `$scope.menu[n].state` instead of `$scope.menu[n].link` when ui-router is chosen (attempt to remove possible confusion) - Omit `$scope.isActive` when ui-router is chosen - Simplify `navbar(jade).jade` templating (remove extra `<% if (filters.auth) %>` tag) - Add `/logout` route for both ng-route and ui-router - Use `$routeChangeStart` or `$stateChangeStart` to pass refering route or state to `/logout` - Add `stateMock` for testing `$state` transitions - Use `stateMock` in main.controller for expecting template requests from state transistions Closes #331 --- .../app/account(auth)/account(coffee).coffee | 26 ++++++++++++- .../client/app/account(auth)/account(js).js | 38 ++++++++++++++++++- .../app/account(auth)/login/login(html).html | 2 +- .../app/account(auth)/login/login(jade).jade | 2 +- .../login/login.controller(coffee).coffee | 4 +- .../login/login.controller(js).js | 4 +- .../account(auth)/signup/signup(html).html | 2 +- .../account(auth)/signup/signup(jade).jade | 2 +- .../signup/signup.controller(coffee).coffee | 4 +- .../signup/signup.controller(js).js | 4 +- app/templates/client/app/app(coffee).coffee | 11 +++--- app/templates/client/app/app(js).js | 15 ++++---- .../main/main.controller.spec(coffee).coffee | 13 ++++--- .../app/main/main.controller.spec(js).js | 11 ++++-- .../components/navbar/navbar(html).html | 14 +++---- .../components/navbar/navbar(jade).jade | 24 ++++++------ .../navbar/navbar.controller(coffee).coffee | 12 ++---- .../navbar/navbar.controller(js).js | 15 +++----- .../ui-router.mock(coffee).coffee | 26 +++++++++++++ .../ui-router(uirouter)/ui-router.mock(js).js | 34 +++++++++++++++++ 20 files changed, 191 insertions(+), 72 deletions(-) create mode 100644 app/templates/client/components/ui-router(uirouter)/ui-router.mock(coffee).coffee create mode 100644 app/templates/client/components/ui-router(uirouter)/ui-router.mock(js).js diff --git a/app/templates/client/app/account(auth)/account(coffee).coffee b/app/templates/client/app/account(auth)/account(coffee).coffee index 2b7b8b23b..088fb6840 100644 --- a/app/templates/client/app/account(auth)/account(coffee).coffee +++ b/app/templates/client/app/account(auth)/account(coffee).coffee @@ -7,6 +7,14 @@ angular.module '<%= scriptAppName %>' templateUrl: 'app/account/login/login.html' controller: 'LoginCtrl' + .when '/logout', + name: 'logout' + referrer: '/' + controller: ($location, $route, Auth) -> + referrer = $route.current.params.referrer or $route.current.referrer or "/" + Auth.logout() + $location.path referrer + .when '/signup', templateUrl: 'app/account/signup/signup.html' controller: 'SignupCtrl' @@ -15,6 +23,10 @@ angular.module '<%= scriptAppName %>' templateUrl: 'app/account/settings/settings.html' controller: 'SettingsCtrl' authenticate: true + +.run ($rootScope) -> + $rootScope.$on '$routeChangeStart', (event, next, current) -> + next.referrer = current.originalPath if next.name is "logout" and current and current.originalPath and not current.authenticate <% } %><% if(filters.uirouter) { %>.config ($stateProvider) -> $stateProvider .state 'login', @@ -22,6 +34,14 @@ angular.module '<%= scriptAppName %>' templateUrl: 'app/account/login/login.html' controller: 'LoginCtrl' + .state 'logout', + url: '/logout?referrer' + referrer: 'main' + controller: ($state, Auth) -> + referrer = $state.params.referrer or $state.current.referrer or "main" + Auth.logout() + $state.go referrer + .state 'signup', url: '/signup' templateUrl: 'app/account/signup/signup.html' @@ -32,4 +52,8 @@ angular.module '<%= scriptAppName %>' templateUrl: 'app/account/settings/settings.html' controller: 'SettingsCtrl' authenticate: true -<% } %> \ No newline at end of file + +.run ($rootScope) -> + $rootScope.$on '$stateChangeStart', (event, next, nextParams, current) -> + next.referrer = current.name if next.name is "logout" and current and current.name and not current.authenticate +<% } %> diff --git a/app/templates/client/app/account(auth)/account(js).js b/app/templates/client/app/account(auth)/account(js).js index 0e30543a5..d31ff19d2 100644 --- a/app/templates/client/app/account(auth)/account(js).js +++ b/app/templates/client/app/account(auth)/account(js).js @@ -7,6 +7,17 @@ angular.module('<%= scriptAppName %>') templateUrl: 'app/account/login/login.html', controller: 'LoginCtrl' }) + .when('/logout', { + name: 'logout', + referrer: '/', + controller: function($location, $route, Auth) { + var referrer = $route.current.params.referrer || + $route.current.referrer || + '/'; + Auth.logout(); + $location.path(referrer); + } + }) .when('/signup', { templateUrl: 'app/account/signup/signup.html', controller: 'SignupCtrl' @@ -16,6 +27,13 @@ angular.module('<%= scriptAppName %>') controller: 'SettingsCtrl', authenticate: true }); + }) + .run(function($rootScope) { + $rootScope.$on('$routeChangeStart', function(event, next, current) { + if (next.name === 'logout' && current && current.originalPath && !current.authenticate) { + next.referrer = current.originalPath; + } + }); });<% } %><% if(filters.uirouter) { %>.config(function ($stateProvider) { $stateProvider .state('login', { @@ -23,6 +41,17 @@ angular.module('<%= scriptAppName %>') templateUrl: 'app/account/login/login.html', controller: 'LoginCtrl' }) + .state('logout', { + url: '/logout?referrer', + referrer: 'main', + controller: function($state, Auth) { + var referrer = $state.params.referrer || + $state.current.referrer || + 'main'; + Auth.logout(); + $state.go(referrer); + } + }) .state('signup', { url: '/signup', templateUrl: 'app/account/signup/signup.html', @@ -34,4 +63,11 @@ angular.module('<%= scriptAppName %>') controller: 'SettingsCtrl', authenticate: true }); - });<% } %> \ No newline at end of file + }) + .run(function($rootScope) { + $rootScope.$on('$stateChangeStart', function(event, next, nextParams, current) { + if (next.name === 'logout' && current && current.name && !current.authenticate) { + next.referrer = current.name; + } + }); + });<% } %> diff --git a/app/templates/client/app/account(auth)/login/login(html).html b/app/templates/client/app/account(auth)/login/login(html).html index 572f2e144..49a81b55d 100644 --- a/app/templates/client/app/account(auth)/login/login(html).html +++ b/app/templates/client/app/account(auth)/login/login(html).html @@ -37,7 +37,7 @@

Login

- + ui-sref="signup"<% } else { %>href="/signup"<% } %>> Register diff --git a/app/templates/client/app/account(auth)/login/login(jade).jade b/app/templates/client/app/account(auth)/login/login(jade).jade index 4b13c0b13..686b1769e 100644 --- a/app/templates/client/app/account(auth)/login/login(jade).jade +++ b/app/templates/client/app/account(auth)/login/login(jade).jade @@ -34,7 +34,7 @@ div(ng-include='"components/navbar/navbar.html"') button.btn.btn-inverse.btn-lg.btn-login(type='submit') | Login = ' ' - a.btn.btn-default.btn-lg.btn-register(href='/signup') + a.btn.btn-default.btn-lg.btn-register(<% if(filters.uirouter) { %>ui-sref='signup'<% } else { %>href='/signup'<% } %>) | Register <% if(filters.oauth) {%> hr diff --git a/app/templates/client/app/account(auth)/login/login.controller(coffee).coffee b/app/templates/client/app/account(auth)/login/login.controller(coffee).coffee index 3f90c25d7..036191f93 100644 --- a/app/templates/client/app/account(auth)/login/login.controller(coffee).coffee +++ b/app/templates/client/app/account(auth)/login/login.controller(coffee).coffee @@ -1,7 +1,7 @@ 'use strict' angular.module '<%= scriptAppName %>' -.controller 'LoginCtrl', ($scope, Auth, $location<% if(filters.oauth) {%>, $window<% } %>) -> +.controller 'LoginCtrl', ($scope, Auth<% if(filters.ngroute) { %>, $location<% } %><% if(filters.uirouter) { %>, $state<% } %><% if(filters.oauth) {%>, $window<% } %>) -> $scope.user = {} $scope.errors = {} $scope.login = (form) -> @@ -14,7 +14,7 @@ angular.module '<%= scriptAppName %>' password: $scope.user.password .then -> - $location.path '/' + <% if(filters.ngroute) { %>$location.path '/'<% } %><% if(filters.uirouter) { %>$state.go 'main'<% } %> .catch (err) -> $scope.errors.other = err.message diff --git a/app/templates/client/app/account(auth)/login/login.controller(js).js b/app/templates/client/app/account(auth)/login/login.controller(js).js index 7b13da384..e2c5dcaa4 100644 --- a/app/templates/client/app/account(auth)/login/login.controller(js).js +++ b/app/templates/client/app/account(auth)/login/login.controller(js).js @@ -1,7 +1,7 @@ 'use strict'; angular.module('<%= scriptAppName %>') - .controller('LoginCtrl', function ($scope, Auth, $location<% if (filters.oauth) { %>, $window<% } %>) { + .controller('LoginCtrl', function ($scope, Auth<% if(filters.ngroute) { %>, $location<% } %><% if(filters.uirouter) { %>, $state<% } %><% if (filters.oauth) { %>, $window<% } %>) { $scope.user = {}; $scope.errors = {}; @@ -15,7 +15,7 @@ angular.module('<%= scriptAppName %>') }) .then( function() { // Logged in, redirect to home - $location.path('/'); + <% if(filters.ngroute) { %>$location.path('/');<% } %><% if(filters.uirouter) { %>$state.go('main');<% } %> }) .catch( function(err) { $scope.errors.other = err.message; diff --git a/app/templates/client/app/account(auth)/signup/signup(html).html b/app/templates/client/app/account(auth)/signup/signup(html).html index 59faed568..28d6c39ab 100644 --- a/app/templates/client/app/account(auth)/signup/signup(html).html +++ b/app/templates/client/app/account(auth)/signup/signup(html).html @@ -58,7 +58,7 @@

Sign up

- + ui-sref="login"<% } else { %>href="/login"<% } %>> Login diff --git a/app/templates/client/app/account(auth)/signup/signup(jade).jade b/app/templates/client/app/account(auth)/signup/signup(jade).jade index 43815a21c..7e21b101c 100644 --- a/app/templates/client/app/account(auth)/signup/signup(jade).jade +++ b/app/templates/client/app/account(auth)/signup/signup(jade).jade @@ -36,7 +36,7 @@ div(ng-include='"components/navbar/navbar.html"') button.btn.btn-inverse.btn-lg.btn-login(type='submit') | Sign up = ' ' - a.btn.btn-default.btn-lg.btn-register(href='/login') + a.btn.btn-default.btn-lg.btn-register(<% if(filters.uirouter) { %>ui-sref='login'<% } else { %>href='/login'<% } %>) | Login <% if(filters.oauth) {%> diff --git a/app/templates/client/app/account(auth)/signup/signup.controller(coffee).coffee b/app/templates/client/app/account(auth)/signup/signup.controller(coffee).coffee index 1b9c9696f..ac240faa8 100644 --- a/app/templates/client/app/account(auth)/signup/signup.controller(coffee).coffee +++ b/app/templates/client/app/account(auth)/signup/signup.controller(coffee).coffee @@ -1,7 +1,7 @@ 'use strict' angular.module '<%= scriptAppName %>' -.controller 'SignupCtrl', ($scope, Auth, $location<% if(filters.oauth) {%>, $window<% } %>) -> +.controller 'SignupCtrl', ($scope, Auth<% if(filters.ngroute) { %>, $location<% } %><% if(filters.uirouter) { %>, $state<% } %><% if(filters.oauth) {%>, $window<% } %>) -> $scope.user = {} $scope.errors = {} $scope.register = (form) -> @@ -15,7 +15,7 @@ angular.module '<%= scriptAppName %>' password: $scope.user.password .then -> - $location.path '/' + <% if(filters.ngroute) { %>$location.path '/'<% } %><% if(filters.uirouter) { %>$state.go 'main'<% } %> .catch (err) -> err = err.data diff --git a/app/templates/client/app/account(auth)/signup/signup.controller(js).js b/app/templates/client/app/account(auth)/signup/signup.controller(js).js index 7d6ba3d38..10685079d 100644 --- a/app/templates/client/app/account(auth)/signup/signup.controller(js).js +++ b/app/templates/client/app/account(auth)/signup/signup.controller(js).js @@ -1,7 +1,7 @@ 'use strict'; angular.module('<%= scriptAppName %>') - .controller('SignupCtrl', function ($scope, Auth, $location<% if (filters.oauth) { %>, $window<% } %>) { + .controller('SignupCtrl', function ($scope, Auth<% if(filters.ngroute) { %>, $location<% } %><% if(filters.uirouter) { %>, $state<% } %><% if (filters.oauth) { %>, $window<% } %>) { $scope.user = {}; $scope.errors = {}; @@ -16,7 +16,7 @@ angular.module('<%= scriptAppName %>') }) .then( function() { // Account created, redirect to home - $location.path('/'); + <% if(filters.ngroute) { %>$location.path('/');<% } %><% if(filters.uirouter) { %>$state.go('main');<% } %> }) .catch( function(err) { err = err.data; diff --git a/app/templates/client/app/app(coffee).coffee b/app/templates/client/app/app(coffee).coffee index 9c0a87a0e..cb6d0c5b1 100644 --- a/app/templates/client/app/app(coffee).coffee +++ b/app/templates/client/app/app(coffee).coffee @@ -15,8 +15,9 @@ angular.module '<%= scriptAppName %>', [<%= angularModules %>] $locationProvider.html5Mode true<% if(filters.auth) { %> $httpProvider.interceptors.push 'authInterceptor'<% } %> <% } %><% if(filters.auth) { %> -.factory 'authInterceptor', ($rootScope, $q, $cookieStore, $location) -> - # Add authorization token to headers +.factory 'authInterceptor', ($rootScope, $q, $cookieStore<% if(filters.ngroute) { %>, $location<% } if(filters.uirouter) { %>, $injector<% } %>) -> + <% if(filters.uirouter) { %>state = null + <% } %># Add authorization token to headers request: (config) -> config.headers = config.headers or {} config.headers.Authorization = 'Bearer ' + $cookieStore.get 'token' if $cookieStore.get 'token' @@ -25,15 +26,15 @@ angular.module '<%= scriptAppName %>', [<%= angularModules %>] # Intercept 401s and redirect you to login responseError: (response) -> if response.status is 401 - $location.path '/login' + <% if(filters.ngroute) { %>$location.path '/login'<% } if(filters.uirouter) { %>(state || state = $injector.get '$state').go 'login'<% } %> # remove any stale tokens $cookieStore.remove 'token' $q.reject response -.run ($rootScope, $location, Auth) -> +.run ($rootScope<% if(filters.ngroute) { %>, $location<% } %><% if(filters.uirouter) { %>, $state<% } %>, Auth) -> # Redirect to login if route requires auth and you're not logged in $rootScope.$on <% if(filters.ngroute) { %>'$routeChangeStart'<% } %><% if(filters.uirouter) { %>'$stateChangeStart'<% } %>, (event, next) -> Auth.isLoggedIn (loggedIn) -> - $location.path "/login" if next.authenticate and not loggedIn + <% if(filters.ngroute) { %>$location.path '/login'<% } %><% if(filters.uirouter) { %>$state.go 'login'<% } %> if next.authenticate and not loggedIn <% } %> diff --git a/app/templates/client/app/app(js).js b/app/templates/client/app/app(js).js index 4e6adea0c..35b8ad327 100644 --- a/app/templates/client/app/app(js).js +++ b/app/templates/client/app/app(js).js @@ -9,16 +9,17 @@ angular.module('<%= scriptAppName %>', [<%= angularModules %>]) $locationProvider.html5Mode(true);<% if(filters.auth) { %> $httpProvider.interceptors.push('authInterceptor');<% } %> - })<% } %><% if(filters.uirouter) { %>.config(function ($stateProvider, $urlRouterProvider, $locationProvider<% if(filters.auth) { %>, $httpProvider<% } %>) { + })<% } if(filters.uirouter) { %>.config(function ($stateProvider, $urlRouterProvider, $locationProvider<% if(filters.auth) { %>, $httpProvider<% } %>) { $urlRouterProvider .otherwise('/'); $locationProvider.html5Mode(true);<% if(filters.auth) { %> $httpProvider.interceptors.push('authInterceptor');<% } %> - })<% } %><% if(filters.auth) { %> + })<% } if(filters.auth) { %> - .factory('authInterceptor', function ($rootScope, $q, $cookieStore, $location) { - return { + .factory('authInterceptor', function ($rootScope, $q, $cookieStore<% if(filters.ngroute) { %>, $location<% } if(filters.uirouter) { %>, $injector<% } %>) { + <% if(filters.uirouter) { %>var state; + <% } %>return { // Add authorization token to headers request: function (config) { config.headers = config.headers || {}; @@ -31,7 +32,7 @@ angular.module('<%= scriptAppName %>', [<%= angularModules %>]) // Intercept 401s and redirect you to login responseError: function(response) { if(response.status === 401) { - $location.path('/login'); + <% if(filters.ngroute) { %>$location.path('/login');<% } if(filters.uirouter) { %>(state || (state = $injector.get('$state'))).go('login');<% } %> // remove any stale tokens $cookieStore.remove('token'); return $q.reject(response); @@ -43,12 +44,12 @@ angular.module('<%= scriptAppName %>', [<%= angularModules %>]) }; }) - .run(function ($rootScope, $location, Auth) { + .run(function ($rootScope<% if(filters.ngroute) { %>, $location<% } if(filters.uirouter) { %>, $state<% } %>, Auth) { // Redirect to login if route requires auth and you're not logged in $rootScope.$on(<% if(filters.ngroute) { %>'$routeChangeStart'<% } %><% if(filters.uirouter) { %>'$stateChangeStart'<% } %>, function (event, next) { Auth.isLoggedIn(function(loggedIn) { if (next.authenticate && !loggedIn) { - $location.path('/login'); + <% if(filters.ngroute) { %>$location.path('/login');<% } if(filters.uirouter) { %>$state.go('login');<% } %> } }); }); diff --git a/app/templates/client/app/main/main.controller.spec(coffee).coffee b/app/templates/client/app/main/main.controller.spec(coffee).coffee index efe9b39a6..f974a081d 100644 --- a/app/templates/client/app/main/main.controller.spec(coffee).coffee +++ b/app/templates/client/app/main/main.controller.spec(coffee).coffee @@ -3,15 +3,17 @@ describe 'Controller: MainCtrl', -> # load the controller's module - beforeEach module '<%= scriptAppName %>' <% if(filters.socketio) {%> + beforeEach module '<%= scriptAppName %>' <% if(filters.uirouter) {%> + beforeEach module 'stateMock' <% } %><% if(filters.socketio) {%> beforeEach module 'socketMock' <% } %> MainCtrl = undefined - scope = undefined + scope = undefined<% if(filters.uirouter) {%> + state = undefined<% } %> $httpBackend = undefined # Initialize the controller and a mock scope - beforeEach inject (_$httpBackend_, $controller, $rootScope) -> + beforeEach inject (_$httpBackend_, $controller, $rootScope<% if(filters.uirouter) {%>, $state<% } %>) -> $httpBackend = _$httpBackend_ $httpBackend.expectGET('/api/things').respond [ 'HTML5 Boilerplate' @@ -19,10 +21,11 @@ describe 'Controller: MainCtrl', -> 'Karma' 'Express' ] - scope = $rootScope.$new() + scope = $rootScope.$new()<% if(filters.uirouter) {%> + state = $state<% } %> MainCtrl = $controller 'MainCtrl', $scope: scope it 'should attach a list of things to the scope', -> $httpBackend.flush() - expect(scope.awesomeThings.length).toBe 4 \ No newline at end of file + expect(scope.awesomeThings.length).toBe 4 diff --git a/app/templates/client/app/main/main.controller.spec(js).js b/app/templates/client/app/main/main.controller.spec(js).js index 373e9db08..21b8aba70 100644 --- a/app/templates/client/app/main/main.controller.spec(js).js +++ b/app/templates/client/app/main/main.controller.spec(js).js @@ -3,20 +3,23 @@ describe('Controller: MainCtrl', function () { // load the controller's module - beforeEach(module('<%= scriptAppName %>'));<% if(filters.socketio) {%> + beforeEach(module('<%= scriptAppName %>'));<% if(filters.uirouter) {%> + beforeEach(module('stateMock'));<% } %><% if(filters.socketio) {%> beforeEach(module('socketMock'));<% } %> var MainCtrl, - scope, + scope,<% if(filters.uirouter) {%> + state,<% } %> $httpBackend; // Initialize the controller and a mock scope - beforeEach(inject(function (_$httpBackend_, $controller, $rootScope) { + beforeEach(inject(function (_$httpBackend_, $controller, $rootScope<% if(filters.uirouter) {%>, $state<% } %>) { $httpBackend = _$httpBackend_; $httpBackend.expectGET('/api/things') .respond(['HTML5 Boilerplate', 'AngularJS', 'Karma', 'Express']); - scope = $rootScope.$new(); + scope = $rootScope.$new();<% if(filters.uirouter) {%> + state = $state;<% } %> MainCtrl = $controller('MainCtrl', { $scope: scope }); diff --git a/app/templates/client/components/navbar/navbar(html).html b/app/templates/client/components/navbar/navbar(html).html index 71f8606dd..a93839562 100644 --- a/app/templates/client/components/navbar/navbar(html).html +++ b/app/templates/client/components/navbar/navbar(html).html @@ -11,18 +11,18 @@ diff --git a/app/templates/client/components/navbar/navbar(jade).jade b/app/templates/client/components/navbar/navbar(jade).jade index 2b17f29c3..7863f4e0a 100644 --- a/app/templates/client/components/navbar/navbar(jade).jade +++ b/app/templates/client/components/navbar/navbar(jade).jade @@ -10,25 +10,25 @@ div.navbar.navbar-default.navbar-static-top(ng-controller='NavbarCtrl') div#navbar-main.navbar-collapse.collapse(collapse='isCollapsed') ul.nav.navbar-nav - li(ng-repeat='item in menu', ng-class='{active: isActive(item.link)}') - a(ng-href='{{item.link}}') {{item.title}}<% if(filters.auth) { %> + li(ng-repeat='item in menu', <% if(filters.uirouter) { %>ui-sref-active='active'<% } else { %>ng-class='{active: isActive(item.link)}'<% } %>) + a(<% if(filters.uirouter) { %>ui-sref='{{item.state}}'<% } else { %>ng-href='{{item.link}}'<% } %>) {{item.title}}<% if(filters.auth) { %> - li(ng-show='isAdmin()', ng-class='{active: isActive("/admin")}') - a(href='/admin') Admin<% } %><% if(filters.auth) { %> + li(ng-show='isAdmin()', <% if(filters.uirouter) { %>ui-sref-active='active'<% } else { %>ng-class='{active: isActive("/admin")}'<% } %>) + a(<% if(filters.uirouter) { %>ui-sref='admin'<% } else { %>href='/admin'<% } %>) Admin ul.nav.navbar-nav.navbar-right - li(ng-hide='isLoggedIn()', ng-class='{active: isActive("/signup")}') - a(href='/signup') Sign up + li(ng-hide='isLoggedIn()', <% if(filters.uirouter) { %>ui-sref-active='active'<% } else { %>ng-class='{active: isActive("/signup")}'<% } %>) + a(<% if(filters.uirouter) { %>ui-sref='signup'<% } else { %>href='/signup'<% } %>) Sign up - li(ng-hide='isLoggedIn()', ng-class='{active: isActive("/login")}') - a(href='/login') Login + li(ng-hide='isLoggedIn()', <% if(filters.uirouter) { %>ui-sref-active='active'<% } else { %>ng-class='{active: isActive("/login")}'<% } %>) + a(<% if(filters.uirouter) { %>ui-sref='login'<% } else { %>href='/login'<% } %>) Login li(ng-show='isLoggedIn()') p.navbar-text Hello {{ getCurrentUser().name }} - li(ng-show='isLoggedIn()', ng-class='{active: isActive("/settings")}') - a(href='/settings') + li(ng-show='isLoggedIn()', <% if(filters.uirouter) { %>ui-sref-active='active'<% } else { %>ng-class='{active: isActive("/settings")}'<% } %>) + a(<% if(filters.uirouter) { %>ui-sref='settings'<% } else { %>href='/settings'<% } %>) span.glyphicon.glyphicon-cog - li(ng-show='isLoggedIn()', ng-class='{active: isActive("/logout")}') - a(href='', ng-click='logout()') Logout<% } %> \ No newline at end of file + li(ng-show='isLoggedIn()') + a(<% if(filters.uirouter) { %>ui-sref='logout'<% } else { %>href='/logout'<% } %>) Logout<% } %> \ No newline at end of file diff --git a/app/templates/client/components/navbar/navbar.controller(coffee).coffee b/app/templates/client/components/navbar/navbar.controller(coffee).coffee index d3804c5eb..121437cf1 100644 --- a/app/templates/client/components/navbar/navbar.controller(coffee).coffee +++ b/app/templates/client/components/navbar/navbar.controller(coffee).coffee @@ -1,19 +1,15 @@ 'use strict' angular.module '<%= scriptAppName %>' -.controller 'NavbarCtrl', ($scope, $location<% if(filters.auth) {%>, Auth<% } %>) -> +.controller 'NavbarCtrl', ($scope<% if(!filters.uirouter) { %>, $location<% } %><% if(filters.auth) {%>, Auth<% } %>) -> $scope.menu = [ title: 'Home' - link: '/' + <% if(filters.uirouter) { %>state: 'main'<% } else { %>link: '/'<% } %> ] $scope.isCollapsed = true<% if(filters.auth) {%> $scope.isLoggedIn = Auth.isLoggedIn $scope.isAdmin = Auth.isAdmin - $scope.getCurrentUser = Auth.getCurrentUser - - $scope.logout = -> - Auth.logout() - $location.path '/login'<% } %> + $scope.getCurrentUser = Auth.getCurrentUser<% } %><% if(!filters.uirouter) { %> $scope.isActive = (route) -> - route is $location.path() \ No newline at end of file + route is $location.path()<% } %> \ No newline at end of file diff --git a/app/templates/client/components/navbar/navbar.controller(js).js b/app/templates/client/components/navbar/navbar.controller(js).js index 4ce9dbcb5..2428ac15b 100644 --- a/app/templates/client/components/navbar/navbar.controller(js).js +++ b/app/templates/client/components/navbar/navbar.controller(js).js @@ -1,23 +1,18 @@ 'use strict'; angular.module('<%= scriptAppName %>') - .controller('NavbarCtrl', function ($scope, $location<% if(filters.auth) {%>, Auth<% } %>) { + .controller('NavbarCtrl', function ($scope<% if(!filters.uirouter) { %>, $location<% } %><% if(filters.auth) {%>, Auth<% } %>) { $scope.menu = [{ 'title': 'Home', - 'link': '/' + <% if(filters.uirouter) { %>'state': 'main'<% } else { %>'link': '/'<% } %> }]; $scope.isCollapsed = true;<% if(filters.auth) {%> $scope.isLoggedIn = Auth.isLoggedIn; $scope.isAdmin = Auth.isAdmin; - $scope.getCurrentUser = Auth.getCurrentUser; - - $scope.logout = function() { - Auth.logout(); - $location.path('/login'); - };<% } %> + $scope.getCurrentUser = Auth.getCurrentUser;<% } %><% if(!filters.uirouter) { %> $scope.isActive = function(route) { return route === $location.path(); - }; - }); \ No newline at end of file + };<% } %> + }); diff --git a/app/templates/client/components/ui-router(uirouter)/ui-router.mock(coffee).coffee b/app/templates/client/components/ui-router(uirouter)/ui-router.mock(coffee).coffee new file mode 100644 index 000000000..ff3937c35 --- /dev/null +++ b/app/templates/client/components/ui-router(uirouter)/ui-router.mock(coffee).coffee @@ -0,0 +1,26 @@ +'use strict' + +angular.module 'stateMock', [] +angular.module('stateMock').service '$state', ($q) -> + @expectedTransitions = [] + + @transitionTo = (stateName) -> + if @expectedTransitions.length > 0 + expectedState = @expectedTransitions.shift() + throw Error('Expected transition to state: ' + expectedState + ' but transitioned to ' + stateName) if expectedState isnt stateName + else + throw Error('No more transitions were expected! Tried to transition to ' + stateName) + console.log 'Mock transition to: ' + stateName + deferred = $q.defer() + promise = deferred.promise + deferred.resolve() + promise + + @go = @transitionTo + + @expectTransitionTo = (stateName) -> + @expectedTransitions.push stateName + + @ensureAllTransitionsHappened = -> + throw Error('Not all transitions happened!') if @expectedTransitions.length > 0 + @ diff --git a/app/templates/client/components/ui-router(uirouter)/ui-router.mock(js).js b/app/templates/client/components/ui-router(uirouter)/ui-router.mock(js).js new file mode 100644 index 000000000..a5a1bf413 --- /dev/null +++ b/app/templates/client/components/ui-router(uirouter)/ui-router.mock(js).js @@ -0,0 +1,34 @@ +'use strict'; + +angular.module('stateMock', []); +angular.module('stateMock').service('$state', function($q) { + this.expectedTransitions = []; + + this.transitionTo = function(stateName) { + if (this.expectedTransitions.length > 0) { + var expectedState = this.expectedTransitions.shift(); + if (expectedState !== stateName) { + throw Error('Expected transition to state: ' + expectedState + ' but transitioned to ' + stateName); + } + } else { + throw Error('No more transitions were expected! Tried to transition to ' + stateName); + } + console.log('Mock transition to: ' + stateName); + var deferred = $q.defer(); + var promise = deferred.promise; + deferred.resolve(); + return promise; + }; + + this.go = this.transitionTo; + + this.expectTransitionTo = function(stateName) { + this.expectedTransitions.push(stateName); + }; + + this.ensureAllTransitionsHappened = function() { + if (this.expectedTransitions.length > 0) { + throw Error('Not all transitions happened!'); + } + }; +}); From fb28c28daf8ac777f15644bc2cde2fd149f90788 Mon Sep 17 00:00:00 2001 From: Tyler Henkel Date: Tue, 16 Sep 2014 23:22:45 -0600 Subject: [PATCH 18/20] expect uirouter mocks in tests --- test/test-file-creation.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/test-file-creation.js b/test/test-file-creation.js index b6171fabd..05e83c77f 100644 --- a/test/test-file-creation.js +++ b/test/test-file-creation.js @@ -174,11 +174,17 @@ describe('angular-fullstack generator', function () { 'protractor.conf.js' ]); + if (ops.router === 'uirouter') { + files = files.concat([ + 'client/components/ui-router/ui-router.mock.' + script + ]); + } + if (ops.uibootstrap) { files = files.concat([ 'client/components/modal/modal.' + markup, 'client/components/modal/modal.' + stylesheet, - 'client/components/modal/modal.service.' + script, + 'client/components/modal/modal.service.' + script ]); } From 18c6222c8528bd0eb1b041a8bc653009e3c51f9a Mon Sep 17 00:00:00 2001 From: eike thies Date: Fri, 19 Sep 2014 20:12:59 +0200 Subject: [PATCH 19/20] renamed password field for exclution --- app/templates/server/api/user(auth)/user.controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/server/api/user(auth)/user.controller.js b/app/templates/server/api/user(auth)/user.controller.js index 28e30199a..b70cef327 100644 --- a/app/templates/server/api/user(auth)/user.controller.js +++ b/app/templates/server/api/user(auth)/user.controller.js @@ -31,7 +31,7 @@ function respondWith(res, statusCode){ * restriction: 'admin' */ exports.index = function(req, res) { - User.findAsync({}, '-salt -hashedPassword') + User.findAsync({}, '-salt -password') .then(function (users) { res.json(200, users); }) @@ -108,7 +108,7 @@ exports.changePassword = function(req, res, next) { exports.me = function(req, res, next) { var userId = req.user._id; - User.findOneAsync({ _id: userId }, '-salt -hashedPassword') + User.findOneAsync({ _id: userId }, '-salt -password') .then(function(user) { // don't ever give out the password or salt if (!user) { return res.json(401); } res.json(user); From 91c92232f4bc4570a01a63d4b82e89225dff6fed Mon Sep 17 00:00:00 2001 From: eike thies Date: Fri, 19 Sep 2014 20:13:57 +0200 Subject: [PATCH 20/20] jwt.sign was receiving not the correct ID because the async function returns an array. fix by using spread function --- app/templates/server/api/user(auth)/user.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/server/api/user(auth)/user.controller.js b/app/templates/server/api/user(auth)/user.controller.js index b70cef327..410aa8afd 100644 --- a/app/templates/server/api/user(auth)/user.controller.js +++ b/app/templates/server/api/user(auth)/user.controller.js @@ -46,7 +46,7 @@ exports.create = function (req, res, next) { newUser.provider = 'local'; newUser.role = 'user'; newUser.saveAsync() - .then(function(user) { + .spread(function (user, affectedRows) { var token = jwt.sign({_id: user._id }, config.secrets.session, { expiresInMinutes: 60*5 }); res.json({ token: token }); })