diff --git a/Gruntfile.js b/Gruntfile.js index fefc5ec..f2142b8 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -148,7 +148,7 @@ module.exports = function(grunt) { expand: true, flatten: true, cwd: 'node_modules/', - src: ['requirejs/require.js', 'mocha/mocha.css', 'mocha/mocha.js', 'chai/chai.js', 'sinon/pkg/sinon.js'], + src: ['requirejs/require.js', 'mocha/mocha.css', 'mocha/mocha.js', 'chai/chai.js', 'sinon/pkg/sinon.js', 'angularjs/src/ngMock/angular-mocks.js'], dest: 'test/lib/' }, cryptoLib: { diff --git a/package.json b/package.json index 08d8359..a452889 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "requirejs": "2.1.8" }, "devDependencies": { + "angular": "https://github.com/angular/angular.js/tarball/v1.2.0", "grunt": "0.4.1", "mocha": "1.13.0", "phantomjs": "1.9.1-9", diff --git a/src/js/controller/account.js b/src/js/controller/account.js index 78ffae4..6ef300f 100644 --- a/src/js/controller/account.js +++ b/src/js/controller/account.js @@ -37,7 +37,7 @@ define(function(require) { $scope.exportKeyFile = function() { emailDao._crypto.exportKeys(function(err, keys) { if (err) { - console.error(err); + $scope.onError(err); return; } @@ -46,16 +46,14 @@ define(function(require) { content: keys.publicKeyArmored + keys.privateKeyArmored, filename: id + '.asc', contentType: 'text/plain' - }, onSave); + }, function onSave(err) { + if (err) { + $scope.onError(err); + return; + } + }); }); }; - - function onSave(err) { - if (err) { - console.error(err); - return; - } - } }; return AccountCtrl; diff --git a/src/js/controller/login-initial.js b/src/js/controller/login-initial.js index 4bbec86..f70e815 100644 --- a/src/js/controller/login-initial.js +++ b/src/js/controller/login-initial.js @@ -31,16 +31,16 @@ define(function(require) { return; } - setState(states.PROCESSING); + $scope.setState(states.PROCESSING); setTimeout(function() { emailDao.unlock({}, passphrase, function(err) { if (err) { console.error(err); - setState(states.IDLE, true); + $scope.setState(states.IDLE, true); return; } - setState(states.DONE, true); + $scope.setState(states.DONE, true); }); }, 500); }; @@ -75,13 +75,13 @@ define(function(require) { $location.path('/desktop'); }; - function setState(state, async) { + $scope.setState = function(state, async) { $scope.state.ui = state; if (async) { $scope.$apply(); } - } + }; }; return LoginInitialCtrl; diff --git a/src/require-config.js b/src/require-config.js index 979a0c7..90c1a8c 100644 --- a/src/require-config.js +++ b/src/require-config.js @@ -27,12 +27,6 @@ angular: { exports: 'angular' }, - openpgp: { - exports: 'window' - }, - iscroll: { - exports: 'IScroll' - }, angularRoute: { exports: 'angular', deps: ['angular'] @@ -41,6 +35,12 @@ exports: 'angular', deps: ['angular'] }, + openpgp: { + exports: 'window' + }, + iscroll: { + exports: 'IScroll' + }, lawnchair: { exports: 'Lawnchair' }, diff --git a/src/tpl/login-initial.html b/src/tpl/login-initial.html index 6e19087..ba1e1fa 100644 --- a/src/tpl/login-initial.html +++ b/src/tpl/login-initial.html @@ -29,7 +29,7 @@

Keypair generated

Your personal keypair has been generated. You can export it (e.g. to a USB flash drive) to setup whiteout on another computer or as a backup.

