diff options
27 files changed, 519 insertions, 399 deletions
@@ -1,3 +1,9 @@ #!/bin/bash -python setup.py $* +if [ "$1" == 'test' ] +then + nosetests --nocapture test/unit + nosetests --nocapture test/integration +else + python setup.py $* +fi diff --git a/service/pixelated/adapter/search.py b/service/pixelated/adapter/search.py index cd900f87..6c62ccc3 100644 --- a/service/pixelated/adapter/search.py +++ b/service/pixelated/adapter/search.py @@ -102,7 +102,7 @@ class SearchEngine(object): raw=TEXT(stored=False)) def _create_index(self): - masterkey = self.soledad_querier.get_index_masterkey + masterkey = self.soledad_querier.get_index_masterkey() storage = EncryptedFileStorage(self.INDEX_FOLDER, masterkey) return FileIndex.create(storage, self._mail_schema(), indexname='mails') diff --git a/service/pixelated/adapter/soledad_querier.py b/service/pixelated/adapter/soledad_querier.py index 2e67a5ab..a11ff7a3 100644 --- a/service/pixelated/adapter/soledad_querier.py +++ b/service/pixelated/adapter/soledad_querier.py @@ -26,7 +26,6 @@ class SoledadQuerier: def __init__(self, soledad): self.soledad = soledad - @property def get_index_masterkey(self): index_key = self.soledad.get_from_index('by-type', 'index_key') if len(index_key) == 0: diff --git a/service/pixelated/controllers/__init__.py b/service/pixelated/controllers/__init__.py index 9e447d4d..e1c13515 100644 --- a/service/pixelated/controllers/__init__.py +++ b/service/pixelated/controllers/__init__.py @@ -22,6 +22,13 @@ def respond_json(entity, request, status_code=200): return json_response +def respond_json_deferred(entity, request, status_code=200): + json_response = json.dumps(entity) + request.responseHeaders.addRawHeader(b"content-type", b"application/json") + request.code = status_code + request.write(json_response) + request.finish() + import json from home_controller import HomeController diff --git a/service/pixelated/controllers/tags_controller.py b/service/pixelated/controllers/tags_controller.py index 0b9a94ac..b6741dcc 100644 --- a/service/pixelated/controllers/tags_controller.py +++ b/service/pixelated/controllers/tags_controller.py @@ -14,8 +14,8 @@ # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. -from flask import request -from pixelated.controllers import respond_json +from pixelated.controllers import respond_json_deferred +from twisted.internet.threads import deferToThread class TagsController: @@ -25,6 +25,9 @@ class TagsController: def tags(self, request): query = request.args.get('q', [''])[0] - skip_default_tags = request.args.get('skipDefaultTags') - tags = self._search_engine.tags(query=query, skip_default_tags=skip_default_tags) - return respond_json(tags, request) + skip_default_tags = request.args.get('skipDefaultTags', [False])[0] + + d = deferToThread(lambda: self._search_engine.tags(query=query, skip_default_tags=skip_default_tags)) + d.addCallback(lambda tags: respond_json_deferred(tags, request)) + + return d diff --git a/service/test/functional/features/environment.py b/service/test/functional/features/environment.py index cb9e0876..537cd969 100644 --- a/service/test/functional/features/environment.py +++ b/service/test/functional/features/environment.py @@ -15,27 +15,20 @@ # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. import time import multiprocessing - -from selenium import webdriver -from test.support.integration_helper import SoledadTestBase -import pixelated.runserver import logging -import pixelated.controllers.features_controller +from test.support.integration import AppTestClient +from selenium import webdriver +import pixelated def before_all(context): - context.soledad_test_base = SoledadTestBase() - context.soledad_test_base.setup_soledad() - - context.mailboxes = context.soledad_test_base.mailboxes - context.app = pixelated.runserver.app - context.app.mail_service = context.soledad_test_base.mail_service pixelated.controllers.features_controller.FeaturesController.DISABLED_FEATURES.append('autoRefresh') + client = AppTestClient() + context.client = client logging.disable('INFO') - worker = lambda app, port: pixelated.runserver.app.run(host='localhost', port=4567, - logFile=open('/tmp/behave-tests.log', 'w')) - context._process = multiprocessing.Process(target=worker, args=(context.app, 4567)) + worker = lambda: client.app.run(host='localhost', port=4567, logFile=open('/tmp/behave-tests.log', 'w')) + context._process = multiprocessing.Process(target=worker) context._process.start() # we must wait the server start listening @@ -43,7 +36,6 @@ def before_all(context): def after_all(context): - context.soledad_test_base.teardown_soledad() context._process.terminate() diff --git a/service/test/functional/features/steps/data_setup.py b/service/test/functional/features/steps/data_setup.py index 51c5f9b8..4e349f05 100644 --- a/service/test/functional/features/steps/data_setup.py +++ b/service/test/functional/features/steps/data_setup.py @@ -13,10 +13,10 @@ # # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. -from test.support.integration_helper import MailBuilder +from test.support.integration import MailBuilder @given('I have a mail in my inbox') def add_mail_impl(context): input_mail = MailBuilder().build_input_mail() - context.soledad_test_base.add_mail_to_inbox(input_mail) + context.client.add_mail_to_inbox(input_mail) diff --git a/service/test/integration/delete_mail_test.py b/service/test/integration/delete_mail_test.py index 10c09e7a..15232b0d 100644 --- a/service/test/integration/delete_mail_test.py +++ b/service/test/integration/delete_mail_test.py @@ -13,22 +13,21 @@ # # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. -import unittest -from test.support.integration_helper import MailBuilder, SoledadTestBase +from test.support.integration import * -class DeleteMailTest(unittest.TestCase, SoledadTestBase): +class DeleteMailTest(SoledadTestBase): def setUp(self): - self.setup_soledad() + SoledadTestBase.setUp(self) def tearDown(self): - self.teardown_soledad() + SoledadTestBase.tearDown(self) def test_move_mail_to_trash_when_deleting(self): input_mail = MailBuilder().with_subject('Mail with tags').build_input_mail() - self.add_mail_to_inbox(input_mail) + self.client.add_mail_to_inbox(input_mail) inbox_mails = self.get_mails_by_tag('inbox') self.assertEquals(1, len(inbox_mails)) @@ -41,7 +40,7 @@ class DeleteMailTest(unittest.TestCase, SoledadTestBase): self.assertEquals(1, len(trash_mails)) def test_delete_mail_when_trashing_mail_from_trash_mailbox(self): - mails = self.add_multiple_to_mailbox(1, 'trash') + mails = self.client.add_multiple_to_mailbox(1, 'trash') self.delete_mail(mails[0].ident) trash_mails = self.get_mails_by_tag('trash') diff --git a/service/test/integration/drafts_test.py b/service/test/integration/drafts_test.py index 5d2118df..2ba14dfd 100644 --- a/service/test/integration/drafts_test.py +++ b/service/test/integration/drafts_test.py @@ -13,18 +13,17 @@ # # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. -import unittest -from test.support.integration_helper import MailBuilder, SoledadTestBase +from test.support.integration import * -class DraftsTest(unittest.TestCase, SoledadTestBase): +class DraftsTest(SoledadTestBase): def setUp(self): - self.setup_soledad() + SoledadTestBase.setUp(self) def tearDown(self): - self.teardown_soledad() + SoledadTestBase.tearDown(self) def test_post_sends_mail_and_deletes_previous_draft_if_it_exists(self): # creates one draft diff --git a/service/test/integration/mark_as_read_unread_test.py b/service/test/integration/mark_as_read_unread_test.py index dc21c7b7..55467e9e 100644 --- a/service/test/integration/mark_as_read_unread_test.py +++ b/service/test/integration/mark_as_read_unread_test.py @@ -13,23 +13,22 @@ # # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. -import unittest -from test.support.integration_helper import MailBuilder, SoledadTestBase +from test.support.integration import * from pixelated.adapter.status import Status -class MarkAsReadUnreadTest(unittest.TestCase, SoledadTestBase): +class MarkAsReadUnreadTest(SoledadTestBase): def setUp(self): - self.setup_soledad() + SoledadTestBase.setUp(self) def tearDown(self): - self.teardown_soledad() + SoledadTestBase.tearDown(self) def test_mark_single_as_read(self): input_mail = MailBuilder().build_input_mail() - self.add_mail_to_inbox(input_mail) + self.client.add_mail_to_inbox(input_mail) mails = self.get_mails_by_tag('inbox') self.assertNotIn('read', mails[0].status) @@ -41,7 +40,7 @@ class MarkAsReadUnreadTest(unittest.TestCase, SoledadTestBase): def test_mark_single_as_unread(self): input_mail = MailBuilder().with_status([Status.SEEN]).build_input_mail() - self.add_mail_to_inbox(input_mail) + self.client.add_mail_to_inbox(input_mail) self.mark_as_unread(input_mail.ident) mail = self.get_mails_by_tag('inbox')[0] @@ -52,8 +51,8 @@ class MarkAsReadUnreadTest(unittest.TestCase, SoledadTestBase): input_mail = MailBuilder().with_status([Status.SEEN]).build_input_mail() input_mail2 = MailBuilder().with_status([Status.SEEN]).build_input_mail() - self.add_mail_to_inbox(input_mail) - self.add_mail_to_inbox(input_mail2) + self.client.add_mail_to_inbox(input_mail) + self.client.add_mail_to_inbox(input_mail2) self.mark_many_as_unread([input_mail.ident, input_mail2.ident]) @@ -66,8 +65,8 @@ class MarkAsReadUnreadTest(unittest.TestCase, SoledadTestBase): input_mail = MailBuilder().build_input_mail() input_mail2 = MailBuilder().build_input_mail() - self.add_mail_to_inbox(input_mail) - self.add_mail_to_inbox(input_mail2) + self.client.add_mail_to_inbox(input_mail) + self.client.add_mail_to_inbox(input_mail2) mails = self.get_mails_by_tag('inbox') @@ -86,8 +85,8 @@ class MarkAsReadUnreadTest(unittest.TestCase, SoledadTestBase): input_mail = MailBuilder().build_input_mail() input_mail2 = MailBuilder().with_status([Status.SEEN]).build_input_mail() - self.add_mail_to_inbox(input_mail) - self.add_mail_to_inbox(input_mail2) + self.client.add_mail_to_inbox(input_mail) + self.client.add_mail_to_inbox(input_mail2) mails = self.get_mails_by_tag('inbox') diff --git a/service/test/integration/retrieve_attachment_test.py b/service/test/integration/retrieve_attachment_test.py index 37bc3ca2..5f754a27 100644 --- a/service/test/integration/retrieve_attachment_test.py +++ b/service/test/integration/retrieve_attachment_test.py @@ -13,18 +13,17 @@ # # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. -import unittest -from test.support.integration_helper import SoledadTestBase +from test.support.integration.soledad_test_base import SoledadTestBase -class RetrieveAttachmentTest(unittest.TestCase, SoledadTestBase): +class RetrieveAttachmentTest(SoledadTestBase): def setUp(self): - self.setup_soledad() + SoledadTestBase.setUp(self) def tearDown(self): - self.teardown_soledad() + SoledadTestBase.tearDown(self) def test_attachment_content_is_retrieved(self): ident = 'F4E99C1CEC4D300A4223A96CCABBE0304BDBC31C550A5A03E207A5E4C3C71A22' @@ -35,7 +34,7 @@ class RetrieveAttachmentTest(unittest.TestCase, SoledadTestBase): 'phash': ident, 'content-type': 'text/plain; charset=US-ASCII; name="attachment_pequeno.txt"'} - self.add_document_to_soledad(attachment_dict) + self.client.add_document_to_soledad(attachment_dict) attachment = self.get_attachment(ident, 'base64') diff --git a/service/test/integration/search_test.py b/service/test/integration/search_test.py index 649f7b96..21326ec7 100644 --- a/service/test/integration/search_test.py +++ b/service/test/integration/search_test.py @@ -13,72 +13,86 @@ # # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. -import unittest -from test.support.integration_helper import MailBuilder, SoledadTestBase +from nose.twistedtools import deferred +from test.support.integration import * -class SearchTest(unittest.TestCase, SoledadTestBase): +class SearchTest(SoledadTestBase): def setUp(self): - self.setup_soledad() - - def tearDown(self): - self.teardown_soledad() + SoledadTestBase.setUp(self) + @deferred(timeout=SoledadTestBase.DEFERRED_TIMEOUT) def test_that_tags_returns_all_tags(self): input_mail = MailBuilder().with_tags(['important']).build_input_mail() - self.add_mail_to_inbox(input_mail) + self.client.add_mail_to_inbox(input_mail) - all_tags = self.get_tags() + d = self.get_tags() - all_tag_names = [t['name'] for t in all_tags] - self.assertTrue('inbox' in all_tag_names) - self.assertTrue('sent' in all_tag_names) - self.assertTrue('trash' in all_tag_names) - self.assertTrue('drafts' in all_tag_names) - self.assertTrue('important' in all_tag_names) + def _assert(all_tags): + all_tag_names = [t['name'] for t in all_tags] + self.assertTrue('inbox' in all_tag_names) + self.assertTrue('sent' in all_tag_names) + self.assertTrue('trash' in all_tag_names) + self.assertTrue('drafts' in all_tag_names) + self.assertTrue('important' in all_tag_names) + d.addCallback(_assert) + return d + @deferred(timeout=SoledadTestBase.DEFERRED_TIMEOUT) def test_that_tags_are_filtered_by_query(self): input_mail = MailBuilder().with_tags(['ateu', 'catoa', 'luat', 'zuado']).build_input_mail() - self.add_mail_to_inbox(input_mail) + self.client.add_mail_to_inbox(input_mail) + + d = self.get_tags(q=["at"], skipDefaultTags=["true"]) - all_tags = self.get_tags(q=["at"], skipDefaultTags=["true"]) + def _assert(all_tags): + all_tag_names = [t['name'] for t in all_tags] + self.assertEqual(3, len(all_tag_names)) + self.assertTrue('ateu' in all_tag_names) + self.assertTrue('catoa' in all_tag_names) + self.assertTrue('luat' in all_tag_names) - all_tag_names = [t['name'] for t in all_tags] - self.assertEqual(3, len(all_tag_names)) - self.assertTrue('ateu' in all_tag_names) - self.assertTrue('catoa' in all_tag_names) - self.assertTrue('luat' in all_tag_names) + d.addCallback(_assert) + return d + @deferred(timeout=SoledadTestBase.DEFERRED_TIMEOUT) def test_that_default_tags_are_ignorable(self): input_mail = MailBuilder().with_tags(['sometag']).build_input_mail() - self.add_mail_to_inbox(input_mail) + self.client.add_mail_to_inbox(input_mail) - all_tags = self.get_tags(skipDefaultTags=["true"]) + d = self.get_tags(skipDefaultTags=["true"]) - all_tag_names = [t['name'] for t in all_tags] - self.assertEqual(1, len(all_tag_names)) - self.assertTrue('sometag' in all_tag_names) + def _assert(all_tags): + all_tag_names = [t['name'] for t in all_tags] + self.assertEqual(1, len(all_tag_names)) + self.assertTrue('sometag' in all_tag_names) + d.addCallback(_assert) + return d + @deferred(timeout=SoledadTestBase.DEFERRED_TIMEOUT_LONG) def test_tags_count(self): - self.add_multiple_to_mailbox(num=10, mailbox='inbox', flags=['\\Recent']) - self.add_multiple_to_mailbox(num=5, mailbox='inbox', flags=['\\Seen']) - self.add_multiple_to_mailbox(num=3, mailbox='inbox', flags=['\\Recent'], tags=['important', 'later']) - self.add_multiple_to_mailbox(num=1, mailbox='inbox', flags=['\\Seen'], tags=['important']) + self.client.add_multiple_to_mailbox(num=10, mailbox='inbox', flags=['\\Recent']) + self.client.add_multiple_to_mailbox(num=5, mailbox='inbox', flags=['\\Seen']) + self.client.add_multiple_to_mailbox(num=3, mailbox='inbox', flags=['\\Recent'], tags=['important', 'later']) + self.client.add_multiple_to_mailbox(num=1, mailbox='inbox', flags=['\\Seen'], tags=['important']) - tags_count = self.get_tags() + d = self.get_tags() - self.assertEqual(self.get_count(tags_count, 'inbox')['total'], 19) - self.assertEqual(self.get_count(tags_count, 'inbox')['read'], 6) - self.assertEqual(self.get_count(tags_count, 'important')['total'], 4) - self.assertEqual(self.get_count(tags_count, 'important')['read'], 1) + def _assert(tags_count): + self.assertEqual(self.get_count(tags_count, 'inbox')['total'], 19) + self.assertEqual(self.get_count(tags_count, 'inbox')['read'], 6) + self.assertEqual(self.get_count(tags_count, 'important')['total'], 4) + self.assertEqual(self.get_count(tags_count, 'important')['read'], 1) + d.addCallback(_assert) + return d def test_search_mails_different_window(self): input_mail = MailBuilder().build_input_mail() input_mail2 = MailBuilder().build_input_mail() - self.add_mail_to_inbox(input_mail) - self.add_mail_to_inbox(input_mail2) + self.client.add_mail_to_inbox(input_mail) + self.client.add_mail_to_inbox(input_mail2) first_page = self.get_mails_by_tag('inbox', page=1, window=1) @@ -87,8 +101,8 @@ class SearchTest(unittest.TestCase, SoledadTestBase): def test_search_mails_with_multiple_pages(self): input_mail = MailBuilder().build_input_mail() input_mail2 = MailBuilder().build_input_mail() - self.add_mail_to_inbox(input_mail) - self.add_mail_to_inbox(input_mail2) + self.client.add_mail_to_inbox(input_mail) + self.client.add_mail_to_inbox(input_mail2) first_page = self.get_mails_by_tag('inbox', page=1, window=1) second_page = self.get_mails_by_tag('inbox', page=2, window=1) @@ -100,7 +114,7 @@ class SearchTest(unittest.TestCase, SoledadTestBase): def test_page_zero_fetches_first_page(self): input_mail = MailBuilder().build_input_mail() - self.add_mail_to_inbox(input_mail) + self.client.add_mail_to_inbox(input_mail) page = self.get_mails_by_tag('inbox', page=0, window=1) self.assertEqual(page[0].ident, input_mail.ident) @@ -113,8 +127,8 @@ class SearchTest(unittest.TestCase, SoledadTestBase): input_mail = MailBuilder().with_date('2014-10-15T15:15').build_input_mail() input_mail2 = MailBuilder().with_date('2014-10-15T15:16').build_input_mail() - self.add_mail_to_inbox(input_mail) - self.add_mail_to_inbox(input_mail2) + self.client.add_mail_to_inbox(input_mail) + self.client.add_mail_to_inbox(input_mail2) results = self.get_mails_by_tag('inbox') self.assertEqual(results[0].ident, input_mail2.ident) diff --git a/service/test/integration/soledad_querier_test.py b/service/test/integration/soledad_querier_test.py index 5d9bcccd..4a99a620 100644 --- a/service/test/integration/soledad_querier_test.py +++ b/service/test/integration/soledad_querier_test.py @@ -15,19 +15,21 @@ # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. import copy -import unittest import time -from test.support.integration_helper import SoledadTestBase, MailBuilder + +from test.support.integration import * from leap.mail.imap.fields import WithMsgFields -class SoledadQuerierTest(unittest.TestCase, SoledadTestBase, WithMsgFields): +class SoledadQuerierTest(SoledadTestBase, WithMsgFields): def setUp(self): - self.setup_soledad() + SoledadTestBase.setUp(self) + self.soledad = self.client.soledad + self.soledad_querier = self.client.soledad_querier def tearDown(self): - self.teardown_soledad() + SoledadTestBase.tearDown(self) def _get_empty_mailbox(self): return copy.deepcopy(self.EMPTY_MBOX) @@ -42,7 +44,7 @@ class SoledadQuerierTest(unittest.TestCase, SoledadTestBase, WithMsgFields): return [m for m in self.soledad.get_from_index('by-type', 'mbox') if m.content['mbox'] == mailbox_name] def test_remove_dup_mailboxes_keeps_the_one_with_the_highest_last_uid(self): - self.add_multiple_to_mailbox(3, 'INBOX') # by now we already have one inbox with 3 mails + self.client.add_multiple_to_mailbox(3, 'INBOX') # by now we already have one inbox with 3 mails self._create_mailbox('INBOX') # now we have a duplicate # make sure we have two @@ -77,7 +79,7 @@ class SoledadQuerierTest(unittest.TestCase, SoledadTestBase, WithMsgFields): self.assertEqual(1, len(mails)) def test_get_mails_by_chash(self): - mails = self.add_multiple_to_mailbox(3, 'INBOX') + mails = self.client.add_multiple_to_mailbox(3, 'INBOX') chashes = [mail.ident for mail in mails] fetched_mails = self.soledad_querier.mails(chashes) diff --git a/service/test/integration/tags_test.py b/service/test/integration/tags_test.py index bd22e4b5..03d6f4b3 100644 --- a/service/test/integration/tags_test.py +++ b/service/test/integration/tags_test.py @@ -14,25 +14,24 @@ # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. import json -import unittest -from test.support.integration_helper import MailBuilder, SoledadTestBase +from test.support.integration import * -class TagsTest(unittest.TestCase, SoledadTestBase): +class TagsTest(SoledadTestBase): def setUp(self): - self.setup_soledad() + SoledadTestBase.setUp(self) def tearDown(self): - self.teardown_soledad() + SoledadTestBase.tearDown(self) def _tags_json(self, tags): return json.dumps({'newtags': tags}) def test_add_tag_to_an_inbox_mail_and_query(self): mail = MailBuilder().with_subject('Mail with tags').build_input_mail() - self.add_mail_to_inbox(mail) + self.client.add_mail_to_inbox(mail) self.post_tags(mail.ident, self._tags_json(['IMPORTANT'])) @@ -44,10 +43,10 @@ class TagsTest(unittest.TestCase, SoledadTestBase): def test_addition_of_reserved_tags_is_not_allowed(self): mail = MailBuilder().with_subject('Mail with tags').build_input_mail() - self.add_mail_to_inbox(mail) + self.client.add_mail_to_inbox(mail) response = self.post_tags(mail.ident, self._tags_json(['DRAFTS'])) self.assertEquals("None of the following words can be used as tags: drafts", response) - mail = self.mailboxes.inbox().mail(mail.ident) + mail = self.client.mailboxes.inbox().mail(mail.ident) self.assertNotIn('drafts', mail.tags) diff --git a/service/test/support/integration/__init__.py b/service/test/support/integration/__init__.py new file mode 100644 index 00000000..8d0bbb7a --- /dev/null +++ b/service/test/support/integration/__init__.py @@ -0,0 +1,18 @@ +# +# Copyright (c) 2014 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see <http://www.gnu.org/licenses/>. +from .app_test_client import AppTestClient +from .model import MailBuilder, ResponseMail +from .soledad_test_base import SoledadTestBase diff --git a/service/test/support/integration/app_test_client.py b/service/test/support/integration/app_test_client.py new file mode 100644 index 00000000..f6a95422 --- /dev/null +++ b/service/test/support/integration/app_test_client.py @@ -0,0 +1,156 @@ +# +# Copyright (c) 2014 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see <http://www.gnu.org/licenses/>. +import json +import shutil + +from klein.test_resource import requestMock, _render +from leap.mail.imap.account import SoledadBackedAccount +from leap.soledad.client import Soledad +from mock import MagicMock, Mock +import os +from pixelated.adapter.draft_service import DraftService +from pixelated.adapter.mail_service import MailService +from pixelated.adapter.mailboxes import Mailboxes +from pixelated.adapter.soledad_querier import SoledadQuerier +from pixelated.adapter.tag_service import TagService +from pixelated.config import app_factory +from pixelated.controllers import FeaturesController, HomeController, MailsController, TagsController, \ + SyncInfoController, AttachmentsController +import pixelated.runserver +from pixelated.adapter.mail import PixelatedMail +from pixelated.adapter.search import SearchEngine +from test.support.integration.model import MailBuilder + + +class AppTestClient: + def __init__(self, soledad_test_folder='soledad-test'): + + self.soledad = initialize_soledad(tempdir=soledad_test_folder) + self.mail_address = "test@pixelated.org" + + # setup app + PixelatedMail.from_email_address = self.mail_address + + SearchEngine.INDEX_FOLDER = soledad_test_folder + '/search_index' + + self.app = pixelated.runserver.app + + self.soledad_querier = SoledadQuerier(self.soledad) + self.soledad_querier.get_index_masterkey = lambda: '_yg2oG_5ELM8_-sQYcsxI37WesI0dOtZQXpwAqjvhR4=' + + self.account = SoledadBackedAccount('test', self.soledad, MagicMock()) + self.mailboxes = Mailboxes(self.account, self.soledad_querier) + self.mail_sender = Mock() + self.tag_service = TagService() + self.draft_service = DraftService(self.mailboxes) + self.mail_service = MailService(self.mailboxes, self.mail_sender, self.tag_service, + self.soledad_querier) + self.search_engine = SearchEngine(self.soledad_querier) + self.search_engine.index_mails(self.mail_service.all_mails()) + + features_controller = FeaturesController() + features_controller.DISABLED_FEATURES.append('autoReload') + home_controller = HomeController() + mails_controller = MailsController(mail_service=self.mail_service, + draft_service=self.draft_service, + search_engine=self.search_engine) + tags_controller = TagsController(search_engine=self.search_engine) + sync_info_controller = SyncInfoController() + attachments_controller = AttachmentsController(self.soledad_querier) + + app_factory._setup_routes(self.app, home_controller, mails_controller, tags_controller, + features_controller, sync_info_controller, attachments_controller) + + def _render(self, request, as_json=True): + def get_request_written_data(_=None): + written_data = request.getWrittenData() + if written_data: + return json.loads(written_data) if as_json else written_data + + d = _render(self.app.resource(), request) + if request.finished: + return get_request_written_data(), request + else: + d.addCallback(get_request_written_data) + return d, request + + def get(self, path, get_args, as_json=True): + request = requestMock(path) + request.args = get_args + return self._render(request, as_json) + + def post(self, path, body=''): + request = requestMock(path=path, method="POST", body=body, headers={'Content-Type': ['application/json']}) + return self._render(request) + + def put(self, path, body): + request = requestMock(path=path, method="PUT", body=body, headers={'Content-Type': ['application/json']}) + return self._render(request) + + def delete(self, path): + request = requestMock(path=path, method="DELETE") + return self._render(request) + + def add_document_to_soledad(self, _dict): + self.soledad_querier.soledad.create_doc(_dict) + + def add_mail_to_inbox(self, input_mail): + mail = self.mailboxes.inbox().add(input_mail) + mail.update_tags(input_mail.tags) + self.search_engine.index_mail(mail) + + def add_multiple_to_mailbox(self, num, mailbox='', flags=[], tags=[]): + mails = [] + for _ in range(num): + input_mail = MailBuilder().with_status(flags).with_tags(tags).build_input_mail() + mail = self.mailboxes._create_or_get(mailbox).add(input_mail) + mails.append(mail) + mail.update_tags(input_mail.tags) + self.search_engine.index_mail(mail) + return mails + + +def initialize_soledad(tempdir): + if os.path.isdir(tempdir): + shutil.rmtree(tempdir) + + uuid = "foobar-uuid" + passphrase = u"verysecretpassphrase" + secret_path = os.path.join(tempdir, "secret.gpg") + local_db_path = os.path.join(tempdir, "soledad.u1db") + server_url = "http://provider" + cert_file = "" + + class MockSharedDB(object): + get_doc = Mock(return_value=None) + put_doc = Mock() + lock = Mock(return_value=('atoken', 300)) + unlock = Mock(return_value=True) + close = Mock() + + def __call__(self): + return self + + Soledad._shared_db = MockSharedDB() + + _soledad = Soledad( + uuid, + passphrase, + secret_path, + local_db_path, + server_url, + cert_file) + return _soledad diff --git a/service/test/support/integration/model.py b/service/test/support/integration/model.py new file mode 100644 index 00000000..17c29c2b --- /dev/null +++ b/service/test/support/integration/model.py @@ -0,0 +1,88 @@ +# +# Copyright (c) 2014 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see <http://www.gnu.org/licenses/>. +import json +from pixelated.adapter.mail import InputMail +from pixelated.adapter.status import Status + + +class MailBuilder: + def __init__(self): + self.mail = { + 'header': { + 'to': ['recipient@to.com'], + 'cc': ['recipient@cc.com'], + 'bcc': ['recipient@bcc.com'], + 'subject': 'Hi! This the subject' + }, + 'body': "Hello,\nThis is the body of this message\n\nRegards,\n\n--\nPixelated.\n", + 'status': [] + } + + def with_body(self, body): + self.mail['body'] = body + return self + + def with_tags(self, tags): + self.mail['tags'] = tags + return self + + def with_subject(self, subject): + self.mail['header']['subject'] = subject + return self + + def with_status(self, flags): + for status in Status.from_flags(flags): + self.mail['status'].append(status) + return self + + def with_date(self, date_string): + self.mail['header']['date'] = date_string + return self + + def with_ident(self, ident): + self.mail['ident'] = ident + return self + + def build_json(self): + return json.dumps(self.mail) + + def build_input_mail(self): + return InputMail.from_dict(self.mail) + + +class ResponseMail: + def __init__(self, mail_dict): + self.mail_dict = mail_dict + + @property + def subject(self): + return self.headers['subject'] + + @property + def headers(self): + return self.mail_dict['header'] + + @property + def ident(self): + return self.mail_dict['ident'] + + @property + def tags(self): + return self.mail_dict['tags'] + + @property + def status(self): + return self.mail_dict['status'] diff --git a/service/test/support/integration/soledad_test_base.py b/service/test/support/integration/soledad_test_base.py new file mode 100644 index 00000000..2221679f --- /dev/null +++ b/service/test/support/integration/soledad_test_base.py @@ -0,0 +1,83 @@ +# +# Copyright (c) 2014 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see <http://www.gnu.org/licenses/>. +import unittest + +from pixelated.controllers import * +from test.support.integration.app_test_client import AppTestClient +from test.support.integration.model import ResponseMail + + +class SoledadTestBase(unittest.TestCase): + # these are so long because our CI is so slow at the moment. + DEFERRED_TIMEOUT = 120 + DEFERRED_TIMEOUT_LONG = 300 + + @classmethod + def setUpClass(cls): + from nose.twistedtools import threaded_reactor + + threaded_reactor() + + def setUp(self): + self.client = AppTestClient() + + def get_mails_by_tag(self, tag, page=1, window=100): + res, req = self.client.get("/mails", { + 'q': ['tag:%s' % tag], + 'w': [str(window)], + 'p': [str(page)] + }) + return [ResponseMail(m) for m in res['mails']] + + def post_mail(self, data): + res, req = self.client.post('/mails', data) + return ResponseMail(res) + + def get_attachment(self, ident, encoding): + res, req = self.client.get("/attachment/%s" % ident, {'encoding': [encoding]}, as_json=False) + return res + + def put_mail(self, data): + res, req = self.client.put('/mails', data) + return res['ident'] + + def post_tags(self, mail_ident, tags_json): + res, req = self.client.post("/mail/%s/tags" % mail_ident, tags_json) + return res + + def get_tags(self, **kwargs): + res, req = self.client.get('/tags', kwargs) + return res + + def delete_mail(self, mail_ident): + res, req = self.client.delete("/mail/%s" % mail_ident) + return req + + def mark_as_read(self, mail_ident): + res, req = self.client.post("/mail/%s/read" % mail_ident) + return req + + def mark_as_unread(self, mail_ident): + res, req = self.client.post("/mail/%s/unread" % mail_ident) + return req + + def mark_many_as_unread(self, idents): + res, req = self.client.post('/mails/unread', json.dumps({'idents': idents})) + return req + + def mark_many_as_read(self, idents): + res, req = self.client.post('/mails/read', json.dumps({'idents': idents})) + return req diff --git a/service/test/support/integration_helper.py b/service/test/support/integration_helper.py deleted file mode 100644 index 0411abea..00000000 --- a/service/test/support/integration_helper.py +++ /dev/null @@ -1,278 +0,0 @@ -# -# Copyright (c) 2014 ThoughtWorks, Inc. -# -# Pixelated is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Pixelated is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with Pixelated. If not, see <http://www.gnu.org/licenses/>. -import shutil -from klein.resource import KleinResource - -from leap.soledad.client import Soledad -from mockito import mock -import os -from mock import Mock, MagicMock -from pixelated.adapter.mail_service import MailService -from pixelated.adapter.search import SearchEngine -from pixelated.adapter.status import Status -from pixelated.adapter.tag_service import TagService -from pixelated.adapter.draft_service import DraftService -from pixelated.adapter.mail import PixelatedMail, InputMail -import pixelated.runserver -from pixelated.adapter.mailboxes import Mailboxes -from pixelated.adapter.soledad_querier import SoledadQuerier -from pixelated.controllers import * -import pixelated.config.app_factory as app_factory -from leap.mail.imap.account import SoledadBackedAccount -from klein.test_resource import requestMock, _render - - -soledad_test_folder = "soledad-test" - - -def initialize_soledad(tempdir): - if os.path.isdir(soledad_test_folder): - shutil.rmtree(soledad_test_folder) - - uuid = "foobar-uuid" - passphrase = u"verysecretpassphrase" - secret_path = os.path.join(tempdir, "secret.gpg") - local_db_path = os.path.join(tempdir, "soledad.u1db") - server_url = "http://provider" - cert_file = "" - - class MockSharedDB(object): - get_doc = Mock(return_value=None) - put_doc = Mock() - lock = Mock(return_value=('atoken', 300)) - unlock = Mock(return_value=True) - close = Mock() - - def __call__(self): - return self - - Soledad._shared_db = MockSharedDB() - - _soledad = Soledad( - uuid, - passphrase, - secret_path, - local_db_path, - server_url, - cert_file) - # - # from leap.mail.imap.fields import fields - # - # for name, expression in fields.INDEXES.items(): - # _soledad.create_index(name, *expression) - # - return _soledad - - -class MailBuilder: - def __init__(self): - self.mail = { - 'header': { - 'to': ['recipient@to.com'], - 'cc': ['recipient@cc.com'], - 'bcc': ['recipient@bcc.com'], - 'subject': 'Hi! This the subject' - }, - 'body': "Hello,\nThis is the body of this message\n\nRegards,\n\n--\nPixelated.\n", - 'status': [] - } - - def with_body(self, body): - self.mail['body'] = body - return self - - def with_tags(self, tags): - self.mail['tags'] = tags - return self - - def with_subject(self, subject): - self.mail['header']['subject'] = subject - return self - - def with_status(self, flags): - for status in Status.from_flags(flags): - self.mail['status'].append(status) - return self - - def with_date(self, date_string): - self.mail['header']['date'] = date_string - return self - - def with_ident(self, ident): - self.mail['ident'] = ident - return self - - def build_json(self): - return json.dumps(self.mail) - - def build_input_mail(self): - return InputMail.from_dict(self.mail) - - -class SoledadTestBase: - def __init__(self): - pass - - def teardown_soledad(self): - pass - - def setup_soledad(self): - self.soledad = initialize_soledad(tempdir=soledad_test_folder) - self.mail_address = "test@pixelated.org" - - # setup app - PixelatedMail.from_email_address = self.mail_address - - SearchEngine.INDEX_FOLDER = soledad_test_folder + '/search_index' - - self.app = pixelated.runserver.app - - self.soledad_querier = SoledadQuerier(self.soledad) - - self.account = SoledadBackedAccount('test', self.soledad, MagicMock()) - self.mailboxes = Mailboxes(self.account, self.soledad_querier) - self.mail_sender = mock() - self.tag_service = TagService() - self.draft_service = DraftService(self.mailboxes) - self.mail_service = MailService(self.mailboxes, self.mail_sender, self.tag_service, - self.soledad_querier) - self.search_engine = SearchEngine(self.soledad_querier) - self.search_engine.index_mails(self.mail_service.all_mails()) - - features_controller = FeaturesController() - features_controller.DISABLED_FEATURES.append('autoReload') - home_controller = HomeController() - mails_controller = MailsController(mail_service=self.mail_service, - draft_service=self.draft_service, - search_engine=self.search_engine) - tags_controller = TagsController(search_engine=self.search_engine) - sync_info_controller = SyncInfoController() - attachments_controller = AttachmentsController(self.soledad_querier) - - app_factory._setup_routes(self.app, home_controller, mails_controller, tags_controller, - features_controller, sync_info_controller, attachments_controller) - self.resource = KleinResource(self.app) - - def get_mails_by_tag(self, tag, page=1, window=100): - request = requestMock(path="/mails") - request.args = { - 'q': ['tag:%s' % tag], - 'w': [str(window)], - 'p': [str(page)] - } - _render(self.resource, request) - response = json.loads(request.getWrittenData()) - return [ResponseMail(m) for m in response['mails']] - - def post_mail(self, data): - request = requestMock(path='/mails', method="POST", body=data, headers={'Content-Type': ['application/json']}) - _render(self.resource, request) - response = json.loads(request.getWrittenData()) - return ResponseMail(response) - - def get_attachment(self, ident, encoding): - request = requestMock(path='/attachment/' + ident) - request.args = { - 'encoding': [encoding] - } - _render(self.resource, request) - return request.getWrittenData() - - def put_mail(self, data): - request = requestMock('/mails', method="PUT", body=data, headers={'Content-Type': ['application/json']}) - _render(self.resource, request) - response = json.loads(request.getWrittenData()) - return response['ident'] - - def post_tags(self, mail_ident, tags_json): - request = requestMock('/mail/' + mail_ident + '/tags', method="POST", body=tags_json, - headers={'Content-Type': ['application/json']}) - _render(self.resource, request) - return json.loads(request.getWrittenData()) - - def get_tags(self, **kwargs): - request = requestMock('/tags') - request.args = kwargs - _render(self.resource, request) - return json.loads(request.getWrittenData()) - - def delete_mail(self, mail_ident): - request = requestMock(path='/mail/' + mail_ident, method="DELETE") - _render(self.resource, request) - return request - - def mark_as_read(self, mail_ident): - request = requestMock('/mail/' + mail_ident + '/read', method="POST", headers={'Content-Type': ['application/json']}) - _render(self.resource, request) - return request - - def mark_as_unread(self, mail_ident): - request = requestMock('/mail/' + mail_ident + '/unread', method="POST", headers={'Content-Type': ['application/json']}) - _render(self.resource, request) - return request - - def mark_many_as_unread(self, idents): - request = requestMock('/mails/unread', method="POST", body=json.dumps({'idents': idents}), headers={'Content-Type': ['application/json']}) - _render(self.resource, request) - return request - - def mark_many_as_read(self, idents): - request = requestMock('/mails/read', method="POST", body=json.dumps({'idents': idents}), headers={'Content-Type': ['application/json']}) - _render(self.resource, request) - return request - - def add_document_to_soledad(self, _dict): - self.soledad_querier.soledad.create_doc(_dict) - - def add_mail_to_inbox(self, input_mail): - mail = self.mailboxes.inbox().add(input_mail) - mail.update_tags(input_mail.tags) - self.search_engine.index_mail(mail) - - def add_multiple_to_mailbox(self, num, mailbox='', flags=[], tags=[]): - mails = [] - for _ in range(num): - input_mail = MailBuilder().with_status(flags).with_tags(tags).build_input_mail() - mail = self.mailboxes._create_or_get(mailbox).add(input_mail) - mails.append(mail) - mail.update_tags(input_mail.tags) - self.search_engine.index_mail(mail) - return mails - - -class ResponseMail: - def __init__(self, mail_dict): - self.mail_dict = mail_dict - - @property - def subject(self): - return self.headers['subject'] - - @property - def headers(self): - return self.mail_dict['header'] - - @property - def ident(self): - return self.mail_dict['ident'] - - @property - def tags(self): - return self.mail_dict['tags'] - - @property - def status(self): - return self.mail_dict['status'] diff --git a/service/test/unit/bitmask_libraries/smtp_test.py b/service/test/unit/bitmask_libraries/smtp_test.py index 2bb3dcab..ca8a89e4 100644 --- a/service/test/unit/bitmask_libraries/smtp_test.py +++ b/service/test/unit/bitmask_libraries/smtp_test.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. import os +import sys from mock import MagicMock, patch from abstract_leap_test import AbstractLeapTest from pixelated.bitmask_libraries.smtp import LeapSmtp diff --git a/web-ui/app/js/mail_list/ui/mail_items/mail_item.js b/web-ui/app/js/mail_list/ui/mail_items/mail_item.js index 51ace714..24c7b043 100644 --- a/web-ui/app/js/mail_list/ui/mail_items/mail_item.js +++ b/web-ui/app/js/mail_list/ui/mail_items/mail_item.js @@ -64,6 +64,7 @@ define( this.attr.ident = mail.ident; this.attr.statuses = viewHelper.formatStatusClasses(mail.status); this.attr.tags = mail.tags; + this.attr.attachments = mail.attachments; this.attr.mailbox = mail.mailbox; this.attr.header.formattedDate = this.formattedDate(mail.header.date); }; diff --git a/web-ui/app/js/mail_view/ui/recipients/recipients_input.js b/web-ui/app/js/mail_view/ui/recipients/recipients_input.js index e9211d85..8f647d01 100644 --- a/web-ui/app/js/mail_view/ui/recipients/recipients_input.js +++ b/web-ui/app/js/mail_view/ui/recipients/recipients_input.js @@ -34,7 +34,6 @@ define([ 9: 'tab', 186: 'semicolon', 188: 'comma', - 32: 'space', 13: 'enter', 27: 'esc' }, @@ -101,7 +100,7 @@ define([ return; } - if (isEnterAddressKey(keyPressed)) { + if (!event.shiftKey && isEnterAddressKey(keyPressed)) { this.tokenizeRecipient(event); if ((keyPressed !== 9 /* tab */)) { diff --git a/web-ui/app/scss/styles.scss b/web-ui/app/scss/styles.scss index ef3691f0..7fe0ec05 100644 --- a/web-ui/app/scss/styles.scss +++ b/web-ui/app/scss/styles.scss @@ -133,6 +133,11 @@ right: 10px; font-size: 0.7em; } + + .attachment-indicator { + margin: 2px 0 0 25px; + font-size: initial; + } .from { white-space: nowrap; font-size: 0.8em; diff --git a/web-ui/app/templates/mails/full_view.hbs b/web-ui/app/templates/mails/full_view.hbs index 4cdf25b9..d05537f3 100644 --- a/web-ui/app/templates/mails/full_view.hbs +++ b/web-ui/app/templates/mails/full_view.hbs @@ -69,7 +69,7 @@ {{#if attachments}} <div class="attachmentsAreaWrap"> <div class="attachmentsArea column large-12"> - <p><strong> {{ attachments.length }} attachment(s):</strong></p> + <p><strong><i class="fa fa-paperclip"></i> {{ attachments.length }} attachment(s):</strong></p> <ul> {{#each attachments }} <li> diff --git a/web-ui/app/templates/mails/single.hbs b/web-ui/app/templates/mails/single.hbs index baf6251c..997ab44f 100644 --- a/web-ui/app/templates/mails/single.hbs +++ b/web-ui/app/templates/mails/single.hbs @@ -3,8 +3,13 @@ </span> <span> <a href="/#/{{ tag }}/mail/{{ ident }}"> - <span class="received-date">{{ header.formattedDate }}</span> - + <span class="received-date">{{ header.formattedDate }} + {{#if attachments}} + <div class="attachment-indicator"> + <i class="fa fa-paperclip"></i> + </div> + {{/if}} + </span> <div class="from">{{#if header.from }}{{ header.from }}{{else}}{{t "you"}}{{/if}}</div> <div class="subject-and-tags"> {{ header.subject }} diff --git a/web-ui/test/spec/mail_list/ui/mail_list.spec.js b/web-ui/test/spec/mail_list/ui/mail_list.spec.js index d90f906c..23ca87fb 100644 --- a/web-ui/test/spec/mail_list/ui/mail_list.spec.js +++ b/web-ui/test/spec/mail_list/ui/mail_list.spec.js @@ -225,7 +225,7 @@ describeComponent('mail_list/ui/mail_list', function () { expect(node.html()).toMatch('id="mail-' + mail.ident + '"'); expect(node.html()).toMatch('<div class="subject-and-tags">'); expect(node.html()).toMatch('<div class="from">' + mail.header.from + '</div>'); - expect(node.html()).toMatch('<span class="received-date">' + mail.header.formattedDate + '</span>'); + expect(node.html()).toMatch('<span class="received-date">' + mail.header.formattedDate); } function matchSelectedMail(mail, node) { diff --git a/web-ui/test/spec/mail_view/ui/recipients/recipients_input.spec.js b/web-ui/test/spec/mail_view/ui/recipients/recipients_input.spec.js index 6b486fa9..433c145b 100644 --- a/web-ui/test/spec/mail_view/ui/recipients/recipients_input.spec.js +++ b/web-ui/test/spec/mail_view/ui/recipients/recipients_input.spec.js @@ -10,7 +10,6 @@ describeComponent('mail_view/ui/recipients/recipients_input',function () { _.each([ [186, 'semicolon'], [188, 'comma'], - [32, 'space'] ], function (keycode) { @@ -34,6 +33,16 @@ describeComponent('mail_view/ui/recipients/recipients_input',function () { expect(addressEnteredEvent).not.toHaveBeenTriggeredOnAndWith(this, { name: 'to', address: '' }); }); + it('wont add address if shift key is pressed together: ' + keycode[1], function () { + var addressEnteredEvent = spyOnEvent(this.$node, Pixelated.events.ui.recipients.entered); + + var enterAddressKeyPressEvent = $.Event('keydown', { which: keycode[0], shiftKey: true }); + this.$node.val('a@b.c'); + this.$node.trigger(enterAddressKeyPressEvent); + + expect(addressEnteredEvent).not.toHaveBeenTriggeredOnAndWith(this, { name: 'to', address: 'a@b.c' }); + }); + it('prevents event default regardless on input val when key is ' + keycode[1], function () { var enterAddressKeyPressEvent = $.Event('keydown', { which: keycode[0] }); spyOn(enterAddressKeyPressEvent, 'preventDefault'); @@ -77,6 +86,21 @@ describeComponent('mail_view/ui/recipients/recipients_input',function () { expect(addressEnteredEvent).not.toHaveBeenTriggeredOnAndWith(this, { name: 'to', address: ''}); }); }); + + describe('when space is pressed', function () { + it('address input should not finish', function () { + var addressEnteredEvent = spyOnEvent(this.$node, Pixelated.events.ui.recipients.entered); + + var spaceKeyPressEvent = $.Event('keydown', { which: 32}); + spyOn(spaceKeyPressEvent, 'preventDefault'); + + this.$node.val('a@b.c'); + this.$node.trigger(spaceKeyPressEvent); + + expect(spaceKeyPressEvent.preventDefault).not.toHaveBeenCalled(); + expect(addressEnteredEvent).not.toHaveBeenTriggeredOnAndWith(this, { name: 'to', address: 'a@b.c' }); + }); + }); }); describe('on keyup', function () { |