diff --git a/.gitattributes b/.gitattributes index 176a458f9..fcadb2cf9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -* text=auto +* text eol=lf 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/.travis.yml b/.travis.yml index 5100940c2..814f378a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,26 @@ +sudo: false language: node_js node_js: - '0.12' +env: + global: + - SAUCE_USERNAME=fullstack_ci + - SAUCE_ACCESS_KEY=1a527ca6-4aa5-4618-86ce-0278bf158cbf before_install: + - ./scripts/sauce_connect_setup.sh - gem update --system - gem install sass --version "=3.3.7" - npm install -g bower grunt-cli services: mongodb +cache: + directories: + - node_modules + - test/fixtures/node_modules + - test/fixtures/bower_components notifications: webhooks: urls: - - https://webhooks.gitter.im/e/911ed472ef19bcb27858 + - secure: "DhPNqHXuUIeIGE9Ek3+63qhco+4MozXqMZL6dAKoq1MHQ2RAPO6SYIkUYZqDnuWYlwWao2EnTYcDREivIV/m/RnkP9bKlpX/n/RNJe+X4bwFaCU55fVKgkAFn3takSBC5SVoeTWHdWu3WhhqSdioWjT7mlE1wtt/RanSMb5Id8M=" on_success: change # options: [always|never|change] default: always on_failure: always # options: [always|never|change] default: always on_start: false # default: false diff --git a/CHANGELOG.md b/CHANGELOG.md index ced95fa81..9d265082f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -582,4 +582,4 @@ Closes #18, #17 #### Features -* **gen:** include MongoDB as an option When selected, sets up database with Mongoose. Repl ([280cc84d](http://github.com/DaftMonk/generator-angular-fullstack/commit/280cc84d735c60b1c261540dceda34dd7f91c93c), closes [#2](http://github.com/DaftMonk/generator-angular-fullstack/issues/2)) \ No newline at end of file +* **gen:** include MongoDB as an option When selected, sets up database with Mongoose. Repl ([280cc84d](http://github.com/DaftMonk/generator-angular-fullstack/commit/280cc84d735c60b1c261540dceda34dd7f91c93c), closes [#2](http://github.com/DaftMonk/generator-angular-fullstack/issues/2)) diff --git a/Gruntfile.js b/Gruntfile.js index 9819a4041..38fc08efc 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,16 +1,19 @@ 'use strict'; -var markdown = require('marked'); + var semver = require('semver'); -var _s = require('underscore.string'); var shell = require('shelljs'); -var process = require('child_process'); +var child_process = require('child_process'); var Q = require('q'); var helpers = require('yeoman-generator').test; -var fs = require('fs-extra'); +var fs = require('fs'); var path = require('path'); module.exports = function (grunt) { - require('load-grunt-tasks')(grunt); + // Load grunt tasks automatically, when needed + require('jit-grunt')(grunt, { + buildcontrol: 'grunt-build-control', + changelog: 'grunt-conventional-changelog' + }); grunt.initConfig({ config: { @@ -58,6 +61,11 @@ module.exports = function (grunt) { }, all: ['Gruntfile.js', '*/index.js'] }, + env: { + fast: { + SKIP_E2E: true + } + }, mochaTest: { test: { src: [ @@ -82,6 +90,16 @@ module.exports = function (grunt) { ] }] } + }, + david: { + gen: { + options: {} + }, + app: { + options: { + package: 'test/fixtures/package.json' + } + } } }); @@ -139,6 +157,7 @@ module.exports = function (grunt) { bootstrap: true, uibootstrap: true, mongoose: true, + testing: 'jasmine', auth: true, oauth: ['googleAuth', 'twitterAuth'], socketio: true @@ -202,7 +221,6 @@ module.exports = function (grunt) { }); grunt.registerTask('updateFixtures', 'updates package and bower fixtures', function() { - var done = this.async(); var packageJson = fs.readFileSync(path.resolve('app/templates/_package.json'), 'utf8'); var bowerJson = fs.readFileSync(path.resolve('app/templates/_bower.json'), 'utf8'); @@ -215,11 +233,8 @@ module.exports = function (grunt) { bowerJson = bowerJson.replace(/<%(.*)%>/g, ''); // save files - fs.writeFile(path.resolve(__dirname + '/test/fixtures/package.json'), packageJson, function() { - fs.writeFile(path.resolve(__dirname + '/test/fixtures/bower.json'), bowerJson, function() { - done(); - }); - }); + fs.writeFileSync(path.resolve(__dirname + '/test/fixtures/package.json'), packageJson); + fs.writeFileSync(path.resolve(__dirname + '/test/fixtures/bower.json'), bowerJson); }); grunt.registerTask('installFixtures', 'install package and bower fixtures', function() { @@ -227,12 +242,21 @@ module.exports = function (grunt) { shell.cd('test/fixtures'); grunt.log.ok('installing npm dependencies for generated app'); - process.exec('npm install --quiet', {cwd: '../fixtures'}, function (error, stdout, stderr) { + child_process.exec('npm install --quiet', {cwd: '../fixtures'}, function (error, stdout, stderr) { grunt.log.ok('installing bower dependencies for generated app'); - process.exec('bower install', {cwd: '../fixtures'}, function (error, stdout, stderr) { - shell.cd('../../'); - done(); + child_process.exec('bower install', {cwd: '../fixtures'}, function (error, stdout, stderr) { + + if(!process.env.SAUCE_USERNAME) { + grunt.log.ok('running npm run update-webdriver'); + child_process.exec('npm run update-webdriver', function() { + shell.cd('../../'); + done(); + }); + } else { + shell.cd('../../'); + done(); + } }) }); }); @@ -242,6 +266,24 @@ module.exports = function (grunt) { 'installFixtures', 'mochaTest' ]); + grunt.registerTask('test', function(target, option) { + if (target === 'fast') { + grunt.task.run([ + 'env:fast' + ]); + } + + return grunt.task.run([ + 'updateFixtures', + 'installFixtures', + 'mochaTest' + ]) + }); + + grunt.registerTask('deps', function(target) { + if (!target || target === 'app') grunt.task.run(['updateFixtures']); + grunt.task.run(['david:' + (target || '')]); + }); grunt.registerTask('demo', [ 'clean:demo', diff --git a/app/index.js b/app/index.js index 194b337a6..ec954aa46 100644 --- a/app/index.js +++ b/app/index.js @@ -5,274 +5,415 @@ var util = require('util'); var genUtils = require('../util.js'); var yeoman = require('yeoman-generator'); var chalk = require('chalk'); -var wiredep = require('wiredep'); var AngularFullstackGenerator = yeoman.generators.Base.extend({ - init: function () { - this.argument('name', { type: String, required: false }); - this.appname = this.name || path.basename(process.cwd()); - this.appname = this._.camelize(this._.slugify(this._.humanize(this.appname))); - - this.option('app-suffix', { - desc: 'Allow a custom suffix to be added to the module name', - type: String, - required: 'false' - }); - this.scriptAppName = this.appname + genUtils.appName(this); - this.appPath = this.env.options.appPath; - this.pkg = require('../package.json'); - - this.filters = {}; - }, + initializing: { - info: function () { - this.log(this.yeoman); - this.log('Out of the box I create an AngularJS app with an Express server.\n'); - }, + init: function () { + this.argument('name', { type: String, required: false }); + this.appname = this.name || path.basename(process.cwd()); + this.appname = this._.camelize(this._.slugify(this._.humanize(this.appname))); - checkForConfig: function() { - var cb = this.async(); + this.option('app-suffix', { + desc: 'Allow a custom suffix to be added to the module name', + type: String, + required: 'false' + }); + this.scriptAppName = this.appname + genUtils.appName(this); + this.appPath = this.env.options.appPath; + this.pkg = require('../package.json'); - if(this.config.get('filters')) { - this.prompt([{ - type: "confirm", - name: "skipConfig", - message: "Existing .yo-rc configuration found, would you like to use it?", - default: true, - }], function (answers) { - this.skipConfig = answers.skipConfig; + this.filters = {}; + + // dynamic assertion statement + this.does = this.is = function(foo) { + foo = this.engine(foo.replace(/\(;>%%<;\)/g, '<%') + .replace(/\(;>%<;\)/g, '%>'), this); + if (this.filters.should) { + return foo + '.should'; + } else { + return 'expect(' + foo + ').to'; + } + }.bind(this); + }, - // NOTE: temp(?) fix for #403 - if(typeof this.oauth==='undefined') { - var strategies = Object.keys(this.filters).filter(function(key) { - return key.match(/Auth$/) && key; + info: function () { + this.log(this.welcome); + this.log('Out of the box I create an AngularJS app with an Express server.\n'); + }, + + checkForConfig: function() { + var cb = this.async(); + + if(this.config.get('filters')) { + this.prompt([{ + type: 'confirm', + name: 'skipConfig', + message: 'Existing .yo-rc configuration found, would you like to use it?', + default: true, + }], function (answers) { + this.skipConfig = answers.skipConfig; + + this.filters = this._.defaults(this.config.get('filters'), { + bootstrap: true, + uibootstrap: true, + jasmine: true }); - if(strategies.length) this.config.set('oauth', true); - } + this.config.set('filters', this.filters); + this.config.forceSave(); + cb(); + }.bind(this)); + } else { cb(); - }.bind(this)); - } else { - cb(); + } } + }, - clientPrompts: function() { - if(this.skipConfig) return; - var cb = this.async(); + prompting: { - this.log('# Client\n'); + clientPrompts: function() { + if(this.skipConfig) return; + var cb = this.async(); - this.prompt([{ - type: "list", - name: "script", - message: "What would you like to write scripts with?", - choices: [ "JavaScript", "CoffeeScript"], - filter: function( val ) { - var filterMap = { - 'JavaScript': 'js', - 'CoffeeScript': 'coffee' - }; + this.log('# Client\n'); - return filterMap[val]; + this.prompt([{ + type: 'list', + name: 'script', + message: 'What would you like to write scripts with?', + choices: [ 'JavaScript', 'JavaScript + Babel', 'CoffeeScript'], + filter: function( val ) { + return { + 'JavaScript': 'js', + 'JavaScript + Babel': 'babel', + 'CoffeeScript': 'coffee' + }[val]; + } + }, { + type: 'list', + name: 'markup', + message: 'What would you like to write markup with?', + choices: ['HTML', 'Jade'], + filter: function( val ) { return val.toLowerCase(); } + }, { + type: 'list', + name: 'stylesheet', + default: 1, + message: 'What would you like to write stylesheets with?', + choices: [ 'CSS', 'Sass', 'Stylus', 'Less'], + filter: function( val ) { return val.toLowerCase(); } + }, { + type: 'list', + name: 'router', + default: 1, + message: 'What Angular router would you like to use?', + choices: [ 'ngRoute', 'uiRouter'], + filter: function( val ) { return val.toLowerCase(); } + }, { + type: 'confirm', + name: 'bootstrap', + message: 'Would you like to include Bootstrap?' + }, { + type: 'confirm', + name: 'uibootstrap', + message: 'Would you like to include UI Bootstrap?', + when: function (answers) { + return answers.bootstrap; + } + }], function (answers) { + + // also set 'js' to true if using babel + if(answers.script === 'babel') { this.filters.js = true; } + this.filters[answers.script] = true; + this.filters[answers.markup] = true; + this.filters[answers.stylesheet] = true; + this.filters[answers.router] = true; + this.filters.bootstrap = !!answers.bootstrap; + this.filters.uibootstrap = !!answers.uibootstrap; + cb(); + }.bind(this)); + }, + + serverPrompts: function() { + if(this.skipConfig) return; + var cb = this.async(); + var self = this; + + this.log('\n# Server\n'); + + this.prompt([{ + type: 'checkbox', + name: 'odms', + message: 'What would you like to use for data modeling?', + choices: [ + { + value: 'mongoose', + name: 'Mongoose (MongoDB)', + checked: true + }, + { + value: 'sequelize', + name: 'Sequelize (MySQL, SQLite, MariaDB, PostgreSQL)', + checked: false + } + ] + }, { + type: 'list', + name: 'models', + message: 'What would you like to use for the default models?', + choices: [ 'Mongoose', 'Sequelize' ], + filter: function( val ) { + return val.toLowerCase(); + }, + when: function(answers) { + return answers.odms && answers.odms.length > 1; } }, { - type: "confirm", - name: "babel", - message: "Would you like to use Javascript ES6 in your client by preprocessing it with Babel?", + type: 'confirm', + name: 'auth', + message: 'Would you scaffold out an authentication boilerplate?', when: function (answers) { - return answers.script === 'js'; + return answers.odms && answers.odms.length !== 0; } }, { - type: "list", - name: "markup", - message: "What would you like to write markup with?", - choices: [ "HTML", "Jade"], - filter: function( val ) { return val.toLowerCase(); } - }, { - type: "list", - name: "stylesheet", - default: 1, - message: "What would you like to write stylesheets with?", - choices: [ "CSS", "Sass", "Stylus", "Less"], - filter: function( val ) { return val.toLowerCase(); } - }, { - type: "list", - name: "router", - default: 1, - message: "What Angular router would you like to use?", - choices: [ "ngRoute", "uiRouter"], - filter: function( val ) { return val.toLowerCase(); } - }, { - type: "confirm", - name: "bootstrap", - message: "Would you like to include Bootstrap?" + type: 'checkbox', + name: 'oauth', + message: 'Would you like to include additional oAuth strategies?', + when: function (answers) { + return answers.auth; + }, + choices: [ + { + value: 'googleAuth', + name: 'Google', + checked: false + }, + { + value: 'facebookAuth', + name: 'Facebook', + checked: false + }, + { + value: 'twitterAuth', + name: 'Twitter', + checked: false + } + ] }, { - type: "confirm", - name: "uibootstrap", - message: "Would you like to include UI Bootstrap?", + type: 'confirm', + name: 'socketio', + message: 'Would you like to use socket.io?', + // to-do: should not be dependent on ODMs when: function (answers) { - return answers.bootstrap; - } + return answers.odms && answers.odms.length !== 0; + }, + default: true }], function (answers) { - - this.filters.babel = !!answers.babel; - if(this.filters.babel){ this.filters.js = true; } - this.filters[answers.script] = true; - this.filters[answers.markup] = true; - this.filters[answers.stylesheet] = true; - this.filters[answers.router] = true; - this.filters.bootstrap = !!answers.bootstrap; - this.filters.uibootstrap = !!answers.uibootstrap; - cb(); + if(answers.socketio) this.filters.socketio = true; + if(answers.auth) this.filters.auth = true; + if(answers.odms && answers.odms.length > 0) { + var models; + if(!answers.models) { + models = answers.odms[0]; + } else { + models = answers.models; + } + this.filters.models = true; + this.filters[models + 'Models'] = true; + answers.odms.forEach(function(odm) { + this.filters[odm] = true; + }.bind(this)); + } else { + this.filters.noModels = true; + } + if(answers.oauth) { + if(answers.oauth.length) this.filters.oauth = true; + answers.oauth.forEach(function(oauthStrategy) { + this.filters[oauthStrategy] = true; + }.bind(this)); + } + + cb(); }.bind(this)); - }, + }, - serverPrompts: function() { - if(this.skipConfig) return; - var cb = this.async(); - var self = this; - - this.log('\n# Server\n'); - - this.prompt([{ - type: "confirm", - name: "mongoose", - message: "Would you like to use mongoDB with Mongoose for data modeling?" - }, { - type: "confirm", - name: "auth", - message: "Would you scaffold out an authentication boilerplate?", - when: function (answers) { - return answers.mongoose; - } - }, { - type: 'checkbox', - name: 'oauth', - message: 'Would you like to include additional oAuth strategies?', - when: function (answers) { - return answers.auth; - }, - choices: [ - { - value: 'googleAuth', - name: 'Google', - checked: false - }, - { - value: 'facebookAuth', - name: 'Facebook', - checked: false + projectPrompts: function() { + if(this.skipConfig) return; + var cb = this.async(); + var self = this; + + this.log('\n# Project\n'); + + this.prompt([{ + type: 'list', + name: 'testing', + message: 'What would you like to write tests with?', + choices: [ 'Jasmine', 'Mocha + Chai + Sinon'], + filter: function( val ) { + var filterMap = { + 'Jasmine': 'jasmine', + 'Mocha + Chai + Sinon': 'mocha' + }; + + return filterMap[val]; + } + }, { + type: 'list', + name: 'chai', + message: 'What would you like to write Chai assertions with?', + choices: ['Expect', 'Should'], + filter: function( val ) { + return val.toLowerCase(); }, - { - value: 'twitterAuth', - name: 'Twitter', - checked: false + when: function( answers ) { + return answers.testing === 'mocha'; } - ] - }, { - type: "confirm", - name: "socketio", - message: "Would you like to use socket.io?", - // to-do: should not be dependent on mongoose - when: function (answers) { - return answers.mongoose; - }, - default: true - }], function (answers) { - if(answers.socketio) this.filters.socketio = true; - if(answers.mongoose) this.filters.mongoose = true; - if(answers.auth) this.filters.auth = true; - if(answers.oauth) { - if(answers.oauth.length) this.filters.oauth = true; - answers.oauth.forEach(function(oauthStrategy) { - this.filters[oauthStrategy] = true; - }.bind(this)); - } + }], function (answers) { + /** + * Default to grunt until gulp support is implemented + */ + this.filters.grunt = true; + + this.filters[answers.testing] = true; + if (answers.testing === 'mocha') { + this.filters.jasmine = false; + this.filters.should = false; + this.filters.expect = false; + this.filters[answers.chai] = true; + } + if (answers.testing === 'jasmine') { + this.filters.mocha = false; + this.filters.should = false; + this.filters.expect = false; + } + + cb(); + }.bind(this)); + } - cb(); - }.bind(this)); }, - saveSettings: function() { - if(this.skipConfig) return; - this.config.set('insertRoutes', true); - this.config.set('registerRoutesFile', 'server/routes.js'); - this.config.set('routesNeedle', '// Insert routes below'); + configuring: { - this.config.set('routesBase', '/api/'); - this.config.set('pluralizeRoutes', true); + saveSettings: function() { + if(this.skipConfig) return; + this.config.set('endpointDirectory', 'server/api/'); + this.config.set('insertRoutes', true); + this.config.set('registerRoutesFile', 'server/routes.js'); + this.config.set('routesNeedle', '// Insert routes below'); - this.config.set('insertSockets', true); - this.config.set('registerSocketsFile', 'server/config/socketio.js'); - this.config.set('socketsNeedle', '// Insert sockets below'); + this.config.set('routesBase', '/api/'); + this.config.set('pluralizeRoutes', true); + + this.config.set('insertSockets', true); + this.config.set('registerSocketsFile', 'server/config/socketio.js'); + this.config.set('socketsNeedle', '// Insert sockets below'); + + this.config.set('insertModels', true); + this.config.set('registerModelsFile', 'server/sqldb/index.js'); + this.config.set('modelsNeedle', '// Insert models below'); + + this.config.set('filters', this.filters); + this.config.forceSave(); + }, + + ngComponent: function() { + if(this.skipConfig) return; + var appPath = 'client/app/'; + var extensions = []; + var filters = [ + 'ngroute', + 'uirouter', + 'jasmine', + 'mocha', + 'expect', + 'should' + ].filter(function(v) {return this.filters[v];}, this); + + if(this.filters.ngroute) filters.push('ngroute'); + if(this.filters.uirouter) filters.push('uirouter'); + if(this.filters.babel) extensions.push('babel'); + if(this.filters.coffee) extensions.push('coffee'); + if(this.filters.js) extensions.push('js'); + if(this.filters.html) extensions.push('html'); + if(this.filters.jade) extensions.push('jade'); + if(this.filters.css) extensions.push('css'); + if(this.filters.stylus) extensions.push('styl'); + if(this.filters.sass) extensions.push('scss'); + if(this.filters.less) extensions.push('less'); + + this.composeWith('ng-component', { + options: { + 'routeDirectory': appPath, + 'directiveDirectory': appPath, + 'filterDirectory': appPath, + 'serviceDirectory': appPath, + 'filters': filters, + 'extensions': extensions, + 'basePath': 'client' + } + }, { local: require.resolve('generator-ng-component/app/index.js') }); + }, + + ngModules: function() { + var angModules = [ + "'ngCookies'", + "'ngResource'", + "'ngSanitize'" + ]; + if(this.filters.ngroute) angModules.push("'ngRoute'"); + if(this.filters.socketio) angModules.push("'btford.socket-io'"); + if(this.filters.uirouter) angModules.push("'ui.router'"); + if(this.filters.uibootstrap) angModules.push("'ui.bootstrap'"); + + this.angularModules = '\n ' + angModules.join(',\n ') +'\n'; + } - this.config.set('filters', this.filters); - this.config.forceSave(); }, - compose: function() { - if(this.skipConfig) return; - var appPath = 'client/app/'; - var extensions = []; - var filters = []; - - if(this.filters.ngroute) filters.push('ngroute'); - if(this.filters.uirouter) filters.push('uirouter'); - if(this.filters.babel) extensions.push('babel'); - if(this.filters.coffee) extensions.push('coffee'); - if(this.filters.js) extensions.push('js'); - if(this.filters.html) extensions.push('html'); - if(this.filters.jade) extensions.push('jade'); - if(this.filters.css) extensions.push('css'); - if(this.filters.stylus) extensions.push('styl'); - if(this.filters.sass) extensions.push('scss'); - if(this.filters.less) extensions.push('less'); - - this.composeWith('ng-component', { - options: { - 'routeDirectory': appPath, - 'directiveDirectory': appPath, - 'filterDirectory': appPath, - 'serviceDirectory': appPath, - 'filters': filters, - 'extensions': extensions, - 'basePath': 'client' + default: {}, + + writing: { + + generateProject: function() { + this.sourceRoot(path.join(__dirname, './templates')); + genUtils.processDirectory(this, '.', '.'); + }, + + generateEndpoint: function() { + var models; + if (this.filters.mongooseModels) { + models = 'mongoose'; + } else if (this.filters.sequelizeModels) { + models = 'sequelize'; } - }, { local: require.resolve('generator-ng-component/app/index.js') }); - }, + this.composeWith('angular-fullstack:endpoint', { + options: { + route: '/api/things', + models: models + }, + args: ['thing'] + }); + } - ngModules: function() { - this.filters = this._.defaults(this.config.get('filters'), { - bootstrap: true, - uibootstrap: true - }); - - var angModules = [ - "'ngCookies'", - "'ngResource'", - "'ngSanitize'" - ]; - if(this.filters.ngroute) angModules.push("'ngRoute'"); - if(this.filters.socketio) angModules.push("'btford.socket-io'"); - if(this.filters.uirouter) angModules.push("'ui.router'"); - if(this.filters.uibootstrap) angModules.push("'ui.bootstrap'"); - - this.angularModules = "\n " + angModules.join(",\n ") +"\n"; }, - generate: function() { - this.sourceRoot(path.join(__dirname, './templates')); - genUtils.processDirectory(this, '.', '.'); + install: { + + installDeps: function() { + this.installDependencies({ + skipInstall: this.options['skip-install'] + }); + } + }, - end: function() { - this.installDependencies({ - skipInstall: this.options['skip-install'] - }); - } + end: {} + }); module.exports = AngularFullstackGenerator; diff --git a/app/templates/.buildignore b/app/templates/.buildignore index fc98b8eb5..3ae6d06a2 100644 --- a/app/templates/.buildignore +++ b/app/templates/.buildignore @@ -1 +1 @@ -*.coffee \ No newline at end of file +*.coffee diff --git a/app/templates/.jscs.json b/app/templates/.jscs.json new file mode 100644 index 000000000..99393d5f6 --- /dev/null +++ b/app/templates/.jscs.json @@ -0,0 +1,44 @@ +{ + "maximumLineLength": { + "value": 100, + "allowComments": true, + "allowRegex": true + }, + "disallowMixedSpacesAndTabs": true, + "disallowMultipleLineStrings": true, + "disallowNewlineBeforeBlockStatements": true, + "disallowSpaceAfterObjectKeys": true, + "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], + "disallowSpaceBeforeBinaryOperators": [","], + "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], + "disallowSpacesInAnonymousFunctionExpression": { + "beforeOpeningRoundBrace": true + }, + "disallowSpacesInFunctionDeclaration": { + "beforeOpeningRoundBrace": true + }, + "disallowSpacesInNamedFunctionExpression": { + "beforeOpeningRoundBrace": true + }, + "disallowSpacesInsideArrayBrackets": true, + "disallowSpacesInsideParentheses": true, + "disallowTrailingComma": true, + "disallowTrailingWhitespace": true, + "requireCommaBeforeLineBreak": true, + "requireLineFeedAtFileEnd": true, + "requireSpaceAfterBinaryOperators": ["?", ":", "+", "-", "/", "*", "%", "==", "===", "!=", "!==", ">", ">=", "<", "<=", "&&", "||"], + "requireSpaceBeforeBinaryOperators": ["?", ":", "+", "-", "/", "*", "%", "==", "===", "!=", "!==", ">", ">=", "<", "<=", "&&", "||"], + "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], + "requireSpaceBeforeBlockStatements": true, + "requireSpacesInConditionalExpression": { + "afterTest": true, + "beforeConsequent": true, + "afterConsequent": true, + "beforeAlternate": true + }, + "requireSpacesInFunction": { + "beforeOpeningCurlyBrace": true + }, + "validateLineBreaks": "LF", + "validateParameterSeparator": ", " +} diff --git a/app/templates/.travis.yml b/app/templates/.travis.yml index 5112a8e88..c12f57edb 100644 --- a/app/templates/.travis.yml +++ b/app/templates/.travis.yml @@ -1,9 +1,8 @@ language: node_js node_js: - - '0.10' - - '0.11' + - '0.12' before_script: - npm install -g bower grunt-cli<% if (filters.sass) { %> - gem install sass<% } %> - bower install -services: mongodb \ No newline at end of file +services: mongodb diff --git a/app/templates/Gruntfile.js b/app/templates/Gruntfile.js index 6685f5108..666681a89 100644 --- a/app/templates/Gruntfile.js +++ b/app/templates/Gruntfile.js @@ -16,7 +16,8 @@ module.exports = function (grunt) { ngtemplates: 'grunt-angular-templates', cdnify: 'grunt-google-cdn', protractor: 'grunt-protractor-runner', - 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 @@ -38,13 +39,13 @@ module.exports = function (grunt) { }, dev: { options: { - script: 'server/app.js', + script: 'server', debug: true } }, prod: { options: { - script: 'dist/server/app.js' + script: 'dist/server' } } }, @@ -53,102 +54,73 @@ module.exports = function (grunt) { url: 'http://localhost:<%%= express.options.port %>' } }, - watch: { + watch: {<% if(filters.babel) { %> + babel: { + files: ['<%%= yeoman.client %>/{app,components}/**/!(*.spec|*.mock).js'], + tasks: ['newer:babel:client'] + },<% } %> injectJS: { files: [ - '<%%= yeoman.client %>/{app,components}/**/*.js', - '!<%%= yeoman.client %>/{app,components}/**/*.spec.js', - '!<%%= yeoman.client %>/{app,components}/**/*.mock.js', - '!<%%= yeoman.client %>/app/app.js'], + '<%%= yeoman.client %>/{app,components}/**/!(*.spec|*.mock).js', + '!<%%= yeoman.client %>/app/app.js' + ], tasks: ['injector:scripts'] }, injectCss: { - files: [ - '<%%= yeoman.client %>/{app,components}/**/*.css' - ], + files: ['<%%= yeoman.client %>/{app,components}/**/*.css'], tasks: ['injector:css'] }, mochaTest: { - files: ['server/**/*.spec.js'], + files: ['server/**/*.{spec,integration}.js'], tasks: ['env:test', 'mochaTest'] }, jsTest: { - files: [ - '<%%= yeoman.client %>/{app,components}/**/*.spec.js', - '<%%= yeoman.client %>/{app,components}/**/*.mock.js' - ], - tasks: ['newer:jshint:all', 'karma'] - },<% if(filters.stylus) { %> + files: ['<%%= yeoman.client %>/{app,components}/**/*.{spec,mock}.js'], + tasks: ['newer:jshint:all', 'wiredep:test', 'karma'] + },<% if (filters.stylus) { %> injectStylus: { - files: [ - '<%%= yeoman.client %>/{app,components}/**/*.styl'], + files: ['<%%= yeoman.client %>/{app,components}/**/*.styl'], tasks: ['injector:stylus'] }, stylus: { - files: [ - '<%%= yeoman.client %>/{app,components}/**/*.styl'], - tasks: ['stylus', 'autoprefixer'] - },<% } %><% if(filters.sass) { %> + files: ['<%%= yeoman.client %>/{app,components}/**/*.styl'], + tasks: ['stylus', 'postcss'] + },<% } if (filters.sass) { %> injectSass: { - files: [ - '<%%= yeoman.client %>/{app,components}/**/*.{scss,sass}'], + files: ['<%%= yeoman.client %>/{app,components}/**/*.{scss,sass}'], tasks: ['injector:sass'] }, sass: { - files: [ - '<%%= yeoman.client %>/{app,components}/**/*.{scss,sass}'], - tasks: ['sass', 'autoprefixer'] - },<% } %><% if(filters.less) { %> + files: ['<%%= yeoman.client %>/{app,components}/**/*.{scss,sass}'], + tasks: ['sass', 'postcss'] + },<% } if (filters.less) { %> injectLess: { - files: [ - '<%%= yeoman.client %>/{app,components}/**/*.less'], + files: ['<%%= yeoman.client %>/{app,components}/**/*.less'], tasks: ['injector:less'] }, less: { - files: [ - '<%%= yeoman.client %>/{app,components}/**/*.less'], - tasks: ['less', 'autoprefixer'] - },<% } %><% if(filters.jade) { %> + files: ['<%%= yeoman.client %>/{app,components}/**/*.less'], + tasks: ['less', 'postcss'] + },<% } if (filters.jade) { %> jade: { - files: [ - '<%%= yeoman.client %>/{app,components}/*', - '<%%= yeoman.client %>/{app,components}/**/*.jade'], + files: ['<%%= yeoman.client %>/{app,components}/**/*.jade'], tasks: ['jade'] - },<% } %><% if(filters.coffee) { %> + },<% } if (filters.coffee) { %> coffee: { - files: [ - '<%%= yeoman.client %>/{app,components}/**/*.{coffee,litcoffee,coffee.md}', - '!<%%= yeoman.client %>/{app,components}/**/*.spec.{coffee,litcoffee,coffee.md}' - ], + files: ['<%%= yeoman.client %>/{app,components}/**/!(*.spec).{coffee,litcoffee,coffee.md}'], tasks: ['newer:coffee', 'injector:scripts'] }, coffeeTest: { - files: [ - '<%%= yeoman.client %>/{app,components}/**/*.spec.{coffee,litcoffee,coffee.md}' - ], + files: ['<%%= yeoman.client %>/{app,components}/**/*.spec.{coffee,litcoffee,coffee.md}'], tasks: ['karma'] - },<% } %><% if(filters.babel) { %> - babel: { - files: [ - '<%%= yeoman.client %>/{app,components}/**/*.js', - '!<%%= yeoman.client %>/{app,components}/**/*.spec.js' - ], - tasks: ['babel'] },<% } %> gruntfile: { files: ['Gruntfile.js'] }, livereload: { files: [ - '{.tmp,<%%= yeoman.client %>}/{app,components}/**/*.css', - '{.tmp,<%%= yeoman.client %>}/{app,components}/**/*.html', - <% if(filters.babel) { %> - '.tmp/{app,components}/**/*.js', - <% } else { %> - '{.tmp,<%%= yeoman.client %>}/{app,components}/**/*.js', - <% } %> - '!{.tmp,<%%= yeoman.client %>}{app,components}/**/*.spec.js', - '!{.tmp,<%%= yeoman.client %>}/{app,components}/**/*.mock.js', + '{.tmp,<%%= yeoman.client %>}/{app,components}/**/*.{css,html}', + '{.tmp,<%%= yeoman.client %>}/{app,components}/**/!(*.spec|*.mock).js', '<%%= yeoman.client %>/assets/images/{,*//*}*.{png,jpg,jpeg,gif,webp,svg}' ], options: { @@ -156,15 +128,17 @@ module.exports = function (grunt) { } }, express: { - files: [ - 'server/**/*.{js,json}' - ], + files: ['server/**/*.{js,json}'], tasks: ['express:dev', 'wait'], options: { livereload: true, - nospawn: true //Without this option specified express won't be reloaded + spawn: false //Without this option specified express won't be reloaded } - } + }, + bower: { + files: ['bower.json'], + tasks: ['wiredep'] + }, }, // Make sure code styles are up to par and there are no obvious mistakes @@ -177,27 +151,31 @@ module.exports = function (grunt) { options: { jshintrc: 'server/.jshintrc' }, - src: [ - 'server/**/*.js', - '!server/**/*.spec.js' - ] + src: ['server/**/!(*.spec|*.integration).js'] }, serverTest: { options: { jshintrc: 'server/.jshintrc-spec' }, - src: ['server/**/*.spec.js'] + src: ['server/**/*.{spec,integration}.js'] }, - all: [ - '<%%= yeoman.client %>/{app,components}/**/*.js', - '!<%%= yeoman.client %>/{app,components}/**/*.spec.js', - '!<%%= yeoman.client %>/{app,components}/**/*.mock.js' - ], + all: ['<%%= yeoman.client %>/{app,components}/**/!(*.spec|*.mock).js'], test: { - src: [ - '<%%= yeoman.client %>/{app,components}/**/*.spec.js', - '<%%= yeoman.client %>/{app,components}/**/*.mock.js' - ] + src: ['<%%= yeoman.client %>/{app,components}/**/*.{spec,mock}.js'] + } + }, + + jscs: { + options: { + config: ".jscs.json" + }, + main: { + files: { + src: [ + '<%%= yeoman.client %>/app/**/*.js', + 'server/**/*.js' + ] + } } }, @@ -208,10 +186,7 @@ module.exports = function (grunt) { dot: true, src: [ '.tmp', - '<%%= yeoman.dist %>/*', - '!<%%= yeoman.dist %>/.git*', - '!<%%= yeoman.dist %>/.openshift', - '!<%%= yeoman.dist %>/Procfile' + '<%%= yeoman.dist %>/!(.git*|.openshift|Procfile)**' ] }] }, @@ -219,9 +194,12 @@ module.exports = function (grunt) { }, // Add vendor prefixed styles - autoprefixer: { + postcss: { options: { - browsers: ['last 1 version'] + map: true, + processors: [ + require('autoprefixer-core')({browsers: ['last 1 version']}) + ] }, dist: { files: [{ @@ -245,7 +223,7 @@ module.exports = function (grunt) { // Use nodemon to run server in debug mode with an initial breakpoint nodemon: { debug: { - script: 'server/app.js', + script: 'server', options: { nodeArgs: ['--debug-brk'], env: { @@ -267,26 +245,36 @@ module.exports = function (grunt) { } }, - // Automatically inject Bower components into the app + // Automatically inject Bower components into the app and karma.conf.js wiredep: { - target: { + options: { + exclude: [ + /bootstrap.js/, + '/json3/', + '/es5-shim/'<% if(!filters.css) { %>, + /font-awesome\.css/<% if(filters.bootstrap) { %>, + /bootstrap\.css/<% if(filters.sass) { %>, + /bootstrap-sass-official/<% }}} %> + ] + }, + client: { src: '<%%= yeoman.client %>/index.html', ignorePath: '<%%= yeoman.client %>/', - exclude: [/bootstrap-sass-official/, /bootstrap.js/, '/json3/', '/es5-shim/'<% if(!filters.css) { %>, /bootstrap.css/, /font-awesome.css/ <% } %>] + }, + test: { + src: './karma.conf.js', + devDependencies: true } }, // Renames files for browser caching purposes - rev: { + filerev: { dist: { - files: { - src: [ - '<%%= yeoman.dist %>/public/{,*/}*.js', - '<%%= yeoman.dist %>/public/{,*/}*.css', - '<%%= yeoman.dist %>/public/assets/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', - '<%%= yeoman.dist %>/public/assets/fonts/*' - ] - } + src: [ + '<%%= yeoman.dist %>/client/!(bower_components){,*/}*.{js,css}', + '<%%= yeoman.dist %>/client/assets/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', + '<%%= yeoman.dist %>/client/assets/fonts/*' + ] } }, @@ -296,19 +284,19 @@ module.exports = function (grunt) { useminPrepare: { html: ['<%%= yeoman.client %>/index.html'], options: { - dest: '<%%= yeoman.dist %>/public' + dest: '<%%= yeoman.dist %>/client' } }, // Performs rewrites based on rev and the useminPrepare configuration usemin: { - html: ['<%%= yeoman.dist %>/public/{,*/}*.html'], - css: ['<%%= yeoman.dist %>/public/{,*/}*.css'], - js: ['<%%= yeoman.dist %>/public/{,*/}*.js'], + html: ['<%%= yeoman.dist %>/client/!(bower_components){,*/}*.html'], + css: ['<%%= yeoman.dist %>/client/!(bower_components){,*/}*.css'], + js: ['<%%= yeoman.dist %>/client/!(bower_components){,*/}*.js'], options: { assetsDirs: [ - '<%%= yeoman.dist %>/public', - '<%%= yeoman.dist %>/public/assets/images' + '<%%= yeoman.dist %>/client', + '<%%= yeoman.dist %>/client/assets/images' ], // This is so we update image references in our ng-templates patterns: { @@ -325,19 +313,8 @@ module.exports = function (grunt) { files: [{ expand: true, cwd: '<%%= yeoman.client %>/assets/images', - src: '{,*/}*.{png,jpg,jpeg,gif}', - dest: '<%%= yeoman.dist %>/public/assets/images' - }] - } - }, - - svgmin: { - dist: { - files: [{ - expand: true, - cwd: '<%%= yeoman.client %>/assets/images', - src: '{,*/}*.svg', - dest: '<%%= yeoman.dist %>/public/assets/images' + src: '{,*/}*.{png,jpg,jpeg,gif,svg}', + dest: '<%%= yeoman.dist %>/client/assets/images' }] } }, @@ -386,7 +363,7 @@ module.exports = function (grunt) { // Replace Google CDN references cdnify: { dist: { - html: ['<%%= yeoman.dist %>/public/*.html'] + html: ['<%%= yeoman.dist %>/client/*.html'] } }, @@ -397,7 +374,7 @@ module.exports = function (grunt) { expand: true, dot: true, cwd: '<%%= yeoman.client %>', - dest: '<%%= yeoman.dist %>/public', + dest: '<%%= yeoman.dist %>/client', src: [ '*.{ico,png,txt}', '.htaccess', @@ -409,7 +386,7 @@ module.exports = function (grunt) { }, { expand: true, cwd: '.tmp/images', - dest: '<%%= yeoman.dist %>/public/assets/images', + dest: '<%%= yeoman.dist %>/client/assets/images', src: ['generated/*'] }, { expand: true, @@ -453,19 +430,19 @@ module.exports = function (grunt) { // Run some tasks in parallel to speed up the build process concurrent: { server: [<% if(filters.coffee) { %> - 'coffee',<% } %><% if(filters.babel) { %> - 'babel',<% } %><% if(filters.jade) { %> - 'jade',<% } %><% if(filters.stylus) { %> - 'stylus',<% } %><% if(filters.sass) { %> - 'sass',<% } %><% if(filters.less) { %> + 'coffee',<% } if(filters.babel) { %> + 'newer:babel:client',<% } if(filters.jade) { %> + 'jade',<% } if(filters.stylus) { %> + 'stylus',<% } if(filters.sass) { %> + 'sass',<% } if(filters.less) { %> 'less',<% } %> ], test: [<% if(filters.coffee) { %> - 'coffee',<% } %><% if(filters.babel) { %> - 'babel',<% } %><% if(filters.jade) { %> - 'jade',<% } %><% if(filters.stylus) { %> - 'stylus',<% } %><% if(filters.sass) { %> - 'sass',<% } %><% if(filters.less) { %> + 'coffee',<% } if(filters.babel) { %> + 'newer:babel:client',<% } if(filters.jade) { %> + 'jade',<% } if(filters.stylus) { %> + 'stylus',<% } if(filters.sass) { %> + 'sass',<% } if(filters.less) { %> 'less',<% } %> ], debug: { @@ -478,14 +455,13 @@ module.exports = function (grunt) { } }, dist: [<% if(filters.coffee) { %> - 'coffee',<% } %><% if(filters.babel) { %> - 'babel',<% } %><% if(filters.jade) { %> - 'jade',<% } %><% if(filters.stylus) { %> - 'stylus',<% } %><% if(filters.sass) { %> - 'sass',<% } %><% if(filters.less) { %> + 'coffee',<% } if(filters.babel) { %> + 'newer:babel:client',<% } if(filters.jade) { %> + 'jade',<% } if(filters.stylus) { %> + 'stylus',<% } if(filters.sass) { %> + 'sass',<% } if(filters.less) { %> 'less',<% } %> - 'imagemin', - 'svgmin' + 'imagemin' ] }, @@ -499,9 +475,53 @@ module.exports = function (grunt) { mochaTest: { options: { - reporter: 'spec' + reporter: 'spec', + require: 'mocha.conf.js', + timeout: 5000 // set default mocha spec timeout + }, + unit: { + src: ['server/**/*.spec.js'] }, - src: ['server/**/*.spec.js'] + integration: { + src: ['server/**/*.integration.js'] + } + }, + + mocha_istanbul: { + unit: { + options: { + excludes: ['**/*.{spec,mock,integration}.js'], + reporter: 'spec', + require: ['mocha.conf.js'], + mask: '**/*.spec.js', + coverageFolder: 'coverage/server/unit' + }, + src: 'server' + }, + integration: { + options: { + excludes: ['**/*.{spec,mock,integration}.js'], + reporter: 'spec', + require: ['mocha.conf.js'], + mask: '**/*.integration.js', + coverageFolder: 'coverage/server/integration' + }, + src: 'server' + } + }, + + istanbul_check_coverage: { + default: { + options: { + coverageFolder: 'coverage/**', + check: { + lines: 80, + statements: 80, + branches: 80, + functions: 80 + } + } + } }, protractor: { @@ -525,7 +545,7 @@ module.exports = function (grunt) { NODE_ENV: 'production' }, all: localConfig - },<% if(filters.jade) { %> + },<% if (filters.jade) { %> // Compiles Jade to html jade: { @@ -538,14 +558,12 @@ module.exports = function (grunt) { files: [{ expand: true, cwd: '<%%= yeoman.client %>', - src: [ - '{app,components}/**/*.jade' - ], + src: ['{app,components}/**/*.jade'], dest: '.tmp', ext: '.html' }] } - },<% } %><% if(filters.coffee) { %> + },<% } if (filters.coffee) { %> // Compiles CoffeeScript to JavaScript coffee: { @@ -557,82 +575,59 @@ module.exports = function (grunt) { files: [{ expand: true, cwd: 'client', - src: [ - '{app,components}/**/*.coffee', - '!{app,components}/**/*.spec.coffee' - ], + src: ['{app,components}/**/!(*.spec).coffee'], dest: '.tmp', ext: '.js' }] } - },<% } %><% if(filters.babel) { %> + },<% } if(filters.babel) { %> // Compiles ES6 to JavaScript using Babel babel: { - options: { + options: { sourceMap: true }, - server: { + client: { files: [{ expand: true, - cwd: 'client', - src: [ - '{app,components}/**/*.js', - '!{app,components}/**/*.spec.js' - ], + cwd: '<%%= yeoman.client %>', + src: ['{app,components}/**/!(*.spec).js'], dest: '.tmp' }] } - },<% } %><% if(filters.stylus) { %> + },<% } if(filters.stylus) { %> // Compiles Stylus to CSS stylus: { server: { options: { - paths: [ - '<%%= yeoman.client %>/bower_components', - '<%%= yeoman.client %>/app', - '<%%= yeoman.client %>/components' - ], "include css": true }, files: { '.tmp/app/app.css' : '<%%= yeoman.client %>/app/app.styl' } } - },<% } %><% if(filters.sass) { %> + },<% } if (filters.sass) { %> // Compiles Sass to CSS sass: { server: { options: { - loadPath: [ - '<%%= yeoman.client %>/bower_components', - '<%%= yeoman.client %>/app', - '<%%= yeoman.client %>/components' - ], compass: false }, files: { '.tmp/app/app.css' : '<%%= yeoman.client %>/app/app.scss' } } - },<% } %><% if(filters.less) { %> + },<% } if (filters.less) { %> // Compiles Less to CSS less: { - options: { - paths: [ - '<%%= yeoman.client %>/bower_components', - '<%%= yeoman.client %>/app', - '<%%= yeoman.client %>/components' - ] - }, server: { files: { '.tmp/app/app.css' : '<%%= yeoman.client %>/app/app.less' } - }, + } },<% } %> injector: { @@ -652,26 +647,21 @@ module.exports = function (grunt) { }, files: { '<%%= yeoman.client %>/index.html': [ - [ - <% if(filters.babel) { %> - '.tmp/{app,components}/**/*.js', - <% } else { %> - '{.tmp,<%%= yeoman.client %>}/{app,components}/**/*.js', - <% } %> - '!{.tmp,<%%= yeoman.client %>}/app/app.js', - '!{.tmp,<%%= yeoman.client %>}/{app,components}/**/*.spec.js', - '!{.tmp,<%%= yeoman.client %>}/{app,components}/**/*.mock.js' + [<% if(filters.babel) { %> + '.tmp/{app,components}/**/!(*.spec|*.mock).js',<% } else { %> + '{.tmp,<%%= yeoman.client %>}/{app,components}/**/!(*.spec|*.mock).js',<% } %> + '!{.tmp,<%%= yeoman.client %>}/app/app.js' ] ] } - },<% if(filters.stylus) { %> + },<% if (filters.stylus) { %> // Inject component styl into app.styl stylus: { options: { transform: function(filePath) { filePath = filePath.replace('/client/app/', ''); - filePath = filePath.replace('/client/components/', ''); + filePath = filePath.replace('/client/components/', '../components/'); return '@import \'' + filePath + '\';'; }, starttag: '// injector', @@ -683,14 +673,14 @@ module.exports = function (grunt) { '!<%%= yeoman.client %>/app/app.styl' ] } - },<% } %><% if(filters.sass) { %> + },<% } if (filters.sass) { %> // Inject component scss into app.scss sass: { options: { transform: function(filePath) { filePath = filePath.replace('/client/app/', ''); - filePath = filePath.replace('/client/components/', ''); + filePath = filePath.replace('/client/components/', '../components/'); return '@import \'' + filePath + '\';'; }, starttag: '// injector', @@ -702,14 +692,14 @@ module.exports = function (grunt) { '!<%%= yeoman.client %>/app/app.{scss,sass}' ] } - },<% } %><% if(filters.less) { %> + },<% } if (filters.less) { %> // Inject component less into app.less less: { options: { transform: function(filePath) { filePath = filePath.replace('/client/app/', ''); - filePath = filePath.replace('/client/components/', ''); + filePath = filePath.replace('/client/components/', '../components/'); return '@import \'' + filePath + '\';'; }, starttag: '// injector', @@ -767,28 +757,28 @@ module.exports = function (grunt) { if (target === 'debug') { return grunt.task.run([ 'clean:server', - 'env:all',<% if(filters.stylus) { %> - 'injector:stylus', <% } %><% if(filters.less) { %> - 'injector:less', <% } %><% if(filters.sass) { %> - 'injector:sass', <% } %> + 'env:all',<% if (filters.stylus) { %> + 'injector:stylus',<% } if (filters.less) { %> + 'injector:less',<% } if (filters.sass) { %> + 'injector:sass',<% } %> 'concurrent:server', 'injector', - 'wiredep', - 'autoprefixer', + 'wiredep:client', + 'postcss', 'concurrent:debug' ]); } grunt.task.run([ 'clean:server', - 'env:all',<% if(filters.stylus) { %> - 'injector:stylus', <% } %><% if(filters.less) { %> - 'injector:less', <% } %><% if(filters.sass) { %> - 'injector:sass', <% } %> + 'env:all',<% if (filters.stylus) { %> + 'injector:stylus',<% } if (filters.less) { %> + 'injector:less',<% } if (filters.sass) { %> + 'injector:sass',<% } %> 'concurrent:server', 'injector', - 'wiredep', - 'autoprefixer', + 'wiredep:client', + 'postcss', 'express:dev', 'wait', 'open', @@ -801,44 +791,94 @@ 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', 'env:test', - 'mochaTest' + 'mochaTest:unit', + 'mochaTest:integration' ]); } else if (target === 'client') { return grunt.task.run([ 'clean:server', - 'env:all',<% if(filters.stylus) { %> - 'injector:stylus', <% } %><% if(filters.less) { %> - 'injector:less', <% } %><% if(filters.sass) { %> - 'injector:sass', <% } %> + 'env:all',<% if (filters.stylus) { %> + 'injector:stylus',<% } if (filters.less) { %> + 'injector:less',<% } if (filters.sass) { %> + 'injector:sass',<% } %> 'concurrent:test', 'injector', - 'autoprefixer', + 'postcss', + 'wiredep:test', 'karma' ]); } else if (target === 'e2e') { - return grunt.task.run([ - 'clean:server', - 'env:all', - 'env:test',<% if(filters.stylus) { %> - 'injector:stylus', <% } %><% if(filters.less) { %> - 'injector:less', <% } %><% if(filters.sass) { %> - 'injector:sass', <% } %> - 'concurrent:test', - 'injector', - 'wiredep', - 'autoprefixer', - 'express:dev', - 'protractor' - ]); + + if (option === 'prod') { + return grunt.task.run([ + 'build', + 'env:all', + 'env:prod', + 'express:prod', + 'protractor' + ]); + } + + else { + return grunt.task.run([ + 'clean:server', + 'env:all', + 'env:test',<% if (filters.stylus) { %> + 'injector:stylus',<% } if (filters.less) { %> + 'injector:less',<% } if (filters.sass) { %> + 'injector:sass',<% } %> + 'concurrent:test', + 'injector', + 'wiredep:client', + 'postcss', + 'express:dev', + 'protractor' + ]); + } + } + + else if (target === 'coverage') { + + if (option === 'unit') { + return grunt.task.run([ + 'env:all', + 'env:test', + 'mocha_istanbul:unit' + ]); + } + + else if (option === 'integration') { + return grunt.task.run([ + 'env:all', + 'env:test', + 'mocha_istanbul:integration' + ]); + } + + 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([ @@ -848,15 +888,15 @@ module.exports = function (grunt) { }); grunt.registerTask('build', [ - 'clean:dist',<% if(filters.stylus) { %> - 'injector:stylus', <% } %><% if(filters.less) { %> - 'injector:less', <% } %><% if(filters.sass) { %> - 'injector:sass', <% } %> + 'clean:dist',<% if (filters.stylus) { %> + 'injector:stylus',<% } if (filters.less) { %> + 'injector:less',<% } if (filters.sass) { %> + 'injector:sass',<% } %> 'concurrent:dist', 'injector', - 'wiredep', + 'wiredep:client', 'useminPrepare', - 'autoprefixer', + 'postcss', 'ngtemplates', 'concat', 'ngAnnotate', @@ -864,7 +904,7 @@ module.exports = function (grunt) { 'cdnify', 'cssmin', 'uglify', - 'rev', + 'filerev', 'usemin' ]); diff --git a/app/templates/README.md b/app/templates/README.md new file mode 100644 index 000000000..958834120 --- /dev/null +++ b/app/templates/README.md @@ -0,0 +1,34 @@ +# <%= _.slugify(_.humanize(appname)) %> + +This project was generated with the [Angular Full-Stack Generator](https://github.com/DaftMonk/generator-angular-fullstack) version <%= pkg.version %>. + +## Getting Started + +### Prerequisites + +- [Git](https://git-scm.com/) +- [Node.js and NPM](nodejs.org) >= v0.12.0 +- [Bower](bower.io) (`npm install --global bower`)<% if(filters.sass) { %> +- [Ruby](https://www.ruby-lang.org) and then `gem install sass`<% } if(filters.grunt) { %> +- [Grunt](http://gruntjs.com/) (`npm install --global grunt-cli`)<% } if(filters.gulp) { %> +- [Gulp](http://gulpjs.com/) (`npm install --global gulp`)<% } if(filters.mongoose) { %> +- [MongoDB](https://www.mongodb.org/) - Keep a running daemon with `mongod`<% } if(filters.sequelize) { %> +- [SQLite](https://www.sqlite.org/quickstart.html)<% } %> + +### Developing<% var i = 1; %> + +<%= i++ %>. Run `npm install` to install server dependencies. + +<%= i++ %>. Run `bower install` to install front-end dependencies.<% if(filters.mongoose) { %> + +<%= i++ %>. Run `mongod` in a separate shell to keep an instance of the MongoDB Daemon running<% } %> + +<%= i++ %>. Run <% if(filters.grunt) { %>`grunt serve`<% } if(filters.grunt && filters.gulp) { %> or <% } if(filters.gulp) { %>`gulp serve`<% } %> to start the development server. It should automatically open the client in your browser when ready. + +## Build & development + +Run `grunt build` for building and `grunt serve` for preview. + +## Testing + +Running `npm test` will run the unit tests with karma. diff --git a/app/templates/_.gitignore b/app/templates/_.gitignore index a5f8174b5..d5ae65fb7 100644 --- a/app/templates/_.gitignore +++ b/app/templates/_.gitignore @@ -1,6 +1,6 @@ node_modules public -.tmp<% if(filters.sass) { %> +.tmp<% if (filters.sass) { %> .sass-cache<% } %> .idea client/bower_components diff --git a/app/templates/_bower.json b/app/templates/_bower.json index 156d04b32..1d41b39d1 100644 --- a/app/templates/_bower.json +++ b/app/templates/_bower.json @@ -2,23 +2,22 @@ "name": "<%= _.slugify(_.humanize(appname)) %>", "version": "0.0.0", "dependencies": { - "angular": ">=1.2.*", + "angular": "~1.4.0", "json3": "~3.3.1", "es5-shim": "~3.0.1",<% if(filters.bootstrap) { %><% if (filters.sass) { %> "bootstrap-sass-official": "~3.1.1",<% } %> "bootstrap": "~3.1.1",<% } %> - "angular-resource": ">=1.2.*", - "angular-cookies": ">=1.2.*", - "angular-sanitize": ">=1.2.*",<% if(filters.ngroute) { %> - "angular-route": ">=1.2.*",<% } %><% if(filters.uibootstrap) { %> - "angular-bootstrap": "~0.11.0",<% } %> + "angular-resource": "~1.4.0", + "angular-cookies": "~1.4.0", + "angular-sanitize": "~1.4.0",<% if (filters.ngroute) { %> + "angular-route": "~1.4.0",<% } %><% if (filters.uibootstrap) { %> + "angular-bootstrap": "~0.13.0",<% } %> "font-awesome": ">=4.1.0", "lodash": "~2.4.1"<% if(filters.socketio) { %>, - "angular-socket-io": "~0.6.0"<% } %><% if(filters.uirouter) { %>, + "angular-socket-io": "~0.7.0"<% } %><% if(filters.uirouter) { %>, "angular-ui-router": "~0.2.15"<% } %> }, "devDependencies": { - "angular-mocks": ">=1.2.*", - "angular-scenario": ">=1.2.*" + "angular-mocks": "~1.4.0" } } diff --git a/app/templates/_package.json b/app/templates/_package.json index 88541b924..53b4e9824 100644 --- a/app/templates/_package.json +++ b/app/templates/_package.json @@ -7,94 +7,110 @@ "morgan": "~1.0.0", "body-parser": "~1.5.0", "method-override": "~1.0.0", - "serve-favicon": "~2.0.1", "cookie-parser": "~1.0.1", "express-session": "~1.0.2", "errorhandler": "~1.0.0", "compression": "~1.0.1", - "lodash": "~2.4.1",<% if(filters.jade) { %> - "jade": "~1.2.0",<% } %><% if(filters.html) { %> - "ejs": "~0.8.4",<% } %><% if(filters.mongoose) { %> - "mongoose": "~4.0.3",<% } %><% if(filters.auth) { %> + "composable-middleware": "^0.3.0", + "lodash": "~2.4.1", + "babel-core": "^5.6.4",<% if (filters.jade) { %> + "jade": "~1.2.0",<% } %><% if (filters.html) { %> + "ejs": "~0.8.4",<% } %><% if (filters.mongoose) { %> + "mongoose": "^4.1.2", + "bluebird": "^2.9.34", + "connect-mongo": "^0.8.1",<% } %><% if (filters.sequelize) { %> + "sequelize": "^3.5.1", + "sqlite3": "~3.0.2", + "express-sequelize-session": "0.4.0",<% } %><% if (filters.auth) { %> "jsonwebtoken": "^5.0.0", "express-jwt": "^3.0.0", "passport": "~0.2.0", - "passport-local": "~0.1.6",<% } %><% if(filters.facebookAuth) { %> - "passport-facebook": "latest",<% } %><% if(filters.twitterAuth) { %> - "passport-twitter": "latest",<% } %><% if(filters.googleAuth) { %> - "passport-google-oauth": "latest",<% } %> - "composable-middleware": "^0.3.0", - "connect-mongo": "^0.8.1"<% if(filters.socketio) { %>, - "socket.io": "^1.0.6", - "socket.io-client": "^1.0.6", - "socketio-jwt": "^3.0.0"<% } %> + "passport-local": "~0.1.6",<% } %><% if (filters.facebookAuth) { %> + "passport-facebook": "latest",<% } %><% if (filters.twitterAuth) { %> + "passport-twitter": "latest",<% } %><% if (filters.googleAuth) { %> + "passport-google-oauth": "latest",<% } %><% if (filters.socketio) { %> + "socket.io": "^1.3.5", + "socket.io-client": "^1.3.5", + "socketio-jwt": "^4.2.0",<% } %> + "serve-favicon": "~2.0.1" }, "devDependencies": { - "grunt": "~0.4.4", - "grunt-autoprefixer": "~0.7.2", - "grunt-wiredep": "~1.8.0", - "grunt-concurrent": "~0.5.0", - "grunt-contrib-clean": "~0.5.0", - "grunt-contrib-concat": "~0.4.0", - "grunt-contrib-copy": "~0.5.0", - "grunt-contrib-cssmin": "~0.9.0", - "grunt-contrib-htmlmin": "~0.2.0", - "grunt-contrib-imagemin": "~0.7.1", - "grunt-contrib-jshint": "~0.10.0", - "grunt-contrib-uglify": "~0.4.0", - "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",<% } %><% if(filters.babel) { %> + "autoprefixer-core": "^5.2.1", + "grunt": "~0.4.5", + "grunt-wiredep": "^2.0.0", + "grunt-concurrent": "^2.0.1", + "grunt-contrib-clean": "^0.6.0", + "grunt-contrib-concat": "^0.5.1", + "grunt-contrib-copy": "^0.8.0", + "grunt-contrib-cssmin": "^0.13.0", + "grunt-contrib-imagemin": "^0.9.4", + "grunt-contrib-jshint": "~0.11.2", + "grunt-contrib-uglify": "^0.9.1", + "grunt-contrib-watch": "~0.6.1",<% if (filters.coffee) { %> + "grunt-contrib-coffee": "^0.13.0",<% } %><% if (filters.jade) { %> + "grunt-contrib-jade": "^0.15.0",<% } %><% if (filters.less) { %> + "grunt-contrib-less": "^1.0.0",<% } %><% if(filters.babel) { %> "karma-babel-preprocessor": "^5.2.1", "grunt-babel": "~5.0.0",<% } %> "grunt-google-cdn": "~0.4.0", - "grunt-newer": "~0.7.0", - "grunt-ng-annotate": "^0.2.3", - "grunt-rev": "~0.1.0", - "grunt-svgmin": "~0.4.0", - "grunt-usemin": "~2.1.1", + "grunt-jscs": "^2.0.0", + "grunt-newer": "^1.1.1", + "grunt-ng-annotate": "^1.0.1", + "grunt-filerev": "^2.3.1", + "grunt-usemin": "^3.0.0", "grunt-env": "~0.4.1", - "grunt-node-inspector": "~0.1.5", - "grunt-nodemon": "~0.2.0", + "grunt-node-inspector": "^0.2.0", + "grunt-nodemon": "^0.4.0", "grunt-angular-templates": "^0.5.4", "grunt-dom-munger": "^3.4.0", - "grunt-protractor-runner": "^1.1.0", - "grunt-injector": "~0.5.4", - "grunt-karma": "~0.8.2", - "grunt-build-control": "~0.4.0", - "grunt-mocha-test": "~0.10.2",<% if(filters.sass) { %> - "grunt-contrib-sass": "^0.7.3",<% } %><% if(filters.stylus) { %> + "grunt-protractor-runner": "^2.0.0", + "grunt-injector": "^0.6.0", + "grunt-karma": "~0.12.0", + "grunt-build-control": "^0.5.0",<% if(filters.sass) { %> + "grunt-contrib-sass": "^0.9.0",<% } %><% if(filters.stylus) { %> "grunt-contrib-stylus": "latest",<% } %> - "jit-grunt": "^0.5.0", - "time-grunt": "~0.3.1", - "grunt-express-server": "~0.4.17", + "jit-grunt": "^0.9.1", + "time-grunt": "^1.2.1", + "grunt-express-server": "^0.5.1", + "grunt-postcss": "^0.5.5", "grunt-open": "~0.2.3", "open": "~0.0.4", - "jshint-stylish": "~0.1.5", - "connect-livereload": "~0.4.0", + "jshint-stylish": "~2.0.1", + "connect-livereload": "^0.5.3", + "mocha": "^2.2.5", + "grunt-mocha-test": "~0.12.7", + "grunt-mocha-istanbul": "^3.0.1", + "istanbul": "^0.3.17", + "chai-as-promised": "^5.1.0", + "chai-things": "^0.2.0", + "sinon-chai": "^2.8.0",<% if (filters.mocha) { %> + "karma-mocha": "^0.2.0", + "karma-chai-plugins": "^0.6.0",<% } if (filters.jasmine) { %> + "jasmine-core": "^2.3.4", + "karma-jasmine": "~0.3.0", + "jasmine-spec-reporter": "^2.4.0",<% } %> "karma-ng-scenario": "~0.1.0", - "karma-firefox-launcher": "~0.1.3", + "karma-firefox-launcher": "~0.1.6", "karma-script-launcher": "~0.1.0", "karma-html2js-preprocessor": "~0.1.0", - "karma-ng-jade2js-preprocessor": "^0.1.2", - "karma-jasmine": "~0.1.5", - "karma-chrome-launcher": "~0.1.3", + "karma-ng-jade2js-preprocessor": "^0.2.0", + "karma-chrome-launcher": "~0.2.0", "requirejs": "~2.1.11", - "karma-requirejs": "~0.2.1", - "karma-coffee-preprocessor": "~0.2.1", + "karma-requirejs": "~0.2.2", + "karma-coffee-preprocessor": "~0.3.0", "karma-jade-preprocessor": "0.0.11", - "karma-phantomjs-launcher": "~0.1.4", - "karma": "~0.12.9", - "karma-ng-html2js-preprocessor": "~0.1.0", - "supertest": "~0.11.0", - "should": "~3.3.1" + "karma-phantomjs-launcher": "~0.2.0", + "karma": "~0.13.3", + "karma-ng-html2js-preprocessor": "~0.1.2", + "karma-spec-reporter": "~0.0.20", + "proxyquire": "^1.0.1", + "supertest": "~0.11.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.12.0" }, "scripts": { - "start": "node server/app.js", + "start": "node server", "test": "grunt test", "update-webdriver": "node node_modules/grunt-protractor-runner/node_modules/protractor/bin/webdriver-manager update" }, diff --git a/app/templates/client/app/account(auth)/account(coffee).coffee b/app/templates/client/app/account(auth)/account(coffee).coffee index 2b7b8b23b..c794d7f04 100644 --- a/app/templates/client/app/account(auth)/account(coffee).coffee +++ b/app/templates/client/app/account(auth)/account(coffee).coffee @@ -1,12 +1,20 @@ 'use strict' angular.module '<%= scriptAppName %>' -<% if(filters.ngroute) { %>.config ($routeProvider) -> +<% if (filters.ngroute) { %>.config ($routeProvider) -> $routeProvider .when '/login', 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,13 +23,25 @@ angular.module '<%= scriptAppName %>' templateUrl: 'app/account/settings/settings.html' controller: 'SettingsCtrl' authenticate: true -<% } %><% if(filters.uirouter) { %>.config ($stateProvider) -> + +.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', url: '/login' 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..d60fd72fe 100644 --- a/app/templates/client/app/account(auth)/account(js).js +++ b/app/templates/client/app/account(auth)/account(js).js @@ -1,12 +1,24 @@ 'use strict'; angular.module('<%= scriptAppName %>') - <% if(filters.ngroute) { %>.config(function ($routeProvider) { + <% if (filters.ngroute) { %>.config(function($routeProvider) { $routeProvider .when('/login', { templateUrl: 'app/account/login/login.html', controller: 'LoginCtrl' }) + .when('/logout', { + name: 'logout', + referrer: '/', + template: '', + 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,13 +28,32 @@ angular.module('<%= scriptAppName %>') controller: 'SettingsCtrl', authenticate: true }); - });<% } %><% if(filters.uirouter) { %>.config(function ($stateProvider) { + }) + .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', { url: '/login', templateUrl: 'app/account/login/login.html', controller: 'LoginCtrl' }) + .state('logout', { + url: '/logout?referrer', + referrer: 'main', + template: '', + 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 +65,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..667ecdd0e 100644 --- a/app/templates/client/app/account(auth)/login/login(html).html +++ b/app/templates/client/app/account(auth)/login/login(html).html @@ -1,4 +1,4 @@ -<div ng-include="'components/navbar/navbar.html'"></div> +<navbar></navbar> <div class="container"> <div class="row"> @@ -37,19 +37,19 @@ <h1>Login</h1> <button class="btn btn-inverse btn-lg btn-login" type="submit"> Login </button> - <a class="btn btn-default btn-lg btn-register" href="/signup"> + <a class="btn btn-default btn-lg btn-register" <% if (filters.uirouter) { %>ui-sref="signup"<% } else { %>href="/signup"<% } %>> Register </a> </div> -<% if(filters.oauth) {%> +<% if (filters.oauth) {%> <hr> - <div><% if(filters.facebookAuth) {%> + <div><% if (filters.facebookAuth) {%> <a class="btn btn-facebook" href="" ng-click="loginOauth('facebook')"> <i class="fa fa-facebook"></i> Connect with Facebook - </a><% } %><% if(filters.googleAuth) {%> + </a><% } %><% if (filters.googleAuth) {%> <a class="btn btn-google-plus" href="" ng-click="loginOauth('google')"> <i class="fa fa-google-plus"></i> Connect with Google+ - </a><% } %><% if(filters.twitterAuth) {%> + </a><% } %><% if (filters.twitterAuth) {%> <a class="btn btn-twitter" href="" ng-click="loginOauth('twitter')"> <i class="fa fa-twitter"></i> Connect with Twitter </a><% } %> 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..e7ce91916 100644 --- a/app/templates/client/app/account(auth)/login/login(jade).jade +++ b/app/templates/client/app/account(auth)/login/login(jade).jade @@ -1,4 +1,4 @@ -div(ng-include='"components/navbar/navbar.html"') +navbar .container .row .col-sm-12 @@ -20,7 +20,7 @@ div(ng-include='"components/navbar/navbar.html"') form.form(name='form', ng-submit='login(form)', novalidate='') .form-group label Email - input.form-control(type='text', name='email', ng-model='user.email') + input.form-control(type='email', name='email', ng-model='user.email') .form-group label Password input.form-control(type='password', name='password', ng-model='user.password') @@ -34,20 +34,20 @@ 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) {%> +<% if (filters.oauth) {%> hr - div<% if(filters.facebookAuth) {%> + div<% if (filters.facebookAuth) {%> a.btn.btn-facebook(href='', ng-click='loginOauth("facebook")') i.fa.fa-facebook | Connect with Facebook - = ' '<% } %><% if(filters.googleAuth) {%> + = ' '<% } %><% if (filters.googleAuth) {%> a.btn.btn-google-plus(href='', ng-click='loginOauth("google")') i.fa.fa-google-plus | Connect with Google+ - = ' '<% } %><% if(filters.twitterAuth) {%> + = ' '<% } %><% if (filters.twitterAuth) {%> a.btn.btn-twitter(href='', ng-click='loginOauth("twitter")') i.fa.fa-twitter | Connect with Twitter<% } %><% } %> diff --git a/app/templates/client/app/account(auth)/login/login(less).less b/app/templates/client/app/account(auth)/login/login(less).less index bd01a056e..6eaecd90c 100644 --- a/app/templates/client/app/account(auth)/login/login(less).less +++ b/app/templates/client/app/account(auth)/login/login(less).less @@ -1,4 +1,4 @@ -<% if(filters.bootstrap) { %>// Colors +<% if (filters.bootstrap) { %>// Colors // -------------------------------------------------- @btnText: #fff; diff --git a/app/templates/client/app/account(auth)/login/login(sass).scss b/app/templates/client/app/account(auth)/login/login(sass).scss index eb214a8ca..5b6956124 100644 --- a/app/templates/client/app/account(auth)/login/login(sass).scss +++ b/app/templates/client/app/account(auth)/login/login(sass).scss @@ -1,4 +1,4 @@ -<% if(filters.bootstrap) { %>// Colors +<% if (filters.bootstrap) { %>// Colors // -------------------------------------------------- $btnText: #fff; 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..7bcb69969 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,10 +14,10 @@ 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 -<% if(filters.oauth) {%> +<% if (filters.oauth) {%> $scope.loginOauth = (provider) -> $window.location.href = '/auth/' + provider<% } %> 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..2417e62f4 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,28 +1,28 @@ '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 = {}; $scope.login = function(form) { $scope.submitted = true; - if(form.$valid) { + if (form.$valid) { Auth.login({ email: $scope.user.email, password: $scope.user.password }) - .then( function() { + .then(function() { // Logged in, redirect to home - $location.path('/'); + <% if (filters.ngroute) { %>$location.path('/');<% } %><% if (filters.uirouter) { %>$state.go('main');<% } %> }) - .catch( function(err) { + .catch(function(err) { $scope.errors.other = err.message; }); } }; -<% if(filters.oauth) {%> +<% if (filters.oauth) {%> $scope.loginOauth = function(provider) { $window.location.href = '/auth/' + provider; };<% } %> diff --git a/app/templates/client/app/account(auth)/settings/settings(html).html b/app/templates/client/app/account(auth)/settings/settings(html).html index bb5d8ded0..ec4e2d820 100644 --- a/app/templates/client/app/account(auth)/settings/settings(html).html +++ b/app/templates/client/app/account(auth)/settings/settings(html).html @@ -1,4 +1,4 @@ -<div ng-include="'components/navbar/navbar.html'"></div> +<navbar></navbar> <div class="container"> <div class="row"> @@ -36,4 +36,4 @@ <h1>Change Password</h1> </form> </div> </div> -</div> \ No newline at end of file +</div> diff --git a/app/templates/client/app/account(auth)/settings/settings(jade).jade b/app/templates/client/app/account(auth)/settings/settings(jade).jade index 2dc55d402..96340b522 100644 --- a/app/templates/client/app/account(auth)/settings/settings(jade).jade +++ b/app/templates/client/app/account(auth)/settings/settings(jade).jade @@ -1,4 +1,4 @@ -div(ng-include='"components/navbar/navbar.html"') +navbar .container .row .col-sm-12 diff --git a/app/templates/client/app/account(auth)/settings/settings.controller(js).js b/app/templates/client/app/account(auth)/settings/settings.controller(js).js index 829bd8248..eeb1219cf 100644 --- a/app/templates/client/app/account(auth)/settings/settings.controller(js).js +++ b/app/templates/client/app/account(auth)/settings/settings.controller(js).js @@ -1,21 +1,21 @@ 'use strict'; angular.module('<%= scriptAppName %>') - .controller('SettingsCtrl', function ($scope, User, Auth) { + .controller('SettingsCtrl', function($scope, User, Auth) { $scope.errors = {}; $scope.changePassword = function(form) { $scope.submitted = true; - if(form.$valid) { - Auth.changePassword( $scope.user.oldPassword, $scope.user.newPassword ) - .then( function() { - $scope.message = 'Password successfully changed.'; - }) - .catch( function() { - form.password.$setValidity('mongoose', false); - $scope.errors.other = 'Incorrect password'; - $scope.message = ''; - }); + if (form.$valid) { + Auth.changePassword($scope.user.oldPassword, $scope.user.newPassword) + .then(function() { + $scope.message = 'Password successfully changed.'; + }) + .catch(function() { + form.password.$setValidity('mongoose', false); + $scope.errors.other = 'Incorrect password'; + $scope.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..5052a02ee 100644 --- a/app/templates/client/app/account(auth)/signup/signup(html).html +++ b/app/templates/client/app/account(auth)/signup/signup(html).html @@ -1,4 +1,4 @@ -<div ng-include="'components/navbar/navbar.html'"></div> +<navbar></navbar> <div class="container"> <div class="row"> @@ -55,22 +55,22 @@ <h1>Sign up</h1> </div> <div> - <button class="btn btn-inverse btn-lg btn-login" type="submit"> + <button class="btn btn-inverse btn-lg btn-register" type="submit"> Sign up </button> - <a class="btn btn-default btn-lg btn-register" href="/login"> + <a class="btn btn-default btn-lg btn-login" <% if (filters.uirouter) { %>ui-sref="login"<% } else { %>href="/login"<% } %>> Login </a> </div> -<% if(filters.oauth) {%> +<% if (filters.oauth) {%> <hr> - <div><% if(filters.facebookAuth) {%> + <div><% if (filters.facebookAuth) {%> <a class="btn btn-facebook" href="" ng-click="loginOauth('facebook')"> <i class="fa fa-facebook"></i> Connect with Facebook - </a><% } %><% if(filters.googleAuth) {%> + </a><% } %><% if (filters.googleAuth) {%> <a class="btn btn-google-plus" href="" ng-click="loginOauth('google')"> <i class="fa fa-google-plus"></i> Connect with Google+ - </a><% } %><% if(filters.twitterAuth) {%> + </a><% } %><% if (filters.twitterAuth) {%> <a class="btn btn-twitter" href="" ng-click="loginOauth('twitter')"> <i class="fa fa-twitter"></i> Connect with Twitter </a><% } %> 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..cca29a28e 100644 --- a/app/templates/client/app/account(auth)/signup/signup(jade).jade +++ b/app/templates/client/app/account(auth)/signup/signup(jade).jade @@ -1,4 +1,4 @@ -div(ng-include='"components/navbar/navbar.html"') +navbar .container .row .col-sm-12 @@ -33,24 +33,24 @@ div(ng-include='"components/navbar/navbar.html"') | {{ errors.password }} div - button.btn.btn-inverse.btn-lg.btn-login(type='submit') + button.btn.btn-inverse.btn-lg.btn-register(type='submit') | Sign up = ' ' - a.btn.btn-default.btn-lg.btn-register(href='/login') + a.btn.btn-default.btn-lg.btn-login(<% if (filters.uirouter) { %>ui-sref='login'<% } else { %>href='/login'<% } %>) | Login -<% if(filters.oauth) {%> +<% if (filters.oauth) {%> hr - div<% if(filters.facebookAuth) {%> + div<% if (filters.facebookAuth) {%> a.btn.btn-facebook(href='', ng-click='loginOauth("facebook")') i.fa.fa-facebook | Connect with Facebook - = ' '<% } %><% if(filters.googleAuth) {%> + = ' '<% } %><% if (filters.googleAuth) {%> a.btn.btn-google-plus(href='', ng-click='loginOauth("google")') i.fa.fa-google-plus | Connect with Google+ - = ' '<% } %><% if(filters.twitterAuth) {%> + = ' '<% } %><% if (filters.twitterAuth) {%> a.btn.btn-twitter(href='', ng-click='loginOauth("twitter")') i.fa.fa-twitter | Connect with Twitter<% } %><% } %> 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..d167b7e30 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,16 +15,22 @@ 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 $scope.errors = {} - +<% if (filters.mongooseModels) { %> # Update validity of form fields that match the mongoose errors angular.forEach err.errors, (error, field) -> form[field].$setValidity 'mongoose', false - $scope.errors[field] = error.message -<% if(filters.oauth) {%> + $scope.errors[field] = error.message<% } + if (filters.sequelizeModels) { %> + # Update validity of form fields that match the sequelize errors + if err.name + angular.forEach err.fields, (field) -> + form[field].$setValidity 'mongoose', false + $scope.errors[field] = err.message<% } %> +<% if (filters.oauth) {%> $scope.loginOauth = (provider) -> $window.location.href = '/auth/' + provider<% } %> 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..346eb7ea7 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,36 +1,44 @@ '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 = {}; $scope.register = function(form) { $scope.submitted = true; - if(form.$valid) { + if (form.$valid) { Auth.createUser({ name: $scope.user.name, email: $scope.user.email, password: $scope.user.password }) - .then( function() { + .then(function() { // Account created, redirect to home - $location.path('/'); + <% if (filters.ngroute) { %>$location.path('/');<% } %><% if (filters.uirouter) { %>$state.go('main');<% } %> }) - .catch( function(err) { + .catch(function(err) { err = err.data; $scope.errors = {}; - +<% if (filters.mongooseModels) { %> // Update validity of form fields that match the mongoose errors angular.forEach(err.errors, function(error, field) { form[field].$setValidity('mongoose', false); $scope.errors[field] = error.message; - }); + });<% } + if (filters.sequelizeModels) { %> + // Update validity of form fields that match the sequelize errors + if (err.name) { + angular.forEach(err.fields, function(field) { + form[field].$setValidity('mongoose', false); + $scope.errors[field] = err.message; + }); + }<% } %> }); } }; -<% if(filters.oauth) {%> +<% if (filters.oauth) {%> $scope.loginOauth = function(provider) { $window.location.href = '/auth/' + provider; };<% } %> diff --git a/app/templates/client/app/admin(auth)/admin(coffee).coffee b/app/templates/client/app/admin(auth)/admin(coffee).coffee index a0497445e..99b49177f 100644 --- a/app/templates/client/app/admin(auth)/admin(coffee).coffee +++ b/app/templates/client/app/admin(auth)/admin(coffee).coffee @@ -1,15 +1,15 @@ 'use strict' angular.module '<%= scriptAppName %>' -<% if(filters.ngroute) { %>.config ($routeProvider) -> +<% if (filters.ngroute) { %>.config ($routeProvider) -> $routeProvider .when '/admin', templateUrl: 'app/admin/admin.html' controller: 'AdminCtrl' -<% } %><% if(filters.uirouter) { %>.config ($stateProvider) -> +<% } %><% if (filters.uirouter) { %>.config ($stateProvider) -> $stateProvider .state 'admin', url: '/admin' templateUrl: 'app/admin/admin.html' controller: 'AdminCtrl' -<% } %> \ No newline at end of file +<% } %> diff --git a/app/templates/client/app/admin(auth)/admin(html).html b/app/templates/client/app/admin(auth)/admin(html).html index 5c27c7af2..7688c9b47 100644 --- a/app/templates/client/app/admin(auth)/admin(html).html +++ b/app/templates/client/app/admin(auth)/admin(html).html @@ -1,4 +1,4 @@ -<div ng-include="'components/navbar/navbar.html'"></div> +<navbar></navbar> <div class="container"> <p>The delete user and user index api routes are restricted to users with the 'admin' role.</p> @@ -9,4 +9,4 @@ <a ng-click="delete(user)" class="trash"><span class="glyphicon glyphicon-trash pull-right"></span></a> </li> </ul> -</div> \ No newline at end of file +</div> diff --git a/app/templates/client/app/admin(auth)/admin(jade).jade b/app/templates/client/app/admin(auth)/admin(jade).jade index fd80a0bb6..bcef64773 100644 --- a/app/templates/client/app/admin(auth)/admin(jade).jade +++ b/app/templates/client/app/admin(auth)/admin(jade).jade @@ -1,4 +1,4 @@ -div(ng-include='"components/navbar/navbar.html"') +navbar .container p | The delete user and user index api routes are restricted to users with the 'admin' role. @@ -8,4 +8,4 @@ div(ng-include='"components/navbar/navbar.html"') br span.text-muted {{user.email}} a.trash(ng-click='delete(user)') - span.glyphicon.glyphicon-trash.pull-right \ No newline at end of file + span.glyphicon.glyphicon-trash.pull-right diff --git a/app/templates/client/app/admin(auth)/admin(js).js b/app/templates/client/app/admin(auth)/admin(js).js index 270e8a974..f37ba9fcc 100644 --- a/app/templates/client/app/admin(auth)/admin(js).js +++ b/app/templates/client/app/admin(auth)/admin(js).js @@ -1,17 +1,17 @@ 'use strict'; angular.module('<%= scriptAppName %>') - <% if(filters.ngroute) { %>.config(function ($routeProvider) { + <% if (filters.ngroute) { %>.config(function($routeProvider) { $routeProvider .when('/admin', { templateUrl: 'app/admin/admin.html', controller: 'AdminCtrl' }); - });<% } %><% if(filters.uirouter) { %>.config(function ($stateProvider) { + });<% } %><% if (filters.uirouter) { %>.config(function($stateProvider) { $stateProvider .state('admin', { url: '/admin', templateUrl: 'app/admin/admin.html', controller: 'AdminCtrl' }); - });<% } %> \ No newline at end of file + });<% } %> diff --git a/app/templates/client/app/admin(auth)/admin(less).less b/app/templates/client/app/admin(auth)/admin(less).less index ad8202750..a6f536dc5 100644 --- a/app/templates/client/app/admin(auth)/admin(less).less +++ b/app/templates/client/app/admin(auth)/admin(less).less @@ -1 +1 @@ -.trash { color:rgb(209, 91, 71); } \ No newline at end of file +.trash { color:rgb(209, 91, 71); } diff --git a/app/templates/client/app/admin(auth)/admin(stylus).styl b/app/templates/client/app/admin(auth)/admin(stylus).styl index d57e50db5..d7d50a172 100644 --- a/app/templates/client/app/admin(auth)/admin(stylus).styl +++ b/app/templates/client/app/admin(auth)/admin(stylus).styl @@ -1,2 +1,2 @@ .trash - color rgb(209, 91, 71) \ No newline at end of file + color rgb(209, 91, 71) diff --git a/app/templates/client/app/admin(auth)/admin.controller(coffee).coffee b/app/templates/client/app/admin(auth)/admin.controller(coffee).coffee index 7a16032da..5183df059 100644 --- a/app/templates/client/app/admin(auth)/admin.controller(coffee).coffee +++ b/app/templates/client/app/admin(auth)/admin.controller(coffee).coffee @@ -9,4 +9,4 @@ angular.module '<%= scriptAppName %>' $scope.delete = (user) -> User.remove id: user._id - _.remove $scope.users, user \ No newline at end of file + $scope.users.splice @$index, 1 diff --git a/app/templates/client/app/admin(auth)/admin.controller(js).js b/app/templates/client/app/admin(auth)/admin.controller(js).js index dd6b09405..3cbfd4b7f 100644 --- a/app/templates/client/app/admin(auth)/admin.controller(js).js +++ b/app/templates/client/app/admin(auth)/admin.controller(js).js @@ -1,17 +1,13 @@ 'use strict'; angular.module('<%= scriptAppName %>') - .controller('AdminCtrl', function ($scope, $http, Auth, User) { + .controller('AdminCtrl', function($scope, $http, Auth, User) { // Use the User $resource to fetch all users $scope.users = User.query(); $scope.delete = function(user) { User.remove({ id: user._id }); - angular.forEach($scope.users, function(u, i) { - if (u === user) { - $scope.users.splice(i, 1); - } - }); + $scope.users.splice(this.$index, 1); }; }); diff --git a/app/templates/client/app/app(coffee).coffee b/app/templates/client/app/app(coffee).coffee index ea9ae3c95..f0c1bd129 100644 --- a/app/templates/client/app/app(coffee).coffee +++ b/app/templates/client/app/app(coffee).coffee @@ -1,39 +1,42 @@ 'use strict' angular.module '<%= scriptAppName %>', [<%= angularModules %>] -<% if(filters.ngroute) { %>.config ($routeProvider, $locationProvider<% if(filters.auth) { %>, $httpProvider<% } %>) -> +<% if (filters.ngroute) { %>.config ($routeProvider, $locationProvider<% if (filters.auth) { %>, $httpProvider<% } %>) -> $routeProvider .otherwise redirectTo: '/' - $locationProvider.html5Mode true<% if(filters.auth) { %> + $locationProvider.html5Mode true<% if (filters.auth) { %> $httpProvider.interceptors.push 'authInterceptor'<% } %> -<% } %><% if(filters.uirouter) { %>.config ($stateProvider, $urlRouterProvider, $locationProvider<% if(filters.auth) { %>, $httpProvider<% } %>) -> +<% } %><% if (filters.uirouter) { %>.config ($stateProvider, $urlRouterProvider, $locationProvider<% if (filters.auth) { %>, $httpProvider<% } %>) -> $urlRouterProvider .otherwise '/' - $locationProvider.html5Mode true<% if(filters.auth) { %> + $locationProvider.html5Mode true<% if (filters.auth) { %> $httpProvider.interceptors.push 'authInterceptor'<% } %> -<% } %><% if(filters.auth) { %> -.factory 'authInterceptor', ($rootScope, $q, $cookieStore, $location) -> - # Add authorization token to headers +<% } %><% if (filters.auth) { %> +.factory 'authInterceptor', ($rootScope, $q, $cookies<% 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' + config.headers.Authorization = 'Bearer ' + $cookies.get 'token' if $cookies.get 'token' config # 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' + $cookies.remove 'token' $q.reject response -.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) -> - $location.path "/login" if next.authenticate and not loggedIn -<% } %> \ No newline at end of file +.run ($rootScope<% if (filters.ngroute) { %>, $location<% } %><% if (filters.uirouter) { %>, $state<% } %>, Auth) -> + # Redirect to login if route requires auth and the user is not logged in + $rootScope.$on <% if (filters.ngroute) { %>'$routeChangeStart'<% } %><% if (filters.uirouter) { %>'$stateChangeStart'<% } %>, (event, next) -> + if next.authenticate + Auth.isLoggedIn (loggedIn) -> + if !loggedIn + event.preventDefault() + <% if (filters.ngroute) { %>$location.path '/login'<% } %><% if (filters.uirouter) { %>$state.go 'login'<% }} %> diff --git a/app/templates/client/app/app(css).css b/app/templates/client/app/app(css).css index f1a61a918..2dbd1e8c5 100644 --- a/app/templates/client/app/app(css).css +++ b/app/templates/client/app/app(css).css @@ -1,15 +1,15 @@ -<% if(filters.bootstrap) { %> +<% if (filters.bootstrap) { %> /** * Bootstrap Fonts */ @font-face { - font-family: 'Glyphicons Halflings'; - src: url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot'); - src: url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), - url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.woff') format('woff'), - url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.ttf') format('truetype'), - url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); + font-family: 'Glyphicons Halflings'; + src: url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot'); + src: url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), + url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.woff') format('woff'), + url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.ttf') format('truetype'), + url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); }<% } %> /** @@ -17,30 +17,30 @@ */ @font-face { - font-family: 'FontAwesome'; - src: url('../bower_components/font-awesome/fonts/fontawesome-webfont.eot?v=4.1.0'); - src: url('../bower_components/font-awesome/fonts/fontawesome-webfont.eot?#iefix&v=4.1.0') format('embedded-opentype'), - url('../bower_components/font-awesome/fonts/fontawesome-webfont.woff?v=4.1.0') format('woff'), - url('../bower_components/font-awesome/fonts/fontawesome-webfont.ttf?v=4.1.0') format('truetype'), - url('../bower_components/font-awesome/fonts/fontawesome-webfont.svg?v=4.1.0#fontawesomeregular') format('svg'); - font-weight: normal; - font-style: normal; + font-family: 'FontAwesome'; + src: url('../bower_components/font-awesome/fonts/fontawesome-webfont.eot?v=4.1.0'); + src: url('../bower_components/font-awesome/fonts/fontawesome-webfont.eot?#iefix&v=4.1.0') format('embedded-opentype'), + url('../bower_components/font-awesome/fonts/fontawesome-webfont.woff?v=4.1.0') format('woff'), + url('../bower_components/font-awesome/fonts/fontawesome-webfont.ttf?v=4.1.0') format('truetype'), + url('../bower_components/font-awesome/fonts/fontawesome-webfont.svg?v=4.1.0#fontawesomeregular') format('svg'); + font-weight: normal; + font-style: normal; } /** * App-wide Styles */ -.browsehappy { - margin: 0.2em 0; - background: #ccc; - color: #000; - padding: 0.2em 0; +.browserupgrade { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0; } -<% if (!filters.bootstrap) { %> +<% if(!filters.bootstrap) { %> /* Responsive: Portrait tablets and up */ @media screen and (min-width: 768px) { - .container { - max-width: 730px; - } -}<% } %> \ No newline at end of file + .container { + max-width: 730px; + } +}<% } %> diff --git a/app/templates/client/app/app(js).js b/app/templates/client/app/app(js).js index c8850ed07..27410af8a 100644 --- a/app/templates/client/app/app(js).js +++ b/app/templates/client/app/app(js).js @@ -1,39 +1,40 @@ 'use strict'; angular.module('<%= scriptAppName %>', [<%= angularModules %>]) - <% if(filters.ngroute) { %>.config(function ($routeProvider, $locationProvider<% if(filters.auth) { %>, $httpProvider<% } %>) { + <% if (filters.ngroute) { %>.config(function($routeProvider, $locationProvider<% if (filters.auth) { %>, $httpProvider<% } %>) { $routeProvider .otherwise({ redirectTo: '/' }); - $locationProvider.html5Mode(true);<% if(filters.auth) { %> + $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) { %> + $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, $cookies<% if (filters.ngroute) { %>, $location<% } if (filters.uirouter) { %>, $injector<% } %>) { + <% if (filters.uirouter) { %>var state; + <% } %>return { // Add authorization token to headers - request: function (config) { + request: function(config) { config.headers = config.headers || {}; - if ($cookieStore.get('token')) { - config.headers.Authorization = 'Bearer ' + $cookieStore.get('token'); + if ($cookies.get('token')) { + config.headers.Authorization = 'Bearer ' + $cookies.get('token'); } return config; }, // Intercept 401s and redirect you to login responseError: function(response) { - if(response.status === 401) { - $location.path('/login'); + if (response.status === 401) { + <% if (filters.ngroute) { %>$location.path('/login');<% } if (filters.uirouter) { %>(state || (state = $injector.get('$state'))).go('login');<% } %> // remove any stale tokens - $cookieStore.remove('token'); + $cookies.remove('token'); return $q.reject(response); } else { @@ -43,14 +44,16 @@ 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) { - if (next.authenticate && !loggedIn) { - event.preventDefault(); - $location.path('/login'); - } - }); + .run(function($rootScope<% if (filters.ngroute) { %>, $location<% } if (filters.uirouter) { %>, $state<% } %>, Auth) { + // Redirect to login if route requires auth and the user is not logged in + $rootScope.$on(<% if (filters.ngroute) { %>'$routeChangeStart'<% } %><% if (filters.uirouter) { %>'$stateChangeStart'<% } %>, function(event, next) { + if (next.authenticate) { + Auth.isLoggedIn(function(loggedIn) { + if (!loggedIn) { + event.preventDefault(); + <% if (filters.ngroute) { %>$location.path('/login');<% } if (filters.uirouter) { %>$state.go('login');<% } %> + } + }); + } }); })<% } %>; diff --git a/app/templates/client/app/app(less).less b/app/templates/client/app/app(less).less index 30639f539..cbfffbe88 100644 --- a/app/templates/client/app/app(less).less +++ b/app/templates/client/app/app(less).less @@ -1,29 +1,29 @@ -<% if(filters.bootstrap) { %>@import 'bootstrap/less/bootstrap.less';<% } %> -@import 'font-awesome/less/font-awesome.less'; +<% if (filters.bootstrap) { %>@import '../bower_components/bootstrap/less/bootstrap.less';<% } %> +@import '../bower_components/font-awesome/less/font-awesome.less'; -<% if(filters.bootstrap) { %>@icon-font-path: '/bower_components/bootstrap/fonts/';<% } %> -@fa-font-path: '/bower_components/font-awesome/fonts'; +<% if (filters.bootstrap) { %>@icon-font-path: '../bower_components/bootstrap/fonts/';<% } %> +@fa-font-path: '../bower_components/font-awesome/fonts'; /** * App-wide Styles */ -.browsehappy { - margin: 0.2em 0; - background: #ccc; - color: #000; - padding: 0.2em 0; +.browserupgrade { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0; } -<% if (!filters.bootstrap) { %> +<% if(!filters.bootstrap) { %> /* Responsive: Portrait tablets and up */ @media screen and (min-width: 768px) { - .container { - max-width: 730px; - } + .container { + max-width: 730px; + } } <% } %> // injector @import 'account/login/login.less'; @import 'admin/admin.less'; @import 'main/main.less'; -// endinjector \ No newline at end of file +// endinjector diff --git a/app/templates/client/app/app(sass).scss b/app/templates/client/app/app(sass).scss index 4b8ae7a04..889878aee 100644 --- a/app/templates/client/app/app(sass).scss +++ b/app/templates/client/app/app(sass).scss @@ -1,25 +1,25 @@ -<% if(filters.bootstrap) { %>$icon-font-path: "/bower_components/bootstrap-sass-official/vendor/assets/fonts/bootstrap/";<% } %> -$fa-font-path: "/bower_components/font-awesome/fonts"; -<% if(filters.bootstrap) { %> -@import 'bootstrap-sass-official/vendor/assets/stylesheets/bootstrap';<% } %> -@import 'font-awesome/scss/font-awesome'; +<% if (filters.bootstrap) { %>$icon-font-path: "../bower_components/bootstrap-sass-official/vendor/assets/fonts/bootstrap/";<% } %> +$fa-font-path: "../bower_components/font-awesome/fonts"; +<% if (filters.bootstrap) { %> +@import '../bower_components/bootstrap-sass-official/vendor/assets/stylesheets/bootstrap';<% } %> +@import '../bower_components/font-awesome/scss/font-awesome'; /** * App-wide Styles */ -.browsehappy { - margin: 0.2em 0; - background: #ccc; - color: #000; - padding: 0.2em 0; +.browserupgrade { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0; } -<% if (!filters.bootstrap) { %> +<% if(!filters.bootstrap) { %> /* Responsive: Portrait tablets and up */ @media screen and (min-width: 768px) { - .container { - max-width: 730px; - } + .container { + max-width: 730px; + } } <% } %> // Component styles are injected through grunt @@ -27,4 +27,4 @@ $fa-font-path: "/bower_components/font-awesome/fonts"; @import 'account/login/login.scss'; @import 'admin/admin.scss'; @import 'main/main.scss'; -// endinjector \ No newline at end of file +// endinjector diff --git a/app/templates/client/app/app(stylus).styl b/app/templates/client/app/app(stylus).styl index b7e4bb9c1..d25cdfc59 100644 --- a/app/templates/client/app/app(stylus).styl +++ b/app/templates/client/app/app(stylus).styl @@ -1,17 +1,17 @@ -@import "font-awesome/css/font-awesome.css" -<% if(filters.bootstrap) { %>@import "bootstrap/dist/css/bootstrap.css" +@import "../bower_components/font-awesome/css/font-awesome.css" +<% if (filters.bootstrap) { %>@import "../bower_components/bootstrap/dist/css/bootstrap.css" // // Bootstrap Fonts // @font-face - font-family: 'Glyphicons Halflings' - src: url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot') - src: url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), - url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.woff') format('woff'), - url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.ttf') format('truetype'), - url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); + font-family: 'Glyphicons Halflings' + src: url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot') + src: url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), + url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.woff') format('woff'), + url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.ttf') format('truetype'), + url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); <% } %> // // Font Awesome Fonts @@ -31,20 +31,20 @@ // App-wide Styles // -.browsehappy - background #ccc - color #000 - margin 0.2em 0 - padding 0.2em 0 -<% if (!filters.bootstrap) { %> +.browserupgrade + background #ccc + color #000 + margin 0.2em 0 + padding 0.2em 0 +<% if(!filters.bootstrap) { %> // Responsive: Portrait tablets and up @media screen and (min-width: 768px) - .container - max-width 730px + .container + max-width 730px <% } %> // Component styles are injected through grunt // injector @import "account/login/login" @import "admin/admin" @import "main/main" -// endinjector \ No newline at end of file +// endinjector diff --git a/app/templates/client/app/main/main(coffee).coffee b/app/templates/client/app/main/main(coffee).coffee index 6d84bdc1e..04cd367bb 100644 --- a/app/templates/client/app/main/main(coffee).coffee +++ b/app/templates/client/app/main/main(coffee).coffee @@ -1,15 +1,15 @@ 'use strict' angular.module '<%= scriptAppName %>' -<% if(filters.ngroute) { %>.config ($routeProvider) -> +<% if (filters.ngroute) { %>.config ($routeProvider) -> $routeProvider .when '/', templateUrl: 'app/main/main.html' controller: 'MainCtrl' -<% } %><% if(filters.uirouter) { %>.config ($stateProvider) -> +<% } %><% if (filters.uirouter) { %>.config ($stateProvider) -> $stateProvider .state 'main', url: '/' templateUrl: 'app/main/main.html' controller: 'MainCtrl' -<% } %> \ No newline at end of file +<% } %> diff --git a/app/templates/client/app/main/main(css).css b/app/templates/client/app/main/main(css).css index c396852d6..b49092ec1 100644 --- a/app/templates/client/app/main/main(css).css +++ b/app/templates/client/app/main/main(css).css @@ -1,34 +1,27 @@ .thing-form { - margin: 20px 0; + margin: 20px 0; } #banner { - border-bottom: none; - margin-top: -20px; + border-bottom: none; + margin-top: -20px; } #banner h1 { - font-size: 60px; - line-height: 1; - letter-spacing: -1px; + font-size: 60px; + line-height: 1; + letter-spacing: -1px; } .hero-unit { - position: relative; - padding: 30px 15px; - color: #F5F5F5; - text-align: center; - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); - background: #4393B9; -} - -.footer { - text-align: center; - padding: 30px 0; - margin-top: 70px; - border-top: 1px solid #E5E5E5; + position: relative; + padding: 30px 15px; + color: #F5F5F5; + text-align: center; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); + background: #4393B9; } .navbar-text { - margin-left: 15px; -} \ No newline at end of file + margin-left: 15px; +} diff --git a/app/templates/client/app/main/main(html).html b/app/templates/client/app/main/main(html).html index cd0f185b2..0d745d9a8 100644 --- a/app/templates/client/app/main/main(html).html +++ b/app/templates/client/app/main/main(html).html @@ -1,4 +1,4 @@ -<div ng-include="'components/navbar/navbar.html'"></div> +<navbar></navbar> <header class="hero-unit" id="banner"> <div class="container"> @@ -13,10 +13,10 @@ <h1>'Allo, 'Allo!</h1> <div class="col-lg-12"> <h1 class="page-header">Features:</h1> <ul class="nav nav-tabs nav-stacked col-md-4 col-lg-4 col-sm-6" ng-repeat="thing in awesomeThings"> - <li><a href="#" tooltip="{{thing.info}}">{{thing.name}}<% if(filters.socketio) { %><button type="button" class="close" ng-click="deleteThing(thing)">×</button><% } %></a></li> + <li><a href="#" tooltip="{{thing.info}}">{{thing.name}}<% if (filters.socketio) { %><button type="button" class="close" ng-click="deleteThing(thing)">×</button><% } %></a></li> </ul> </div> - </div><% if(filters.socketio) { %> + </div><% if (filters.socketio) { %> <form class="thing-form"> <label>Syncs in realtime across clients</label> @@ -29,10 +29,4 @@ <h1 class="page-header">Features:</h1> </form><% } %> </div> -<footer class="footer"> - <div class="container"> - <p>Angular Fullstack v<%= pkg.version %> | - <a href="https://twitter.com/tyhenkel">@tyhenkel</a> | - <a href="https://github.com/DaftMonk/generator-angular-fullstack/issues?state=open">Issues</a></p> - </div> -</footer> +<footer></footer> diff --git a/app/templates/client/app/main/main(jade).jade b/app/templates/client/app/main/main(jade).jade index 76784c855..3277e7b05 100644 --- a/app/templates/client/app/main/main(jade).jade +++ b/app/templates/client/app/main/main(jade).jade @@ -1,4 +1,4 @@ -div(ng-include='"components/navbar/navbar.html"') +navbar header#banner.hero-unit .container @@ -13,8 +13,8 @@ header#banner.hero-unit ul.nav.nav-tabs.nav-stacked.col-md-4.col-lg-4.col-sm-6(ng-repeat='thing in awesomeThings') li a(href='#', tooltip='{{thing.info}}') - | {{thing.name}}<% if(filters.socketio) { %> - button.close(type='button', ng-click='deleteThing(thing)') ×<% } %><% if(filters.socketio) { %> + | {{thing.name}}<% if (filters.socketio) { %> + button.close(type='button', ng-click='deleteThing(thing)') ×<% } %><% if (filters.socketio) { %> form.thing-form label Syncs in realtime across clients @@ -23,11 +23,4 @@ header#banner.hero-unit span.input-group-btn button.btn.btn-primary(type='submit', ng-click='addThing()') Add New<% } %> -footer.footer - .container - p - | Angular Fullstack v<%= pkg.version %> - = ' | ' - a(href='https://twitter.com/tyhenkel') @tyhenkel - = ' | ' - a(href='https://github.com/DaftMonk/generator-angular-fullstack/issues?state=open') Issues \ No newline at end of file +footer diff --git a/app/templates/client/app/main/main(js).js b/app/templates/client/app/main/main(js).js index 1d3bc318a..165181ffe 100644 --- a/app/templates/client/app/main/main(js).js +++ b/app/templates/client/app/main/main(js).js @@ -1,17 +1,17 @@ 'use strict'; angular.module('<%= scriptAppName %>') - <% if(filters.ngroute) { %>.config(function ($routeProvider) { + <% if (filters.ngroute) { %>.config(function($routeProvider) { $routeProvider .when('/', { templateUrl: 'app/main/main.html', controller: 'MainCtrl' }); - });<% } %><% if(filters.uirouter) { %>.config(function ($stateProvider) { + });<% } %><% if (filters.uirouter) { %>.config(function($stateProvider) { $stateProvider .state('main', { url: '/', templateUrl: 'app/main/main.html', controller: 'MainCtrl' }); - });<% } %> \ No newline at end of file + });<% } %> diff --git a/app/templates/client/app/main/main(less).less b/app/templates/client/app/main/main(less).less index 39636ab2d..b49092ec1 100644 --- a/app/templates/client/app/main/main(less).less +++ b/app/templates/client/app/main/main(less).less @@ -3,32 +3,25 @@ } #banner { - border-bottom: none; - margin-top: -20px; + border-bottom: none; + margin-top: -20px; } #banner h1 { - font-size: 60px; - line-height: 1; - letter-spacing: -1px; + font-size: 60px; + line-height: 1; + letter-spacing: -1px; } .hero-unit { - position: relative; - padding: 30px 15px; - color: #F5F5F5; - text-align: center; - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); - background: #4393B9; -} - -.footer { - text-align: center; - padding: 30px 0; - margin-top: 70px; - border-top: 1px solid #E5E5E5; + position: relative; + padding: 30px 15px; + color: #F5F5F5; + text-align: center; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); + background: #4393B9; } .navbar-text { - margin-left: 15px; -} \ No newline at end of file + margin-left: 15px; +} diff --git a/app/templates/client/app/main/main(sass).scss b/app/templates/client/app/main/main(sass).scss index aa19c3649..b49092ec1 100644 --- a/app/templates/client/app/main/main(sass).scss +++ b/app/templates/client/app/main/main(sass).scss @@ -1,34 +1,27 @@ .thing-form { - margin: 20px 0; + margin: 20px 0; } #banner { - border-bottom: none; - margin-top: -20px; + border-bottom: none; + margin-top: -20px; } #banner h1 { - font-size: 60px; - line-height: 1; - letter-spacing: -1px; + font-size: 60px; + line-height: 1; + letter-spacing: -1px; } .hero-unit { - position: relative; - padding: 30px 15px; - color: #F5F5F5; - text-align: center; - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); - background: #4393B9; -} - -.footer { - text-align: center; - padding: 30px 0; - margin-top: 70px; - border-top: 1px solid #E5E5E5; + position: relative; + padding: 30px 15px; + color: #F5F5F5; + text-align: center; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); + background: #4393B9; } .navbar-text { margin-left: 15px; -} \ No newline at end of file +} diff --git a/app/templates/client/app/main/main(stylus).styl b/app/templates/client/app/main/main(stylus).styl index 9ba91c3a3..c3915218c 100644 --- a/app/templates/client/app/main/main(stylus).styl +++ b/app/templates/client/app/main/main(stylus).styl @@ -1,28 +1,22 @@ .thing-form - margin 20px 0 + margin 20px 0 #banner - border-bottom none - margin-top -20px + border-bottom none + margin-top -20px #banner h1 - font-size 60px - letter-spacing -1px - line-height 1 + font-size 60px + letter-spacing -1px + line-height 1 .hero-unit - background #4393B9 - color #F5F5F5 - padding 30px 15px - position relative - text-align center - text-shadow 0 1px 0 rgba(0, 0, 0, 0.1) - -.footer - border-top 1px solid #E5E5E5 - margin-top 70px - padding 30px 0 - text-align center + background #4393B9 + color #F5F5F5 + padding 30px 15px + position relative + text-align center + text-shadow 0 1px 0 rgba(0, 0, 0, 0.1) .navbar-text - margin-left 15px \ No newline at end of file + margin-left 15px diff --git a/app/templates/client/app/main/main.controller(coffee).coffee b/app/templates/client/app/main/main.controller(coffee).coffee index 143e7f387..4b04a951b 100644 --- a/app/templates/client/app/main/main.controller(coffee).coffee +++ b/app/templates/client/app/main/main.controller(coffee).coffee @@ -1,13 +1,13 @@ 'use strict' angular.module '<%= scriptAppName %>' -.controller 'MainCtrl', ($scope, $http<% if(filters.socketio) { %>, socket<% } %>) -> +.controller 'MainCtrl', ($scope, $http<% if (filters.socketio) { %>, socket<% } %>) -> $scope.awesomeThings = [] $http.get('/api/things').success (awesomeThings) -> $scope.awesomeThings = awesomeThings - <% if(filters.socketio) { %>socket.syncUpdates 'thing', $scope.awesomeThings<% } %> -<% if(filters.mongoose) { %> + <% if (filters.socketio) { %>socket.syncUpdates 'thing', $scope.awesomeThings<% } %> +<% if (filters.models) { %> $scope.addThing = -> return if $scope.newThing is '' $http.post '/api/things', @@ -16,7 +16,7 @@ angular.module '<%= scriptAppName %>' $scope.newThing = '' $scope.deleteThing = (thing) -> - $http.delete '/api/things/' + thing._id<% } %><% if(filters.socketio) { %> + $http.delete '/api/things/' + thing._id<% } %><% if (filters.socketio) { %> $scope.$on '$destroy', -> socket.unsyncUpdates 'thing'<% } %> diff --git a/app/templates/client/app/main/main.controller(js).js b/app/templates/client/app/main/main.controller(js).js index 433a10fe4..345d9909d 100644 --- a/app/templates/client/app/main/main.controller(js).js +++ b/app/templates/client/app/main/main.controller(js).js @@ -1,16 +1,16 @@ 'use strict'; angular.module('<%= scriptAppName %>') - .controller('MainCtrl', function ($scope, $http<% if(filters.socketio) { %>, socket<% } %>) { + .controller('MainCtrl', function($scope, $http<% if (filters.socketio) { %>, socket<% } %>) { $scope.awesomeThings = []; $http.get('/api/things').success(function(awesomeThings) { - $scope.awesomeThings = awesomeThings;<% if(filters.socketio) { %> + $scope.awesomeThings = awesomeThings;<% if (filters.socketio) { %> socket.syncUpdates('thing', $scope.awesomeThings);<% } %> }); -<% if(filters.mongoose) { %> +<% if (filters.models) { %> $scope.addThing = function() { - if($scope.newThing === '') { + if ($scope.newThing === '') { return; } $http.post('/api/things', { name: $scope.newThing }); @@ -19,9 +19,9 @@ angular.module('<%= scriptAppName %>') $scope.deleteThing = function(thing) { $http.delete('/api/things/' + thing._id); - };<% } %><% if(filters.socketio) { %> + };<% } %><% if (filters.socketio) { %> - $scope.$on('$destroy', function () { + $scope.$on('$destroy', function() { socket.unsyncUpdates('thing'); });<% } %> }); 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..a72ae8695 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,12 @@ 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 + $httpBackend.flush()<% if (filters.jasmine) { %> + expect(scope.awesomeThings.length).toBe 4 <% } if (filters.mocha) { %> + <%= does("scope.awesomeThings.length") %>.equal 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..71fd2a783 100644 --- a/app/templates/client/app/main/main.controller.spec(js).js +++ b/app/templates/client/app/main/main.controller.spec(js).js @@ -1,29 +1,33 @@ 'use strict'; -describe('Controller: MainCtrl', function () { +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, - $httpBackend; + var MainCtrl; + var scope;<% if (filters.uirouter) {%> + var state;<% } %> + var $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 }); })); - it('should attach a list of things to the scope', function () { - $httpBackend.flush(); - expect(scope.awesomeThings.length).toBe(4); + it('should attach a list of things to the scope', function() { + $httpBackend.flush();<% if (filters.jasmine) { %> + expect(scope.awesomeThings.length).toBe(4);<% } if (filters.mocha) { %> + <%= does("scope.awesomeThings.length") %>.equal(4);<% } %> }); }); 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..2892d04ed 100644 --- a/app/templates/client/components/auth(auth)/auth.service(coffee).coffee +++ b/app/templates/client/components/auth(auth)/auth.service(coffee).coffee @@ -1,43 +1,41 @@ 'use strict' angular.module '<%= scriptAppName %>' -.factory 'Auth', ($location, $rootScope, $http, User, $cookieStore, $q) -> - currentUser = if $cookieStore.get 'token' then User.get() else {} +.factory 'Auth', ($http, User, $cookies, $q) -> + currentUser = if $cookies.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, user) @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) -> + $cookies.put 'token', res.data.token currentUser = User.get() - deferred.resolve data - callback?() + currentUser.$promise - .error (err) => - @logout() - deferred.reject err - callback? err + .then (user) -> + callback? null, user + user - deferred.promise + .catch (err) => + @logout() + callback? err.data + $q.reject err.data ### Delete access token and user info - - @param {Function} ### logout: -> - $cookieStore.remove 'token' + $cookies.remove 'token' currentUser = {} return @@ -46,15 +44,15 @@ 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) -> User.save user, (data) -> - $cookieStore.put 'token', data.token + $cookies.put 'token', data.token currentUser = User.get() - callback? user + callback? null, user , (err) => @logout() @@ -68,7 +66,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) -> @@ -78,8 +76,8 @@ angular.module '<%= scriptAppName %>' oldPassword: oldPassword newPassword: newPassword - , (user) -> - callback? user + , () -> + callback? null , (err) -> callback? err @@ -88,49 +86,65 @@ 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_ ### Get auth token ### getToken: -> - $cookieStore.get 'token' + $cookies.get 'token' 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..2a1fcb480 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, $cookies, $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 ($cookies.get('token')) { currentUser = User.get(); } @@ -13,39 +24,35 @@ angular.module('<%= scriptAppName %>') * Authenticate user and save token * * @param {Object} user - login info - * @param {Function} callback - optional + * @param {Function} callback - optional, function(error, user) * @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) { + $cookies.put('token', res.data.token); currentUser = User.get(); - deferred.resolve(data); - return cb(); - }). - error(function(err) { + return currentUser.$promise; + }) + .then(function(user) { + safeCb(callback)(null, user); + return user; + }) + .catch(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'); + $cookies.remove('token'); currentUser = {}; }, @@ -53,21 +60,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); + $cookies.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,71 +81,90 @@ 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); + }, function() { + return safeCb(callback)(null); }, 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'); + return $cookies.get('token'); } }; }); diff --git a/app/templates/client/components/footer/footer(css).css b/app/templates/client/components/footer/footer(css).css new file mode 100644 index 000000000..cd1753eed --- /dev/null +++ b/app/templates/client/components/footer/footer(css).css @@ -0,0 +1,6 @@ +footer.footer { + text-align: center; + padding: 30px 0; + margin-top: 70px; + border-top: 1px solid #E5E5E5; +} diff --git a/app/templates/client/components/footer/footer(html).html b/app/templates/client/components/footer/footer(html).html new file mode 100644 index 000000000..3f9f7ffb9 --- /dev/null +++ b/app/templates/client/components/footer/footer(html).html @@ -0,0 +1,6 @@ +<div class="container"> + <p>Angular Fullstack v<%= pkg.version %> | + <a href="https://twitter.com/tyhenkel">@tyhenkel</a> | + <a href="https://github.com/DaftMonk/generator-angular-fullstack/issues?state=open">Issues</a> + </p> +</div> diff --git a/app/templates/client/components/footer/footer(jade).jade b/app/templates/client/components/footer/footer(jade).jade new file mode 100644 index 000000000..a0bd84a1d --- /dev/null +++ b/app/templates/client/components/footer/footer(jade).jade @@ -0,0 +1,7 @@ +.container + p + | Angular Fullstack v<%= pkg.version %> + = ' | ' + a(href='https://twitter.com/tyhenkel') @tyhenkel + = ' | ' + a(href='https://github.com/DaftMonk/generator-angular-fullstack/issues?state=open') Issues diff --git a/app/templates/client/components/footer/footer(less).less b/app/templates/client/components/footer/footer(less).less new file mode 100644 index 000000000..cd1753eed --- /dev/null +++ b/app/templates/client/components/footer/footer(less).less @@ -0,0 +1,6 @@ +footer.footer { + text-align: center; + padding: 30px 0; + margin-top: 70px; + border-top: 1px solid #E5E5E5; +} diff --git a/app/templates/client/components/footer/footer(sass).scss b/app/templates/client/components/footer/footer(sass).scss new file mode 100644 index 000000000..cd1753eed --- /dev/null +++ b/app/templates/client/components/footer/footer(sass).scss @@ -0,0 +1,6 @@ +footer.footer { + text-align: center; + padding: 30px 0; + margin-top: 70px; + border-top: 1px solid #E5E5E5; +} diff --git a/app/templates/client/components/footer/footer(stylus).styl b/app/templates/client/components/footer/footer(stylus).styl new file mode 100644 index 000000000..ad725136b --- /dev/null +++ b/app/templates/client/components/footer/footer(stylus).styl @@ -0,0 +1,5 @@ +footer.footer + border-top 1px solid #E5E5E5 + margin-top 70px + padding 30px 0 + text-align center diff --git a/app/templates/client/components/footer/footer.directive(coffee).coffee b/app/templates/client/components/footer/footer.directive(coffee).coffee new file mode 100644 index 000000000..467006759 --- /dev/null +++ b/app/templates/client/components/footer/footer.directive(coffee).coffee @@ -0,0 +1,8 @@ +'use strict' + +angular.module '<%= scriptAppName %>' +.directive 'footer', -> + templateUrl: 'components/footer/footer.html' + restrict: 'E', + link: (scope, element) -> + element.addClass('footer') diff --git a/app/templates/client/components/footer/footer.directive(js).js b/app/templates/client/components/footer/footer.directive(js).js new file mode 100644 index 000000000..a640e2289 --- /dev/null +++ b/app/templates/client/components/footer/footer.directive(js).js @@ -0,0 +1,12 @@ +'use strict'; + +angular.module('<%= scriptAppName %>') + .directive('footer', function () { + return { + templateUrl: 'components/footer/footer.html', + restrict: 'E', + link: function (scope, element) { + element.addClass('footer'); + } + }; + }); diff --git a/app/templates/client/components/modal(uibootstrap)/modal(css).css b/app/templates/client/components/modal(uibootstrap)/modal(css).css index f5cc0d9e7..ae0406856 100644 --- a/app/templates/client/components/modal(uibootstrap)/modal(css).css +++ b/app/templates/client/components/modal(uibootstrap)/modal(css).css @@ -20,4 +20,4 @@ } .modal-danger .modal-header { background: #d9534f; -} \ No newline at end of file +} diff --git a/app/templates/client/components/modal(uibootstrap)/modal(html).html b/app/templates/client/components/modal(uibootstrap)/modal(html).html index 4580254ff..f04d0db03 100644 --- a/app/templates/client/components/modal(uibootstrap)/modal(html).html +++ b/app/templates/client/components/modal(uibootstrap)/modal(html).html @@ -8,4 +8,4 @@ <h4 ng-if="modal.title" ng-bind="modal.title" class="modal-title"></h4> </div> <div class="modal-footer"> <button ng-repeat="button in modal.buttons" ng-class="button.classes" ng-click="button.click($event)" ng-bind="button.text" class="btn"></button> -</div> \ No newline at end of file +</div> diff --git a/app/templates/client/components/mongoose-error(auth)/mongoose-error.directive(coffee).coffee b/app/templates/client/components/mongoose-error(auth)/mongoose-error.directive(coffee).coffee index d255f614d..cf0e1ccf0 100644 --- a/app/templates/client/components/mongoose-error(auth)/mongoose-error.directive(coffee).coffee +++ b/app/templates/client/components/mongoose-error(auth)/mongoose-error.directive(coffee).coffee @@ -9,4 +9,4 @@ angular.module '<%= scriptAppName %>' require: 'ngModel' link: (scope, element, attrs, ngModel) -> element.on 'keydown', -> - ngModel.$setValidity 'mongoose', true \ No newline at end of file + ngModel.$setValidity 'mongoose', true diff --git a/app/templates/client/components/mongoose-error(auth)/mongoose-error.directive(js).js b/app/templates/client/components/mongoose-error(auth)/mongoose-error.directive(js).js index 8a331009b..a71cb03cf 100644 --- a/app/templates/client/components/mongoose-error(auth)/mongoose-error.directive(js).js +++ b/app/templates/client/components/mongoose-error(auth)/mongoose-error.directive(js).js @@ -14,4 +14,4 @@ angular.module('<%= scriptAppName %>') }); } }; - }); \ No newline at end of file + }); diff --git a/app/templates/client/components/navbar/navbar(html).html b/app/templates/client/components/navbar/navbar(html).html index 71f8606dd..ec9e4682d 100644 --- a/app/templates/client/components/navbar/navbar(html).html +++ b/app/templates/client/components/navbar/navbar(html).html @@ -11,18 +11,18 @@ </div> <div collapse="isCollapsed" class="navbar-collapse collapse" id="navbar-main"> <ul class="nav navbar-nav"> - <li ng-repeat="item in menu" ng-class="{active: isActive(item.link)}"> - <a ng-href="{{item.link}}">{{item.title}}</a> - </li><% if(filters.auth) { %> - <li ng-show="isAdmin()" ng-class="{active: isActive('/admin')}"><a href="/admin">Admin</a></li><% } %> - </ul><% 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}}</a> + </li><% 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</a></li><% } %> + </ul><% if (filters.auth) { %> <ul class="nav navbar-nav navbar-right"> - <li ng-hide="isLoggedIn()" ng-class="{active: isActive('/signup')}"><a href="/signup">Sign up</a></li> - <li ng-hide="isLoggedIn()" ng-class="{active: isActive('/login')}"><a href="/login">Login</a></li> + <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</a></li> + <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</a></li> <li ng-show="isLoggedIn()"><p class="navbar-text">Hello {{ getCurrentUser().name }}</p> </li> - <li ng-show="isLoggedIn()" ng-class="{active: isActive('/settings')}"><a href="/settings"><span class="glyphicon glyphicon-cog"></span></a></li> - <li ng-show="isLoggedIn()" ng-class="{active: isActive('/logout')}"><a href="" ng-click="logout()">Logout</a></li> + <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 class="glyphicon glyphicon-cog"></span></a></li> + <li ng-show="isLoggedIn()"><a <% if (filters.uirouter) { %>ui-sref="logout"<% } else { %>href="/logout"<% } %>>Logout</a></li> </ul><% } %> </div> </div> diff --git a/app/templates/client/components/navbar/navbar(jade).jade b/app/templates/client/components/navbar/navbar(jade).jade index 2b17f29c3..e20a8fffa 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<% } %> diff --git a/app/templates/client/components/navbar/navbar.controller(coffee).coffee b/app/templates/client/components/navbar/navbar.controller(coffee).coffee index d3804c5eb..98eaf2213 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.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()<% } %> diff --git a/app/templates/client/components/navbar/navbar.controller(js).js b/app/templates/client/components/navbar/navbar.controller(js).js index 4ce9dbcb5..b3eef7cf6 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.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/navbar/navbar.directive(coffee).coffee b/app/templates/client/components/navbar/navbar.directive(coffee).coffee new file mode 100644 index 000000000..bea476822 --- /dev/null +++ b/app/templates/client/components/navbar/navbar.directive(coffee).coffee @@ -0,0 +1,7 @@ +'use strict' + +angular.module '<%= scriptAppName %>' +.directive 'navbar', -> + templateUrl: 'components/navbar/navbar.html' + restrict: 'E' + controller: 'NavbarCtrl' diff --git a/app/templates/client/components/navbar/navbar.directive(js).js b/app/templates/client/components/navbar/navbar.directive(js).js new file mode 100644 index 000000000..9153e6489 --- /dev/null +++ b/app/templates/client/components/navbar/navbar.directive(js).js @@ -0,0 +1,10 @@ +'use strict'; + +angular.module('<%= scriptAppName %>') + .directive('navbar', function () { + return { + templateUrl: 'components/navbar/navbar.html', + restrict: 'E', + controller: 'NavbarCtrl' + }; + }); 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 98% rename from app/templates/client/components/socket(socketio)/socket.mock.js rename to app/templates/client/components/socket(socketio)/socket.mock(js).js index 84a2e0c36..ba09c1d35 100644 --- a/app/templates/client/components/socket(socketio)/socket.mock.js +++ b/app/templates/client/components/socket(socketio)/socket.mock(js).js @@ -13,4 +13,4 @@ angular.module('socketMock', []) syncUpdates: function() {}, unsyncUpdates: function() {} }; - }); \ No newline at end of file + }); 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 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!'); + } + }; +}); diff --git a/app/templates/client/index.html b/app/templates/client/index.html index e9dcd5729..1c8d00d8e 100644 --- a/app/templates/client/index.html +++ b/app/templates/client/index.html @@ -5,7 +5,7 @@ <!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]--> <head> <meta charset="utf-8"> - <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta http-equiv="x-ua-compatible" content="ie=edge"> <base href="/"> <title></title> <meta name="description" content=""> @@ -16,25 +16,25 @@ <!-- endbower --> <!-- endbuild --> <!-- build:css({.tmp,client}) app/app.css --> - <link rel="stylesheet" href="app/app.css"> + <link rel="stylesheet" href="app/app.css"> <!-- injector:css --> <!-- endinjector --> <!-- endbuild --> </head> <body ng-app="<%= scriptAppName %>"> <!--[if lt IE 7]> - <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p> + <p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p> <![endif]--> <!-- Add your site or application content here --> - <% if(filters.ngroute) { %><div ng-view=""></div><% } %><% if(filters.uirouter) { %><div ui-view=""></div><% } %> + <% if (filters.ngroute) { %><div ng-view=""></div><% } %><% if (filters.uirouter) { %><div ui-view=""></div><% } %> <!-- Google Analytics: change UA-XXXXX-X to be your site's ID --> <script> (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) - })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); + })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); ga('create', 'UA-XXXXX-X'); ga('send', 'pageview'); @@ -46,18 +46,14 @@ <![endif]--> <!-- build:js({client,node_modules}) app/vendor.js --> <!-- bower:js --> - <!-- endbower --><% if(filters.socketio) { %> + <!-- endbower --><% if (filters.socketio) { %> <script src="socket.io-client/socket.io.js"></script><% } %> <!-- endbuild --> - - <% if(filters.babel) { %> - <!-- build:js(.tmp) app/app.js --> - <% } else { %> - <!-- build:js({.tmp,client}) app/app.js --> - <% } %> - <script src="app/app.js"></script> - <!-- injector:js --> - <!-- endinjector --> - <!-- endbuild --> -</body> + <!-- build:js(<% if(filters.babel) { %>.tmp<% } + else { %>{.tmp,client}<% } %>) app/app.js --> + <script src="app/app.js"></script> + <!-- injector:js --> + <!-- endinjector --> + <!-- endbuild --> + </body> </html> diff --git a/app/templates/e2e/account(auth)/login/login.po.js b/app/templates/e2e/account(auth)/login/login.po.js new file mode 100644 index 000000000..1186cdb6b --- /dev/null +++ b/app/templates/e2e/account(auth)/login/login.po.js @@ -0,0 +1,27 @@ +/** + * This file uses the Page Object pattern to define the main page for tests + * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ + */ + +'use strict'; + +var LoginPage = function() { + this.form = element(by.css('.form')); + this.form.email = this.form.element(by.model('user.email')); + this.form.password = this.form.element(by.model('user.password')); + this.form.submit = this.form.element(by.css('.btn-login')); + + this.login = function(data) { + for (var prop in data) { + var formElem = this.form[prop]; + if (data.hasOwnProperty(prop) && formElem && typeof formElem.sendKeys === 'function') { + formElem.sendKeys(data[prop]); + } + } + + this.form.submit.click(); + }; +}; + +module.exports = new LoginPage(); + diff --git a/app/templates/e2e/account(auth)/login/login.spec(jasmine).js b/app/templates/e2e/account(auth)/login/login.spec(jasmine).js new file mode 100644 index 000000000..6875376bc --- /dev/null +++ b/app/templates/e2e/account(auth)/login/login.spec(jasmine).js @@ -0,0 +1,65 @@ +'use strict'; + +var config = browser.params;<% if (filters.mongooseModels) { %> +var UserModel = require(config.serverConfig.root + '/server/api/user/user.model');<% } %><% if (filters.sequelizeModels) { %> +var UserModel = require(config.serverConfig.root + '/server/sqldb').User;<% } %> + +describe('Login View', function() { + var page; + + var loadPage = function() { + browser.get(config.baseUrl + '/login'); + page = require('./login.po'); + }; + + var testUser = { + name: 'Test User', + email: 'test@test.com', + password: 'test' + }; + + beforeEach(function(done) { + <% if (filters.mongooseModels) { %>UserModel.removeAsync()<% } + if (filters.sequelizeModels) { %>UserModel.destroy({ where: {} })<% } %> + .then(function() { + <% if (filters.mongooseModels) { %>return UserModel.createAsync(testUser);<% } + if (filters.sequelizeModels) { %>return UserModel.create(testUser);<% } %> + }) + .then(loadPage) + .finally(done); + }); + + it('should include login form with correct inputs and submit button', function() { + expect(page.form.email.getAttribute('type')).toBe('email'); + expect(page.form.email.getAttribute('name')).toBe('email'); + expect(page.form.password.getAttribute('type')).toBe('password'); + expect(page.form.password.getAttribute('name')).toBe('password'); + expect(page.form.submit.getAttribute('type')).toBe('submit'); + expect(page.form.submit.getText()).toBe('Login'); + }); + + describe('with local auth', function() { + + it('should login a user and redirecting to "/"', function() { + page.login(testUser); + + var navbar = require('../../components/navbar/navbar.po'); + + expect(browser.getCurrentUrl()).toBe(config.baseUrl + '/'); + expect(navbar.navbarAccountGreeting.getText()).toBe('Hello ' + testUser.name); + }); + + it('should indicate login failures', function() { + page.login({ + email: testUser.email, + password: 'badPassword' + }); + + expect(browser.getCurrentUrl()).toBe(config.baseUrl + '/login'); + + var helpBlock = page.form.element(by.css('.form-group.has-error .help-block.ng-binding')); + expect(helpBlock.getText()).toBe('This password is not correct.'); + }); + + }); +}); diff --git a/app/templates/e2e/account(auth)/login/login.spec(mocha).js b/app/templates/e2e/account(auth)/login/login.spec(mocha).js new file mode 100644 index 000000000..a33970e67 --- /dev/null +++ b/app/templates/e2e/account(auth)/login/login.spec(mocha).js @@ -0,0 +1,77 @@ +'use strict'; + +var config = browser.params;<% if (filters.mongooseModels) { %> +var UserModel = require(config.serverConfig.root + '/server/api/user/user.model');<% } %><% if (filters.sequelizeModels) { %> +var UserModel = require(config.serverConfig.root + '/server/sqldb').User;<% } %> + +describe('Login View', function() { + var page; + + var loadPage = function() { + browser.get(config.baseUrl + '/login'); + page = require('./login.po'); + }; + + var testUser = { + name: 'Test User', + email: 'test@test.com', + password: 'test' + }; + + before(function() { + return UserModel + <% if (filters.mongooseModels) { %>.removeAsync()<% } + if (filters.sequelizeModels) { %>.destroy({ where: {} })<% } %> + .then(function() { + <% if (filters.mongooseModels) { %>return UserModel.createAsync(testUser);<% } + if (filters.sequelizeModels) { %>return UserModel.create(testUser);<% } %> + }) + .then(loadPage); + }); + + after(function() { + <% if (filters.mongooseModels) { %>return UserModel.removeAsync();<% } + if (filters.sequelizeModels) { %>return UserModel.destroy({ where: {} });<% } %> + }); + + it('should include login form with correct inputs and submit button', function() { + <%= does("page.form.email.getAttribute('type')") %>.eventually.equal('email'); + <%= does("page.form.email.getAttribute('name')") %>.eventually.equal('email'); + <%= does("page.form.password.getAttribute('type')") %>.eventually.equal('password'); + <%= does("page.form.password.getAttribute('name')") %>.eventually.equal('password'); + <%= does("page.form.submit.getAttribute('type')") %>.eventually.equal('submit'); + <%= does("page.form.submit.getText()") %>.eventually.equal('Login'); + }); + + describe('with local auth', function() { + + it('should login a user and redirecting to "/"', function() { + page.login(testUser); + + var navbar = require('../../components/navbar/navbar.po'); + + <%= does("browser.getCurrentUrl()") %>.eventually.equal(config.baseUrl + '/'); + <%= does("navbar.navbarAccountGreeting.getText()") %>.eventually.equal('Hello ' + testUser.name); + }); + + describe('and invalid credentials', function() { + before(function() { + return loadPage(); + }) + + it('should indicate login failures', function() { + page.login({ + email: testUser.email, + password: 'badPassword' + }); + + <%= does("browser.getCurrentUrl()") %>.eventually.equal(config.baseUrl + '/login'); + + var helpBlock = page.form.element(by.css('.form-group.has-error .help-block.ng-binding')); + <%= does("helpBlock.getText()") %>.eventually.equal('This password is not correct.'); + }); + + }); + + }); +}); diff --git a/app/templates/e2e/account(auth)/logout/logout.spec(jasmine).js b/app/templates/e2e/account(auth)/logout/logout.spec(jasmine).js new file mode 100644 index 000000000..cb727f2c5 --- /dev/null +++ b/app/templates/e2e/account(auth)/logout/logout.spec(jasmine).js @@ -0,0 +1,49 @@ +'use strict'; + +var config = browser.params;<% if (filters.mongooseModels) { %> +var UserModel = require(config.serverConfig.root + '/server/api/user/user.model');<% } %><% if (filters.sequelizeModels) { %> +var UserModel = require(config.serverConfig.root + '/server/sqldb').User;<% } %> + +describe('Logout View', function() { + var login = function(user) { + browser.get(config.baseUrl + '/login'); + require('../login/login.po').login(user); + }; + + var testUser = { + name: 'Test User', + email: 'test@test.com', + password: 'test' + }; + + beforeEach(function(done) { + <% if (filters.mongooseModels) { %>UserModel.removeAsync()<% } + if (filters.sequelizeModels) { %>UserModel.destroy({ where: {} })<% } %> + .then(function() { + <% if (filters.mongooseModels) { %>return UserModel.createAsync(testUser);<% } + if (filters.sequelizeModels) { %>return UserModel.create(testUser);<% } %> + }) + .then(function() { + return login(testUser); + }) + .finally(done); + }); + + describe('with local auth', function() { + + it('should logout a user and redirecting to "/"', function() { + var navbar = require('../../components/navbar/navbar.po'); + + expect(browser.getCurrentUrl()).toBe(config.baseUrl + '/'); + expect(navbar.navbarAccountGreeting.getText()).toBe('Hello ' + testUser.name); + + browser.get(config.baseUrl + '/logout'); + + navbar = require('../../components/navbar/navbar.po'); + + expect(browser.getCurrentUrl()).toBe(config.baseUrl + '/'); + expect(navbar.navbarAccountGreeting.isDisplayed()).toBe(false); + }); + + }); +}); diff --git a/app/templates/e2e/account(auth)/logout/logout.spec(mocha).js b/app/templates/e2e/account(auth)/logout/logout.spec(mocha).js new file mode 100644 index 000000000..1e969ccf2 --- /dev/null +++ b/app/templates/e2e/account(auth)/logout/logout.spec(mocha).js @@ -0,0 +1,54 @@ +'use strict'; + +var config = browser.params;<% if (filters.mongooseModels) { %> +var UserModel = require(config.serverConfig.root + '/server/api/user/user.model');<% } %><% if (filters.sequelizeModels) { %> +var UserModel = require(config.serverConfig.root + '/server/sqldb').User;<% } %> + +describe('Logout View', function() { + var login = function(user) { + browser.get(config.baseUrl + '/login'); + require('../login/login.po').login(user); + }; + + var testUser = { + name: 'Test User', + email: 'test@test.com', + password: 'test' + }; + + beforeEach(function() { + return UserModel + <% if (filters.mongooseModels) { %>.removeAsync()<% } + if (filters.sequelizeModels) { %>.destroy({ where: {} })<% } %> + .then(function() { + <% if (filters.mongooseModels) { %>return UserModel.createAsync(testUser);<% } + if (filters.sequelizeModels) { %>return UserModel.create(testUser);<% } %> + }) + .then(function() { + return login(testUser); + }); + }); + + after(function() { + <% if (filters.mongooseModels) { %>return UserModel.removeAsync();<% } + if (filters.sequelizeModels) { %>return UserModel.destroy({ where: {} });<% } %> + }) + + describe('with local auth', function() { + + it('should logout a user and redirecting to "/"', function() { + var navbar = require('../../components/navbar/navbar.po'); + + <%= does("browser.getCurrentUrl()") %>.eventually.equal(config.baseUrl + '/'); + <%= does("navbar.navbarAccountGreeting.getText()") %>.eventually.equal('Hello ' + testUser.name); + + browser.get(config.baseUrl + '/logout'); + + navbar = require('../../components/navbar/navbar.po'); + + <%= does("browser.getCurrentUrl()") %>.eventually.equal(config.baseUrl + '/'); + <%= does("navbar.navbarAccountGreeting.isDisplayed()") %>.eventually.equal(false); + }); + + }); +}); diff --git a/app/templates/e2e/account(auth)/signup/signup.po.js b/app/templates/e2e/account(auth)/signup/signup.po.js new file mode 100644 index 000000000..3a496c6e3 --- /dev/null +++ b/app/templates/e2e/account(auth)/signup/signup.po.js @@ -0,0 +1,28 @@ +/** + * This file uses the Page Object pattern to define the main page for tests + * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ + */ + +'use strict'; + +var SignupPage = function() { + this.form = element(by.css('.form')); + this.form.name = this.form.element(by.model('user.name')); + this.form.email = this.form.element(by.model('user.email')); + this.form.password = this.form.element(by.model('user.password')); + this.form.submit = this.form.element(by.css('.btn-register')); + + this.signup = function(data) { + for (var prop in data) { + var formElem = this.form[prop]; + if (data.hasOwnProperty(prop) && formElem && typeof formElem.sendKeys === 'function') { + formElem.sendKeys(data[prop]); + } + } + + this.form.submit.click(); + }; +}; + +module.exports = new SignupPage(); + diff --git a/app/templates/e2e/account(auth)/signup/signup.spec(jasmine).js b/app/templates/e2e/account(auth)/signup/signup.spec(jasmine).js new file mode 100644 index 000000000..e0a1230d5 --- /dev/null +++ b/app/templates/e2e/account(auth)/signup/signup.spec(jasmine).js @@ -0,0 +1,64 @@ +'use strict'; + +var config = browser.params;<% if (filters.mongooseModels) { %> +var UserModel = require(config.serverConfig.root + '/server/api/user/user.model');<% } %><% if (filters.sequelizeModels) { %> +var UserModel = require(config.serverConfig.root + '/server/sqldb').User;<% } %> + +describe('Signup View', function() { + var page; + + var loadPage = function() { + browser.manage().deleteAllCookies(); + browser.get(config.baseUrl + '/signup'); + page = require('./signup.po'); + }; + + var testUser = { + name: 'Test', + email: 'test@test.com', + password: 'test' + }; + + beforeEach(function() { + loadPage(); + }); + + it('should include signup form with correct inputs and submit button', function() { + expect(page.form.name.getAttribute('type')).toBe('text'); + expect(page.form.name.getAttribute('name')).toBe('name'); + expect(page.form.email.getAttribute('type')).toBe('email'); + expect(page.form.email.getAttribute('name')).toBe('email'); + expect(page.form.password.getAttribute('type')).toBe('password'); + expect(page.form.password.getAttribute('name')).toBe('password'); + expect(page.form.submit.getAttribute('type')).toBe('submit'); + expect(page.form.submit.getText()).toBe('Sign up'); + }); + + describe('with local auth', function() { + + beforeAll(function(done) { + <% if (filters.mongooseModels) { %>UserModel.removeAsync().then(done);<% } + if (filters.sequelizeModels) { %>UserModel.destroy({ where: {} }).then(done);<% } %> + }); + + it('should signup a new user, log them in, and redirecting to "/"', function() { + page.signup(testUser); + + var navbar = require('../../components/navbar/navbar.po'); + + expect(browser.getCurrentUrl()).toBe(config.baseUrl + '/'); + expect(navbar.navbarAccountGreeting.getText()).toBe('Hello ' + testUser.name); + }); + + it('should indicate signup failures', function() { + page.signup(testUser); + + expect(browser.getCurrentUrl()).toBe(config.baseUrl + '/signup'); + expect(page.form.email.getAttribute('class')).toContain('ng-invalid-mongoose'); + + var helpBlock = page.form.element(by.css('.form-group.has-error .help-block.ng-binding')); + expect(helpBlock.getText()).toBe('The specified email address is already in use.'); + }); + + }); +}); diff --git a/app/templates/e2e/account(auth)/signup/signup.spec(mocha).js b/app/templates/e2e/account(auth)/signup/signup.spec(mocha).js new file mode 100644 index 000000000..c0bade616 --- /dev/null +++ b/app/templates/e2e/account(auth)/signup/signup.spec(mocha).js @@ -0,0 +1,76 @@ +'use strict'; + +var config = browser.params;<% if (filters.mongooseModels) { %> +var UserModel = require(config.serverConfig.root + '/server/api/user/user.model');<% } %><% if (filters.sequelizeModels) { %> +var UserModel = require(config.serverConfig.root + '/server/sqldb').User;<% } %> + +describe('Signup View', function() { + var page; + + var loadPage = function() { + browser.manage().deleteAllCookies() + browser.get(config.baseUrl + '/signup'); + page = require('./signup.po'); + }; + + var testUser = { + name: 'Test', + email: 'test@test.com', + password: 'test' + }; + + before(function() { + return loadPage(); + }); + + after(function() { + <% if (filters.mongooseModels) { %>return UserModel.removeAsync();<% } + if (filters.sequelizeModels) { %>return UserModel.destroy({ where: {} });<% } %> + }); + + it('should include signup form with correct inputs and submit button', function() { + <%= does("page.form.name.getAttribute('type')") %>.eventually.equal('text'); + <%= does("page.form.name.getAttribute('name')") %>.eventually.equal('name'); + <%= does("page.form.email.getAttribute('type')") %>.eventually.equal('email'); + <%= does("page.form.email.getAttribute('name')") %>.eventually.equal('email'); + <%= does("page.form.password.getAttribute('type')") %>.eventually.equal('password'); + <%= does("page.form.password.getAttribute('name')") %>.eventually.equal('password'); + <%= does("page.form.submit.getAttribute('type')") %>.eventually.equal('submit'); + <%= does("page.form.submit.getText()") %>.eventually.equal('Sign up'); + }); + + describe('with local auth', function() { + + before(function() { + <% if (filters.mongooseModels) { %>return UserModel.removeAsync();<% } + if (filters.sequelizeModels) { %>return UserModel.destroy({ where: {} });<% } %> + }) + + it('should signup a new user, log them in, and redirecting to "/"', function() { + page.signup(testUser); + + var navbar = require('../../components/navbar/navbar.po'); + + <%= does("browser.getCurrentUrl()") %>.eventually.equal(config.baseUrl + '/'); + <%= does("navbar.navbarAccountGreeting.getText()") %>.eventually.equal('Hello ' + testUser.name); + }); + + describe('and invalid credentials', function() { + before(function() { + return loadPage(); + }); + + it('should indicate signup failures', function() { + page.signup(testUser); + + <%= does("browser.getCurrentUrl()") %>.eventually.equal(config.baseUrl + '/signup'); + <%= does("page.form.email.getAttribute('class')") %>.eventually.contain('ng-invalid-mongoose'); + + var helpBlock = page.form.element(by.css('.form-group.has-error .help-block.ng-binding')); + <%= does("helpBlock.getText()") %>.eventually.equal('The specified email address is already in use.'); + }); + + }); + + }); +}); diff --git a/app/templates/e2e/components/navbar/navbar.po.js b/app/templates/e2e/components/navbar/navbar.po.js new file mode 100644 index 000000000..80a48418e --- /dev/null +++ b/app/templates/e2e/components/navbar/navbar.po.js @@ -0,0 +1,16 @@ +/** + * This file uses the Page Object pattern to define the main page for tests + * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ + */ + +'use strict'; + +var NavbarComponent = function() { + this.navbar = element(by.css('.navbar')); + this.navbarHeader = this.navbar.element(by.css('.navbar-header')); + this.navbarNav = this.navbar.element(by.css('#navbar-main .nav.navbar-nav:not(.navbar-right)'));<% if (filters.auth) { %> + this.navbarAccount = this.navbar.element(by.css('#navbar-main .nav.navbar-nav.navbar-right')); + this.navbarAccountGreeting = this.navbarAccount.element(by.binding('getCurrentUser().name'));<% } %> +}; + +module.exports = new NavbarComponent(); diff --git a/app/templates/e2e/main/main.spec.js b/app/templates/e2e/main/main.spec(jasmine).js similarity index 70% rename from app/templates/e2e/main/main.spec.js rename to app/templates/e2e/main/main.spec(jasmine).js index 61745a8de..57284495a 100644 --- a/app/templates/e2e/main/main.spec.js +++ b/app/templates/e2e/main/main.spec(jasmine).js @@ -1,16 +1,18 @@ 'use strict'; +var config = browser.params; + describe('Main View', function() { var page; beforeEach(function() { - browser.get('/'); + browser.get(config.baseUrl + '/'); page = require('./main.po'); }); it('should include jumbotron with correct data', function() { expect(page.h1El.getText()).toBe('\'Allo, \'Allo!'); - expect(page.imgEl.getAttribute('src')).toMatch(/assets\/images\/yeoman.png$/); + expect(page.imgEl.getAttribute('src')).toMatch(/yeoman.png$/); expect(page.imgEl.getAttribute('alt')).toBe('I\'m Yeoman'); }); }); diff --git a/app/templates/e2e/main/main.spec(mocha).js b/app/templates/e2e/main/main.spec(mocha).js new file mode 100644 index 000000000..4ea5c1012 --- /dev/null +++ b/app/templates/e2e/main/main.spec(mocha).js @@ -0,0 +1,18 @@ +'use strict'; + +var config = browser.params; + +describe('Main View', function() { + var page; + + beforeEach(function() { + browser.get(config.baseUrl + '/'); + page = require('./main.po'); + }); + + it('should include jumbotron with correct data', function() { + <%= does("page.h1El.getText()") %>.eventually.equal('\'Allo, \'Allo!'); + <%= does("page.imgEl.getAttribute('src')") %>.eventually.match(/yeoman.png$/); + <%= does("page.imgEl.getAttribute('alt')") %>.eventually.equal('I\'m Yeoman'); + }); +}); diff --git a/app/templates/karma.conf.js b/app/templates/karma.conf.js index e7307a90a..9b46a3a22 100644 --- a/app/templates/karma.conf.js +++ b/app/templates/karma.conf.js @@ -6,22 +6,21 @@ module.exports = function(config) { // base path, that will be used to resolve files and exclude basePath: '', - // testing framework to use (jasmine/mocha/qunit/...) - frameworks: ['jasmine'], + // testing framework to use (jasmine/mocha/qunit/...)<% if (filters.jasmine) { %> + frameworks: ['jasmine'],<% } if (filters.mocha) { %> + frameworks: ['mocha', 'chai', 'sinon-chai', 'chai-as-promised', 'chai-things'], + + client: { + mocha: { + timeout: 5000 // set default mocha spec timeout + } + },<% } %> // list of files / patterns to load in the browser files: [ - 'client/bower_components/jquery/dist/jquery.js', - 'client/bower_components/angular/angular.js', - 'client/bower_components/angular-mocks/angular-mocks.js', - 'client/bower_components/angular-resource/angular-resource.js', - 'client/bower_components/angular-cookies/angular-cookies.js', - 'client/bower_components/angular-sanitize/angular-sanitize.js', - 'client/bower_components/angular-route/angular-route.js',<% if(filters.uibootstrap) { %> - 'client/bower_components/angular-bootstrap/ui-bootstrap-tpls.js',<% } %> - 'client/bower_components/lodash/dist/lodash.compat.js',<% if(filters.socketio) { %> - 'client/bower_components/angular-socket-io/socket.js',<% } %><% if(filters.uirouter) { %> - 'client/bower_components/angular-ui-router/release/angular-ui-router.js',<% } %> + // bower:js + // endbower<% if (filters.socketio) { %> + 'node_modules/socket.io-client/socket.io.js',<% } %> 'client/app/app.js', 'client/app/app.coffee', 'client/app/**/*.js', @@ -37,7 +36,7 @@ module.exports = function(config) { preprocessors: { '**/*.jade': 'ng-jade2js', '**/*.html': 'html2js',<% if(filters.babel) { %> - 'client/app/**/*.js': 'babel',<% } %> + 'client/{app,components}/**/*.js': 'babel',<% } %> '**/*.coffee': 'coffee', }, @@ -73,6 +72,14 @@ module.exports = function(config) { // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG logLevel: config.LOG_INFO, + // reporter types: + // - dots + // - progress (default) + // - spec (karma-spec-reporter) + // - junit + // - growl + // - coverage + reporters: ['spec'], // enable / disable watching file and executing tests whenever any file changes autoWatch: false, diff --git a/app/templates/mocha.conf.js b/app/templates/mocha.conf.js new file mode 100644 index 000000000..76f56625e --- /dev/null +++ b/app/templates/mocha.conf.js @@ -0,0 +1,19 @@ +'use strict'; + +// Register the Babel require hook +require('babel-core/register'); + +var chai = require('chai'); + +// Load Chai assertions +global.expect = chai.expect; +global.assert = chai.assert; +chai.should(); + +// Load Sinon +global.sinon = require('sinon'); + +// Initialize Chai plugins +chai.use(require('sinon-chai')); +chai.use(require('chai-as-promised')); +chai.use(require('chai-things')) diff --git a/app/templates/protractor.conf.js b/app/templates/protractor.conf.js index cb66c67c1..6178c1a76 100644 --- a/app/templates/protractor.conf.js +++ b/app/templates/protractor.conf.js @@ -3,7 +3,7 @@ 'use strict'; -exports.config = { +var config = { // The timeout for each script run on the browser. This should be longer // than the maximum time your application needs to stabilize between tasks. allScriptsTimeout: 110000, @@ -12,9 +12,10 @@ exports.config = { // with relative paths will be prepended with this. baseUrl: 'http://localhost:' + (process.env.PORT || '9000'), - // If true, only chromedriver will be started, not a standalone selenium. - // Tests for browsers other than chrome will not run. - chromeOnly: true, + // Credientials for Saucelabs + sauceUser: process.env.SAUCE_USERNAME, + + sauceKey: process.env.SAUCE_ACCESS_KEY, // list of files / patterns to load in the browser specs: [ @@ -31,7 +32,10 @@ exports.config = { // and // https://code.google.com/p/selenium/source/browse/javascript/webdriver/capabilities.js capabilities: { - 'browserName': 'chrome' + 'browserName': 'chrome', + 'name': 'Fullstack E2E', + 'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER, + 'build': process.env.TRAVIS_BUILD_NUMBER }, // ----- The test framework ----- @@ -39,12 +43,49 @@ exports.config = { // Jasmine and Cucumber are fully supported as a test and assertion framework. // Mocha has limited beta support. You will need to include your own // assertion framework if working with mocha. - framework: 'jasmine', - + framework: '<% if (filters.jasmine) { %>jasmine2<% } if (filters.mocha) { %>mocha<% } %>', +<% if (filters.jasmine) { %> // ----- Options to be passed to minijasminenode ----- // - // See the full list at https://github.com/juliemr/minijasminenode + // See the full list at https://github.com/jasmine/jasmine-npm jasmineNodeOpts: { + defaultTimeoutInterval: 30000, + print: function() {} // for jasmine-spec-reporter + },<% } if (filters.mocha) { %> + // ----- Options to be passed to mocha ----- + mochaOpts: { + reporter: 'spec', + timeout: 30000, defaultTimeoutInterval: 30000 + },<% } %> + + // Prepare environment for tests + params: { + serverConfig: require('./server/config/environment') + }, + + onPrepare: function() {<% if (filters.mocha) { %> + // Load Mocha and Chai + plugins + require('./mocha.conf'); + + // Expose should assertions (see https://github.com/angular/protractor/issues/633) + Object.defineProperty( + protractor.promise.Promise.prototype, + 'should', + Object.getOwnPropertyDescriptor(Object.prototype, 'should') + ); +<% } if (filters.jasmine) { %> + var SpecReporter = require('jasmine-spec-reporter'); + // add jasmine spec reporter + jasmine.getEnv().addReporter(new SpecReporter({displayStacktrace: true})); +<% } %> + var serverConfig = config.params.serverConfig;<% if (filters.mongoose) { %> + + // Setup mongo for tests + var mongoose = require('mongoose'); + mongoose.connect(serverConfig.mongo.uri, serverConfig.mongo.options); // Connect to database<% } %> } }; + +config.params.baseUrl = config.baseUrl; +exports.config = config; diff --git a/app/templates/server/.jshintrc b/app/templates/server/.jshintrc index 66d1af7c9..69f3b00e3 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/.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 deleted file mode 100644 index 242ed5901..000000000 --- a/app/templates/server/api/thing/index.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -var express = require('express'); -var controller = require('./thing.controller'); - -var router = express.Router(); - -router.get('/', controller.index);<% if(filters.mongoose) { %> -router.get('/:id', controller.show); -router.post('/', controller.create); -router.put('/:id', controller.update); -router.patch('/:id', controller.update); -router.delete('/:id', controller.destroy);<% } %> - -module.exports = router; \ No newline at end of file diff --git a/app/templates/server/api/thing/thing.controller.js b/app/templates/server/api/thing/thing.controller.js deleted file mode 100644 index 0adc6211c..000000000 --- a/app/templates/server/api/thing/thing.controller.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Using Rails-like standard naming convention for endpoints. - * GET /things -> index - * POST /things -> create - * GET /things/:id -> show - * PUT /things/:id -> update - * DELETE /things/:id -> destroy - */ - -'use strict'; - -var _ = require('lodash');<% if (filters.mongoose) { %> -var Thing = require('./thing.model');<% } %> - -// Get list of things -exports.index = function(req, res) {<% if (!filters.mongoose) { %> - res.json([ - { - 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.mongoose) { %> - Thing.find(function (err, things) { - if(err) { return handleError(res, err); } - return res.status(200).json(things); - });<% } %> -};<% 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.status(404).send('Not Found'); } - return res.json(thing); - }); -}; - -// 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.status(201).json(thing); - }); -}; - -// 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.status(404).send('Not Found'); } - var updated = _.merge(thing, req.body); - updated.save(function (err) { - if (err) { return handleError(res, err); } - return res.status(200).json(thing); - }); - }); -}; - -// 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.status(404).send('Not Found'); } - thing.remove(function(err) { - if(err) { return handleError(res, err); } - return res.status(204).send('No Content'); - }); - }); -}; - -function handleError(res, err) { - return res.status(500).send(err); -}<% } %> \ No newline at end of file diff --git a/app/templates/server/api/thing/thing.model(mongoose).js b/app/templates/server/api/thing/thing.model(mongoose).js deleted file mode 100644 index ed857cd3b..000000000 --- a/app/templates/server/api/thing/thing.model(mongoose).js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -var mongoose = require('mongoose'), - Schema = mongoose.Schema; - -var ThingSchema = new Schema({ - name: String, - info: String, - active: Boolean -}); - -module.exports = mongoose.model('Thing', ThingSchema); \ No newline at end of file diff --git a/app/templates/server/api/thing/thing.socket(socketio).js b/app/templates/server/api/thing/thing.socket(socketio).js deleted file mode 100644 index 79d327695..000000000 --- a/app/templates/server/api/thing/thing.socket(socketio).js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Broadcast updates to client when the model changes - */ - -'use strict'; - -var thing = require('./thing.model'); - -exports.register = function(socket) { - thing.schema.post('save', function (doc) { - onSave(socket, doc); - }); - thing.schema.post('remove', function (doc) { - onRemove(socket, doc); - }); -} - -function onSave(socket, doc, cb) { - socket.emit('thing:save', doc); -} - -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 17c8c6cd0..000000000 --- a/app/templates/server/api/thing/thing.spec.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -var should = require('should'); -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..d2ee914bd --- /dev/null +++ b/app/templates/server/api/user(auth)/index.spec.js @@ -0,0 +1,107 @@ +'use strict'; + +var proxyquire = require('proxyquire').noPreserveCache(); + +var userCtrlStub = { + index: 'userCtrl.index', + destroy: 'userCtrl.destroy', + me: 'userCtrl.me', + changePassword: 'userCtrl.changePassword', + show: 'userCtrl.show', + create: 'userCtrl.create' +}; + +var authServiceStub = { + isAuthenticated: function() { + return 'authService.isAuthenticated'; + }, + hasRole: function(role) { + return 'authService.hasRole.' + role; + } +}; + +var routerStub = { + get: sinon.spy(), + put: sinon.spy(), + post: sinon.spy(), + delete: sinon.spy() +}; + +// require the index with our stubbed out modules +var userIndex = proxyquire('./index', { + 'express': { + Router: function() { + return routerStub; + } + }, + './user.controller': userCtrlStub, + '../../auth/auth.service': authServiceStub +}); + +describe('User API Router:', function() { + + it('should return an express router instance', function() { + userIndex.should.equal(routerStub); + }); + + describe('GET /api/users', function() { + + it('should verify admin role and route to user.controller.index', function() { + routerStub.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() { + routerStub.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() { + routerStub.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() { + routerStub.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() { + routerStub.get + .withArgs('/:id', 'authService.isAuthenticated', 'userCtrl.show') + .should.have.been.calledOnce; + }); + + }); + + describe('POST /api/users', function() { + + it('should route to user.controller.create', function() { + routerStub.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 585e47b67..f1c2498fb 100644 --- a/app/templates/server/api/user(auth)/user.controller.js +++ b/app/templates/server/api/user(auth)/user.controller.js @@ -1,50 +1,98 @@ 'use strict'; - -var User = require('./user.model'); +<% if (filters.mongooseModels) { %> +var User = require('./user.model');<% } %><% if (filters.sequelizeModels) { %> +var _ = require('lodash'); +var sqldb = require('../../sqldb'); +var User = sqldb.User;<% } %> var passport = require('passport'); var config = require('../../config/environment'); var jwt = require('jsonwebtoken'); -var validationError = function(res, err) { - return res.status(422).json(err); -}; +function validationError(res, statusCode) { + statusCode = statusCode || 422; + return function(err) { + res.status(statusCode).json(err); + } +} + +function handleError(res, statusCode) { + statusCode = statusCode || 500; + return function(err) { + res.status(statusCode).send(err); + }; +} + +function respondWith(res, statusCode) { + statusCode = statusCode || 200; + return function() { + res.status(statusCode).end(); + }; +} /** * Get list of users * restriction: 'admin' */ exports.index = function(req, res) { - User.find({}, '-salt -hashedPassword', function (err, users) { - if(err) return res.status(500).send(err); - res.status(200).json(users); - }); + <% if (filters.mongooseModels) { %>User.findAsync({}, '-salt -hashedPassword')<% } + if (filters.sequelizeModels) { %>User.findAll({ + attributes: [ + '_id', + 'name', + 'email', + 'role', + 'provider' + ] + })<% } %> + .then(function(users) { + res.status(200).json(users); + }) + .catch(handleError(res)); }; /** * Creates a new user */ -exports.create = function (req, res, next) { - var newUser = new User(req.body); +exports.create = function(req, res, next) { + <% if (filters.mongooseModels) { %>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()<% } + if (filters.sequelizeModels) { %>var newUser = User.build(req.body); + newUser.setDataValue('provider', 'local'); + newUser.setDataValue('role', 'user'); + newUser.save()<% } %> + <% if (filters.mongooseModels) { %>.spread(function(user) {<% } + if (filters.sequelizeModels) { %>.then(function(user) {<% } %> + var token = jwt.sign({ _id: user._id }, config.secrets.session, { + expiresInMinutes: 60 * 5 + }); + res.json({ token: token }); + }) + .catch(validationError(res)); }; /** * Get a single user */ -exports.show = 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.status(401).send('Unauthorized'); - res.json(user.profile); - }); + <% if (filters.mongooseModels) { %>User.findByIdAsync(userId)<% } + if (filters.sequelizeModels) { %>User.find({ + where: { + _id: userId + } + })<% } %> + .then(function(user) { + if (!user) { + return res.status(404).end(); + } + res.json(user.profile); + }) + .catch(function(err) { + return next(err); + }); }; /** @@ -52,10 +100,12 @@ 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.status(500).send(err); - return res.status(204).send('No Content'); - }); + <% if (filters.mongooseModels) { %>User.findByIdAndRemoveAsync(req.params.id)<% } + if (filters.sequelizeModels) { %>User.destroy({ _id: req.params.id })<% } %> + .then(function() { + res.status(204).end(); + }) + .catch(handleError(res)); }; /** @@ -66,17 +116,25 @@ exports.changePassword = function(req, res, next) { var oldPass = String(req.body.oldPassword); 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.status(200).send('OK'); - }); - } else { - res.status(403).send('Forbidden'); + <% if (filters.mongooseModels) { %>User.findByIdAsync(userId)<% } + if (filters.sequelizeModels) { %>User.find({ + where: { + _id: userId } - }); + })<% } %> + .then(function(user) { + if (user.authenticate(oldPass)) { + user.password = newPass; + <% if (filters.mongooseModels) { %>return user.saveAsync()<% } + if (filters.sequelizeModels) { %>return user.save()<% } %> + .then(function() { + res.status(204).end(); + }) + .catch(validationError(res)); + } else { + return res.status(403).end(); + } + }); }; /** @@ -84,13 +142,29 @@ exports.changePassword = function(req, res, next) { */ 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 - if (err) return next(err); - if (!user) return res.status(401).send('Unauthorized'); - res.json(user); - }); + + <% if (filters.mongooseModels) { %>User.findOneAsync({ _id: userId }, '-salt -hashedPassword')<% } + if (filters.sequelizeModels) { %>User.find({ + where: { + _id: userId + }, + attributes: [ + '_id', + 'name', + 'email', + 'role', + 'provider' + ] + })<% } %> + .then(function(user) { // don't ever give out the password or salt + if (!user) { + return res.status(401).end(); + } + res.json(user); + }) + .catch(function(err) { + return next(err); + }); }; /** diff --git a/app/templates/server/api/user(auth)/user.events.js b/app/templates/server/api/user(auth)/user.events.js new file mode 100644 index 000000000..102fd5d55 --- /dev/null +++ b/app/templates/server/api/user(auth)/user.events.js @@ -0,0 +1,41 @@ +/** + * User model events + */ + +'use strict'; + +var EventEmitter = require('events').EventEmitter;<% if (filters.mongooseModels) { %> +var User = require('./user.model');<% } if (filters.sequelizeModels) { %> +var User = require('../../sqldb').User;<% } %> +var UserEvents = new EventEmitter(); + +// Set max event listeners (0 == unlimited) +UserEvents.setMaxListeners(0); + +// Model events<% if (filters.mongooseModels) { %> +var events = { + 'save': 'save', + 'remove': 'remove' +};<% } if (filters.sequelizeModels) { %> +var events = { + 'afterCreate': 'save', + 'afterUpdate': 'save', + 'afterDestroy': 'remove' +};<% } %> + +// Register the event emitter to the model events +for (var e in events) { + var event = events[e];<% if (filters.mongooseModels) { %> + User.schema.post(e, emitEvent(event));<% } if (filters.sequelizeModels) { %> + User.hook(e, emitEvent(event));<% } %> +} + +function emitEvent(event) { + return function(doc<% if (filters.sequelizeModels) { %>, options, done<% } %>) { + UserEvents.emit(event + ':' + doc._id, doc); + UserEvents.emit(event, doc);<% if (filters.sequelizeModels) { %> + done(null);<% } %> + } +} + +module.exports = UserEvents; diff --git a/app/templates/server/api/user(auth)/user.integration.js b/app/templates/server/api/user(auth)/user.integration.js new file mode 100644 index 000000000..19978ce48 --- /dev/null +++ b/app/templates/server/api/user(auth)/user.integration.js @@ -0,0 +1,70 @@ +'use strict'; + +var app = require('../..');<% if (filters.mongooseModels) { %> +var User = require('./user.model');<% } %><% if (filters.sequelizeModels) { %> +var User = require('../../sqldb').User;<% } %> +var request = require('supertest'); + +describe('User API:', function() { + var user; + + // Clear users before testing + before(function() { + return <% if (filters.mongooseModels) { %>User.removeAsync().then(function() {<% } + if (filters.sequelizeModels) { %>User.destroy({ where: {} }).then(function() {<% } %> + <% if (filters.mongooseModels) { %>user = new User({<% } + if (filters.sequelizeModels) { %>user = User.build({<% } %> + name: 'Fake User', + email: 'test@test.com', + password: 'password' + }); + + return <% if (filters.mongooseModels) { %>user.saveAsync();<% } + if (filters.sequelizeModels) { %>user.save();<% } %> + }); + }); + + // Clear users after testing + after(function() { + <% if (filters.mongooseModels) { %>return User.removeAsync();<% } + if (filters.sequelizeModels) { %>return User.destroy({ where: {} });<% } %> + }); + + 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.toString().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(mongooseModels).js b/app/templates/server/api/user(auth)/user.model(mongooseModels).js new file mode 100644 index 000000000..f8fa923cf --- /dev/null +++ b/app/templates/server/api/user(auth)/user.model(mongooseModels).js @@ -0,0 +1,228 @@ +'use strict'; + +var mongoose = require('bluebird').promisifyAll(require('mongoose')); +var Schema = mongoose.Schema; +var crypto = require('crypto');<% if (filters.oauth) { %> +var authTypes = ['github', 'twitter', 'facebook', 'google'];<% } %> + +var UserSchema = new Schema({ + name: String, + email: { + type: String, + lowercase: true + }, + role: { + type: String, + default: 'user' + }, + password: String, + provider: String, + salt: String<% if (filters.oauth) { %>,<% if (filters.facebookAuth) { %> + facebook: {},<% } %><% if (filters.twitterAuth) { %> + twitter: {},<% } %><% if (filters.googleAuth) { %> + google: {},<% } %> + github: {}<% } %> +}); + +/** + * Virtuals + */ + +// Public profile information +UserSchema + .virtual('profile') + .get(function() { + return { + 'name': this.name, + 'role': this.role + }; + }); + +// Non-sensitive info we'll be putting in the token +UserSchema + .virtual('token') + .get(function() { + return { + '_id': this._id, + 'role': this.role + }; + }); + +/** + * Validations + */ + +// Validate empty email +UserSchema + .path('email') + .validate(function(email) {<% if (filters.oauth) { %> + if (authTypes.indexOf(this.provider) !== -1) { + return true; + }<% } %> + return email.length; + }, 'Email cannot be blank'); + +// Validate empty password +UserSchema + .path('password') + .validate(function(password) {<% if (filters.oauth) { %> + if (authTypes.indexOf(this.provider) !== -1) { + return true; + }<% } %> + return password.length; + }, 'Password cannot be blank'); + +// Validate email is not taken +UserSchema + .path('email') + .validate(function(value, respond) { + var self = this; + 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) { + return value && value.length; +}; + +/** + * Pre-save hook + */ +UserSchema + .pre('save', function(next) { + // Handle new/update passwords + if (this.isModified('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; + _this.encryptPassword(_this.password, function(encryptErr, hashedPassword) { + if (encryptErr) { + next(encryptErr); + } + _this.password = hashedPassword; + next(); + }); + }); + } else { + next(); + } + }); + +/** + * Methods + */ +UserSchema.methods = { + /** + * Authenticate - check if the passwords are the same + * + * @param {String} password + * @param {Function} callback + * @return {Boolean} + * @api public + */ + 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 + * @param {Function} callback + * @return {String} + * @api public + */ + 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 + * @param {Function} callback + * @return {String} + * @api public + */ + encryptPassword: function(password, callback) { + if (!password || !this.salt) { + return null; + } + + var defaultIterations = 10000; + var defaultKeyLength = 64; + var salt = new Buffer(this.salt, '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')); + }); + } +}; + +module.exports = mongoose.model('User', UserSchema); diff --git a/app/templates/server/api/user(auth)/user.model(sequelizeModels).js b/app/templates/server/api/user(auth)/user.model(sequelizeModels).js new file mode 100644 index 000000000..776eafc3e --- /dev/null +++ b/app/templates/server/api/user(auth)/user.model(sequelizeModels).js @@ -0,0 +1,236 @@ +'use strict'; + +var crypto = require('crypto');<% if (filters.oauth) { %> +var authTypes = ['github', 'twitter', 'facebook', 'google'];<% } %> + +var validatePresenceOf = function(value) { + return value && value.length; +}; + +module.exports = function(sequelize, DataTypes) { + var User = sequelize.define('User', { + + _id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true + }, + name: DataTypes.STRING, + email: { + type: DataTypes.STRING, + unique: { + msg: 'The specified email address is already in use.' + }, + validate: { + isEmail: true + } + }, + role: { + type: DataTypes.STRING, + defaultValue: 'user' + }, + password: { + type: DataTypes.STRING, + validate: { + notEmpty: true + } + }, + provider: DataTypes.STRING, + salt: DataTypes.STRING<% if (filters.oauth) { %>,<% if (filters.facebookAuth) { %> + facebook: DataTypes.TEXT,<% } %><% if (filters.twitterAuth) { %> + twitter: DataTypes.TEXT,<% } %><% if (filters.googleAuth) { %> + google: DataTypes.TEXT,<% } %> + github: DataTypes.TEXT<% } %> + + }, { + + /** + * Virtual Getters + */ + getterMethods: { + // Public profile information + profile: function() { + return { + 'name': this.name, + 'role': this.role + }; + }, + + // Non-sensitive info we'll be putting in the token + token: function() { + return { + '_id': this._id, + 'role': this.role + }; + } + }, + + /** + * Pre-save hooks + */ + hooks: { + beforeBulkCreate: function(users, fields, fn) { + var totalUpdated = 0; + users.forEach(function(user) { + user.updatePassword(function(err) { + if (err) { + return fn(err); + } + totalUpdated += 1; + if (totalUpdated === users.length) { + return fn(); + } + }); + }); + }, + beforeCreate: function(user, fields, fn) { + user.updatePassword(fn); + }, + beforeUpdate: function(user, fields, fn) { + if (user.changed('password')) { + return user.updatePassword(fn); + } + fn(); + } + }, + + /** + * Instance Methods + */ + instanceMethods: { + /** + * Authenticate - check if the passwords are the same + * + * @param {String} password + * @param {Function} callback + * @return {Boolean} + * @api public + */ + 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 + * @param {Function} callback + * @return {String} + * @api public + */ + 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 + * @param {Function} callback + * @return {String} + * @api public + */ + encryptPassword: function(password, callback) { + if (!password || !this.salt) { + if (!callback) { + return null; + } + return callback(null); + } + + var defaultIterations = 10000; + var defaultKeyLength = 64; + var salt = new Buffer(this.salt, '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')); + }); + }, + + /** + * Update password field + * + * @param {Function} fn + * @return {String} + * @api public + */ + updatePassword: function(fn) { + // Handle new/update passwords + if (this.password) { + if (!validatePresenceOf(this.password)<% if (filters.oauth) { %> && authTypes.indexOf(this.provider) === -1<% } %>) { + fn(new Error('Invalid password')); + } + + // Make salt with a callback + var _this = this; + this.makeSalt(function(saltErr, salt) { + if (saltErr) { + fn(saltErr); + } + _this.salt = salt; + _this.encryptPassword(_this.password, function(encryptErr, hashedPassword) { + if (encryptErr) { + fn(encryptErr); + } + _this.password = hashedPassword; + fn(null); + }); + }); + } else { + fn(null); + } + } + } + }); + + return User; +}; diff --git a/app/templates/server/api/user(auth)/user.model.js b/app/templates/server/api/user(auth)/user.model.js deleted file mode 100644 index cc8d59263..000000000 --- a/app/templates/server/api/user(auth)/user.model.js +++ /dev/null @@ -1,149 +0,0 @@ -'use strict'; - -var mongoose = require('mongoose'); -var Schema = mongoose.Schema; -var crypto = require('crypto');<% if(filters.oauth) { %> -var authTypes = ['github', 'twitter', 'facebook', 'google'];<% } %> - -var UserSchema = new Schema({ - name: String, - email: { type: String, lowercase: true }, - role: { - type: String, - default: 'user' - }, - hashedPassword: String, - provider: String, - salt: String<% if (filters.oauth) { %>,<% if (filters.facebookAuth) { %> - facebook: {},<% } %><% if (filters.twitterAuth) { %> - twitter: {},<% } %><% if (filters.googleAuth) { %> - google: {},<% } %> - github: {}<% } %> -}); - -/** - * 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 - .virtual('profile') - .get(function() { - return { - 'name': this.name, - 'role': this.role - }; - }); - -// Non-sensitive info we'll be putting in the token -UserSchema - .virtual('token') - .get(function() { - return { - '_id': this._id, - 'role': this.role - }; - }); - -/** - * Validations - */ - -// Validate empty email -UserSchema - .path('email') - .validate(function(email) {<% if (filters.oauth) { %> - if (authTypes.indexOf(this.provider) !== -1) return true;<% } %> - return email.length; - }, 'Email cannot be blank'); - -// Validate empty password -UserSchema - .path('hashedPassword') - .validate(function(hashedPassword) {<% if (filters.oauth) { %> - if (authTypes.indexOf(this.provider) !== -1) return true;<% } %> - return hashedPassword.length; - }, 'Password cannot be blank'); - -// Validate email is not taken -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); - }); -}, 'The specified email address is already in use.'); - -var validatePresenceOf = function(value) { - return value && value.length; -}; - -/** - * Pre-save hook - */ -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 - next(); - }); - -/** - * Methods - */ -UserSchema.methods = { - /** - * Authenticate - check if the passwords are the same - * - * @param {String} plainText - * @return {Boolean} - * @api public - */ - authenticate: function(plainText) { - return this.encryptPassword(plainText) === this.hashedPassword; - }, - - /** - * Make salt - * - * @return {String} - * @api public - */ - makeSalt: function() { - return crypto.randomBytes(16).toString('base64'); - }, - - /** - * Encrypt password - * - * @param {String} password - * @return {String} - * @api public - */ - encryptPassword: function(password) { - if (!password || !this.salt) return ''; - var salt = new Buffer(this.salt, 'base64'); - return crypto.pbkdf2Sync(password, salt, 10000, 64).toString('base64'); - } -}; - -module.exports = mongoose.model('User', UserSchema); diff --git a/app/templates/server/api/user(auth)/user.model.spec(mongooseModels).js b/app/templates/server/api/user(auth)/user.model.spec(mongooseModels).js new file mode 100644 index 000000000..099e4d5c6 --- /dev/null +++ b/app/templates/server/api/user(auth)/user.model.spec(mongooseModels).js @@ -0,0 +1,72 @@ +'use strict'; + +var app = require('../..'); +var User = require('./user.model'); +var user; +var genUser = function() { + user = new User({ + provider: 'local', + name: 'Fake User', + email: 'test@test.com', + password: 'password' + }); + return user; +}; + +describe('User Model', function() { + before(function() { + // Clear users before testing + return User.removeAsync(); + }); + + beforeEach(function() { + genUser(); + }); + + afterEach(function() { + return User.removeAsync(); + }); + + it('should begin with no users', function() { + return User.findAsync({}) + .should.eventually.have.length(0); + }); + + it('should fail when saving a duplicate user', function() { + return user.saveAsync() + .then(function() { + var userDup = genUser(); + return userDup.saveAsync(); + }).should.be.rejected; + }); + + describe('#email', function() { + it('should fail when saving without an email', function() { + user.email = ''; + return user.saveAsync().should.be.rejected; + }); + }); + + describe('#password', function() { + beforeEach(function() { + return user.saveAsync(); + }); + + it('should authenticate user if valid', function() { + user.authenticate('password').should.be.true; + }); + + it('should not authenticate user if invalid', function() { + user.authenticate('blah').should.not.be.true; + }); + + it('should remain the same hash unless the password is updated', function() { + user.name = 'Test User'; + return user.saveAsync() + .spread(function(u) { + return u.authenticate('password'); + }).should.eventually.be.true; + }); + }); + +}); diff --git a/app/templates/server/api/user(auth)/user.model.spec(sequelizeModels).js b/app/templates/server/api/user(auth)/user.model.spec(sequelizeModels).js new file mode 100644 index 000000000..7e0ca0cc4 --- /dev/null +++ b/app/templates/server/api/user(auth)/user.model.spec(sequelizeModels).js @@ -0,0 +1,74 @@ +'use strict'; + +var app = require('../..'); +var User = require('../../sqldb').User; +var user; +var genUser = function() { + user = User.build({ + provider: 'local', + name: 'Fake User', + email: 'test@test.com', + password: 'password' + }); + return user; +}; + +describe('User Model', function() { + before(function() { + // Sync and clear users before testing + return User.sync().then(function() { + return User.destroy({ where: {} }); + }); + }); + + beforeEach(function() { + genUser(); + }); + + afterEach(function() { + return User.destroy({ where: {} }); + }); + + it('should begin with no users', function() { + return User.findAll() + .should.eventually.have.length(0); + }); + + it('should fail when saving a duplicate user', function() { + return user.save() + .then(function() { + var userDup = genUser(); + return userDup.save(); + }).should.be.rejected; + }); + + describe('#email', function() { + it('should fail when saving without an email', function() { + user.email = ''; + return user.save().should.be.rejected; + }); + }); + + describe('#password', function() { + beforeEach(function() { + return user.save(); + }); + + it('should authenticate user if valid', function() { + user.authenticate('password').should.be.true; + }); + + it('should not authenticate user if invalid', function() { + user.authenticate('blah').should.not.be.true; + }); + + it('should remain the same hash unless the password is updated', function() { + user.name = 'Test User'; + return user.save() + .then(function(u) { + return u.authenticate('password'); + }).should.eventually.be.true; + }); + }); + +}); diff --git a/app/templates/server/api/user(auth)/user.model.spec.js b/app/templates/server/api/user(auth)/user.model.spec.js deleted file mode 100644 index 257c95b7c..000000000 --- a/app/templates/server/api/user(auth)/user.model.spec.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -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() { - before(function(done) { - // Clear users before testing - User.remove().exec().then(function() { - done(); - }); - }); - - afterEach(function(done) { - User.remove().exec().then(function() { - done(); - }); - }); - - it('should begin with no users', function(done) { - User.find({}, function(err, users) { - users.should.have.length(0); - done(); - }); - }); - - it('should fail when saving a duplicate user', function(done) { - user.save(function() { - var userDup = new User(user); - userDup.save(function(err) { - should.exist(err); - done(); - }); - }); - }); - - it('should fail when saving without an email', function(done) { - user.email = ''; - user.save(function(err) { - should.exist(err); - done(); - }); - }); - - 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/app/templates/server/app.js b/app/templates/server/app.js index f677d7a43..ca1e4ca78 100644 --- a/app/templates/server/app.js +++ b/app/templates/server/app.js @@ -8,20 +8,21 @@ process.env.NODE_ENV = process.env.NODE_ENV || 'development'; var express = require('express');<% if (filters.mongoose) { %> -var mongoose = require('mongoose');<% } %> +var mongoose = require('mongoose');<% } %><% if (filters.sequelize) { %> +var sqldb = require('./sqldb');<% } %> var config = require('./config/environment'); <% if (filters.mongoose) { %> -// Connect to database +// Connect to MongoDB mongoose.connect(config.mongo.uri, config.mongo.options); mongoose.connection.on('error', function(err) { - console.error('MongoDB connection error: ' + err); - process.exit(-1); - } -); -// Populate DB with sample data -if(config.seedDB) { require('./config/seed'); } - -<% } %>// Setup server + console.error('MongoDB connection error: ' + err); + process.exit(-1); +}); +<% } %><% if (filters.models) { %> +// Populate databases with sample data +if (config.seedDB) { require('./config/seed'); } +<% } %> +// Setup server var app = express(); var server = require('http').createServer(app);<% if (filters.socketio) { %> var socketio = require('socket.io')(server, { @@ -33,9 +34,19 @@ require('./config/express')(app); require('./routes')(app); // Start server -server.listen(config.port, config.ip, function () { - console.log('Express server listening on %d, in %s mode', config.port, app.get('env')); -}); - +function startServer() { + server.listen(config.port, config.ip, function() { + console.log('Express server listening on %d, in %s mode', config.port, app.get('env')); + }); +} +<% if (filters.sequelize) { %> +sqldb.sequelize.sync() + .then(startServer) + .catch(function(err) { + console.log('Server failed to start due to error: %s', err); + }); +<% } else { %> +setImmediate(startServer); +<% } %> // Expose app 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 370dac51e..8c100e84b 100644 --- a/app/templates/server/auth(auth)/auth.service.js +++ b/app/templates/server/auth(auth)/auth.service.js @@ -1,13 +1,15 @@ 'use strict'; -var mongoose = require('mongoose'); var passport = require('passport'); var config = require('../config/environment'); var jwt = require('jsonwebtoken'); var expressJwt = require('express-jwt'); -var compose = require('composable-middleware'); -var User = require('../api/user/user.model'); -var validateJwt = expressJwt({ secret: config.secrets.session }); +var compose = require('composable-middleware');<% if (filters.mongooseModels) { %> +var User = require('../api/user/user.model');<% } %><% if (filters.sequelizeModels) { %> +var User = require('../sqldb').User;<% } %> +var validateJwt = expressJwt({ + secret: config.secrets.session +}); /** * Attaches the user object to the request if authenticated @@ -18,20 +20,29 @@ function isAuthenticated() { // Validate jwt .use(function(req, res, next) { // allow access_token to be passed through query parameter as well - if(req.query && req.query.hasOwnProperty('access_token')) { + if (req.query && req.query.hasOwnProperty('access_token')) { req.headers.authorization = 'Bearer ' + req.query.access_token; } validateJwt(req, res, next); }) // 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.status(401).send('Unauthorized'); - - req.user = user; - next(); - }); + <% if (filters.mongooseModels) { %>User.findByIdAsync(req.user._id)<% } + if (filters.sequelizeModels) { %>User.find({ + where: { + _id: req.user._id + } + })<% } %> + .then(function(user) { + if (!user) { + return res.status(401).end(); + } + req.user = user; + next(); + }) + .catch(function(err) { + return next(err); + }); }); } @@ -39,12 +50,15 @@ 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()) .use(function meetsRequirements(req, res, next) { - if (config.userRoles.indexOf(req.user.role) >= config.userRoles.indexOf(roleRequired)) { + if (config.userRoles.indexOf(req.user.role) >= + config.userRoles.indexOf(roleRequired)) { next(); } else { @@ -56,15 +70,19 @@ function hasRole(roleRequired) { /** * Returns a jwt token signed by the app secret */ -function signToken(id) { - return jwt.sign({ _id: id }, config.secrets.session, { expiresInMinutes: 60*5 }); +function signToken(id, role) { + return jwt.sign({ _id: id, role: role }, config.secrets.session, { + expiresInMinutes: 60 * 5 + }); } /** * Set token cookie directly for oAuth strategies */ function setTokenCookie(req, res) { - if (!req.user) return res.status(404).json({ message: 'Something went wrong, please try again.'}); + if (!req.user) { + return res.status(404).send('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 +91,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/auth(auth)/facebook(facebookAuth)/index.js b/app/templates/server/auth(auth)/facebook(facebookAuth)/index.js index 4a6f87886..f13d463e1 100644 --- a/app/templates/server/auth(auth)/facebook(facebookAuth)/index.js +++ b/app/templates/server/auth(auth)/facebook(facebookAuth)/index.js @@ -18,4 +18,4 @@ router session: false }), auth.setTokenCookie); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/app/templates/server/auth(auth)/facebook(facebookAuth)/passport.js b/app/templates/server/auth(auth)/facebook(facebookAuth)/passport.js index 54574efb6..00b87a226 100644 --- a/app/templates/server/auth(auth)/facebook(facebookAuth)/passport.js +++ b/app/templates/server/auth(auth)/facebook(facebookAuth)/passport.js @@ -1,37 +1,45 @@ var passport = require('passport'); var FacebookStrategy = require('passport-facebook').Strategy; -exports.setup = function (User, config) { +exports.setup = function(User, config) { passport.use(new FacebookStrategy({ - clientID: config.facebook.clientID, - clientSecret: config.facebook.clientSecret, - callbackURL: config.facebook.callbackURL - }, - function(accessToken, refreshToken, profile, done) { - User.findOne({ - 'facebook.id': profile.id - }, - function(err, user) { - if (err) { - return done(err); - } + clientID: config.facebook.clientID, + clientSecret: config.facebook.clientSecret, + callbackURL: config.facebook.callbackURL, + profileFields: [ + 'displayName', + 'emails' + ] + }, + function(accessToken, refreshToken, profile, done) { + <% if (filters.mongooseModels) { %>User.findOneAsync({<% } + if (filters.sequelizeModels) { %>User.find({<% } %> + 'facebook.id': profile.id + }) + .then(function(user) { if (!user) { - user = new User({ + <% if (filters.mongooseModels) { %>user = new User({<% } + if (filters.sequelizeModels) { %>user = User.build({<% } %> name: profile.displayName, email: profile.emails[0].value, role: 'user', - username: profile.username, provider: 'facebook', facebook: profile._json }); - user.save(function(err) { - if (err) return done(err); - done(err, user); - }); + <% if (filters.mongooseModels) { %>user.saveAsync()<% } + if (filters.sequelizeModels) { %>user.save()<% } %> + .then(function(user) { + return done(null, user); + }) + .catch(function(err) { + return done(err); + }); } else { - return done(err, user); + return done(null, user); } }) - } - )); + .catch(function(err) { + return done(err); + }); + })); }; diff --git a/app/templates/server/auth(auth)/google(googleAuth)/index.js b/app/templates/server/auth(auth)/google(googleAuth)/index.js index 9b1ce39fe..7789def92 100644 --- a/app/templates/server/auth(auth)/google(googleAuth)/index.js +++ b/app/templates/server/auth(auth)/google(googleAuth)/index.js @@ -10,8 +10,8 @@ router .get('/', passport.authenticate('google', { failureRedirect: '/signup', scope: [ - 'https://www.googleapis.com/auth/userinfo.profile', - 'https://www.googleapis.com/auth/userinfo.email' + 'profile', + 'email' ], session: false })) @@ -21,4 +21,4 @@ router session: false }), auth.setTokenCookie); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/app/templates/server/auth(auth)/google(googleAuth)/passport.js b/app/templates/server/auth(auth)/google(googleAuth)/passport.js index c9754c83a..f74594c12 100644 --- a/app/templates/server/auth(auth)/google(googleAuth)/passport.js +++ b/app/templates/server/auth(auth)/google(googleAuth)/passport.js @@ -1,33 +1,42 @@ var passport = require('passport'); var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy; -exports.setup = function (User, config) { +exports.setup = function(User, config) { passport.use(new GoogleStrategy({ - clientID: config.google.clientID, - clientSecret: config.google.clientSecret, - callbackURL: config.google.callbackURL - }, - function(accessToken, refreshToken, profile, done) { - User.findOne({ - 'google.id': profile.id - }, function(err, user) { + clientID: config.google.clientID, + clientSecret: config.google.clientSecret, + callbackURL: config.google.callbackURL + }, + function(accessToken, refreshToken, profile, done) { + <% if (filters.mongooseModels) { %>User.findOneAsync({<% } + if (filters.sequelizeModels) { %>User.find({<% } %> + 'google.id': profile.id + }) + .then(function(user) { if (!user) { - user = new User({ + <% if (filters.mongooseModels) { %>user = new User({<% } + if (filters.sequelizeModels) { %>user = User.build({<% } %> name: profile.displayName, email: profile.emails[0].value, role: 'user', - username: profile.username, + username: profile.emails[0].value.split('@')[0], provider: 'google', google: profile._json }); - user.save(function(err) { - if (err) return done(err); - done(err, user); - }); + <% if (filters.mongooseModels) { %>user.saveAsync()<% } + if (filters.sequelizeModels) { %>user.save()<% } %> + .then(function(user) { + return done(null, user); + }) + .catch(function(err) { + return done(err); + }); } else { - return done(err, user); + return done(null, user); } + }) + .catch(function(err) { + return done(err); }); - } - )); + })); }; diff --git a/app/templates/server/auth(auth)/index.js b/app/templates/server/auth(auth)/index.js index e3e6c87ad..75ddfdcb8 100644 --- a/app/templates/server/auth(auth)/index.js +++ b/app/templates/server/auth(auth)/index.js @@ -2,8 +2,9 @@ var express = require('express'); var passport = require('passport'); -var config = require('../config/environment'); -var User = require('../api/user/user.model'); +var config = require('../config/environment');<% if (filters.mongooseModels) { %> +var User = require('../api/user/user.model');<% } %><% if (filters.sequelizeModels) { %> +var User = require('../sqldb').User;<% } %> // Passport Configuration require('./local/passport').setup(User, config);<% if (filters.facebookAuth) { %> @@ -18,4 +19,4 @@ router.use('/facebook', require('./facebook'));<% } %><% if (filters.twitterAuth router.use('/twitter', require('./twitter'));<% } %><% if (filters.googleAuth) { %> router.use('/google', require('./google'));<% } %> -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/app/templates/server/auth(auth)/local/index.js b/app/templates/server/auth(auth)/local/index.js index 2e761a52d..e94d0da1a 100644 --- a/app/templates/server/auth(auth)/local/index.js +++ b/app/templates/server/auth(auth)/local/index.js @@ -7,14 +7,18 @@ var auth = require('../auth.service'); var router = express.Router(); router.post('/', function(req, res, next) { - passport.authenticate('local', function (err, user, info) { + passport.authenticate('local', function(err, user, info) { var error = err || info; - if (error) return res.status(401).json(error); - if (!user) return res.status(404).json({message: 'Something went wrong, please try again.'}); + if (error) { + return res.status(401).json(error); + } + if (!user) { + return res.status(404).json({message: 'Something went wrong, please try again.'}); + } var token = auth.signToken(user._id, user.role); - res.json({token: token}); + res.json({ token: token }); })(req, res, next) }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/app/templates/server/auth(auth)/local/passport.js b/app/templates/server/auth(auth)/local/passport.js index ac82b42a2..2bd3366f8 100644 --- a/app/templates/server/auth(auth)/local/passport.js +++ b/app/templates/server/auth(auth)/local/passport.js @@ -1,25 +1,44 @@ var passport = require('passport'); var LocalStrategy = require('passport-local').Strategy; -exports.setup = function (User, config) { - passport.use(new LocalStrategy({ - usernameField: 'email', - passwordField: 'password' // this is the virtual field on the model - }, - function(email, password, done) { - User.findOne({ - email: email.toLowerCase() - }, function(err, user) { - if (err) return done(err); - - if (!user) { - return done(null, false, { message: 'This email is not registered.' }); +function localAuthenticate(User, email, password, done) { + <% if (filters.mongooseModels) { %>User.findOneAsync({ + email: email.toLowerCase() + })<% } + if (filters.sequelizeModels) { %>User.find({ + where: { + email: email.toLowerCase() + } + })<% } %> + .then(function(user) { + if (!user) { + return done(null, false, { + message: 'This email is not registered.' + }); + } + user.authenticate(password, function(authError, authenticated) { + if (authError) { + return done(authError); } - if (!user.authenticate(password)) { - return done(null, false, { message: 'This password is not correct.' }); + if (!authenticated) { + return done(null, false, { + message: 'This password is not correct.' + }); + } else { + return done(null, user); } - return done(null, user); }); - } - )); -}; \ No newline at end of file + }) + .catch(function(err) { + return done(err); + }); +} + +exports.setup = function(User, config) { + passport.use(new LocalStrategy({ + usernameField: 'email', + passwordField: 'password' // this is the virtual field on the model + }, function(email, password, done) {<% if (filters.models) { %> + return localAuthenticate(User, email, password, done); +<% } %> })); +}; diff --git a/app/templates/server/auth(auth)/twitter(twitterAuth)/index.js b/app/templates/server/auth(auth)/twitter(twitterAuth)/index.js index 8360247b8..8e6f32b5d 100644 --- a/app/templates/server/auth(auth)/twitter(twitterAuth)/index.js +++ b/app/templates/server/auth(auth)/twitter(twitterAuth)/index.js @@ -17,4 +17,4 @@ router session: false }), auth.setTokenCookie); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/app/templates/server/auth(auth)/twitter(twitterAuth)/passport.js b/app/templates/server/auth(auth)/twitter(twitterAuth)/passport.js index 4544ce186..bf23bd3ba 100644 --- a/app/templates/server/auth(auth)/twitter(twitterAuth)/passport.js +++ b/app/templates/server/auth(auth)/twitter(twitterAuth)/passport.js @@ -1,4 +1,4 @@ -exports.setup = function (User, config) { +exports.setup = function(User, config) { var passport = require('passport'); var TwitterStrategy = require('passport-twitter').Strategy; @@ -8,28 +8,34 @@ exports.setup = function (User, config) { callbackURL: config.twitter.callbackURL }, function(token, tokenSecret, profile, done) { - User.findOne({ + <% if (filters.mongooseModels) { %>User.findOneAsync({<% } + if (filters.sequelizeModels) { %>User.find({<% } %> 'twitter.id_str': profile.id - }, function(err, user) { - if (err) { + }) + .then(function(user) { + if (!user) { + <% if (filters.mongooseModels) { %>user = new User({<% } + if (filters.sequelizeModels) { %>user = User.build({<% } %> + name: profile.displayName, + username: profile.username, + role: 'user', + provider: 'twitter', + twitter: profile._json + }); + <% if (filters.mongooseModels) { %>user.saveAsync()<% } + if (filters.sequelizeModels) { %>user.save()<% } %> + .then(function(user) { + return done(null, user); + }) + .catch(function(err) { + return done(err); + }); + } else { + return done(null, user); + } + }) + .catch(function(err) { return done(err); - } - if (!user) { - user = new User({ - name: profile.displayName, - username: profile.username, - role: 'user', - provider: 'twitter', - twitter: profile._json - }); - user.save(function(err) { - if (err) return done(err); - done(err, user); - }); - } else { - return done(err, user); - } - }); - } - )); + }); + })); }; diff --git a/app/templates/server/components/errors/index.js b/app/templates/server/components/errors/index.js index 4c5a57c99..ba71c73ba 100644 --- a/app/templates/server/components/errors/index.js +++ b/app/templates/server/components/errors/index.js @@ -12,9 +12,11 @@ module.exports[404] = function pageNotFound(req, res) { }; res.status(result.status); - res.render(viewFilePath, function (err) { - if (err) { return res.json(result, result.status); } + res.render(viewFilePath, {}, function(err, html) { + if (err) { + return res.json(result, result.status); + } - res.render(viewFilePath); + res.send(html); }); }; diff --git a/app/templates/server/config/_local.env.js b/app/templates/server/config/_local.env.js index c24fffd3a..12b78192e 100644 --- a/app/templates/server/config/_local.env.js +++ b/app/templates/server/config/_local.env.js @@ -7,7 +7,7 @@ module.exports = { DOMAIN: 'http://localhost:9000', - SESSION_SECRET: "<%= _.slugify(appname) + '-secret' %>",<% if (filters.facebookAuth) { %> + SESSION_SECRET: '<%= _.slugify(appname) + "-secret" %>',<% if (filters.facebookAuth) { %> FACEBOOK_ID: 'app-id', FACEBOOK_SECRET: 'secret',<% } if (filters.twitterAuth) { %> diff --git a/app/templates/server/config/environment/development.js b/app/templates/server/config/environment/development.js index fb33d6eab..20656595b 100644 --- a/app/templates/server/config/environment/development.js +++ b/app/templates/server/config/environment/development.js @@ -7,6 +7,16 @@ module.exports = { mongo: { uri: 'mongodb://localhost/<%= _.slugify(appname) %>-dev' }, + sequelize: { + uri: 'sqlite://', + options: { + logging: false, + storage: 'dev.sqlite', + define: { + timestamps: false + } + } + }, seedDB: true }; diff --git a/app/templates/server/config/environment/index.js b/app/templates/server/config/environment/index.js index a57261ddc..547f6bbac 100644 --- a/app/templates/server/config/environment/index.js +++ b/app/templates/server/config/environment/index.js @@ -4,7 +4,7 @@ var path = require('path'); var _ = require('lodash'); function requiredProcessEnv(name) { - if(!process.env[name]) { + if (!process.env[name]) { throw new Error('You must set the ' + name + ' environment variable'); } return process.env[name]; @@ -42,20 +42,20 @@ var all = { safe: true } } - }, -<% if(filters.facebookAuth) { %> + }<% if (filters.facebookAuth) { %>, + facebook: { clientID: process.env.FACEBOOK_ID || 'id', clientSecret: process.env.FACEBOOK_SECRET || 'secret', callbackURL: (process.env.DOMAIN || '') + '/auth/facebook/callback' - }, -<% } %><% if(filters.twitterAuth) { %> + }<% } %><% if (filters.twitterAuth) { %>, + twitter: { clientID: process.env.TWITTER_ID || 'id', clientSecret: process.env.TWITTER_SECRET || 'secret', callbackURL: (process.env.DOMAIN || '') + '/auth/twitter/callback' - }, -<% } %><% if(filters.googleAuth) { %> + }<% } %><% if (filters.googleAuth) { %>, + google: { clientID: process.env.GOOGLE_ID || 'id', clientSecret: process.env.GOOGLE_SECRET || 'secret', diff --git a/app/templates/server/config/environment/production.js b/app/templates/server/config/environment/production.js index 1704df619..e0b77bf97 100644 --- a/app/templates/server/config/environment/production.js +++ b/app/templates/server/config/environment/production.js @@ -17,7 +17,8 @@ module.exports = { mongo: { uri: process.env.MONGOLAB_URI || process.env.MONGOHQ_URL || - process.env.OPENSHIFT_MONGODB_DB_URL+process.env.OPENSHIFT_APP_NAME || + process.env.OPENSHIFT_MONGODB_DB_URL + + process.env.OPENSHIFT_APP_NAME || 'mongodb://localhost/<%= _.slugify(appname) %>' } -}; \ No newline at end of file +}; diff --git a/app/templates/server/config/environment/test.js b/app/templates/server/config/environment/test.js index 711c98660..021938424 100644 --- a/app/templates/server/config/environment/test.js +++ b/app/templates/server/config/environment/test.js @@ -6,5 +6,15 @@ module.exports = { // MongoDB connection options mongo: { uri: 'mongodb://localhost/<%= _.slugify(appname) %>-test' + }, + sequelize: { + uri: 'sqlite://', + options: { + logging: false, + storage: 'test.sqlite', + define: { + timestamps: false + } + } } -}; \ No newline at end of file +}; diff --git a/app/templates/server/config/express.js b/app/templates/server/config/express.js index f04098387..aa32be65a 100644 --- a/app/templates/server/config/express.js +++ b/app/templates/server/config/express.js @@ -15,9 +15,11 @@ var errorHandler = require('errorhandler'); var path = require('path'); var config = require('./environment');<% if (filters.auth) { %> var passport = require('passport');<% } %><% if (filters.twitterAuth) { %> -var session = require('express-session'); +var session = require('express-session');<% if (filters.mongoose) { %> var mongoStore = require('connect-mongo')(session); -var mongoose = require('mongoose');<% } %> +var mongoose = require('mongoose');<% } else if(filters.sequelize) { %> +var sqldb = require('../sqldb'); +var Store = require('express-sequelize-session')(session.Store);<% } %><% } %> module.exports = function(app) { var env = app.get('env'); @@ -30,34 +32,38 @@ module.exports = function(app) { app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); app.use(methodOverride()); - app.use(cookieParser()); - <% if (filters.auth) { %>app.use(passport.initialize());<% } %><% if (filters.twitterAuth) { %> + app.use(cookieParser());<% if (filters.auth) { %> + app.use(passport.initialize());<% } %><% if (filters.twitterAuth) { %> - // Persist sessions with mongoStore + // Persist sessions with mongoStore / sequelizeStore // We need to enable sessions for passport twitter because its an oauth 1.0 strategy app.use(session({ secret: config.secrets.session, resave: true, - saveUninitialized: true, + saveUninitialized: true<% if (filters.mongoose) { %>, store: new mongoStore({ mongooseConnection: mongoose.connection, db: '<%= _.slugify(_.humanize(appname)) %>' - }) + })<% } else if(filters.sequelize) { %>, + store: new Store(sqldb.sequelize)<% } %> })); - <% } %> +<% } %> + app.set('appPath', path.join(config.root, 'client')); + if ('production' === env) { - app.use(favicon(path.join(config.root, 'public', 'favicon.ico'))); - app.use(express.static(path.join(config.root, 'public'))); - app.set('appPath', path.join(config.root, 'public')); + app.use(favicon(path.join(config.root, 'client', 'favicon.ico'))); + app.use(express.static(app.get('appPath'))); app.use(morgan('dev')); } - if ('development' === env || 'test' === env) { + if ('development' === env) { app.use(require('connect-livereload')()); + } + + if ('development' === env || 'test' === env) { app.use(express.static(path.join(config.root, '.tmp'))); - app.use(express.static(path.join(config.root, 'client'))); - app.set('appPath', path.join(config.root, 'client')); + app.use(express.static(app.get('appPath'))); 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(models).js b/app/templates/server/config/seed(models).js new file mode 100644 index 000000000..20ba6b0f2 --- /dev/null +++ b/app/templates/server/config/seed(models).js @@ -0,0 +1,76 @@ +/** + * Populate DB with sample data on server start + * to disable, edit config/environment/index.js, and set `seedDB: false` + */ + +'use strict'; +<% if (filters.mongooseModels) { %> +var Thing = require('../api/thing/thing.model'); +<% if (filters.auth) { %>var User = require('../api/user/user.model');<% } %> +<% } %><% if (filters.sequelizeModels) { %> +var sqldb = require('../sqldb'); +var Thing = sqldb.Thing; +<% if (filters.auth) { %>var User = sqldb.User;<% } %> +<% } %> +<% if (filters.mongooseModels) { %>Thing.find({}).removeAsync()<% } + if (filters.sequelizeModels) { %>Thing.sync() + .then(function() { + return Thing.destroy({ where: {} }); + })<% } %> + .then(function() { + <% if (filters.mongooseModels) { %>Thing.create({<% } + if (filters.sequelizeModels) { %>Thing.bulkCreate([{<% } %> + 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.mongooseModels) { %>});<% } + if (filters.sequelizeModels) { %>}]);<% } %> + }); +<% if (filters.auth) { %> +<% if (filters.mongooseModels) { %>User.find({}).removeAsync()<% } + if (filters.sequelizeModels) { %>User.sync() + .then(function() { + return User.destroy({ where: {} }); + })<% } %> + .then(function() { + <% if (filters.mongooseModels) { %>User.createAsync({<% } + if (filters.sequelizeModels) { %>User.bulkCreate([{<% } %> + provider: 'local', + name: 'Test User', + email: 'test@test.com', + password: 'test' + }, { + provider: 'local', + role: 'admin', + name: 'Admin', + email: 'admin@admin.com', + password: 'admin' + <% if (filters.mongooseModels) { %>})<% } + if (filters.sequelizeModels) { %>}])<% } %> + .then(function() { + console.log('finished populating users'); + }); + });<% } %> diff --git a/app/templates/server/config/seed(mongoose).js b/app/templates/server/config/seed(mongoose).js deleted file mode 100644 index 27ab19417..000000000 --- a/app/templates/server/config/seed(mongoose).js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Populate DB with sample data on server start - * to disable, edit config/environment/index.js, and set `seedDB: false` - */ - -'use strict'; - -var Thing = require('../api/thing/thing.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' - }); -});<% 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 diff --git a/app/templates/server/config/socketio(socketio).js b/app/templates/server/config/socketio(socketio).js index 2fbbc07d6..92f629729 100644 --- a/app/templates/server/config/socketio(socketio).js +++ b/app/templates/server/config/socketio(socketio).js @@ -13,7 +13,7 @@ function onDisconnect(socket) { // When the user connects.. perform this function onConnect(socket) { // When the client emits 'info', this listens and executes - socket.on('info', function (data) { + socket.on('info', function(data) { console.info('[%s] %s', socket.address, JSON.stringify(data, null, 2)); }); @@ -21,13 +21,13 @@ function onConnect(socket) { require('../api/thing/thing.socket').register(socket); } -module.exports = function (socketio) { +module.exports = function(socketio) { // socket.io (v1.x.x) is powered by debug. // In order to see all the debug output, set DEBUG (in server/config/local.env.js) to including the desired scope. // // ex: DEBUG: "http*,socket.io:socket" - // We can authenticate socket.io users and access their token through socket.handshake.decoded_token + // We can authenticate socket.io users and access their token through socket.decoded_token // // 1. You will need to send the token in `client/components/socket/socket.service.js` // @@ -37,15 +37,14 @@ module.exports = function (socketio) { // handshake: true // })); - socketio.on('connection', function (socket) { - socket.address = socket.handshake.address !== null ? - socket.handshake.address.address + ':' + socket.handshake.address.port : - process.env.DOMAIN; + socketio.on('connection', function(socket) { + socket.address = socket.request.connection.remoteAddress + + ':' + socket.request.connection.remotePort; socket.connectedAt = new Date(); // Call onDisconnect. - socket.on('disconnect', function () { + socket.on('disconnect', function() { onDisconnect(socket); console.info('[%s] DISCONNECTED', socket.address); }); @@ -54,4 +53,4 @@ module.exports = function (socketio) { onConnect(socket); console.info('[%s] CONNECTED', socket.address); }); -}; \ No newline at end of file +}; diff --git a/app/templates/server/index.js b/app/templates/server/index.js new file mode 100644 index 000000000..fc65cd5f4 --- /dev/null +++ b/app/templates/server/index.js @@ -0,0 +1,7 @@ +'use strict'; + +// Register the Babel require hook +require('babel-core/register'); + +// Export the application +exports = module.exports = require('./app'); diff --git a/app/templates/server/routes.js b/app/templates/server/routes.js index ebcd79dc6..8330b35fe 100644 --- a/app/templates/server/routes.js +++ b/app/templates/server/routes.js @@ -10,11 +10,11 @@ var path = require('path'); module.exports = function(app) { // Insert routes below - app.use('/api/things', require('./api/thing')); - <% if (filters.auth) { %>app.use('/api/users', require('./api/user')); + app.use('/api/things', require('./api/thing'));<% if (filters.auth) { %> + app.use('/api/users', require('./api/user')); app.use('/auth', require('./auth')); - <% } %> +<% } %> // All undefined asset or api routes should return a 404 app.route('/:url(api|auth|components|app|bower_components|assets)/*') .get(errors[404]); diff --git a/app/templates/server/sqldb(sequelize)/index.js b/app/templates/server/sqldb(sequelize)/index.js new file mode 100644 index 000000000..2500a2213 --- /dev/null +++ b/app/templates/server/sqldb(sequelize)/index.js @@ -0,0 +1,20 @@ +/** + * Sequelize initialization module + */ + +'use strict'; + +var path = require('path'); +var config = require('../config/environment'); + +var Sequelize = require('sequelize'); + +var db = { + Sequelize: Sequelize, + sequelize: new Sequelize(config.sequelize.uri, config.sequelize.options) +}; + +// Insert models below<% if (filters.sequelizeModels && filters.auth) { %> +db.User = db.sequelize.import('../api/user/user.model');<% } %> + +module.exports = db; diff --git a/controller/index.js b/controller/index.js index 29f65325b..6d8897d61 100644 --- a/controller/index.js +++ b/controller/index.js @@ -7,4 +7,4 @@ var Generator = yeoman.generators.Base.extend({ } }); -module.exports = Generator; \ No newline at end of file +module.exports = Generator; diff --git a/decorator/index.js b/decorator/index.js index b28be5c88..ae8193eb7 100644 --- a/decorator/index.js +++ b/decorator/index.js @@ -7,4 +7,4 @@ var Generator = yeoman.generators.Base.extend({ } }); -module.exports = Generator; \ No newline at end of file +module.exports = Generator; diff --git a/directive/index.js b/directive/index.js index 298f4240e..257a4b19a 100644 --- a/directive/index.js +++ b/directive/index.js @@ -7,4 +7,4 @@ var Generator = yeoman.generators.Base.extend({ } }); -module.exports = Generator; \ No newline at end of file +module.exports = Generator; diff --git a/endpoint/index.js b/endpoint/index.js index 2b3d7eb22..cbb8d5aeb 100644 --- a/endpoint/index.js +++ b/endpoint/index.js @@ -7,12 +7,59 @@ var ScriptBase = require('../script-base.js'); var Generator = module.exports = function Generator() { ScriptBase.apply(this, arguments); + + this.option('route', { + desc: 'URL for the endpoint', + type: String + }); + + this.option('models', { + desc: 'Specify which model(s) to use', + type: String + }); + + this.option('endpointDirectory', { + desc: 'Parent directory for enpoints', + type: String + }); }; util.inherits(Generator, ScriptBase); -Generator.prototype.askFor = function askFor() { +Generator.prototype.prompting = function askFor() { var done = this.async(); + var promptCb = function (props) { + if(props.route.charAt(0) !== '/') { + props.route = '/' + props.route; + } + + this.route = props.route; + + if (props.models) { + delete this.filters.mongoose; + delete this.filters.mongooseModels; + delete this.filters.sequelize; + delete this.filters.sequelizeModels; + + this.filters[props.models] = true; + this.filters[props.models + 'Models'] = true; + } + done(); + }.bind(this); + + if (this.options.route) { + if (this.filters.mongoose && this.filters.sequelize) { + if (this.options.models) { + return promptCb(this.options); + } + } else { + if (this.filters.mongooseModels) { this.options.models = 'mongoose'; } + else if (this.filters.sequelizeModels) { this.options.models = 'sequelize'; } + else { delete this.options.models; } + return promptCb(this.options); + } + } + var name = this.name; var base = this.config.get('routesBase') || '/api/'; @@ -25,52 +72,79 @@ Generator.prototype.askFor = function askFor() { name = name + 's'; } + var self = this; var prompts = [ { name: 'route', message: 'What will the url of your endpoint be?', default: base + name + }, + { + type: 'list', + name: 'models', + message: 'What would you like to use for the endpoint\'s models?', + choices: [ 'Mongoose', 'Sequelize' ], + filter: function( val ) { + return val.toLowerCase(); + }, + when: function() { + return self.filters.mongoose && self.filters.sequelize; + } } ]; - this.prompt(prompts, function (props) { - if(props.route.charAt(0) !== '/') { - props.route = '/' + props.route; - } + this.prompt(prompts, promptCb); +}; - this.route = props.route; - done(); - }.bind(this)); +Generator.prototype.configuring = function config() { + this.routeDest = path.join(this.options.endpointDirectory || + this.config.get('endpointDirectory') || 'server/api/', this.name); }; -Generator.prototype.registerEndpoint = function registerEndpoint() { +Generator.prototype.writing = function createFiles() { + this.sourceRoot(path.join(__dirname, './templates')); + ngUtil.processDirectory(this, '.', this.routeDest); +}; + +Generator.prototype.end = function registerEndpoint() { if(this.config.get('insertRoutes')) { + var routesFile = this.config.get('registerRoutesFile'); + var reqPath = this.relativeRequire(this.routeDest, routesFile); var routeConfig = { - file: this.config.get('registerRoutesFile'), + file: routesFile, needle: this.config.get('routesNeedle'), splicable: [ - "app.use(\'" + this.route +"\', require(\'./api/" + this.name + "\'));" + "app.use(\'" + this.route +"\', require(\'" + reqPath + "\'));" ] }; ngUtil.rewriteFile(routeConfig); } - if (this.filters.socketio) { - if(this.config.get('insertSockets')) { - var socketConfig = { - file: this.config.get('registerSocketsFile'), - needle: this.config.get('socketsNeedle'), - splicable: [ - "require(\'../api/" + this.name + '/' + this.name + ".socket\').register(socket);" - ] - }; - ngUtil.rewriteFile(socketConfig); - } + if (this.filters.socketio && this.config.get('insertSockets')) { + var socketsFile = this.config.get('registerSocketsFile'); + var reqPath = this.relativeRequire(this.routeDest + '/' + this.basename + + '.socket', socketsFile); + var socketConfig = { + file: socketsFile, + needle: this.config.get('socketsNeedle'), + splicable: [ + "require(\'" + reqPath + "\').register(socket);" + ] + }; + ngUtil.rewriteFile(socketConfig); } -}; -Generator.prototype.createFiles = function createFiles() { - var dest = this.config.get('endpointDirectory') || 'server/api/' + this.name; - this.sourceRoot(path.join(__dirname, './templates')); - ngUtil.processDirectory(this, '.', dest); + if (this.filters.sequelize && this.config.get('insertModels')) { + var modelsFile = this.config.get('registerModelsFile'); + var reqPath = this.relativeRequire(this.routeDest + '/' + this.basename + + '.model', modelsFile); + var modelConfig = { + file: modelsFile, + needle: this.config.get('modelsNeedle'), + splicable: [ + "db." + this.classedName + " = db.sequelize.import(\'" + reqPath +"\');" + ] + }; + ngUtil.rewriteFile(modelConfig); + } }; diff --git a/endpoint/templates/basename.controller.js b/endpoint/templates/basename.controller.js new file mode 100644 index 000000000..cd151b13a --- /dev/null +++ b/endpoint/templates/basename.controller.js @@ -0,0 +1,125 @@ +/** + * Using Rails-like standard naming convention for endpoints. + * GET <%= route %> -> index<% if (filters.models) { %> + * POST <%= route %> -> create + * GET <%= route %>/:id -> show + * PUT <%= route %>/:id -> update + * DELETE <%= route %>/:id -> destroy<% } %> + */ + +'use strict';<% if (filters.models) { %> + +var _ = require('lodash');<% if (filters.mongooseModels) { %> +var <%= classedName %> = require('./<%= basename %>.model');<% } if (filters.sequelizeModels) { %> +var sqldb = require('<%= relativeRequire(config.get('registerModelsFile')) %>'); +var <%= classedName %> = sqldb.<%= classedName %>;<% } %> + +function handleError(res, statusCode) { + statusCode = statusCode || 500; + return function(err) { + res.status(statusCode).send(err); + }; +} + +function responseWithResult(res, statusCode) { + statusCode = statusCode || 200; + return function(entity) { + if (entity) { + res.status(statusCode).json(entity); + } + }; +} + +function handleEntityNotFound(res) { + return function(entity) { + if (!entity) { + res.status(404).end(); + return null; + } + return entity; + }; +} + +function saveUpdates(updates) { + return function(entity) { + <% if (filters.mongooseModels) { %>var updated = _.merge(entity, updates); + return updated.saveAsync() + .spread(function(updated) {<% } + if (filters.sequelizeModels) { %>return entity.updateAttributes(updates) + .then(function(updated) {<% } %> + return updated; + }); + }; +} + +function removeEntity(res) { + return function(entity) { + if (entity) { + <% if (filters.mongooseModels) { %>return entity.removeAsync()<% } + if (filters.sequelizeModels) { %>return entity.destroy()<% } %> + .then(function() { + res.status(204).end(); + }); + } + }; +}<% } %> + +// Gets a list of <%= classedName %>s +exports.index = function(req, res) {<% if (!filters.models) { %> + res.json([]);<% } else { %> + <% if (filters.mongooseModels) { %><%= classedName %>.findAsync()<% } + if (filters.sequelizeModels) { %><%= classedName %>.findAll()<% } %> + .then(responseWithResult(res)) + .catch(handleError(res));<% } %> +};<% if (filters.models) { %> + +// Gets a single <%= classedName %> from the DB +exports.show = function(req, res) { + <% if (filters.mongooseModels) { %><%= classedName %>.findByIdAsync(req.params.id)<% } + if (filters.sequelizeModels) { %><%= classedName %>.find({ + where: { + _id: req.params.id + } + })<% } %> + .then(handleEntityNotFound(res)) + .then(responseWithResult(res)) + .catch(handleError(res)); +}; + +// Creates a new <%= classedName %> in the DB +exports.create = function(req, res) { + <% if (filters.mongooseModels) { %><%= classedName %>.createAsync(req.body)<% } + if (filters.sequelizeModels) { %><%= classedName %>.create(req.body)<% } %> + .then(responseWithResult(res, 201)) + .catch(handleError(res)); +}; + +// Updates an existing <%= classedName %> in the DB +exports.update = function(req, res) { + if (req.body._id) { + delete req.body._id; + } + <% if (filters.mongooseModels) { %><%= classedName %>.findByIdAsync(req.params.id)<% } + if (filters.sequelizeModels) { %><%= classedName %>.find({ + where: { + _id: req.params.id + } + })<% } %> + .then(handleEntityNotFound(res)) + .then(saveUpdates(req.body)) + .then(responseWithResult(res)) + .catch(handleError(res)); +}; + +// Deletes a <%= classedName %> from the DB +exports.destroy = function(req, res) { + <% if (filters.mongooseModels) { %><%= classedName %>.findByIdAsync(req.params.id)<% } + if (filters.sequelizeModels) { %><%= classedName %>.find({ + where: { + _id: req.params.id + } + })<% } %> + .then(handleEntityNotFound(res)) + .then(removeEntity(res)) + .catch(handleError(res)); +};<% } %> diff --git a/endpoint/templates/basename.events(models).js b/endpoint/templates/basename.events(models).js new file mode 100644 index 000000000..f39b5b0be --- /dev/null +++ b/endpoint/templates/basename.events(models).js @@ -0,0 +1,41 @@ +/** + * <%= classedName %> model events + */ + +'use strict'; + +var EventEmitter = require('events').EventEmitter;<% if (filters.mongooseModels) { %> +var <%= classedName %> = require('./<%= basename %>.model');<% } if (filters.sequelizeModels) { %> +var <%= classedName %> = require('<%= relativeRequire(config.get('registerModelsFile')) %>').<%= classedName %>;<% } %> +var <%= classedName %>Events = new EventEmitter(); + +// Set max event listeners (0 == unlimited) +<%= classedName %>Events.setMaxListeners(0); + +// Model events<% if (filters.mongooseModels) { %> +var events = { + 'save': 'save', + 'remove': 'remove' +};<% } if (filters.sequelizeModels) { %> +var events = { + 'afterCreate': 'save', + 'afterUpdate': 'save', + 'afterDestroy': 'remove' +};<% } %> + +// Register the event emitter to the model events +for (var e in events) { + var event = events[e];<% if (filters.mongooseModels) { %> + <%= classedName %>.schema.post(e, emitEvent(event));<% } if (filters.sequelizeModels) { %> + <%= classedName %>.hook(e, emitEvent(event));<% } %> +} + +function emitEvent(event) { + return function(doc<% if (filters.sequelizeModels) { %>, options, done<% } %>) { + <%= classedName %>Events.emit(event + ':' + doc._id, doc); + <%= classedName %>Events.emit(event, doc);<% if (filters.sequelizeModels) { %> + done(null);<% } %> + } +} + +module.exports = <%= classedName %>Events; diff --git a/endpoint/templates/basename.integration.js b/endpoint/templates/basename.integration.js new file mode 100644 index 000000000..067898d72 --- /dev/null +++ b/endpoint/templates/basename.integration.js @@ -0,0 +1,147 @@ +'use strict'; + +var app = require('<%= relativeRequire('server') %>'); +var request = require('supertest');<% if(filters.models) { %> + +var new<%= classedName %>;<% } %> + +describe('<%= classedName %> API:', function() { + + describe('GET <%= route %>', function() { + var <%= cameledName %>s; + + beforeEach(function(done) { + request(app) + .get('<%= route %>') + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if (err) { + return done(err); + } + <%= cameledName %>s = res.body; + done(); + }); + }); + + it('should respond with JSON array', function() { + <%= cameledName %>s.should.be.instanceOf(Array); + }); + + });<% if(filters.models) { %> + + describe('POST <%= route %>', function() { + beforeEach(function(done) { + request(app) + .post('<%= route %>') + .send({ + name: 'New <%= classedName %>', + info: 'This is the brand new <%= cameledName %>!!!' + }) + .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 <%= cameledName %>', function() { + new<%= classedName %>.name.should.equal('New <%= classedName %>'); + new<%= classedName %>.info.should.equal('This is the brand new <%= cameledName %>!!!'); + }); + + }); + + describe('GET <%= route %>/:id', function() { + var <%= cameledName %>; + + 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); + } + <%= cameledName %> = res.body; + done(); + }); + }); + + afterEach(function() { + <%= cameledName %> = {}; + }); + + it('should respond with the requested <%= cameledName %>', function() { + <%= cameledName %>.name.should.equal('New <%= classedName %>'); + <%= cameledName %>.info.should.equal('This is the brand new <%= cameledName %>!!!'); + }); + + }); + + 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 <%= cameledName %>!!!' + }) + .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 <%= cameledName %>', function() { + updated<%= classedName %>.name.should.equal('Updated <%= classedName %>'); + updated<%= classedName %>.info.should.equal('This is the updated <%= cameledName %>!!!'); + }); + + }); + + 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 <%= cameledName %> does not exist', 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.model(mongoose).js b/endpoint/templates/basename.model(mongooseModels).js similarity index 60% rename from endpoint/templates/name.model(mongoose).js rename to endpoint/templates/basename.model(mongooseModels).js index 89e0dfaa7..09787cdf3 100644 --- a/endpoint/templates/name.model(mongoose).js +++ b/endpoint/templates/basename.model(mongooseModels).js @@ -1,7 +1,7 @@ 'use strict'; -var mongoose = require('mongoose'), - Schema = mongoose.Schema; +var mongoose = require('bluebird').promisifyAll(require('mongoose')); +var Schema = mongoose.Schema; var <%= classedName %>Schema = new Schema({ name: String, @@ -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); diff --git a/endpoint/templates/basename.model(sequelizeModels).js b/endpoint/templates/basename.model(sequelizeModels).js new file mode 100644 index 000000000..051c5daf2 --- /dev/null +++ b/endpoint/templates/basename.model(sequelizeModels).js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = function(sequelize, DataTypes) { + return sequelize.define('<%= classedName %>', { + _id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true + }, + name: DataTypes.STRING, + info: DataTypes.STRING, + active: DataTypes.BOOLEAN + }); +}; diff --git a/endpoint/templates/basename.socket(socketio).js b/endpoint/templates/basename.socket(socketio).js new file mode 100644 index 000000000..037f6113a --- /dev/null +++ b/endpoint/templates/basename.socket(socketio).js @@ -0,0 +1,34 @@ +/** + * Broadcast updates to client when the model changes + */ + +'use strict'; + +var <%= classedName %>Events = require('./<%= basename %>.events'); + +// Model events to emit +var events = ['save', 'remove']; + +exports.register = function(socket) { + // Bind model events to socket events + for (var i = 0, eventsLength = events.length; i < eventsLength; i++) { + var event = events[i]; + var listener = createListener('<%= cameledName %>:' + event, socket); + + <%= classedName %>Events.on(event, listener); + socket.on('disconnect', removeListener(event, listener)); + } +}; + + +function createListener(event, socket) { + return function(doc) { + socket.emit(event, doc); + }; +} + +function removeListener(event, listener) { + return function() { + <%= classedName %>Events.removeListener(event, listener); + }; +} diff --git a/endpoint/templates/index.js b/endpoint/templates/index.js index 03fdc9779..26dc430dd 100644 --- a/endpoint/templates/index.js +++ b/endpoint/templates/index.js @@ -1,15 +1,15 @@ 'use strict'; var express = require('express'); -var controller = require('./<%= name %>.controller'); +var controller = require('./<%= basename %>.controller'); var router = express.Router(); -router.get('/', controller.index);<% if(filters.mongoose) { %> +router.get('/', controller.index);<% if (filters.models) { %> router.get('/:id', controller.show); router.post('/', controller.create); 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/endpoint/templates/index.spec.js b/endpoint/templates/index.spec.js new file mode 100644 index 000000000..4bd178948 --- /dev/null +++ b/endpoint/templates/index.spec.js @@ -0,0 +1,97 @@ +'use strict'; + +var proxyquire = require('proxyquire').noPreserveCache(); + +var <%= cameledName %>CtrlStub = { + index: '<%= cameledName %>Ctrl.index'<% if(filters.models) { %>, + show: '<%= cameledName %>Ctrl.show', + create: '<%= cameledName %>Ctrl.create', + update: '<%= cameledName %>Ctrl.update', + destroy: '<%= cameledName %>Ctrl.destroy'<% } %> +}; + +var routerStub = { + get: sinon.spy()<% if(filters.models) { %>, + put: sinon.spy(), + patch: sinon.spy(), + post: sinon.spy(), + delete: sinon.spy()<% } %> +}; + +// require the index with our stubbed out modules +var <%= cameledName %>Index = proxyquire('./index.js', { + 'express': { + Router: function() { + return routerStub; + } + }, + './<%= basename %>.controller': <%= cameledName %>CtrlStub +}); + +describe('<%= classedName %> API Router:', function() { + + it('should return an express router instance', function() { + <%= cameledName %>Index.should.equal(routerStub); + }); + + describe('GET <%= route %>', function() { + + it('should route to <%= cameledName %>.controller.index', function() { + routerStub.get + .withArgs('/', '<%= cameledName %>Ctrl.index') + .should.have.been.calledOnce; + }); + + });<% if(filters.models) { %> + + describe('GET <%= route %>/:id', function() { + + it('should route to <%= cameledName %>.controller.show', function() { + routerStub.get + .withArgs('/:id', '<%= cameledName %>Ctrl.show') + .should.have.been.calledOnce; + }); + + }); + + describe('POST <%= route %>', function() { + + it('should route to <%= cameledName %>.controller.create', function() { + routerStub.post + .withArgs('/', '<%= cameledName %>Ctrl.create') + .should.have.been.calledOnce; + }); + + }); + + describe('PUT <%= route %>/:id', function() { + + it('should route to <%= cameledName %>.controller.update', function() { + routerStub.put + .withArgs('/:id', '<%= cameledName %>Ctrl.update') + .should.have.been.calledOnce; + }); + + }); + + describe('PATCH <%= route %>/:id', function() { + + it('should route to <%= cameledName %>.controller.update', function() { + routerStub.patch + .withArgs('/:id', '<%= cameledName %>Ctrl.update') + .should.have.been.calledOnce; + }); + + }); + + describe('DELETE <%= route %>/:id', function() { + + it('should route to <%= cameledName %>.controller.destroy', function() { + routerStub.delete + .withArgs('/:id', '<%= cameledName %>Ctrl.destroy') + .should.have.been.calledOnce; + }); + + });<% } %> + +}); diff --git a/endpoint/templates/name.controller.js b/endpoint/templates/name.controller.js deleted file mode 100644 index 3d46b2ad4..000000000 --- a/endpoint/templates/name.controller.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -var _ = require('lodash');<% if (filters.mongoose) { %> -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) { - if(err) { return handleError(res, err); } - return res.status(200).json(<%= name %>s); - });<% } %> -};<% 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.status(404).send('Not Found'); } - return res.json(<%= name %>); - }); -}; - -// 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.status(201).json(<%= name %>); - }); -}; - -// 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.status(404).send('Not Found'); } - var updated = _.merge(<%= name %>, req.body); - updated.save(function (err) { - if (err) { return handleError(res, err); } - return res.status(200).json(<%= name %>); - }); - }); -}; - -// 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.status(404).send('Not Found'); } - <%= name %>.remove(function(err) { - if(err) { return handleError(res, err); } - return res.status(204).send('No Content'); - }); - }); -}; - -function handleError(res, err) { - return res.status(500).send(err); -}<% } %> \ No newline at end of file diff --git a/endpoint/templates/name.socket(socketio).js b/endpoint/templates/name.socket(socketio).js deleted file mode 100644 index 886f585ee..000000000 --- a/endpoint/templates/name.socket(socketio).js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Broadcast updates to client when the model changes - */ - -'use strict'; - -var <%= classedName %> = require('./<%= name %>.model'); - -exports.register = function(socket) { - <%= classedName %>.schema.post('save', function (doc) { - onSave(socket, doc); - }); - <%= classedName %>.schema.post('remove', function (doc) { - onRemove(socket, doc); - }); -} - -function onSave(socket, doc, cb) { - socket.emit('<%= name %>:save', doc); -} - -function onRemove(socket, doc, cb) { - socket.emit('<%= name %>:remove', doc); -} \ No newline at end of file diff --git a/endpoint/templates/name.spec.js b/endpoint/templates/name.spec.js deleted file mode 100644 index fcad73ebd..000000000 --- a/endpoint/templates/name.spec.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -var should = require('should'); -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(); - }); - }); -}); \ No newline at end of file diff --git a/factory/index.js b/factory/index.js index 584079bad..c303eb9b8 100644 --- a/factory/index.js +++ b/factory/index.js @@ -7,4 +7,4 @@ var Generator = yeoman.generators.Base.extend({ } }); -module.exports = Generator; \ No newline at end of file +module.exports = Generator; diff --git a/filter/index.js b/filter/index.js index 8aafad6f7..d1119b27d 100644 --- a/filter/index.js +++ b/filter/index.js @@ -7,4 +7,4 @@ var Generator = yeoman.generators.Base.extend({ } }); -module.exports = Generator; \ No newline at end of file +module.exports = Generator; diff --git a/generators/constant/index.js b/generators/constant/index.js index fd7b5c574..4524a8eed 100644 --- a/generators/constant/index.js +++ b/generators/constant/index.js @@ -11,4 +11,4 @@ util.inherits(Generator, yeoman.generators.Base); Generator.prototype.deprecated = function deprecated() { this.log(chalk.yellow('This sub-generator is deprecated. \n')); -}; \ No newline at end of file +}; diff --git a/generators/deploy/index.js b/generators/deploy/index.js index 6a3d5ec9c..7fb3452fa 100644 --- a/generators/deploy/index.js +++ b/generators/deploy/index.js @@ -12,4 +12,4 @@ util.inherits(Generator, yeoman.generators.NamedBase); Generator.prototype.deprecated = function deprecated() { this.log(chalk.yellow(chalk.bold('yo angular-fullstack:deploy') + ' is deprecated, instead use: \n') + chalk.green('yo angular-fullstack:heroku') + ' or ' + chalk.green('yo angular-fullstack:openshift')); -}; \ No newline at end of file +}; diff --git a/generators/readme.md b/generators/readme.md index 670a62a57..d56c72138 100644 --- a/generators/readme.md +++ b/generators/readme.md @@ -1 +1 @@ -This folder is for deprecated generators only. \ No newline at end of file +This folder is for deprecated generators only. diff --git a/generators/value/index.js b/generators/value/index.js index fd7b5c574..4524a8eed 100644 --- a/generators/value/index.js +++ b/generators/value/index.js @@ -11,4 +11,4 @@ util.inherits(Generator, yeoman.generators.Base); Generator.prototype.deprecated = function deprecated() { this.log(chalk.yellow('This sub-generator is deprecated. \n')); -}; \ No newline at end of file +}; diff --git a/generators/view/index.js b/generators/view/index.js index fd7b5c574..4524a8eed 100644 --- a/generators/view/index.js +++ b/generators/view/index.js @@ -11,4 +11,4 @@ util.inherits(Generator, yeoman.generators.Base); Generator.prototype.deprecated = function deprecated() { this.log(chalk.yellow('This sub-generator is deprecated. \n')); -}; \ No newline at end of file +}; diff --git a/openshift/USAGE b/openshift/USAGE index b3dd18759..a57763b36 100644 --- a/openshift/USAGE +++ b/openshift/USAGE @@ -5,4 +5,4 @@ Example: yo angular-fullstack:openshift This will create: - a dist folder and initialize an openshift app \ No newline at end of file + a dist folder and initialize an openshift app diff --git a/openshift/index.js b/openshift/index.js index 518806b08..7929c0e09 100644 --- a/openshift/index.js +++ b/openshift/index.js @@ -107,7 +107,7 @@ Generator.prototype.rhcAppShow = function rhcAppShow() { this.abort = true; } // No remote found - else if (stdout.search('not found.') < 0) { + else if (stdout.search('not found.') >= 0) { console.log('No existing app found.'); } // Error diff --git a/package.json b/package.json index dbf3f3662..ccfcd3ff6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "generator-angular-fullstack", - "version": "2.1.1", + "version": "3.0.0-rc4", "description": "Yeoman generator for creating MEAN stack applications, using MongoDB, Express, AngularJS, and Node", "keywords": [ "yeoman-generator", @@ -26,39 +26,32 @@ "test": "grunt test" }, "dependencies": { - "yeoman-generator": "~0.17.0", - "chalk": "~0.4.0", - "wiredep": "~0.4.2", - "generator-ng-component": "~0.0.4" - }, - "peerDependencies": { - "yo": ">=1.2.0" + "chalk": "^1.1.0", + "generator-ng-component": "~0.1.0", + "yeoman-generator": "~0.18.10" }, "devDependencies": { - "chai": "^1.9.1", - "fs-extra": "^0.9.1", + "chai": "^3.2.0", "grunt": "~0.4.1", - "grunt-build-control": "DaftMonk/grunt-build-control", + "grunt-build-control": "^0.5.0", "grunt-contrib-clean": "^0.6.0", - "grunt-contrib-jshint": "^0.10.0", + "grunt-contrib-jshint": "^0.11.2", "grunt-conventional-changelog": "~1.0.0", - "grunt-mocha-test": "^0.11.0", - "grunt-release": "~0.6.0", - "load-grunt-tasks": "~0.2.0", - "marked": "~0.2.8", - "mocha": "~1.21.0", + "grunt-david": "~0.5.0", + "grunt-env": "^0.4.1", + "grunt-mocha-test": "^0.12.7", + "grunt-release": "^0.13.0", + "jit-grunt": "^0.9.1", + "mocha": "^2.2.5", "q": "^1.0.1", - "semver": "~2.2.1", - "shelljs": "^0.3.0", - "underscore.string": "^2.3.3" + "recursive-readdir": "^1.2.0", + "semver": "^5.0.1", + "shelljs": "^0.5.3", + "underscore.string": "^3.1.1" }, "engines": { - "node": ">=0.10.0", + "node": ">=0.12.0", "npm": ">=1.2.10" }, - "licenses": [ - { - "type": "BSD" - } - ] + "license": "BSD-2-Clause" } diff --git a/provider/index.js b/provider/index.js index ed40ef29d..5e3ac882e 100644 --- a/provider/index.js +++ b/provider/index.js @@ -7,4 +7,4 @@ var Generator = yeoman.generators.Base.extend({ } }); -module.exports = Generator; \ No newline at end of file +module.exports = Generator; diff --git a/readme.md b/readme.md index 21f73197a..f5f37e9e1 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,5 @@ # AngularJS Full-Stack generator -[](http://travis-ci.org/DaftMonk/generator-angular-fullstack) [](http://badge.fury.io/js/generator-angular-fullstack)  [](https://gitter.im/DaftMonk/generator-angular-fullstack) +[](http://travis-ci.org/DaftMonk/generator-angular-fullstack) [](http://badge.fury.io/js/generator-angular-fullstack) [](https://david-dm.org/daftmonk/generator-angular-fullstack) [](https://gitter.im/DaftMonk/generator-angular-fullstack) > Yeoman generator for creating MEAN stack applications, using MongoDB, Express, AngularJS, and Node - lets you quickly set up a project following best practices. @@ -11,9 +11,9 @@ Source code: https://github.com/DaftMonk/fullstack-demo ## Usage -Install `generator-angular-fullstack`: +Install `yo`, `grunt-cli`, `bower`, and `generator-angular-fullstack`: ``` -npm install -g generator-angular-fullstack +npm install -g yo grunt-cli bower generator-angular-fullstack ``` Make a new directory, and `cd` into it: @@ -263,7 +263,7 @@ To work with your new heroku app using the command line, you will need to run an If you're using mongoDB you will need to add a database to your app: - heroku addons:add mongolab + heroku addons:create mongolab Your app should now be live. To view it run `heroku open`. @@ -302,7 +302,6 @@ The following packages are always installed by the [app](#app) generator: * angular-mocks * angular-resource * angular-sanitize -* angular-scenario * es5-shim * font-awesome * json3 @@ -340,6 +339,19 @@ To setup protractor e2e tests, you must first run Use `grunt test:e2e` to have protractor go through tests located in the `e2e` folder. +**Code Coverage** + +Use `grunt test:coverage` to run mocha-istanbul and generate code coverage reports. + +`coverage/server` will be populated with `e2e` and `unit` folders containing the `lcov` reports. + +The coverage taget has 3 available options: +- `test:coverage:unit` generate server unit test coverage +- `test:coverage:e2e` generate server e2e test coverage +- `test:coverage:check` combine the coverage reports and check against predefined thresholds + +* *when no option is given `test:coverage` runs all options in the above order* + ## Environment Variables Keeping your app secrets and other sensitive information in source control isn't a good idea. To have grunt launch your app with specific environment variables, add them to the git ignored environment config file: `server/config/local.env.js`. diff --git a/route/index.js b/route/index.js index 2a69476c5..cc8569854 100644 --- a/route/index.js +++ b/route/index.js @@ -7,4 +7,4 @@ var Generator = yeoman.generators.Base.extend({ } }); -module.exports = Generator; \ No newline at end of file +module.exports = Generator; diff --git a/script-base.js b/script-base.js index 2f44ce09f..f24b61f85 100644 --- a/script-base.js +++ b/script-base.js @@ -15,11 +15,33 @@ var Generator = module.exports = function Generator() { this.appname = this._.slugify(this._.humanize(this.appname)); this.scriptAppName = this._.camelize(this.appname) + angularUtils.appName(this); - this.cameledName = this._.camelize(this.name); - this.classedName = this._.classify(this.name); + var name = this.name.replace(/\//g, '-'); + + this.cameledName = this._.camelize(name); + this.classedName = this._.classify(name); + + this.basename = path.basename(this.name); + this.dirname = (this.name.indexOf('/') >= 0) ? path.dirname(this.name) : this.name; + + // dynamic assertion statement + this.does = this.is = function(foo) { + foo = this.engine(foo.replace(/\(;>%%<;\)/g, '<%') + .replace(/\(;>%<;\)/g, '%>'), this); + if (this.filters.should) { + return foo + '.should'; + } else { + return 'expect(' + foo + ').to'; + } + }.bind(this); + + // dynamic relative require path + this.relativeRequire = function(to, fr) { + fr = fr || this.filePath; + return angularUtils.relativeRequire(this, to, fr); + }.bind(this); this.filters = this.config.get('filters'); this.sourceRoot(path.join(__dirname, '/templates')); }; -util.inherits(Generator, yeoman.generators.NamedBase); \ No newline at end of file +util.inherits(Generator, yeoman.generators.NamedBase); diff --git a/scripts/sauce_connect_setup.sh b/scripts/sauce_connect_setup.sh new file mode 100755 index 000000000..4348e3662 --- /dev/null +++ b/scripts/sauce_connect_setup.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# Setup and start Sauce Connect for your TravisCI build +# This script requires your .travis.yml to include the following two private env variables: +# SAUCE_USERNAME +# SAUCE_ACCESS_KEY +# Follow the steps at https://saucelabs.com/opensource/travis to set that up. +# https://gist.githubusercontent.com/santiycr/4683e262467c0a84d857/raw/1254ace59257e341ab200cbc8946e626756f079f/sauce-connect.sh + +if [ -z "${SAUCE_USERNAME}" ] || [ -z "${SAUCE_ACCESS_KEY}" ]; then + echo "This script can't run without your Sauce credentials" + echo "Please set SAUCE_USERNAME and SAUCE_ACCESS_KEY env variables" + echo "export SAUCE_USERNAME=ur-username" + echo "export SAUCE_ACCESS_KEY=ur-access-key" + exit 1 +fi + +SAUCE_TMP_DIR="$(mktemp -d -t sc.XXXX)" +echo "Using temp dir $SAUCE_TMP_DIR" +pushd $SAUCE_TMP_DIR + +SAUCE_CONNECT_PLATFORM=$(uname | sed -e 's/Darwin/osx/' -e 's/Linux/linux/') +case "${SAUCE_CONNECT_PLATFORM}" in + linux) + SC_DISTRIBUTION_FMT=tar.gz;; + *) + SC_DISTRIBUTION_FMT=zip;; +esac +SC_DISTRIBUTION=sc-latest-${SAUCE_CONNECT_PLATFORM}.${SC_DISTRIBUTION_FMT} +SC_READYFILE=sauce-connect-ready-$RANDOM +SC_LOGFILE=$HOME/sauce-connect.log +if [ ! -z "${TRAVIS_JOB_NUMBER}" ]; then + SC_TUNNEL_ID="-i ${TRAVIS_JOB_NUMBER}" +fi +echo "Downloading Sauce Connect" +wget https://saucelabs.com/downloads/${SC_DISTRIBUTION} +SC_DIR=$(tar -ztf ${SC_DISTRIBUTION} | head -n1) + +echo "Extracting Sauce Connect" +case "${SC_DISTRIBUTION_FMT}" in + tar.gz) + tar zxf $SC_DISTRIBUTION;; + zip) + unzip $SC_DISTRIBUTION;; +esac + +echo "Starting Sauce Connect" +${SC_DIR}/bin/sc \ + ${SC_TUNNEL_ID} \ + -f ${SC_READYFILE} \ + -l ${SC_LOGFILE} & + +echo "Waiting for Sauce Connect readyfile" +while [ ! -f ${SC_READYFILE} ]; do + sleep .5 +done + +unset SAUCE_CONNECT_PLATFORM SAUCE_TMP_DIR SC_DIR SC_DISTRIBUTION SC_READYFILE SC_LOGFILE SC_TUNNEL_ID + +popd diff --git a/service/index.js b/service/index.js index d133abdbc..9aa5f48b0 100644 --- a/service/index.js +++ b/service/index.js @@ -7,4 +7,4 @@ var Generator = yeoman.generators.Base.extend({ } }); -module.exports = Generator; \ No newline at end of file +module.exports = Generator; diff --git a/test/fixtures/.yo-rc.json b/test/fixtures/.yo-rc.json index b4b338ac4..01d568984 100644 --- a/test/fixtures/.yo-rc.json +++ b/test/fixtures/.yo-rc.json @@ -1,19 +1,36 @@ { "generator-angular-fullstack": { - "insertRoutes": "true", + "endpointDirectory": "server/api/", + "insertRoutes": true, "registerRoutesFile": "server/routes.js", "routesNeedle": "// Insert routes below", - "insertSockets": "true", + "routesBase": "/api/", + "pluralizeRoutes": true, + "insertSockets": true, "registerSocketsFile": "server/config/socketio.js", "socketsNeedle": "// Insert sockets below", + "insertModels": true, + "registerModelsFile": "server/sqldb/index.js", + "modelsNeedle": "// Insert models below", "filters": { "coffee": true, "html": true, "less": true, "uirouter": true, + "bootstrap": false, + "uibootstrap": false, "socketio": true, + "auth": true, + "models": true, + "mongooseModels": true, "mongoose": true, - "auth": true + "oauth": true, + "googleAuth": true, + "grunt": true, + "mocha": true, + "jasmine": false, + "should": true, + "expect": false } } -} \ No newline at end of file +} diff --git a/test/fixtures/bower.json b/test/fixtures/bower.json deleted file mode 100644 index 10dff6513..000000000 --- a/test/fixtures/bower.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "tempApp", - "version": "0.0.0", - "dependencies": { - "angular": ">=1.2.*", - "json3": "~3.3.1", - "es5-shim": "~3.0.1", - "bootstrap-sass-official": "~3.1.1", - "bootstrap": "~3.1.1", - "angular-resource": ">=1.2.*", - "angular-cookies": ">=1.2.*", - "angular-sanitize": ">=1.2.*", - "angular-route": ">=1.2.*", - "angular-bootstrap": "~0.11.0", - "font-awesome": ">=4.1.0", - "lodash": "~2.4.1", - "angular-socket-io": "~0.6.0", - "angular-ui-router": "~0.2.10" - }, - "devDependencies": { - "angular-mocks": ">=1.2.*", - "angular-scenario": ">=1.2.*" - } -} diff --git a/test/fixtures/package.json b/test/fixtures/package.json deleted file mode 100644 index c110f7838..000000000 --- a/test/fixtures/package.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "name": "tempApp", - "version": "0.0.0", - "main": "server/app.js", - "dependencies": { - "express": "~4.9.0", - "morgan": "~1.0.0", - "body-parser": "~1.5.0", - "method-override": "~1.0.0", - "serve-favicon": "~2.0.1", - "cookie-parser": "~1.0.1", - "express-session": "~1.0.2", - "errorhandler": "~1.0.0", - "compression": "~1.0.1", - "lodash": "~2.4.1", - "jade": "~1.2.0", - "ejs": "~0.8.4", - "mongoose": "~3.8.8", - "jsonwebtoken": "^0.3.0", - "express-jwt": "^0.1.3", - "passport": "~0.2.0", - "passport-local": "~0.1.6", - "passport-facebook": "latest", - "passport-twitter": "latest", - "passport-google-oauth": "latest", - "composable-middleware": "^0.3.0", - "connect-mongo": "^0.4.1", - "socket.io": "^1.0.6", - "socket.io-client": "^1.0.6", - "socketio-jwt": "^2.0.2" - }, - "devDependencies": { - "grunt": "~0.4.4", - "grunt-autoprefixer": "~0.7.2", - "grunt-wiredep": "~1.8.0", - "grunt-concurrent": "~0.5.0", - "grunt-contrib-clean": "~0.5.0", - "grunt-contrib-concat": "~0.4.0", - "grunt-contrib-copy": "~0.5.0", - "grunt-contrib-cssmin": "~0.9.0", - "grunt-contrib-htmlmin": "~0.2.0", - "grunt-contrib-imagemin": "~0.7.1", - "grunt-contrib-jshint": "~0.10.0", - "grunt-contrib-uglify": "~0.4.0", - "grunt-contrib-watch": "~0.6.1", - "grunt-contrib-coffee": "^0.10.1", - "grunt-contrib-jade": "^0.11.0", - "grunt-contrib-less": "^0.11.0", - "karma-babel-preprocessor": "^5.2.1", - "grunt-babel": "~5.0.0", - "grunt-google-cdn": "~0.4.0", - "grunt-newer": "~0.7.0", - "grunt-ng-annotate": "^0.2.3", - "grunt-rev": "~0.1.0", - "grunt-svgmin": "~0.4.0", - "grunt-usemin": "~2.1.1", - "grunt-env": "~0.4.1", - "grunt-node-inspector": "~0.1.5", - "grunt-nodemon": "~0.2.0", - "grunt-angular-templates": "^0.5.4", - "grunt-dom-munger": "^3.4.0", - "grunt-protractor-runner": "^1.1.0", - "grunt-injector": "~0.5.4", - "grunt-karma": "~0.8.2", - "grunt-build-control": "DaftMonk/grunt-build-control", - "grunt-mocha-test": "~0.10.2", - "grunt-contrib-sass": "^0.7.3", - "grunt-contrib-stylus": "latest", - "jit-grunt": "^0.5.0", - "time-grunt": "~0.3.1", - "grunt-express-server": "~0.4.17", - "grunt-open": "~0.2.3", - "open": "~0.0.4", - "jshint-stylish": "~0.1.5", - "connect-livereload": "~0.4.0", - "karma-ng-scenario": "~0.1.0", - "karma-firefox-launcher": "~0.1.3", - "karma-script-launcher": "~0.1.0", - "karma-html2js-preprocessor": "~0.1.0", - "karma-ng-jade2js-preprocessor": "^0.1.2", - "karma-jasmine": "~0.1.5", - "karma-chrome-launcher": "~0.1.3", - "requirejs": "~2.1.11", - "karma-requirejs": "~0.2.1", - "karma-coffee-preprocessor": "~0.2.1", - "karma-jade-preprocessor": "0.0.11", - "karma-phantomjs-launcher": "~0.1.4", - "karma": "~0.12.9", - "karma-ng-html2js-preprocessor": "~0.1.0", - "supertest": "~0.11.0", - "should": "~3.3.1" - }, - "engines": { - "node": ">=0.10.0" - }, - "scripts": { - "start": "node server/app.js", - "test": "grunt test", - "update-webdriver": "node node_modules/grunt-protractor-runner/node_modules/protractor/bin/webdriver-manager update" - }, - "private": true -} diff --git a/test/readme.md b/test/readme.md deleted file mode 100644 index 6c709f50b..000000000 --- a/test/readme.md +++ /dev/null @@ -1 +0,0 @@ -Run bower install and npm install in the fixtures folder before running tests \ No newline at end of file diff --git a/test/test-file-creation.js b/test/test-file-creation.js index 594dab05a..7850fc9e4 100644 --- a/test/test-file-creation.js +++ b/test/test-file-creation.js @@ -1,43 +1,322 @@ /*global describe, beforeEach, it */ 'use strict'; var path = require('path'); +var fs = require('fs'); +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 = { script: 'js', + babel: true, markup: 'html', stylesheet: 'sass', router: 'uirouter', + testing: 'mocha', + chai: 'expect', bootstrap: true, uibootstrap: true, - mongoose: true, + odms: [ 'mongoose' ], auth: true, oauth: [], socketio: true }, dependenciesInstalled = false; + function copySync(s, d) { fs.writeFileSync(d, fs.readFileSync(s)); } + function generatorTest(generatorType, name, mockPrompt, callback) { - gen.run({}, function () { + gen.run(function () { var afGenerator; var deps = [path.join('../..', generatorType)]; afGenerator = helpers.createGenerator('angular-fullstack:' + generatorType, deps, [name]); helpers.mockPrompt(afGenerator, mockPrompt); - afGenerator.run([], function () { + afGenerator.run(function () { callback(); }); }); } + /** + * Assert that only an array of files exist at a given path + * + * @param {Array} expectedFiles - array of files + * @param {Function} done - callback(error{Error}) + * @param {String} topLevelPath - top level path to assert files at (optional) + * @param {Array} skip - array of paths to skip/ignore (optional) + * + */ + function assertOnlyFiles(expectedFiles, done, topLevelPath, skip) { + topLevelPath = topLevelPath || './'; + skip = skip || ['node_modules', 'client/bower_components']; + + recursiveReadDir(topLevelPath, skip, function(err, actualFiles) { + if (err) { return done(err); } + var files = actualFiles.concat(); + + expectedFiles.forEach(function(file, i) { + var index = files.indexOf(path.normalize(file)); + if (index >= 0) { + files.splice(index, 1); + } + }); + + if (files.length !== 0) { + err = new Error('unexpected files found'); + err.expected = expectedFiles.join('\n'); + err.actual = files.join('\n'); + return done(err); + } + + done(); + }); + } + + /** + * Exec a command and run test assertion(s) based on command type + * + * @param {String} cmd - the command to exec + * @param {Object} self - context of the test + * @param {Function} cb - callback() + * @param {String} endpoint - endpoint to generate before exec (optional) + * @param {Number} timeout - timeout for the exec and test (optional) + * + */ + 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); + + var execFn = function() { + var cmdCode; + var cp = exec(cmd, function(error, stdout, stderr) { + if(cmdCode !== 0) { + console.error(stdout); + throw new Error('Error running command: ' + cmd); + } + cb(); + }); + cp.on('exit', function (code) { + cmdCode = code; + }); + }; + + if (endpoint) { + generatorTest('endpoint', endpoint, {}, execFn); + } else { + gen.run(execFn); + } + } + + /** + * Generate an array of files to expect from a set of options + * + * @param {Object} ops - generator options + * @return {Array} - array of files + * + */ + 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 = []; + + /** + * Generate an array of OAuth files based on type + * + * @param {String} type - type of oauth + * @return {Array} - array of 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], + models = ops.models ? ops.models : ops.odms[0]; + + /* Core Files */ + 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/footer/footer.' + stylesheet, + 'client/components/footer/footer.' + markup, + 'client/components/footer/footer.directive.' + script, + 'client/components/navbar/navbar.' + markup, + 'client/components/navbar/navbar.controller.' + script, + 'client/components/navbar/navbar.directive.' + script, + 'server/.jshintrc', + 'server/.jshintrc-spec', + 'server/app.js', + 'server/index.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.integration.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, + 'e2e/main/main.po.js', + 'e2e/main/main.spec.js', + 'e2e/components/navbar/navbar.po.js', + '.bowerrc', + '.buildignore', + '.editorconfig', + '.gitattributes', + '.gitignore', + '.travis.yml', + '.jscs.json', + '.yo-rc.json', + 'Gruntfile.js', + 'package.json', + 'bower.json', + 'karma.conf.js', + 'mocha.conf.js', + 'protractor.conf.js', + 'README.md' + ]); + + /* Ui-Router */ + if (ops.router === 'uirouter') { + files = files.concat([ + 'client/components/ui-router/ui-router.mock.' + script + ]); + } + + /* Ui-Bootstrap */ + if (ops.uibootstrap) { + files = files.concat([ + 'client/components/modal/modal.' + markup, + 'client/components/modal/modal.' + stylesheet, + 'client/components/modal/modal.service.' + script + ]); + } + + /* Models - Mongoose or Sequelize */ + if (models) { + files = files.concat([ + 'server/api/thing/thing.model.js', + 'server/api/thing/thing.events.js', + 'server/config/seed.js' + ]); + } + + /* Sequelize */ + if (ops.odms.indexOf('sequelize') !== -1) { + files = files.concat([ + 'server/sqldb/index.js' + ]); + } + + /* Authentication */ + 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.integration.js', + 'server/api/user/user.model.js', + 'server/api/user/user.model.spec.js', + 'server/api/user/user.events.js', + 'server/auth/index.js', + 'server/auth/auth.service.js', + 'server/auth/local/index.js', + 'server/auth/local/passport.js', + 'e2e/account/login/login.po.js', + 'e2e/account/login/login.spec.js', + 'e2e/account/logout/logout.spec.js', + 'e2e/account/signup/signup.po.js', + 'e2e/account/signup/signup.spec.js' + ]); + } + + /* OAuth (see oauthFiles function above) */ + if (ops.oauth) { + ops.oauth.forEach(function(type, i) { + files = files.concat(oauthFiles(type.replace('Auth', ''))); + }); + } + + /* Socket.IO */ + 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; + } + + + /** + * Generator tests + */ + beforeEach(function (done) { this.timeout(10000); var deps = [ '../../app', + '../../endpoint', [ helpers.createDummyGenerator(), 'ng-component:app' @@ -55,6 +334,41 @@ describe('angular-fullstack generator', function () { }.bind(this)); }); + describe('making sure test fixtures are present', function() { + + it('should have package.json in fixtures', function() { + helpers.assertFile([ + path.join(__dirname, 'fixtures', 'package.json') + ]); + }); + + it('should have bower.json in fixtures', function() { + helpers.assertFile([ + path.join(__dirname, 'fixtures', 'bower.json') + ]); + }); + + it('should have all npm packages in fixtures/node_modules', function() { + var packageJson = require('./fixtures/package.json'); + var deps = Object.keys(packageJson.dependencies); + deps = deps.concat(Object.keys(packageJson.devDependencies)); + deps = deps.map(function(dep) { + return path.join(__dirname, 'fixtures', 'node_modules', dep); + }); + helpers.assertFile(deps); + }); + + it('should have all bower packages in fixtures/bower_components', function() { + var bowerJson = require('./fixtures/bower.json'); + var deps = Object.keys(bowerJson.dependencies); + deps = deps.concat(Object.keys(bowerJson.devDependencies)); + deps = deps.map(function(dep) { + return path.join(__dirname, 'fixtures', 'bower_components', dep); + }); + helpers.assertFile(deps); + }); + }); + describe('running app', function() { beforeEach(function() { @@ -70,50 +384,70 @@ 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 SUCCESS'); - 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(); - }); - }); + it('should pass jscs', function(done) { + runTest('grunt jscs', this, done); + }); + + it('should pass lint', function(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 pass jscs with generated endpoint', function(done) { + runTest('grunt jscs', this, done, 'foo'); + }); + + 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 pass lint with generated path name endpoint', function(done) { + runTest('grunt jshint', this, done, 'foo/bar'); + }); + + it('should run server tests successfully with generated path name endpoint', function(done) { + runTest('grunt test:server', this, done, 'foo/bar'); + }); + + it('should generate expected files with path name endpoint', function(done) { + runTest('(exit 0)', this, function() { + helpers.assertFile([ + 'server/api/foo/bar/index.js', + 'server/api/foo/bar/index.spec.js', + 'server/api/foo/bar/bar.controller.js', + 'server/api/foo/bar/bar.events.js', + 'server/api/foo/bar/bar.integration.js', + 'server/api/foo/bar/bar.model.js', + 'server/api/foo/bar/bar.socket.js' + ]); + done(); + }, 'foo/bar'); }); it('should use existing config if available', function(done) { this.timeout(60000); - fs.copySync(__dirname + '/fixtures/.yo-rc.json', __dirname + '/temp/.yo-rc.json'); + copySync(__dirname + '/fixtures/.yo-rc.json', __dirname + '/temp/.yo-rc.json'); var gen = helpers.createGenerator('angular-fullstack:app', [ '../../app', + '../../endpoint', [ helpers.createDummyGenerator(), 'ng-component:app' @@ -123,232 +457,317 @@ describe('angular-fullstack generator', function () { helpers.mockPrompt(gen, { skipConfig: true }); - gen.run({}, function () { + gen.run(function () { helpers.assertFile([ 'client/app/main/main.less', - 'client/app/main/main.coffee' + 'client/app/main/main.coffee', + 'server/auth/google/passport.js' ]); done(); }); }); -// it('should run e2e tests successfully', function(done) { -// this.timeout(80000); -// gen.run({}, function () { -// exec('npm run update-webdriver', function (error, stdout, stderr) { -// exec('grunt test:e2e', function (error, stdout, stderr) { -// expect(stdout, 'Client tests failed \n' + stdout ).to.contain('Done, without errors.'); -// done(); -// }); -// }); -// }) -// }); + 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); + }); + }); + + if(!process.env.SKIP_E2E) { + it('should run e2e tests successfully', function(done) { + runTest('grunt test:e2e', this, done, 240000); + }); + + //it('should run e2e tests successfully for production app', function(done) { + // runTest('grunt test:e2e:prod', this, done, 240000); + //}); + } }); - describe('with Babel ES6 preprocessor', function() { + describe('with other preprocessors and oauth', function() { + var testOptions = { + script: 'coffee', + markup: 'jade', + stylesheet: 'less', + router: 'uirouter', + testing: 'jasmine', + odms: [ 'mongoose' ], + auth: true, + oauth: ['twitterAuth', 'facebookAuth', 'googleAuth'], + socketio: true, + bootstrap: true, + uibootstrap: true + }; + beforeEach(function() { - helpers.mockPrompt(gen, { - script: 'js', - babel: true, - markup: 'jade', - stylesheet: 'less', - router: 'uirouter' - }); + 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 SUCCESS'); - 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(); - }); - }); + it('should pass jscs', function(done) { + runTest('grunt jscs', this, done); + }); + + it('should pass lint', function(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 pass jscs with generated endpoint', function(done) { + runTest('grunt jscs', this, done, 'foo'); + }); + + 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)); + done(); + }); + }); + + it('should not generate unexpected files', function (done) { + gen.run(function () { + assertOnlyFiles(genFiles(testOptions), done); }); }); + + if(!process.env.SKIP_E2E) { + it('should run e2e tests successfully', function (done) { + runTest('grunt test:e2e', this, done, 240000); + }); + + //it('should run e2e tests successfully for production app', function (done) { + // runTest('grunt test:e2e:prod', this, done, 240000); + //}); + } + }); + describe('with sequelize models, auth', function() { + var testOptions = { + script: 'js', + markup: 'jade', + stylesheet: 'stylus', + router: 'uirouter', + testing: 'jasmine', + odms: [ 'sequelize' ], + auth: true, + oauth: ['twitterAuth', 'facebookAuth', 'googleAuth'], + socketio: true, + bootstrap: true, + uibootstrap: true + }; - describe('with other preprocessors and oauth', function() { 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 SUCCESS'); - 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(); - }); - }); + it('should pass jscs', function(done) { + runTest('grunt jscs', this, done); + }); + + it('should pass lint', function(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 pass jscs with generated endpoint', function(done) { + runTest('grunt jscs', this, done, 'foo'); + }); + + 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)); + done(); + }); + }); + + it('should not generate unexpected files', function (done) { + gen.run(function () { + assertOnlyFiles(genFiles(testOptions), done); }); }); + + if(!process.env.SKIP_E2E) { + it('should run e2e tests successfully', function (done) { + runTest('grunt test:e2e', this, done, 240000); + }); + + //it('should run e2e tests successfully for production app', function (done) { + // runTest('grunt test:e2e:prod', this, done, 240000); + //}); + } + }); describe('with other preprocessors and no server options', function() { + var testOptions = { + script: 'coffee', + markup: 'jade', + stylesheet: 'stylus', + router: 'ngroute', + testing: 'mocha', + chai: 'should', + odms: [], + auth: false, + oauth: [], + socketio: false, + bootstrap: false, + uibootstrap: 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 SUCCESS'); - 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(); - }); - }); + it('should pass jscs', function(done) { + runTest('grunt jscs', this, done); + }); + + it('should pass lint', function(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 pass jscs with generated endpoint', function(done) { + runTest('grunt jscs', this, done, 'foo'); + }); + + 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)); + done(); + }); + }); + + it('should not generate unexpected files', function (done) { + gen.run(function () { + assertOnlyFiles(genFiles(testOptions), done); }); }); + + if(!process.env.SKIP_E2E) { + it('should run e2e tests successfully', function (done) { + runTest('grunt test:e2e', this, done, 240000); + }); + + //it('should run e2e tests successfully for production app', function (done) { + // runTest('grunt test:e2e:prod', this, done, 240000); + //}); + } + }); describe('with no preprocessors and no server options', function() { + var testOptions = { + script: 'js', + markup: 'html', + stylesheet: 'css', + router: 'ngroute', + testing: 'jasmine', + odms: [], + auth: false, + oauth: [], + socketio: false, + bootstrap: true, + uibootstrap: true + }; + 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 SUCCESS'); - 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(); - }); - }); + it('should pass jscs', function(done) { + runTest('grunt jscs', this, done); + }); + + it('should pass lint', function(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']); + gen.run(function () { + helpers.assertFile(genFiles(testOptions)); done(); }); }); + + it('should not generate unexpected files', function (done) { + gen.run(function () { + assertOnlyFiles(genFiles(testOptions), done); + }); + }); + + if(!process.env.SKIP_E2E) { + it('should run e2e tests successfully', function (done) { + runTest('grunt test:e2e', this, done, 240000); + }); + + //it('should run e2e tests successfully for production app', function (done) { + // runTest('grunt test:e2e:prod', this, done, 240000); + //}); + } + }); }); }); diff --git a/util.js b/util.js index 7544f8c8e..6657cfc9f 100644 --- a/util.js +++ b/util.js @@ -6,7 +6,8 @@ module.exports = { rewrite: rewrite, rewriteFile: rewriteFile, appName: appName, - processDirectory: processDirectory + processDirectory: processDirectory, + relativeRequire: relativeRequire }; function rewriteFile (args) { @@ -74,6 +75,23 @@ function appName (self) { return suffix ? self._.classify(suffix) : ''; } +function destinationPath (self, filepath) { + filepath = path.normalize(filepath); + if (!path.isAbsolute(filepath)) { + filepath = path.join(self.destinationRoot(), filepath); + } + + return filepath; +} + +function relativeRequire (self, to, fr) { + fr = destinationPath(self, fr); + to = destinationPath(self, to); + return path.relative(path.dirname(fr), to) + .replace(/^(?!\.\.)(.*)/, './$1') + .replace(/[\/\\]index\.js$/, ''); +} + function filterFile (template) { // Find matches for parans var filterMatches = template.match(/\(([^)]+)\)/g); @@ -109,6 +127,9 @@ function processDirectory (self, source, destination) { files.forEach(function(f) { var filteredFile = filterFile(f); + if(self.basename) { + filteredFile.name = filteredFile.name.replace('basename', self.basename); + } if(self.name) { filteredFile.name = filteredFile.name.replace('name', self.name); } @@ -133,8 +154,10 @@ function processDirectory (self, source, destination) { if(copy) { self.copy(src, dest); } else { + self.filePath = dest; self.template(src, dest); + delete self.filePath; } } }); -} \ No newline at end of file +}