diff --git a/Gruntfile.js b/Gruntfile.js index 377eb50..40aa9d2 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -213,6 +213,20 @@ module.exports = function(grunt) { } }, + manifest: { + generate: { + options: { + basePath: 'dist/', + timestamp: true, + hash: true, + cache: ['socket.io/socket.io.js'], + master: ['index.html'] + }, + src: ['**/*.*'], + dest: 'dist/appcache.manifest' + } + }, + nodewebkit: { options: { version: '0.9.2', // node-webkit version @@ -238,12 +252,13 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-contrib-compress'); grunt.loadNpmTasks('grunt-node-webkit-builder'); + grunt.loadNpmTasks('grunt-manifest'); // Build tasks grunt.registerTask('dist-npm', ['copy:npm', 'copy:npmDev', 'copy:cryptoLib']); grunt.registerTask('dist-css', ['sass', 'autoprefixer', 'csso']); grunt.registerTask('dist-copy', ['copy']); - grunt.registerTask('dist', ['clean', 'dist-npm', 'dist-css', 'dist-copy']); + grunt.registerTask('dist', ['clean', 'dist-npm', 'dist-css', 'dist-copy', 'manifest']); // Test/Dev tasks grunt.registerTask('dev', ['connect:dev']); diff --git a/README.md b/README.md index ea96681..826f347 100644 --- a/README.md +++ b/README.md @@ -50,11 +50,21 @@ Then visit [http://localhost:8580/dist/#/desktop?dev=true](http://localhost:8580 grunt watch -## Releasing +## Releasing Chrome App grunt release-test --release=0.0.0.x grunt release-stable --release=0.x.0 +## Deploying Web App + +First build and generate the `dist/` directory: + + grunt + +Then deploy that directoy by adding it to a local git branch. Push that branch to your node.js server and then start the server: + + npm start + ## License Copyright © 2014, Whiteout Networks GmbH. All rights reserved. diff --git a/config/default.js b/config/default.js new file mode 100644 index 0000000..5a7447f --- /dev/null +++ b/config/default.js @@ -0,0 +1,12 @@ +'use strict'; + +module.exports = { + server: { + port: process.env.PORT || 8889, + host: "0.0.0.0" + }, + log: { + level: "silly", + http: ':remote-addr [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer"' + } +}; \ No newline at end of file diff --git a/config/integration.js b/config/integration.js new file mode 100644 index 0000000..2833006 --- /dev/null +++ b/config/integration.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports = { + server: { + port: 8889 + }, + log: { + level: "error" + } +}; \ No newline at end of file diff --git a/package.json b/package.json index 35c72ff..ca7da8f 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "node": ">=0.10" }, "scripts": { - "postinstall": "grunt", "test": "grunt && grunt test", "start": "node server.js" }, @@ -39,26 +38,31 @@ "ng-infinite-scroll": "~1.1.2", "pgpbuilder": "~0.4.0", "pgpmailer": "~0.4.0", - "requirejs": "~2.1.14" + "requirejs": "~2.1.14", + "config": "^1.0.2", + "morgan": "^1.2.3", + "npmlog": "^0.1.1", + "socket.io": "^1.0.6" }, "devDependencies": { "angularjs": "https://github.com/whiteout-io/angular.js/tarball/npm-version", "browsercrow": "https://github.com/whiteout-io/browsercrow/tarball/master", "browsersmtp": "https://github.com/whiteout-io/browsersmtp/tarball/master", - "grunt": "~0.4.1", - "mocha": "~1.13.0", "chai": "~1.7.2", - "sinon": "~1.7.3", + "grunt": "~0.4.1", + "grunt-contrib-clean": "~0.5.0", + "grunt-contrib-copy": "~0.4.1", + "grunt-manifest": "^0.4.0", + "grunt-autoprefixer": "~0.7.2", + "grunt-contrib-compress": "~0.5.2", "grunt-contrib-connect": "~0.5.0", "grunt-contrib-jshint": "~0.6.4", - "grunt-mocha": "~0.4.1", - "grunt-contrib-clean": "~0.5.0", - "grunt-csso": "~0.6.1", "grunt-contrib-sass": "~0.7.3", - "grunt-autoprefixer": "~0.7.2", "grunt-contrib-watch": "~0.5.3", - "grunt-contrib-copy": "~0.4.1", - "grunt-contrib-compress": "~0.5.2", - "grunt-node-webkit-builder": "~0.1.17" + "grunt-csso": "~0.6.1", + "grunt-mocha": "~0.4.1", + "grunt-node-webkit-builder": "~0.1.17", + "mocha": "~1.13.0", + "sinon": "~1.7.3" } } \ No newline at end of file diff --git a/server.js b/server.js index e949b62..23c12c2 100644 --- a/server.js +++ b/server.js @@ -1,8 +1,70 @@ 'use strict'; -var express = require('express'), - compression = require('compression'), - app = express(); +process.chdir(__dirname); + +var cluster = require('cluster'); +var config = require('config'); +var log = require('npmlog'); + +log.level = config.log.level; + +// Handle error conditions +process.on('SIGTERM', function() { + log.warn('exit', 'Exited on SIGTERM'); + process.exit(0); +}); + +process.on('SIGINT', function() { + log.warn('exit', 'Exited on SIGINT'); + process.exit(0); +}); + +process.on('uncaughtException', function(err) { + log.error('uncaughtException ', err); + process.exit(1); +}); + +if (cluster.isMaster) { + // MASTER process + + cluster.on('fork', function(worker) { + log.info('cluster', 'Forked worker #%s [pid:%s]', worker.id, worker.process.pid); + }); + + cluster.on('exit', function(worker) { + log.warn('cluster', 'Worker #%s [pid:%s] died', worker.id, worker.process.pid); + setTimeout(function() { + cluster.fork(); + }, 1000); + }); + + // Fork a single worker + cluster.fork(); + return; +} + +// WORKER process + +var express = require('express'); +var compression = require('compression'); +var app = express(); +var server = require('http').Server(app); +var io = require('socket.io')(server); +var net = require('net'); + +// Setup logger. Stream all http logs to general logger +app.use(require('morgan')(config.log.http, { + 'stream': { + 'write': function(line) { + if ((line = (line || '').trim())) { + log.http('express', line); + } + } + } +})); + +// Do not advertise Express +app.disable('x-powered-by'); // // web server config @@ -17,7 +79,8 @@ app.use(function(req, res, next) { // HSTS res.set('Strict-Transport-Security', 'max-age=16070400; includeSubDomains'); // CSP - res.set('Content-Security-Policy', "default-src 'self'; object-src 'none'; connect-src *; style-src 'self' 'unsafe-inline'; img-src 'self' data:"); + var iframe = development ? "http://" + req.hostname + ":" + port : "https://" + req.hostname; // allow iframe to load assets + res.set('Content-Security-Policy', "default-src 'self' " + iframe + "; object-src 'none'; connect-src *; style-src 'self' 'unsafe-inline' " + iframe + "; img-src 'self' data:"); return next(); }); @@ -38,11 +101,84 @@ app.use(express.static(__dirname + '/dist', { maxAge: oneDay })); +// +// Socket.io proxy +// + +io.on('connection', function(socket) { + + log.info('io', 'New connection [%s]', socket.conn.id); + + var idCounter = 0; + + socket.on('open', function(data, fn) { + var socketId = ++idCounter; + var tcp; + + log.verbose('io', 'Open request to %s:%s [%s:%s]', data.host, data.port, socket.conn.id, socketId); + + tcp = net.connect(data.port, data.host, function() { + log.verbose('io', 'Opened tcp connection to %s:%s [%s:%s]', data.host, data.port, socket.conn.id, socketId); + + tcp.on('data', function(chunk) { + log.silly('io', 'Received %s bytes from %s:%s [%s:%s]', chunk.length, data.host, data.port, socket.conn.id, socketId); + socket.emit('data-' + socketId, chunk); + }); + + tcp.on('error', function(err) { + log.verbose('io', 'Error for %s:%s [%s:%s]: %s', data.host, data.port, socket.conn.id, socketId, err.message); + socket.emit('error-' + socketId, err.message); + }); + + tcp.on('end', function() { + socket.emit('end-' + socketId); + }); + + tcp.on('close', function() { + log.verbose('io', 'Closed tcp connection to %s:%s [%s:%s]', data.host, data.port, socket.conn.id, socketId); + socket.emit('close-' + socketId); + + socket.removeAllListeners('data-' + socketId); + socket.removeAllListeners('end-' + socketId); + }); + + socket.on('data-' + socketId, function(chunk, fn) { + if (!chunk || !chunk.length) { + if (typeof fn === 'function') { + fn(); + } + return; + } + log.silly('io', 'Sending %s bytes to %s:%s [%s:%s]', chunk.length, data.host, data.port, socket.conn.id, socketId); + tcp.write(chunk, function() { + if (typeof fn === 'function') { + fn(); + } + }); + }); + + socket.on('end-' + socketId, function() { + log.verbose('io', 'Received request to close connection to %s:%s [%s:%s]', data.host, data.port, socket.conn.id, socketId); + tcp.end(); + }); + + if (typeof fn === 'function') { + fn(socketId); + } + }); + }); + + socket.on('disconnect', function() { + log.info('io', 'Closed connection [%s]', socket.conn.id); + socket.removeAllListeners(); + }); +}); + // // start server // -app.listen(port); +server.listen(port); if (development) { console.log(' > starting in development mode'); } diff --git a/src/index.html b/src/index.html index c43d2fa..74de1ae 100644 --- a/src/index.html +++ b/src/index.html @@ -1,5 +1,5 @@ - + Whiteout Mail @@ -29,6 +29,7 @@ + diff --git a/src/js/controller/add-account.js b/src/js/controller/add-account.js index 6732271..2530a41 100644 --- a/src/js/controller/add-account.js +++ b/src/js/controller/add-account.js @@ -3,7 +3,12 @@ define(function(require) { var appCtrl = require('js/app-controller'); - var AddAccountCtrl = function($scope, $location) { + var AddAccountCtrl = function($scope, $location, $routeParams) { + if (!appCtrl._auth && !$routeParams.dev) { + $location.path('/'); // init app + return; + } + $scope.connectToGoogle = function() { // test for oauth support if (appCtrl._auth._oauth.isSupported()) { diff --git a/src/js/controller/login-existing.js b/src/js/controller/login-existing.js index 20858c6..35aa6bd 100644 --- a/src/js/controller/login-existing.js +++ b/src/js/controller/login-existing.js @@ -3,7 +3,12 @@ define(function(require) { var appController = require('js/app-controller'); - var LoginExistingCtrl = function($scope, $location) { + var LoginExistingCtrl = function($scope, $location, $routeParams) { + if (!appController._emailDao && !$routeParams.dev) { + $location.path('/'); // init app + return; + } + var emailDao = appController._emailDao; $scope.buttonEnabled = true; diff --git a/src/js/controller/login-initial.js b/src/js/controller/login-initial.js index f2e2246..8c04c7d 100644 --- a/src/js/controller/login-initial.js +++ b/src/js/controller/login-initial.js @@ -3,7 +3,12 @@ define(function(require) { var appController = require('js/app-controller'); - var LoginInitialCtrl = function($scope, $location) { + var LoginInitialCtrl = function($scope, $location, $routeParams) { + if (!appController._emailDao && !$routeParams.dev) { + $location.path('/'); // init app + return; + } + var emailDao = appController._emailDao, states, termsMsg = 'You must accept the Terms of Service to continue.'; diff --git a/src/js/controller/login-new-device.js b/src/js/controller/login-new-device.js index 60d8897..b39091a 100644 --- a/src/js/controller/login-new-device.js +++ b/src/js/controller/login-new-device.js @@ -4,7 +4,12 @@ define(function(require) { var angular = require('angular'), appController = require('js/app-controller'); - var LoginExistingCtrl = function($scope, $location) { + var LoginExistingCtrl = function($scope, $location, $routeParams) { + if (!appController._emailDao && !$routeParams.dev) { + $location.path('/'); // init app + return; + } + var emailDao = appController._emailDao, pgp = appController._pgp; diff --git a/src/js/controller/login-privatekey-download.js b/src/js/controller/login-privatekey-download.js index 481d093..18256b8 100644 --- a/src/js/controller/login-privatekey-download.js +++ b/src/js/controller/login-privatekey-download.js @@ -3,7 +3,12 @@ define(function(require) { var appController = require('js/app-controller'); - var LoginPrivateKeyDownloadCtrl = function($scope, $location) { + var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams) { + if (!appController._emailDao && !$routeParams.dev) { + $location.path('/'); // init app + return; + } + var keychain = appController._keychain, emailDao = appController._emailDao, userId = emailDao._account.emailAddress; diff --git a/src/js/controller/login-set-credentials.js b/src/js/controller/login-set-credentials.js index 25fdb32..81efbde 100644 --- a/src/js/controller/login-set-credentials.js +++ b/src/js/controller/login-set-credentials.js @@ -10,7 +10,12 @@ define(function(require) { ImapClient = require('imap-client'), SmtpClient = require('smtpclient'); - var SetCredentialsCtrl = function($scope, $location) { + var SetCredentialsCtrl = function($scope, $location, $routeParams) { + if (!appCtrl._emailDao && !$routeParams.dev) { + $location.path('/'); // init app + return; + } + var auth = appCtrl._auth; var provider; diff --git a/src/js/controller/navigation.js b/src/js/controller/navigation.js index 7d7ccb8..54aae00 100644 --- a/src/js/controller/navigation.js +++ b/src/js/controller/navigation.js @@ -13,7 +13,12 @@ define(function(require) { // Controller // - var NavigationCtrl = function($scope, $routeParams) { + var NavigationCtrl = function($scope, $routeParams, $location) { + if (!appController._emailDao && !$routeParams.dev) { + $location.path('/'); // init app + return; + } + emailDao = appController._emailDao; outboxBo = appController._outboxBo; diff --git a/test/unit/add-account-ctrl-test.js b/test/unit/add-account-ctrl-test.js index 8b6410b..00fe15f 100644 --- a/test/unit/add-account-ctrl-test.js +++ b/test/unit/add-account-ctrl-test.js @@ -9,39 +9,44 @@ define(function(require) { appController = require('js/app-controller'); describe('Add Account Controller unit test', function() { - var scope, location, ctrl, authStub; + var scope, location, ctrl, authStub, origAuth; + + beforeEach(function() { + // remember original module to restore later, then replace it + origAuth = appController._auth; + appController._auth = authStub = sinon.createStubInstance(Auth); + + angular.module('addaccounttest', []); + mocks.module('addaccounttest'); + mocks.inject(function($controller, $rootScope, $location) { + location = $location; + scope = $rootScope.$new(); + scope.state = {}; + + sinon.stub(location, 'path').returns(location); + sinon.stub(location, 'search').returns(location); + sinon.stub(scope, '$apply', function() {}); + + ctrl = $controller(AddAccountCtrl, { + $location: location, + $scope: scope, + $routeParams: {} + }); + }); + }); + + afterEach(function() { + // restore the app controller module + appController._auth = origAuth; + + location.path.restore(); + location.search.restore(); + scope.$apply.restore(); + }); describe('connectToGoogle', function() { - var origAuth; - beforeEach(function() { - // remember original module to restore later, then replace it - origAuth = appController._auth; - appController._auth = authStub = sinon.createStubInstance(Auth); - }); - - afterEach(function() { - // restore the app controller module - appController._auth = origAuth; - }); it('should forward to login', function() { - angular.module('addaccounttest', []); - mocks.module('addaccounttest'); - mocks.inject(function($controller, $rootScope, $location) { - location = $location; - scope = $rootScope.$new(); - scope.state = {}; - - sinon.stub(location, 'path').returns(location); - sinon.stub(location, 'search').returns(location); - sinon.stub(scope, '$apply', function() {}); - - ctrl = $controller(AddAccountCtrl, { - $location: location, - $scope: scope - }); - }); - authStub._oauth = { isSupported: function() { return true; @@ -57,30 +62,9 @@ define(function(require) { provider: 'gmail' })).to.be.true; expect(authStub.getOAuthToken.calledOnce).to.be.true; - - location.path.restore(); - location.search.restore(); - scope.$apply.restore(); }); it('should not use oauth for gmail', function() { - angular.module('addaccounttest', []); - mocks.module('addaccounttest'); - mocks.inject(function($controller, $rootScope, $location) { - location = $location; - scope = $rootScope.$new(); - scope.state = {}; - - sinon.stub(location, 'path').returns(location); - sinon.stub(location, 'search').returns(location); - sinon.stub(scope, '$apply', function() {}); - - ctrl = $controller(AddAccountCtrl, { - $location: location, - $scope: scope - }); - }); - authStub._oauth = { isSupported: function() { return false; @@ -94,30 +78,9 @@ define(function(require) { provider: 'gmail' })).to.be.true; expect(authStub.getOAuthToken.called).to.be.false; - - location.path.restore(); - location.search.restore(); - scope.$apply.restore(); }); it('should not forward to login when oauth fails', function(done) { - angular.module('addaccounttest', []); - mocks.module('addaccounttest'); - mocks.inject(function($controller, $rootScope, $location) { - location = $location; - scope = $rootScope.$new(); - scope.state = {}; - - sinon.stub(location, 'path').returns(location); - sinon.stub(location, 'search').returns(location); - sinon.stub(scope, '$apply', function() {}); - - ctrl = $controller(AddAccountCtrl, { - $location: location, - $scope: scope - }); - }); - authStub._oauth = { isSupported: function() { return true; @@ -131,10 +94,6 @@ define(function(require) { expect(location.path.called).to.be.false; expect(location.search.called).to.be.false; - location.path.restore(); - location.search.restore(); - scope.$apply.restore(); - done(); }; @@ -144,193 +103,67 @@ define(function(require) { describe('connectToYahoo', function() { it('should forward to login', function() { - angular.module('addaccounttest', []); - mocks.module('addaccounttest'); - mocks.inject(function($controller, $rootScope, $location) { - location = $location; - scope = $rootScope.$new(); - scope.state = {}; - - sinon.stub(location, 'path').returns(location); - sinon.stub(location, 'search').returns(location); - sinon.stub(scope, '$apply', function() {}); - - ctrl = $controller(AddAccountCtrl, { - $location: location, - $scope: scope - }); - }); - scope.connectToYahoo(); expect(location.path.calledWith('/login-set-credentials')).to.be.true; expect(location.search.calledWith({ provider: 'yahoo' })).to.be.true; - - location.path.restore(); - location.search.restore(); - scope.$apply.restore(); }); }); describe('connectToTonline', function() { it('should forward to login', function() { - angular.module('addaccounttest', []); - mocks.module('addaccounttest'); - mocks.inject(function($controller, $rootScope, $location) { - location = $location; - scope = $rootScope.$new(); - scope.state = {}; - - sinon.stub(location, 'path').returns(location); - sinon.stub(location, 'search').returns(location); - sinon.stub(scope, '$apply', function() {}); - - ctrl = $controller(AddAccountCtrl, { - $location: location, - $scope: scope - }); - }); - scope.connectToTonline(); expect(location.path.calledWith('/login-set-credentials')).to.be.true; expect(location.search.calledWith({ provider: 'tonline' })).to.be.true; - - location.path.restore(); - location.search.restore(); - scope.$apply.restore(); }); }); describe('connectToOutlook', function() { it('should forward to login', function() { - angular.module('addaccounttest', []); - mocks.module('addaccounttest'); - mocks.inject(function($controller, $rootScope, $location) { - location = $location; - scope = $rootScope.$new(); - scope.state = {}; - - sinon.stub(location, 'path').returns(location); - sinon.stub(location, 'search').returns(location); - sinon.stub(scope, '$apply', function() {}); - - ctrl = $controller(AddAccountCtrl, { - $location: location, - $scope: scope - }); - }); - scope.connectToOutlook(); expect(location.path.calledWith('/login-set-credentials')).to.be.true; expect(location.search.calledWith({ provider: 'outlook' })).to.be.true; - - location.path.restore(); - location.search.restore(); - scope.$apply.restore(); }); }); describe('connectToGmx', function() { it('should forward to login', function() { - angular.module('addaccounttest', []); - mocks.module('addaccounttest'); - mocks.inject(function($controller, $rootScope, $location) { - location = $location; - scope = $rootScope.$new(); - scope.state = {}; - - sinon.stub(location, 'path').returns(location); - sinon.stub(location, 'search').returns(location); - sinon.stub(scope, '$apply', function() {}); - - ctrl = $controller(AddAccountCtrl, { - $location: location, - $scope: scope - }); - }); - scope.connectToGmx(); expect(location.path.calledWith('/login-set-credentials')).to.be.true; expect(location.search.calledWith({ provider: 'gmx' })).to.be.true; - - location.path.restore(); - location.search.restore(); - scope.$apply.restore(); }); }); describe('connectToWebde', function() { it('should forward to login', function() { - angular.module('addaccounttest', []); - mocks.module('addaccounttest'); - mocks.inject(function($controller, $rootScope, $location) { - location = $location; - scope = $rootScope.$new(); - scope.state = {}; - - sinon.stub(location, 'path').returns(location); - sinon.stub(location, 'search').returns(location); - sinon.stub(scope, '$apply', function() {}); - - ctrl = $controller(AddAccountCtrl, { - $location: location, - $scope: scope - }); - }); - scope.connectToWebde(); expect(location.path.calledWith('/login-set-credentials')).to.be.true; expect(location.search.calledWith({ provider: 'webde' })).to.be.true; - - location.path.restore(); - location.search.restore(); - scope.$apply.restore(); }); }); describe('connectOther', function() { it('should forward to login', function() { - angular.module('addaccounttest', []); - mocks.module('addaccounttest'); - mocks.inject(function($controller, $rootScope, $location) { - location = $location; - scope = $rootScope.$new(); - scope.state = {}; - - sinon.stub(location, 'path').returns(location); - sinon.stub(location, 'search').returns(location); - sinon.stub(scope, '$apply', function() {}); - - ctrl = $controller(AddAccountCtrl, { - $location: location, - $scope: scope - }); - }); - scope.connectOther(); expect(location.path.calledWith('/login-set-credentials')).to.be.true; expect(location.search.calledWith({ provider: 'custom' })).to.be.true; - - location.path.restore(); - location.search.restore(); - scope.$apply.restore(); }); }); }); diff --git a/test/unit/login-existing-ctrl-test.js b/test/unit/login-existing-ctrl-test.js index 15b223b..baa16e0 100644 --- a/test/unit/login-existing-ctrl-test.js +++ b/test/unit/login-existing-ctrl-test.js @@ -39,7 +39,8 @@ define(function(require) { scope = $rootScope.$new(); scope.state = {}; ctrl = $controller(LoginExistingCtrl, { - $scope: scope + $scope: scope, + $routeParams: {} }); }); }); diff --git a/test/unit/login-initial-ctrl-test.js b/test/unit/login-initial-ctrl-test.js index b6c6a87..533a1f4 100644 --- a/test/unit/login-initial-ctrl-test.js +++ b/test/unit/login-initial-ctrl-test.js @@ -43,7 +43,8 @@ define(function(require) { ui: {} }; ctrl = $controller(LoginInitialCtrl, { - $scope: scope + $scope: scope, + $routeParams: {} }); }); }); diff --git a/test/unit/login-new-device-ctrl-test.js b/test/unit/login-new-device-ctrl-test.js index 3d9b48e..977bedd 100644 --- a/test/unit/login-new-device-ctrl-test.js +++ b/test/unit/login-new-device-ctrl-test.js @@ -41,7 +41,8 @@ define(function(require) { ui: {} }; ctrl = $controller(LoginNewDeviceCtrl, { - $scope: scope + $scope: scope, + $routeParams: {} }); }); }); diff --git a/test/unit/login-privatekey-download-ctrl-test.js b/test/unit/login-privatekey-download-ctrl-test.js index 78a8463..532dbf1 100644 --- a/test/unit/login-privatekey-download-ctrl-test.js +++ b/test/unit/login-privatekey-download-ctrl-test.js @@ -38,7 +38,8 @@ define(function(require) { scope.state = {}; ctrl = $controller(LoginPrivateKeyDownloadCtrl, { $location: location, - $scope: scope + $scope: scope, + $routeParams: {} }); done(); }); @@ -251,7 +252,8 @@ define(function(require) { scope.state = {}; ctrl = $controller(LoginPrivateKeyDownloadCtrl, { $location: location, - $scope: scope + $scope: scope, + $routeParams: {} }); }); diff --git a/test/unit/login-set-credentials-ctrl-test.js b/test/unit/login-set-credentials-ctrl-test.js index 3dd48a3..e39d248 100644 --- a/test/unit/login-set-credentials-ctrl-test.js +++ b/test/unit/login-set-credentials-ctrl-test.js @@ -33,7 +33,8 @@ define(function(require) { scope.state = {}; setCredentialsCtrl = $controller(SetCredentialsCtrl, { - $scope: scope + $scope: scope, + $routeParams: {} }); }); });