From 03b2e10bc3f4332b20f17436d5906df2b98418bb Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 26 Nov 2014 12:59:44 +0100 Subject: [PATCH] Fix mail-list-ctrl unit test and move functions to services --- Gruntfile.js | 5 +- src/js/controller/app/mail-list.js | 144 +-------------- src/js/email/index.js | 3 +- src/js/email/search.js | 80 +++++++++ src/js/util/dummy.js | 73 ++++++++ src/js/util/index.js | 1 + .../controller/app/mail-list-ctrl-test.js | 169 +++++------------- test/unit/email/search-test.js | 96 ++++++++++ 8 files changed, 300 insertions(+), 271 deletions(-) create mode 100644 src/js/email/search.js create mode 100644 src/js/util/dummy.js create mode 100644 test/unit/email/search-test.js diff --git a/Gruntfile.js b/Gruntfile.js index b126fa4..b5c82ee 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -178,6 +178,7 @@ module.exports = function(grunt) { 'test/unit/email/outbox-bo-test.js', 'test/unit/email/email-dao-test.js', 'test/unit/email/account-test.js', + 'test/unit/email/search-test.js', 'test/unit/controller/app/dialog-ctrl-test.js', 'test/unit/controller/login/add-account-ctrl-test.js', 'test/unit/controller/login/create-account-ctrl-test.js', @@ -194,8 +195,8 @@ module.exports = function(grunt) { 'test/unit/controller/app/contacts-ctrl-test.js', 'test/unit/controller/app/read-ctrl-test.js', 'test/unit/controller/app/navigation-ctrl-test.js', - /*'test/unit/mail-list-ctrl-test.js', - 'test/unit/write-ctrl-test.js', + 'test/unit/controller/app/mail-list-ctrl-test.js', + /*'test/unit/write-ctrl-test.js', 'test/unit/action-bar-ctrl-test.js',*/ ] }, diff --git a/src/js/controller/app/mail-list.js b/src/js/controller/app/mail-list.js index 9124c6b..c899370 100644 --- a/src/js/controller/app/mail-list.js +++ b/src/js/controller/app/mail-list.js @@ -11,7 +11,7 @@ var INIT_DISPLAY_LEN = 20, FOLDER_TYPE_INBOX = 'Inbox', NOTIFICATION_INBOX_TIMEOUT = 5000; -var MailListCtrl = function($scope, $routeParams, statusDisplay, notification, email, keychain, dialog) { +var MailListCtrl = function($scope, $routeParams, statusDisplay, notification, email, keychain, dialog, search, dummy) { // // Init @@ -116,7 +116,7 @@ var MailListCtrl = function($scope, $routeParams, statusDisplay, notification, e // in development, display dummy mail objects if ($routeParams.dev) { statusDisplay.update('Last update: ', new Date()); - currentFolder().messages = createDummyMails(); + currentFolder().messages = dummy.listMails(); return; } @@ -186,83 +186,13 @@ var MailListCtrl = function($scope, $routeParams, statusDisplay, notification, e searchTimeout = setTimeout(function() { $scope.$apply(function() { // filter relevant messages - $scope.displayMessages = $scope.search(currentFolder().messages, searchText); + $scope.displayMessages = search.filter(currentFolder().messages, searchText); statusDisplay.setSearching(false); statusDisplay.update('Matches in this folder'); }); }, 500); }; - /** - * Do full text search on messages. Parse meta data first - */ - $scope.search = function(messages, searchText) { - // don't filter on empty searchText - if (!searchText) { - return messages; - } - - // escape search string - searchText = searchText.replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1"); - // compare all strings (case insensitive) - var regex = new RegExp(searchText, 'i'); - - function contains(input) { - if (!input) { - return false; - } - return regex.test(input); - } - - function checkAddresses(header) { - if (!header || !header.length) { - return false; - } - - for (var i = 0; i < header.length; i++) { - if (contains(header[i].name) || contains(header[i].address)) { - return true; - } - } - - return false; - } - - /** - * Filter meta data first and then only look at plaintext and decrypted message bodies - */ - function matchMetaDataFirst(m) { - // compare subject - if (contains(m.subject)) { - return true; - } - // compares address headers - if (checkAddresses(m.from) || checkAddresses(m.to) || checkAddresses(m.cc) || checkAddresses(m.bcc)) { - return true; - } - // compare plaintext body - if (m.body && !m.encrypted && contains(m.body)) { - return true; - } - // compare decrypted body - if (m.body && m.encrypted && m.decrypted && contains(m.body)) { - return true; - } - // compare plaintex html body - if (m.html && !m.encrypted && contains(m.html)) { - return true; - } - // compare decrypted html body - if (m.html && m.encrypted && m.decrypted && contains(m.html)) { - return true; - } - return false; - } - - // user native js Array.filter - return messages.filter(matchMetaDataFirst); - }; - /** * Sync current folder when client comes back online */ @@ -441,72 +371,4 @@ function byUidDescending(a, b) { } } -// Helper for development mode - -function createDummyMails() { - var uid = 1000000; - - var Email = function(unread, attachments, answered) { - this.uid = uid--; - this.from = [{ - name: 'Whiteout Support', - address: 'support@whiteout.io' - }]; // sender address - this.to = [{ - address: 'max.musterman@gmail.com' - }, { - address: 'max.musterman@gmail.com' - }]; // list of receivers - this.cc = [{ - address: 'john.doe@gmail.com' - }]; // list of receivers - this.attachments = attachments ? [{ - "filename": "a.md", - "filesize": 123, - "mimeType": "text/x-markdown", - "part": "2", - "content": null - }, { - "filename": "b.md", - "filesize": 456, - "mimeType": "text/x-markdown", - "part": "3", - "content": null - }, { - "filename": "c.md", - "filesize": 789, - "mimeType": "text/x-markdown", - "part": "4", - "content": null - }] : []; - this.unread = unread; - this.answered = answered; - this.sentDate = new Date('Thu Sep 19 2013 20:41:23 GMT+0200 (CEST)'); - this.subject = 'Getting started'; // Subject line - this.body = 'And a good day to you too sir. \n' + - '\n' + - 'Thursday, Apr 24, 2014 3:33 PM safewithme.testuser@gmail.com wrote:\n' + - '> adsfadfasdfasdfasfdasdfasdfas\n' + - '\n' + - 'http://example.com\n' + - '\n' + - '> Tuesday, Mar 25, 2014 4:19 PM gianniarcore@gmail.com wrote:\n' + - '>> from 0.7.0.1\n' + - '>>\n' + - '>> God speed!'; // plaintext body - //this.html = '

Hello there' + Math.random() + '

'; - this.encrypted = true; - this.decrypted = true; - }; - - var dummies = [], - i = 100; - while (i--) { - // every second/third/fourth dummy mail with unread/attachments/answered - dummies.push(new Email((i % 2 === 0), (i % 3 === 0), (i % 5 === 0))); - } - - return dummies; -} - module.exports = MailListCtrl; \ No newline at end of file diff --git a/src/js/email/index.js b/src/js/email/index.js index d5c14d5..4eaece7 100644 --- a/src/js/email/index.js +++ b/src/js/email/index.js @@ -6,4 +6,5 @@ require('./mailreader'); require('./pgpbuilder'); require('./email'); require('./outbox'); -require('./account'); \ No newline at end of file +require('./account'); +require('./search'); \ No newline at end of file diff --git a/src/js/email/search.js b/src/js/email/search.js new file mode 100644 index 0000000..fc45059 --- /dev/null +++ b/src/js/email/search.js @@ -0,0 +1,80 @@ +'use strict'; + +var ngModule = angular.module('woEmail'); +ngModule.service('search', Search); +module.exports = Search; + +function Search() {} + +/** + * Do full text search on messages. Parse meta data first. + * @param {Array} messages The messages to be filtered + * @param {String} query The text query used to filter messages + * @return {Array} The filtered messages + */ +Search.prototype.filter = function(messages, query) { + // don't filter on empty query + if (!query) { + return messages; + } + + // escape search string + query = query.replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1"); + // compare all strings (case insensitive) + var regex = new RegExp(query, 'i'); + + function contains(input) { + if (!input) { + return false; + } + return regex.test(input); + } + + function checkAddresses(header) { + if (!header || !header.length) { + return false; + } + + for (var i = 0; i < header.length; i++) { + if (contains(header[i].name) || contains(header[i].address)) { + return true; + } + } + + return false; + } + + /** + * Filter meta data first and then only look at plaintext and decrypted message bodies + */ + function matchMetaDataFirst(m) { + // compare subject + if (contains(m.subject)) { + return true; + } + // compares address headers + if (checkAddresses(m.from) || checkAddresses(m.to) || checkAddresses(m.cc) || checkAddresses(m.bcc)) { + return true; + } + // compare plaintext body + if (m.body && !m.encrypted && contains(m.body)) { + return true; + } + // compare decrypted body + if (m.body && m.encrypted && m.decrypted && contains(m.body)) { + return true; + } + // compare plaintex html body + if (m.html && !m.encrypted && contains(m.html)) { + return true; + } + // compare decrypted html body + if (m.html && m.encrypted && m.decrypted && contains(m.html)) { + return true; + } + return false; + } + + // user native js Array.filter + return messages.filter(matchMetaDataFirst); +}; \ No newline at end of file diff --git a/src/js/util/dummy.js b/src/js/util/dummy.js new file mode 100644 index 0000000..b9a257b --- /dev/null +++ b/src/js/util/dummy.js @@ -0,0 +1,73 @@ +'use strict'; + +var ngModule = angular.module('woUtil'); +ngModule.service('dummy', Dummy); +module.exports = Dummy; + +function Dummy() {} + +Dummy.prototype.listMails = function() { + var uid = 1000000; + + var Email = function(unread, attachments, answered) { + this.uid = uid--; + this.from = [{ + name: 'Whiteout Support', + address: 'support@whiteout.io' + }]; // sender address + this.to = [{ + address: 'max.musterman@gmail.com' + }, { + address: 'max.musterman@gmail.com' + }]; // list of receivers + this.cc = [{ + address: 'john.doe@gmail.com' + }]; // list of receivers + this.attachments = attachments ? [{ + "filename": "a.md", + "filesize": 123, + "mimeType": "text/x-markdown", + "part": "2", + "content": null + }, { + "filename": "b.md", + "filesize": 456, + "mimeType": "text/x-markdown", + "part": "3", + "content": null + }, { + "filename": "c.md", + "filesize": 789, + "mimeType": "text/x-markdown", + "part": "4", + "content": null + }] : []; + this.unread = unread; + this.answered = answered; + this.sentDate = new Date('Thu Sep 19 2013 20:41:23 GMT+0200 (CEST)'); + this.subject = 'Getting started'; // Subject line + this.body = 'And a good day to you too sir. \n' + + '\n' + + 'Thursday, Apr 24, 2014 3:33 PM safewithme.testuser@gmail.com wrote:\n' + + '> adsfadfasdfasdfasfdasdfasdfas\n' + + '\n' + + 'http://example.com\n' + + '\n' + + '> Tuesday, Mar 25, 2014 4:19 PM gianniarcore@gmail.com wrote:\n' + + '>> from 0.7.0.1\n' + + '>>\n' + + '>> God speed!'; // plaintext body + //this.html = '

Hello there' + Math.random() + '

'; + this.encrypted = true; + this.decrypted = true; + }; + + var dummies = [], + i = 100; + while (i--) { + // every second/third/fourth dummy mail with unread/attachments/answered + dummies.push(new Email((i % 2 === 0), (i % 3 === 0), (i % 5 === 0))); + } + + return dummies; +}; \ No newline at end of file diff --git a/src/js/util/index.js b/src/js/util/index.js index 1536843..86c9d72 100644 --- a/src/js/util/index.js +++ b/src/js/util/index.js @@ -3,6 +3,7 @@ angular.module('woUtil', []); require('./axe'); +require('./dummy'); require('./dialog'); require('./connection-doctor'); require('./update/update-handler'); diff --git a/test/unit/controller/app/mail-list-ctrl-test.js b/test/unit/controller/app/mail-list-ctrl-test.js index ee915ea..2d7724a 100644 --- a/test/unit/controller/app/mail-list-ctrl-test.js +++ b/test/unit/controller/app/mail-list-ctrl-test.js @@ -1,17 +1,17 @@ 'use strict'; var mocks = angular.mock, - MailListCtrl = require('../../src/js/controller/mail-list'), - EmailDAO = require('../../src/js/dao/email-dao'), - DeviceStorageDAO = require('../../src/js/dao/devicestorage-dao'), - KeychainDAO = require('../../src/js/dao/keychain-dao'), - appController = require('../../src/js/app-controller'), - notification = require('../../src/js/util/notification'); + MailListCtrl = require('../../../../src/js/controller/app/mail-list'), + EmailDAO = require('../../../../src/js/email/email'), + KeychainDAO = require('../../../../src/js/service/keychain'), + StatusDisplay = require('../../../../src/js/util/status-display'), + Dialog = require('../../../../src/js/util/dialog'), + Search = require('../../../../src/js/email/search'); chai.config.includeStack = true; describe('Mail List controller unit test', function() { - var scope, ctrl, origEmailDao, emailDaoMock, keychainMock, deviceStorageMock, + var scope, ctrl, statusDisplayMock, notificationMock, emailMock, keychainMock, dialogMock, searchMock, emailAddress, emails, hasChrome, hasSocket, hasRuntime, hasIdentity; @@ -41,26 +41,21 @@ describe('Mail List controller unit test', function() { }, { unread: true }]; - appController._outboxBo = { - pendingEmails: emails - }; - origEmailDao = appController._emailDao; - emailDaoMock = sinon.createStubInstance(EmailDAO); - appController._emailDao = emailDaoMock; emailAddress = 'fred@foo.com'; - emailDaoMock._account = { - emailAddress: emailAddress, + + notificationMock = { + create: function() {}, + close: function() {} }; - + statusDisplayMock = sinon.createStubInstance(StatusDisplay); + emailMock = sinon.createStubInstance(EmailDAO); keychainMock = sinon.createStubInstance(KeychainDAO); - appController._keychain = keychainMock; + dialogMock = sinon.createStubInstance(Dialog); + searchMock = sinon.createStubInstance(Search); - deviceStorageMock = sinon.createStubInstance(DeviceStorageDAO); - emailDaoMock._devicestorage = deviceStorageMock; - - angular.module('maillisttest', []); + angular.module('maillisttest', ['woEmail', 'woServices', 'woUtil']); mocks.module('maillisttest'); mocks.inject(function($rootScope, $controller) { scope = $rootScope.$new(); @@ -73,7 +68,13 @@ describe('Mail List controller unit test', function() { scope.loadVisibleBodies = function() {}; ctrl = $controller(MailListCtrl, { $scope: scope, - $routeParams: {} + $routeParams: {}, + statusDisplay: statusDisplayMock, + notification: notificationMock, + email: emailMock, + keychain: keychainMock, + dialog: dialogMock, + search: searchMock }); }); }); @@ -91,9 +92,6 @@ describe('Mail List controller unit test', function() { if (!hasIdentity) { delete window.chrome.identity; } - - // restore the module - appController._emailDao = origEmailDao; }); describe('displayMore', function() { @@ -137,104 +135,21 @@ describe('Mail List controller unit test', function() { it('should show initial message on empty', function() { scope.displaySearchResults(); - expect(scope.state.mailList.searching).to.be.false; - expect(scope.state.mailList.lastUpdateLbl).to.equal('Online'); + expect(statusDisplayMock.setSearching.withArgs(false).calledOnce).to.be.true; + expect(statusDisplayMock.update.withArgs('Online').calledOnce).to.be.true; expect(scope.displayMessages.length).to.equal(2); }); it('should show initial message on empty', function() { - var searchStub = sinon.stub(scope, 'search'); - searchStub.returns(['a']); - + searchMock.filter.returns(['a']); scope.displaySearchResults('query'); - expect(scope.state.mailList.searching).to.be.true; - expect(scope.state.mailList.lastUpdateLbl).to.equal('Searching ...'); + expect(statusDisplayMock.setSearching.withArgs(true).calledOnce).to.be.true; + expect(statusDisplayMock.update.withArgs('Searching ...').calledOnce).to.be.true; clock.tick(500); expect(scope.displayMessages).to.deep.equal(['a']); - expect(scope.state.mailList.searching).to.be.false; - expect(scope.state.mailList.lastUpdateLbl).to.equal('Matches in this folder'); - - }); - }); - - describe('search', function() { - var message1 = { - to: [{ - name: 'name1', - address: 'address1' - }], - subject: 'subject1', - body: 'body1', - html: 'html1' - }, - message2 = { - to: [{ - name: 'name2', - address: 'address2' - }], - subject: 'subject2', - body: 'body2', - html: 'html2' - }, - message3 = { - to: [{ - name: 'name3', - address: 'address3' - }], - subject: 'subject3', - body: 'body1', - html: 'html1', - encrypted: true - }, - message4 = { - to: [{ - name: 'name4', - address: 'address4' - }], - subject: 'subject4', - body: 'body1', - html: 'html1', - encrypted: true, - decrypted: true - }, - testMessages = [message1, message2, message3, message4]; - - it('return same messages array on empty query string', function() { - var result = scope.search(testMessages, ''); - expect(result).to.equal(testMessages); - }); - - it('return message1 on matching subject', function() { - var result = scope.search(testMessages, 'subject1'); - expect(result.length).to.equal(1); - expect(result[0]).to.equal(message1); - }); - - it('return message1 on matching name', function() { - var result = scope.search(testMessages, 'name1'); - expect(result.length).to.equal(1); - expect(result[0]).to.equal(message1); - }); - - it('return message1 on matching address', function() { - var result = scope.search(testMessages, 'address1'); - expect(result.length).to.equal(1); - expect(result[0]).to.equal(message1); - }); - - it('return plaintext and decrypted messages on matching body', function() { - var result = scope.search(testMessages, 'body1'); - expect(result.length).to.equal(2); - expect(result[0]).to.equal(message1); - expect(result[1]).to.equal(message4); - }); - - it('return plaintext and decrypted messages on matching html', function() { - var result = scope.search(testMessages, 'html1'); - expect(result.length).to.equal(2); - expect(result[0]).to.equal(message1); - expect(result[1]).to.equal(message4); + expect(statusDisplayMock.setSearching.withArgs(false).calledOnce).to.be.true; + expect(statusDisplayMock.update.withArgs('Matches in this folder').calledOnce).to.be.true; }); }); @@ -251,7 +166,7 @@ describe('Mail List controller unit test', function() { }); afterEach(function() { - notification.create.restore(); + notificationMock.create.restore(); }); it('should succeed for single mail', function(done) { @@ -264,7 +179,7 @@ describe('Mail List controller unit test', function() { unread: true }; - sinon.stub(notification, 'create', function(opts) { + sinon.stub(notificationMock, 'create', function(opts) { expect(opts.title).to.equal(mail.from[0].address); expect(opts.message).to.equal(mail.subject); @@ -280,7 +195,7 @@ describe('Mail List controller unit test', function() { } }; - emailDaoMock.onIncomingMessage([mail]); + emailMock.onIncomingMessage([mail]); }); it('should succeed for multiple mails', function(done) { @@ -307,7 +222,7 @@ describe('Mail List controller unit test', function() { unread: false }]; - sinon.stub(notification, 'create', function(opts) { + sinon.stub(notificationMock, 'create', function(opts) { expect(opts.title).to.equal('2 new messages'); expect(opts.message).to.equal(mails[0].subject + '\n' + mails[1].subject); @@ -323,7 +238,7 @@ describe('Mail List controller unit test', function() { } }; - emailDaoMock.onIncomingMessage(mails); + emailMock.onIncomingMessage(mails); }); }); @@ -336,14 +251,14 @@ describe('Mail List controller unit test', function() { }; scope.getBody(); - expect(emailDaoMock.getBody.calledOnce).to.be.true; + expect(emailMock.getBody.calledOnce).to.be.true; }); }); describe('select', function() { it('should decrypt, focus mark an unread mail as read', function() { scope.pendingNotifications = ['asd']; - sinon.stub(notification, 'close'); + sinon.stub(notificationMock, 'close'); var mail = { from: [{ @@ -372,13 +287,13 @@ describe('Mail List controller unit test', function() { scope.select(mail); - expect(emailDaoMock.decryptBody.calledOnce).to.be.true; + expect(emailMock.decryptBody.calledOnce).to.be.true; expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true; expect(scope.state.mailList.selected).to.equal(mail); - expect(notification.close.calledWith('asd')).to.be.true; - expect(notification.close.calledOnce).to.be.true; + expect(notificationMock.close.calledWith('asd')).to.be.true; + expect(notificationMock.close.calledOnce).to.be.true; - notification.close.restore(); + notificationMock.close.restore(); }); it('should decrypt and focus a read mail', function() { @@ -407,7 +322,7 @@ describe('Mail List controller unit test', function() { scope.select(mail); - expect(emailDaoMock.decryptBody.calledOnce).to.be.true; + expect(emailMock.decryptBody.calledOnce).to.be.true; expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true; expect(scope.state.mailList.selected).to.equal(mail); }); diff --git a/test/unit/email/search-test.js b/test/unit/email/search-test.js new file mode 100644 index 0000000..40a4ef0 --- /dev/null +++ b/test/unit/email/search-test.js @@ -0,0 +1,96 @@ +'use strict'; + +describe('Search Service unit test', function() { + var search; + + beforeEach(function() { + angular.module('search-test', ['woEmail']); + angular.mock.module('search-test'); + angular.mock.inject(function($injector) { + search = $injector.get('search'); + }); + }); + + afterEach(function() {}); + + describe('filter', function() { + var message1 = { + to: [{ + name: 'name1', + address: 'address1' + }], + subject: 'subject1', + body: 'body1', + html: 'html1' + }, + message2 = { + to: [{ + name: 'name2', + address: 'address2' + }], + subject: 'subject2', + body: 'body2', + html: 'html2' + }, + message3 = { + to: [{ + name: 'name3', + address: 'address3' + }], + subject: 'subject3', + body: 'body1', + html: 'html1', + encrypted: true + }, + message4 = { + to: [{ + name: 'name4', + address: 'address4' + }], + subject: 'subject4', + body: 'body1', + html: 'html1', + encrypted: true, + decrypted: true + }, + testMessages = [message1, message2, message3, message4]; + + it('return same messages array on empty query string', function() { + var result = search.filter(testMessages, ''); + expect(result).to.equal(testMessages); + }); + + it('return message1 on matching subject', function() { + var result = search.filter(testMessages, 'subject1'); + expect(result.length).to.equal(1); + expect(result[0]).to.equal(message1); + }); + + it('return message1 on matching name', function() { + var result = search.filter(testMessages, 'name1'); + expect(result.length).to.equal(1); + expect(result[0]).to.equal(message1); + }); + + it('return message1 on matching address', function() { + var result = search.filter(testMessages, 'address1'); + expect(result.length).to.equal(1); + expect(result[0]).to.equal(message1); + }); + + it('return plaintext and decrypted messages on matching body', function() { + var result = search.filter(testMessages, 'body1'); + expect(result.length).to.equal(2); + expect(result[0]).to.equal(message1); + expect(result[1]).to.equal(message4); + }); + + it('return plaintext and decrypted messages on matching html', function() { + var result = search.filter(testMessages, 'html1'); + expect(result.length).to.equal(2); + expect(result[0]).to.equal(message1); + expect(result[1]).to.equal(message4); + }); + }); + +}); \ No newline at end of file