- + diff --git a/test/new-unit/account-ctrl-test.js b/test/new-unit/account-ctrl-test.js new file mode 100644 index 0000000..0c9978d --- /dev/null +++ b/test/new-unit/account-ctrl-test.js @@ -0,0 +1,115 @@ +define(function(require) { + 'use strict'; + + var expect = chai.expect, + angular = require('angular'), + mocks = require('angularMocks'), + AccountCtrl = require('js/controller/account'), + EmailDAO = require('js/dao/email-dao'), + PGP = require('js/crypto/pgp'), + dl = require('js/util/download'), + appController = require('js/app-controller'); + + describe('Account Controller unit test', function() { + var scope, accountCtrl, origEmailDao, emailDaoMock, + dummyFingerprint, expectedFingerprint, + dummyKeyId, expectedKeyId, + emailAddress, + keySize, + cryptoMock; + + beforeEach(function() { + origEmailDao = appController._emailDao; + cryptoMock = sinon.createStubInstance(PGP); + emailDaoMock = sinon.createStubInstance(EmailDAO); + emailDaoMock._crypto = cryptoMock; + appController._emailDao = emailDaoMock; + + dummyFingerprint = '3A2D39B4E1404190B8B949DE7D7E99036E712926'; + expectedFingerprint = '3A2D 39B4 E140 4190 B8B9 49DE 7D7E 9903 6E71 2926'; + dummyKeyId = '9FEB47936E712926'; + expectedKeyId = '6E712926'; + cryptoMock.getFingerprint.returns(dummyFingerprint); + cryptoMock.getKeyId.returns(dummyKeyId); + emailAddress = 'fred@foo.com'; + keySize = 1234; + emailDaoMock._account = { + emailAddress: emailAddress, + asymKeySize: keySize + }; + + angular.module('accounttest', []); + mocks.module('accounttest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = {}; + accountCtrl = $controller(AccountCtrl, { + $scope: scope + }); + }); + }); + + afterEach(function() { + // restore the module + appController._emailDao = origEmailDao; + }); + + describe('scope variables', function() { + it('should be set correctly', function() { + expect(scope.eMail).to.equal(emailAddress); + expect(scope.keyId).to.equal(expectedKeyId); + expect(scope.fingerprint).to.equal(expectedFingerprint); + expect(scope.keysize).to.equal(keySize); + }); + }); + describe('export to key file', function() { + it('should work', function() { + var createDownloadMock = sinon.stub(dl, 'createDownload'); + cryptoMock.exportKeys.yields(null, { + publicKeyArmored: 'a', + privateKeyArmored: 'b', + keyId: dummyKeyId + }); + createDownloadMock.withArgs(sinon.match(function(arg) { + return arg.content === 'ab' && arg.filename === expectedKeyId + '.asc' && arg.contentType === 'text/plain'; + })).yields(); + + scope.exportKeyFile(); + + expect(cryptoMock.exportKeys.calledOnce).to.be.true; + expect(dl.createDownload.calledOnce).to.be.true; + dl.createDownload.restore(); + }); + + it('should not work when key export failed', function(done) { + cryptoMock.exportKeys.yields(new Error('asdasd')); + scope.onError = function() { + expect(cryptoMock.exportKeys.calledOnce).to.be.true; + done(); + }; + + scope.exportKeyFile(); + }); + + it('should not work when create download failed', function(done) { + var createDownloadMock = sinon.stub(dl, 'createDownload'); + cryptoMock.exportKeys.yields(null, { + publicKeyArmored: 'a', + privateKeyArmored: 'b', + keyId: dummyKeyId + }); + createDownloadMock.withArgs(sinon.match(function(arg) { + return arg.content === 'ab' && arg.filename === expectedKeyId + '.asc' && arg.contentType === 'text/plain'; + })).yields(new Error('asdasd')); + scope.onError = function() { + expect(cryptoMock.exportKeys.calledOnce).to.be.true; + expect(dl.createDownload.calledOnce).to.be.true; + dl.createDownload.restore(); + done(); + }; + + scope.exportKeyFile(); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/new-unit/dialog-ctrl-test.js b/test/new-unit/dialog-ctrl-test.js new file mode 100644 index 0000000..7ff73cd --- /dev/null +++ b/test/new-unit/dialog-ctrl-test.js @@ -0,0 +1,50 @@ +define(function(require) { + 'use strict'; + + var expect = chai.expect, + angular = require('angular'), + mocks = require('angularMocks'), + DialogCtrl = require('js/controller/dialog'); + + describe('Dialog Controller unit test', function() { + var scope, dialogCtrl; + + beforeEach(function() { + angular.module('dialogtest', []); + mocks.module('dialogtest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = { + dialog: {} + }; + dialogCtrl = $controller(DialogCtrl, { + $scope: scope + }); + }); + }); + + afterEach(function() {}); + + describe('confirm', function() { + it('should work', function(done) { + scope.state.dialog.callback = function(confirmed) { + expect(confirmed).to.be.true; + expect(scope.state.dialog.open).to.be.false; + done(); + }; + scope.confirm(true); + }); + }); + + describe('cancel', function() { + it('should work', function(done) { + scope.state.dialog.callback = function(confirmed) { + expect(confirmed).to.be.false; + expect(scope.state.dialog.open).to.be.false; + done(); + }; + scope.confirm(false); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/new-unit/login-ctrl-test.js b/test/new-unit/login-ctrl-test.js new file mode 100644 index 0000000..04aec20 --- /dev/null +++ b/test/new-unit/login-ctrl-test.js @@ -0,0 +1,192 @@ +define(function(require) { + 'use strict'; + + var expect = chai.expect, + angular = require('angular'), + mocks = require('angularMocks'), + LoginCtrl = require('js/controller/login'), + EmailDAO = require('js/dao/email-dao'), + appController = require('js/app-controller'); + + describe('Login Controller unit test', function() { + var scope, location, ctrl, origEmailDao, emailDaoMock, + emailAddress = 'fred@foo.com', + oauthToken = 'foobarfoobar', + startAppStub, + checkForUpdateStub, + fetchOAuthStub, + initStub; + + describe('initialization', function() { + var hasChrome, hasIdentity; + + beforeEach(function() { + hasChrome = !!window.chrome; + hasIdentity = !!window.chrome.identity; + window.chrome = window.chrome || {}; + window.chrome.identity = window.chrome.identity || {}; + + // remember original module to restore later, then replace it + origEmailDao = appController._emailDao; + emailDaoMock = sinon.createStubInstance(EmailDAO); + appController._emailDao = emailDaoMock; + }); + + afterEach(function() { + // restore the browser + if (!hasIdentity) { + delete window.chrome.identity; + } + + if (!hasChrome) { + delete window.chrome; + } + + // restore the app controller module + appController._emailDao = origEmailDao; + appController.start.restore && appController.start.restore(); + appController.checkForUpdate.restore && appController.checkForUpdate.restore(); + appController.fetchOAuthToken.restore && appController.fetchOAuthToken.restore(); + appController.init.restore && appController.init.restore(); + location.path.restore && location.path.restore(); + }); + + it('should forward to existing user login', function(done) { + startAppStub = sinon.stub(appController, 'start'); + startAppStub.yields(); + checkForUpdateStub = sinon.stub(appController, 'checkForUpdate'); + fetchOAuthStub = sinon.stub(appController, 'fetchOAuthToken'); + fetchOAuthStub.yields(null, { + emailAddress: emailAddress, + token: oauthToken + }); + initStub = sinon.stub(appController, 'init'); + initStub.yields(null, { + privateKey: 'a', + publicKey: 'b' + }); + + emailDaoMock.imapLogin.yields(); + + angular.module('logintest', []); + mocks.module('logintest'); + mocks.inject(function($controller, $rootScope, $location) { + location = $location; + sinon.stub(location, 'path', function(path) { + expect(path).to.equal('/login-existing'); + expect(emailDaoMock.imapLogin.calledOnce).to.be.true; + expect(startAppStub.calledOnce).to.be.true; + expect(checkForUpdateStub.calledOnce).to.be.true; + expect(fetchOAuthStub.calledOnce).to.be.true; + done(); + }); + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(LoginCtrl, { + $location: location, + $scope: scope + }); + }); + }); + + it('should forward to new device login', function(done) { + startAppStub = sinon.stub(appController, 'start'); + startAppStub.yields(); + checkForUpdateStub = sinon.stub(appController, 'checkForUpdate'); + fetchOAuthStub = sinon.stub(appController, 'fetchOAuthToken'); + fetchOAuthStub.yields(null, { + emailAddress: emailAddress, + token: oauthToken + }); + initStub = sinon.stub(appController, 'init'); + initStub.yields(null, { + publicKey: 'b' + }); + + emailDaoMock.imapLogin.yields(); + + angular.module('logintest', []); + mocks.module('logintest'); + mocks.inject(function($controller, $rootScope, $location) { + location = $location; + sinon.stub(location, 'path', function(path) { + expect(path).to.equal('/login-new-device'); + expect(emailDaoMock.imapLogin.calledOnce).to.be.true; + expect(startAppStub.calledOnce).to.be.true; + expect(checkForUpdateStub.calledOnce).to.be.true; + expect(fetchOAuthStub.calledOnce).to.be.true; + done(); + }); + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(LoginCtrl, { + $location: location, + $scope: scope + }); + }); + }); + + it('should forward to initial login', function(done) { + startAppStub = sinon.stub(appController, 'start'); + startAppStub.yields(); + checkForUpdateStub = sinon.stub(appController, 'checkForUpdate'); + fetchOAuthStub = sinon.stub(appController, 'fetchOAuthToken'); + fetchOAuthStub.yields(null, { + emailAddress: emailAddress, + token: oauthToken + }); + initStub = sinon.stub(appController, 'init'); + initStub.yields(); + + emailDaoMock.imapLogin.yields(); + + angular.module('logintest', []); + mocks.module('logintest'); + mocks.inject(function($controller, $rootScope, $location) { + location = $location; + sinon.stub(location, 'path', function(path) { + expect(path).to.equal('/login-initial'); + expect(emailDaoMock.imapLogin.calledOnce).to.be.true; + expect(startAppStub.calledOnce).to.be.true; + expect(checkForUpdateStub.calledOnce).to.be.true; + expect(fetchOAuthStub.calledOnce).to.be.true; + done(); + }); + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(LoginCtrl, { + $location: location, + $scope: scope + }); + }); + }); + + it('should fall back to dev mode', function(done) { + var chromeIdentity; + + chromeIdentity = window.chrome.identity; + delete window.chrome.identity; + + startAppStub = sinon.stub(appController, 'start'); + startAppStub.yields(); + + angular.module('logintest', []); + mocks.module('logintest'); + mocks.inject(function($controller, $rootScope, $location) { + location = $location; + sinon.stub(location, 'path', function(path) { + expect(path).to.equal('/desktop'); + window.chrome.identity = chromeIdentity; + done(); + }); + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(LoginCtrl, { + $location: location, + $scope: scope + }); + }); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/new-unit/login-existing-ctrl-test.js b/test/new-unit/login-existing-ctrl-test.js new file mode 100644 index 0000000..2e081b7 --- /dev/null +++ b/test/new-unit/login-existing-ctrl-test.js @@ -0,0 +1,105 @@ +define(function(require) { + 'use strict'; + + var expect = chai.expect, + angular = require('angular'), + mocks = require('angularMocks'), + LoginExistingCtrl = require('js/controller/login-existing'), + EmailDAO = require('js/dao/email-dao'), + KeychainDAO = require('js/dao/keychain-dao'), + appController = require('js/app-controller'); + + describe('Login (existing user) Controller unit test', function() { + var scope, location, ctrl, origEmailDao, emailDaoMock, + emailAddress = 'fred@foo.com', + passphrase = 'asd', + keychainMock; + + beforeEach(function() { + // remember original module to restore later + origEmailDao = appController._emailDao; + + emailDaoMock = sinon.createStubInstance(EmailDAO); + appController._emailDao = emailDaoMock; + + keychainMock = sinon.createStubInstance(KeychainDAO); + emailDaoMock._keychain = keychainMock; + + emailDaoMock._account = { + emailAddress: emailAddress, + }; + + angular.module('loginexistingtest', []); + mocks.module('loginexistingtest'); + mocks.inject(function($rootScope, $controller, $location) { + location = $location; + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(LoginExistingCtrl, { + $scope: scope + }); + }); + }); + + afterEach(function() { + // restore the module + appController._emailDao = origEmailDao; + }); + + describe('initial state', function() { + it('should be well defined', function() { + expect(scope.buttonEnabled).to.be.true; + expect(scope.incorrect).to.be.false; + expect(scope.change).to.exist; + expect(scope.confirmPassphrase).to.exist; + }); + }); + + describe('functionality', function() { + describe('change', function() { + it('should set incorrect to false', function() { + scope.incorrect = true; + + scope.change(); + expect(scope.incorrect).to.be.false; + }); + }); + + describe('confirm passphrase', function() { + it('should unlock crypto and start', function() { + var keypair = {}, + pathSpy = sinon.spy(location, 'path'); + scope.passphrase = passphrase; + keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, keypair); + emailDaoMock.unlock.withArgs(keypair, passphrase).yields(null); + + + scope.confirmPassphrase(); + + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + expect(emailDaoMock.unlock.calledOnce).to.be.true; + expect(pathSpy.calledOnce).to.be.true; + expect(pathSpy.calledWith('/desktop')).to.be.true; + }); + + it('should not do anything without passphrase', function() { + var pathSpy = sinon.spy(location, 'path'); + scope.passphrase = ''; + + scope.confirmPassphrase(); + expect(pathSpy.callCount).to.equal(0); + }); + + it('should not work when keypair unavailable', function(done) { + scope.passphrase = passphrase; + keychainMock.getUserKeyPair.withArgs(emailAddress).yields(new Error('asd')); + + scope.confirmPassphrase(); + + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + done(); + }); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/new-unit/login-initial-ctrl-test.js b/test/new-unit/login-initial-ctrl-test.js new file mode 100644 index 0000000..017b63f --- /dev/null +++ b/test/new-unit/login-initial-ctrl-test.js @@ -0,0 +1,171 @@ +define(function(require) { + 'use strict'; + + var expect = chai.expect, + angular = require('angular'), + mocks = require('angularMocks'), + LoginInitialCtrl = require('js/controller/login-initial'), + dl = require('js/util/download'), + PGP = require('js/crypto/pgp'), + EmailDAO = require('js/dao/email-dao'), + appController = require('js/app-controller'); + + describe('Login (initial user) Controller unit test', function() { + var scope, ctrl, location, origEmailDao, emailDaoMock, + emailAddress = 'fred@foo.com', + passphrase = 'asd', + keyId, expectedKeyId, + cryptoMock; + + beforeEach(function() { + // remember original module to restore later + origEmailDao = appController._emailDao; + + emailDaoMock = sinon.createStubInstance(EmailDAO); + appController._emailDao = emailDaoMock; + + keyId = '9FEB47936E712926'; + expectedKeyId = '6E712926'; + cryptoMock = sinon.createStubInstance(PGP); + emailDaoMock._crypto = cryptoMock; + + emailDaoMock._account = { + emailAddress: emailAddress, + }; + + angular.module('logininitialtest', []); + mocks.module('logininitialtest'); + mocks.inject(function($rootScope, $controller, $location) { + scope = $rootScope.$new(); + location = $location; + scope.state = { + ui: {} + }; + ctrl = $controller(LoginInitialCtrl, { + $scope: scope + }); + }); + }); + + afterEach(function() { + // restore the module + appController._emailDao = origEmailDao; + }); + + describe('initial state', function() { + it('should be well defined', function() { + expect(scope.proceed).to.exist; + expect(scope.exportKeypair).to.exist; + expect(scope.confirmPassphrase).to.exist; + expect(scope.state.ui).to.equal(1); + }); + }); + + describe('confirm passphrase', function() { + var setStateStub; + + it('should unlock crypto', function(done) { + scope.state.passphrase = passphrase; + scope.state.confirmation = passphrase; + emailDaoMock.unlock.withArgs({}, passphrase).yields(); + setStateStub = sinon.stub(scope, 'setState', function(state) { + if (setStateStub.calledOnce) { + expect(state).to.equal(2); + } else if (setStateStub.calledTwice) { + expect(state).to.equal(4); + expect(emailDaoMock.unlock.calledOnce).to.be.true; + scope.setState.restore(); + done(); + } + }); + + scope.confirmPassphrase(); + }); + + it('should not do anything matching passphrases', function() { + scope.state.passphrase = 'a'; + scope.state.confirmation = 'b'; + + scope.confirmPassphrase(); + }); + + it('should not work when keypair generation fails', function(done) { + scope.state.passphrase = passphrase; + scope.state.confirmation = passphrase; + emailDaoMock.unlock.withArgs({}, passphrase).yields(new Error('asd')); + setStateStub = sinon.stub(scope, 'setState', function(state) { + if (setStateStub.calledOnce) { + expect(state).to.equal(2); + } else if (setStateStub.calledTwice) { + expect(state).to.equal(1); + expect(emailDaoMock.unlock.calledOnce).to.be.true; + scope.setState.restore(); + done(); + } + }); + + scope.confirmPassphrase(); + }); + }); + + describe('proceed', function() { + it('should forward', function() { + var locationSpy = sinon.spy(location, 'path'); + + scope.proceed(); + + expect(locationSpy.calledWith('/desktop')).to.be.true; + }); + }); + + describe('export keypair', function() { + it('should work', function() { + var locationSpy, createDownloadMock; + + createDownloadMock = sinon.stub(dl, 'createDownload'); + cryptoMock.exportKeys.yields(null, { + publicKeyArmored: 'a', + privateKeyArmored: 'b', + keyId: keyId + }); + createDownloadMock.withArgs(sinon.match(function(arg) { + return arg.content === 'ab' && arg.filename === expectedKeyId + '.asc' && arg.contentType === 'text/plain'; + })).yields(); + + locationSpy = sinon.spy(location, 'path'); + + scope.exportKeypair(); + + expect(cryptoMock.exportKeys.calledOnce).to.be.true; + expect(createDownloadMock.calledOnce).to.be.true; + expect(locationSpy.calledWith('/desktop')).to.be.true; + dl.createDownload.restore(); + }); + + it('should not work when download fails', function() { + var createDownloadMock = sinon.stub(dl, 'createDownload'); + cryptoMock.exportKeys.yields(null, { + publicKeyArmored: 'a', + privateKeyArmored: 'b', + keyId: keyId + }); + createDownloadMock.yields({ + errMsg: 'snafu.' + }); + scope.exportKeypair(); + + expect(cryptoMock.exportKeys.calledOnce).to.be.true; + expect(createDownloadMock.calledOnce).to.be.true; + dl.createDownload.restore(); + }); + + it('should not work when export fails', function() { + cryptoMock.exportKeys.yields(new Error('snafu.')); + + scope.exportKeypair(); + + expect(cryptoMock.exportKeys.calledOnce).to.be.true; + }); + }); + }); +}); \ No newline at end of file diff --git a/test/new-unit/login-new-device-ctrl-test.js b/test/new-unit/login-new-device-ctrl-test.js new file mode 100644 index 0000000..b44e457 --- /dev/null +++ b/test/new-unit/login-new-device-ctrl-test.js @@ -0,0 +1,142 @@ +define(function(require) { + 'use strict'; + + var expect = chai.expect, + angular = require('angular'), + mocks = require('angularMocks'), + LoginNewDeviceCtrl = require('js/controller/login-new-device'), + KeychainDAO = require('js/dao/keychain-dao'), + EmailDAO = require('js/dao/email-dao'), + appController = require('js/app-controller'); + + describe('Login (new device) Controller unit test', function() { + var scope, ctrl, origEmailDao, emailDaoMock, + emailAddress = 'fred@foo.com', + passphrase = 'asd', + keyId, + keychainMock; + + beforeEach(function() { + // remember original module to restore later + origEmailDao = appController._emailDao; + + emailDaoMock = sinon.createStubInstance(EmailDAO); + appController._emailDao = emailDaoMock; + + keyId = '9FEB47936E712926'; + keychainMock = sinon.createStubInstance(KeychainDAO); + emailDaoMock._keychain = keychainMock; + + emailDaoMock._account = { + emailAddress: emailAddress, + }; + + angular.module('loginnewdevicetest', []); + mocks.module('loginnewdevicetest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = { + ui: {} + }; + ctrl = $controller(LoginNewDeviceCtrl, { + $scope: scope + }); + }); + }); + + afterEach(function() { + // restore the module + appController._emailDao = origEmailDao; + }); + + describe('initial state', function() { + it('should be well defined', function() { + expect(scope.incorrect).to.be.false; + expect(scope.confirmPassphrase).to.exist; + }); + }); + + describe('confirm passphrase', function() { + it('should unlock crypto', function() { + scope.passphrase = passphrase; + scope.key = { + privateKeyArmored: 'b' + }; + keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, { + _id: keyId, + publicKey: 'a' + }); + emailDaoMock.unlock.withArgs(sinon.match.any, passphrase).yields(); + keychainMock.putUserKeyPair.yields(); + + scope.confirmPassphrase(); + + expect(emailDaoMock.unlock.calledOnce).to.be.true; + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + }); + + it('should not do anything without passphrase', function() { + scope.state.passphrase = ''; + + scope.confirmPassphrase(); + + expect(scope.incorrect).to.be.true; + }); + + it('should not work when keypair upload fails', function() { + scope.passphrase = passphrase; + scope.key = { + privateKeyArmored: 'b' + }; + + keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, { + _id: keyId, + publicKey: 'a' + }); + emailDaoMock.unlock.withArgs(sinon.match.any, passphrase).yields(); + keychainMock.putUserKeyPair.yields({ + errMsg: 'yo mamma.' + }); + + scope.confirmPassphrase(); + + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + expect(emailDaoMock.unlock.calledOnce).to.be.true; + expect(keychainMock.putUserKeyPair.calledOnce).to.be.true; + }); + + it('should not work when unlock fails', function() { + scope.passphrase = passphrase; + scope.key = { + privateKeyArmored: 'b' + }; + + keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, { + _id: keyId, + publicKey: 'a' + }); + emailDaoMock.unlock.withArgs(sinon.match.any, passphrase).yields({ + errMsg: 'yo mamma.' + }); + + scope.confirmPassphrase(); + + expect(scope.incorrect).to.be.true; + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + expect(emailDaoMock.unlock.calledOnce).to.be.true; + }); + + it('should not work when keypair retrieval', function() { + scope.passphrase = passphrase; + + keychainMock.getUserKeyPair.withArgs(emailAddress).yields({ + errMsg: 'yo mamma.' + }); + + scope.confirmPassphrase(); + + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + }); + }); + }); +}); \ No newline at end of file diff --git a/test/new-unit/mail-list-ctrl-test.js b/test/new-unit/mail-list-ctrl-test.js new file mode 100644 index 0000000..a4cd6b7 --- /dev/null +++ b/test/new-unit/mail-list-ctrl-test.js @@ -0,0 +1,253 @@ +define(function(require) { + 'use strict'; + + var expect = chai.expect, + angular = require('angular'), + mocks = require('angularMocks'), + MailListCtrl = require('js/controller/mail-list'), + EmailDAO = require('js/dao/email-dao'), + DeviceStorageDAO = require('js/dao/devicestorage-dao'), + KeychainDAO = require('js/dao/keychain-dao'), + appController = require('js/app-controller'); + + describe('Mail List controller unit test', function() { + var scope, ctrl, origEmailDao, emailDaoMock, keychainMock, deviceStorageMock, + emailAddress, notificationClickedHandler, + hasChrome, hasNotifications, hasSocket, hasRuntime, hasIdentity; + + beforeEach(function() { + hasChrome = !! window.chrome; + hasNotifications = !! window.chrome.notifications; + hasSocket = !! window.chrome.socket; + hasIdentity = !! window.chrome.identity; + if (!hasChrome) { + window.chrome = {}; + } + if (!hasNotifications) { + window.chrome.notifications = { + onClicked: { + addListener: function(handler) { + notificationClickedHandler = handler; + } + }, + create: function() {} + }; + } + if (!hasSocket) { + window.chrome.socket = {}; + } + if (!hasRuntime) { + window.chrome.runtime = { + getURL: function() {} + }; + } + if (!hasIdentity) { + window.chrome.identity = {}; + } + origEmailDao = appController._emailDao; + emailDaoMock = sinon.createStubInstance(EmailDAO); + appController._emailDao = emailDaoMock; + emailAddress = 'fred@foo.com'; + emailDaoMock._account = { + emailAddress: emailAddress, + }; + + + keychainMock = sinon.createStubInstance(KeychainDAO); + emailDaoMock._keychain = keychainMock; + + deviceStorageMock = sinon.createStubInstance(DeviceStorageDAO); + emailDaoMock._devicestorage = deviceStorageMock; + + angular.module('maillisttest', []); + mocks.module('maillisttest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(MailListCtrl, { + $scope: scope + }); + }); + }); + + afterEach(function() { + if (!hasNotifications) { + delete window.chrome.notifications; + } + if (!hasSocket) { + delete window.chrome.socket; + } + if (!hasRuntime) { + delete window.chrome.runtime; + } + if (!hasChrome) { + delete window.chrome; + } + if (!hasIdentity) { + delete window.chrome.identity; + } + + // restore the module + appController._emailDao = origEmailDao; + }); + + describe('scope variables', function() { + it('should be set correctly', function() { + expect(scope.select).to.exist; + expect(scope.synchronize).to.exist; + expect(scope.remove).to.exist; + expect(scope.state.mailList).to.exist; + expect(emailDaoMock.onIncomingMessage).to.exist; + }); + }); + + describe('push notification', function() { + it('should focus mail and not mark it read', function(done) { + var uid, mail, currentFolder; + + uid = 123; + mail = { + uid: uid, + from: [{ + address: 'asd' + }], + subject: '[whiteout] asdasd', + unread: true + }; + currentFolder = 'asd'; + scope.state.nav = { + currentFolder: currentFolder + }; + scope.state.read = { + toggle: function() {} + }; + scope.emails = [mail]; + emailDaoMock.imapMarkMessageRead.withArgs({ + folder: currentFolder, + uid: uid + }).yields(); + emailDaoMock.unreadMessages.yieldsAsync(null, 10); + emailDaoMock.imapSync.yieldsAsync(); + emailDaoMock.listMessages.yieldsAsync(null, [mail]); + window.chrome.notifications.create = function(id, opts) { + expect(id).to.equal('123'); + expect(opts.type).to.equal('basic'); + expect(opts.message).to.equal('asdasd'); + expect(opts.title).to.equal('asd'); + expect(scope.state.mailList.selected).to.deep.equal(mail); + expect(emailDaoMock.imapMarkMessageRead.callCount).to.equal(0); + done(); + }; + + emailDaoMock.onIncomingMessage(mail); + }); + }); + + describe('clicking push notification', function() { + it('should focus mail and mark it read', function() { + var uid, mail, currentFolder; + + uid = 123; + mail = { + uid: uid, + from: [{ + address: 'asd' + }], + subject: '[whiteout] asdasd', + unread: true + }; + currentFolder = 'asd'; + scope.state.nav = { + currentFolder: currentFolder + }; + scope.state.read = { + toggle: function() {} + }; + scope.emails = [mail]; + emailDaoMock.imapMarkMessageRead.withArgs({ + folder: currentFolder, + uid: uid + }).yields(); + + notificationClickedHandler('123'); // first select, irrelevant + notificationClickedHandler('123'); + + expect(scope.state.mailList.selected).to.deep.equal(mail); + expect(emailDaoMock.imapMarkMessageRead.callCount).to.be.at.least(1); + }); + }); + describe('remove', function() { + it('should not delete without a selected mail', function() { + scope.remove(); + + expect(emailDaoMock.imapDeleteMessage.called).to.be.false; + }); + + it('should delete the selected mail from trash folder after clicking ok', function() { + var uid, mail, currentFolder; + + uid = 123; + mail = { + uid: uid, + from: [{ + address: 'asd' + }], + subject: '[whiteout] asdasd', + unread: true + }; + scope.emails = [mail]; + currentFolder = { + type: 'Trash' + }; + scope.folders = [currentFolder]; + scope.state.nav = { + currentFolder: currentFolder + }; + emailDaoMock.imapDeleteMessage.yields(); + + scope.remove(mail); + scope.state.dialog.callback(true); + + expect(emailDaoMock.imapDeleteMessage.calledOnce).to.be.true; + expect(scope.state.mailList.selected).to.not.exist; + }); + + it('should move the selected mail to the trash folder', function() { + var uid, mail, currentFolder, trashFolder; + + uid = 123; + mail = { + uid: uid, + from: [{ + address: 'asd' + }], + subject: '[whiteout] asdasd', + unread: true + }; + scope.emails = [mail]; + currentFolder = { + type: 'Inbox', + path: 'INBOX' + }; + trashFolder = { + type: 'Trash', + path: 'TRASH' + }; + scope.folders = [currentFolder, trashFolder]; + scope.state.nav = { + currentFolder: currentFolder + }; + emailDaoMock.imapMoveMessage.withArgs({ + folder: currentFolder, + uid: uid, + destination: trashFolder.path + }).yields(); + + scope.remove(mail); + + expect(emailDaoMock.imapMoveMessage.calledOnce).to.be.true; + expect(scope.state.mailList.selected).to.not.exist; + }); + }); + }); +}); \ No newline at end of file diff --git a/test/new-unit/main.js b/test/new-unit/main.js index 0fc6400..1a15c04 100644 --- a/test/new-unit/main.js +++ b/test/new-unit/main.js @@ -2,9 +2,19 @@ require(['../../src/require-config'], function() { require.config({ - baseUrl: '../../src/lib' + baseUrl: '../../src/lib', + paths: { + angularMocks: '../../test/lib/angular-mocks' + }, + shim: { + angularMocks: { + exports: 'angular.mock', + deps: ['angular'] + } + } }); + // Start the main app logic. require(['js/app-config', 'cordova'], function(app) { window.Worker = undefined; // disable web workers since mocha doesn't support them @@ -27,7 +37,17 @@ function startTests() { 'test/new-unit/publickey-dao-test', 'test/new-unit/lawnchair-dao-test', 'test/new-unit/keychain-dao-test', - 'test/new-unit/devicestorage-dao-test' + 'test/new-unit/devicestorage-dao-test', + 'test/new-unit/dialog-ctrl-test', + 'test/new-unit/account-ctrl-test', + 'test/new-unit/login-existing-ctrl-test', + 'test/new-unit/login-initial-ctrl-test', + 'test/new-unit/login-new-device-ctrl-test', + 'test/new-unit/login-ctrl-test', + 'test/new-unit/read-ctrl-test', + 'test/new-unit/navigation-ctrl-test', + 'test/new-unit/mail-list-ctrl-test', + 'test/new-unit/write-ctrl-test' ], function() { //Tests loaded, run tests mocha.run(); diff --git a/test/new-unit/navigation-ctrl-test.js b/test/new-unit/navigation-ctrl-test.js new file mode 100644 index 0000000..056bf5e --- /dev/null +++ b/test/new-unit/navigation-ctrl-test.js @@ -0,0 +1,120 @@ +define(function(require) { + 'use strict'; + + var expect = chai.expect, + angular = require('angular'), + mocks = require('angularMocks'), + NavigationCtrl = require('js/controller/navigation'), + EmailDAO = require('js/dao/email-dao'), + DeviceStorageDAO = require('js/dao/devicestorage-dao'), + appController = require('js/app-controller'); + + describe('Navigation Controller unit test', function() { + var scope, ctrl, origEmailDao, emailDaoMock, deviceStorageMock, tempChrome; + + beforeEach(function() { + if (window.chrome.identity) { + tempChrome = window.chrome.identity; + delete window.chrome.identity; + } + // remember original module to restore later + origEmailDao = appController._emailDao; + + emailDaoMock = sinon.createStubInstance(EmailDAO); + appController._emailDao = emailDaoMock; + + deviceStorageMock = sinon.createStubInstance(DeviceStorageDAO); + appController._emailDao._devicestorage = deviceStorageMock; + + angular.module('navigationtest', []); + mocks.module('navigationtest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(NavigationCtrl, { + $scope: scope + }); + }); + }); + + afterEach(function() { + // restore the module + appController._emailDao = origEmailDao; + if (tempChrome) { + window.chrome.identity = tempChrome; + } + }); + + describe('initial state', function() { + it('should be well defined', function() { + expect(scope.state).to.exist; + expect(scope.state.nav.open).to.be.false; + expect(scope.folders).to.not.be.empty; + + expect(scope.onError).to.exist; + expect(scope.openFolder).to.exist; + expect(scope.emptyOutbox).to.exist; + }); + }); + + describe('open/close nav view', function() { + it('should open/close', function() { + expect(scope.state.nav.open).to.be.false; + scope.state.nav.toggle(true); + expect(scope.state.nav.open).to.be.true; + scope.state.nav.toggle(false); + expect(scope.state.nav.open).to.be.false; + }); + }); + + describe('open folder', function() { + it('should work', function() { + scope.state.nav.open = true; + + scope.openFolder('asd'); + expect(scope.state.nav.currentFolder).to.equal('asd'); + expect(scope.state.nav.open).to.be.false; + }); + }); + + describe('empty outbox', function() { + it('should work', function() { + deviceStorageMock.listItems.yields(null, [{ + id: 1 + }, { + id: 2 + }, { + id: 3 + }]); + emailDaoMock.smtpSend.yields(); + deviceStorageMock.removeList.yields(); + + scope.emptyOutbox(); + + expect(deviceStorageMock.listItems.calledOnce).to.be.true; + expect(emailDaoMock.smtpSend.calledThrice).to.be.true; + expect(deviceStorageMock.removeList.calledThrice).to.be.true; + }); + + it('should not work when device storage errors', function() { + deviceStorageMock.listItems.yields({errMsg: 'error'}); + + scope.emptyOutbox(); + + expect(deviceStorageMock.listItems.calledOnce).to.be.true; + }); + + it('should not work when smtp send fails', function() { + deviceStorageMock.listItems.yields(null, [{ + id: 1 + }]); + emailDaoMock.smtpSend.yields({errMsg: 'error'}); + + scope.emptyOutbox(); + + expect(deviceStorageMock.listItems.calledOnce).to.be.true; + expect(emailDaoMock.smtpSend.calledOnce).to.be.true; + }); + }); + }); +}); \ No newline at end of file diff --git a/test/new-unit/read-ctrl-test.js b/test/new-unit/read-ctrl-test.js new file mode 100644 index 0000000..5525c9d --- /dev/null +++ b/test/new-unit/read-ctrl-test.js @@ -0,0 +1,44 @@ +define(function(require) { + 'use strict'; + + var expect = chai.expect, + angular = require('angular'), + mocks = require('angularMocks'), + ReadCtrl = require('js/controller/read'); + + describe('Read Controller unit test', function() { + var scope, ctrl; + + beforeEach(function() { + angular.module('readtest', []); + mocks.module('readtest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(ReadCtrl, { + $scope: scope + }); + }); + }); + + afterEach(function() {}); + + describe('scope variables', function() { + it('should be set correctly', function() { + expect(scope.state.read).to.exist; + expect(scope.state.read.open).to.be.false; + expect(scope.state.read.toggle).to.exist; + }); + }); + + describe('open/close read view', function() { + it('should open/close', function() { + expect(scope.state.read.open).to.be.false; + scope.state.read.toggle(true); + expect(scope.state.read.open).to.be.true; + scope.state.read.toggle(false); + expect(scope.state.read.open).to.be.false; + }); + }); + }); +}); \ No newline at end of file diff --git a/test/new-unit/write-ctrl-test.js b/test/new-unit/write-ctrl-test.js new file mode 100644 index 0000000..4d07d65 --- /dev/null +++ b/test/new-unit/write-ctrl-test.js @@ -0,0 +1,205 @@ +define(function(require) { + 'use strict'; + + var expect = chai.expect, + angular = require('angular'), + mocks = require('angularMocks'), + WriteCtrl = require('js/controller/write'), + EmailDAO = require('js/dao/email-dao'), + DeviceStorageDAO = require('js/dao/devicestorage-dao'), + KeychainDAO = require('js/dao/keychain-dao'), + appController = require('js/app-controller'); + + describe('Write controller unit test', function() { + var ctrl, scope, origEmailDao, emailDaoMock, keychainMock, deviceStorageMock, emailAddress; + + beforeEach(function() { + origEmailDao = appController._emailDao; + emailDaoMock = sinon.createStubInstance(EmailDAO); + appController._emailDao = emailDaoMock; + emailAddress = 'fred@foo.com'; + emailDaoMock._account = { + emailAddress: emailAddress, + }; + + keychainMock = sinon.createStubInstance(KeychainDAO); + emailDaoMock._keychain = keychainMock; + + deviceStorageMock = sinon.createStubInstance(DeviceStorageDAO); + emailDaoMock._devicestorage = deviceStorageMock; + + angular.module('writetest', []); + mocks.module('writetest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(WriteCtrl, { + $scope: scope + }); + }); + }); + + afterEach(function() { + // restore the module + appController._emailDao = origEmailDao; + }); + + describe('scope variables', function() { + it('should be set correctly', function() { + expect(scope.state.writer).to.exist; + expect(scope.state.writer.open).to.be.false; + expect(scope.state.writer.write).to.exist; + expect(scope.state.writer.close).to.exist; + expect(scope.verifyTo).to.exist; + expect(scope.updatePreview).to.exist; + expect(scope.sendToOutbox).to.exist; + }); + }); + + describe('close', function() { + it('should close the writer', function() { + scope.state.writer.open = true; + + scope.state.writer.close(); + + expect(scope.state.writer.open).to.be.false; + }); + }); + + describe('write', function() { + it('should prepare write view', function() { + var verifyToMock = sinon.stub(scope, 'verifyTo'); + + scope.state.writer.write(); + + expect(scope.writerTitle).to.equal('New email'); + expect(scope.to).to.equal(''); + expect(scope.subject).to.equal(''); + expect(scope.body).to.equal(''); + expect(scope.ciphertextPreview).to.equal(''); + expect(verifyToMock.calledOnce).to.be.true; + + scope.verifyTo.restore(); + }); + + it('should prefill write view for response', function() { + var verifyToMock = sinon.stub(scope, 'verifyTo'), + address = 'pity@dafool', + subject = 'Ermahgerd!', + body = 'so much body!', + re = { + from: [{ + address: address + }], + subject: subject, + sentDate: new Date(), + body: body + }; + + scope.state.writer.write(re); + + expect(scope.writerTitle).to.equal('Reply'); + expect(scope.to).to.equal(address); + expect(scope.subject).to.equal('Re: ' + subject); + expect(scope.body).to.contain(body); + expect(scope.ciphertextPreview).to.not.be.empty; + expect(verifyToMock.calledOnce).to.be.true; + + scope.verifyTo.restore(); + }); + + it('should prevent markup injection', function() { + var address = 'pity@dafool', + subject = 'Ermahgerd!', + body = '
markup
moreMarkup
', + re = { + from: [{ + address: address + }], + subject: subject, + sentDate: new Date(), + body: body, + html: false + }; + + sinon.stub(scope, 'verifyTo'); + + scope.state.writer.write(re); + + expect(scope.body).to.contain('
markupmoreMarkup'); + + scope.verifyTo.restore(); + }); + + }); + + describe('verifyTo', function() { + it('should verify the recipient as secure', function() { + var id = scope.to = 'pity@da.fool'; + keychainMock.getReceiverPublicKey.withArgs(id).yields(null, { + userId: id + }); + + scope.verifyTo(); + + expect(scope.toSecure).to.be.true; + expect(scope.sendBtnText).to.equal('Send securely'); + }); + + it('should verify the recipient as not secure', function() { + var id = scope.to = 'pity@da.fool'; + keychainMock.getReceiverPublicKey.withArgs(id).yields({ + errMsg: '404 not found yadda yadda' + }); + + scope.verifyTo(); + + expect(scope.toSecure).to.be.false; + expect(scope.sendBtnText).to.equal('Invite & send securely'); + }); + + it('should reset display if there is no recipient', function() { + scope.to = undefined; + scope.verifyTo(); + }); + }); + + describe('send to outbox', function() { + it('should work', function(done) { + scope.state.writer.open = true; + scope.to = 'a, b, c'; + scope.body = 'asd'; + scope.subject = 'yaddablabla'; + + deviceStorageMock.storeList.withArgs(sinon.match(function(mail) { + return mail[0].from[0].address === emailAddress && mail[0].to.length === 3; + })).yieldsAsync(); + scope.emptyOutbox = function() { + expect(scope.state.writer.open).to.be.false; + expect(deviceStorageMock.storeList.calledOnce).to.be.true; + done(); + }; + + scope.sendToOutbox(); + }); + + it('should not work and not close the write view', function() { + scope.state.writer.open = true; + scope.to = 'a, b, c'; + scope.body = 'asd'; + scope.subject = 'yaddablabla'; + + deviceStorageMock.storeList.withArgs(sinon.match(function(mail) { + return mail[0].from[0].address === emailAddress && mail[0].to.length === 3; + })).yields({ + errMsg: 'snafu' + }); + + scope.sendToOutbox(); + + expect(scope.state.writer.open).to.be.true; + expect(deviceStorageMock.storeList.calledOnce).to.be.true; + }); + }); + }); +}); \ No newline at end of file