diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Vagrantfile | 112 | ||||
-rw-r--r-- | debian/control | 2 | ||||
-rwxr-xr-x | service/go | 4 | ||||
-rw-r--r-- | service/pixelated/config/app_factory.py | 28 | ||||
-rw-r--r-- | service/pixelated/config/routes.py | 24 | ||||
-rw-r--r-- | service/pixelated/controllers/home_controller.py | 42 | ||||
-rw-r--r-- | service/pixelated/resources/__init__.py (renamed from service/pixelated/controllers/__init__.py) | 9 | ||||
-rw-r--r-- | service/pixelated/resources/attachments_resource.py (renamed from service/pixelated/controllers/attachments_controller.py) | 32 | ||||
-rw-r--r-- | service/pixelated/resources/contacts_resource.py (renamed from service/pixelated/controllers/contacts_controller.py) | 13 | ||||
-rw-r--r-- | service/pixelated/resources/features_resource.py (renamed from service/pixelated/controllers/features_controller.py) | 10 | ||||
-rw-r--r-- | service/pixelated/resources/mail_resource.py | 64 | ||||
-rw-r--r-- | service/pixelated/resources/mails_resource.py (renamed from service/pixelated/controllers/mails_controller.py) | 136 | ||||
-rw-r--r-- | service/pixelated/resources/root_resource.py | 46 | ||||
-rw-r--r-- | service/pixelated/resources/sync_info_resource.py (renamed from service/pixelated/controllers/sync_info_controller.py) | 11 | ||||
-rw-r--r-- | service/pixelated/resources/tags_resource.py (renamed from service/pixelated/controllers/tags_controller.py) | 13 | ||||
-rw-r--r-- | service/pixelated/runserver.py | 13 | ||||
-rw-r--r-- | service/setup.py | 17 | ||||
-rw-r--r-- | service/test/functional/features/environment.py | 4 | ||||
-rw-r--r-- | service/test/integration/test_contacts.py (renamed from service/test/integration/contacts_test.py) | 0 | ||||
-rw-r--r-- | service/test/integration/test_delete_mail.py (renamed from service/test/integration/delete_mail_test.py) | 0 | ||||
-rw-r--r-- | service/test/integration/test_drafts.py (renamed from service/test/integration/drafts_test.py) | 0 | ||||
-rw-r--r-- | service/test/integration/test_mark_as_read_unread.py (renamed from service/test/integration/mark_as_read_unread_test.py) | 0 | ||||
-rw-r--r-- | service/test/integration/test_retrieve_attachment.py (renamed from service/test/integration/retrieve_attachment_test.py) | 8 | ||||
-rw-r--r-- | service/test/integration/test_search.py (renamed from service/test/integration/search_test.py) | 0 | ||||
-rw-r--r-- | service/test/integration/test_soledad_querier.py (renamed from service/test/integration/soledad_querier_test.py) | 0 | ||||
-rw-r--r-- | service/test/integration/test_tags.py (renamed from service/test/integration/tags_test.py) | 0 | ||||
-rw-r--r-- | service/test/support/integration/app_test_client.py | 67 | ||||
-rw-r--r-- | service/test/support/integration/soledad_test_base.py | 2 | ||||
-rw-r--r-- | service/test/support/test_helper.py | 24 | ||||
-rw-r--r-- | service/test/unit/adapter/test_draft_service.py (renamed from service/test/unit/adapter/draft_service_test.py) | 0 | ||||
-rw-r--r-- | service/test/unit/adapter/test_mail.py (renamed from service/test/unit/adapter/mail_test.py) | 0 | ||||
-rw-r--r-- | service/test/unit/adapter/test_mail_service.py (renamed from service/test/unit/adapter/mail_service_test.py) | 0 | ||||
-rw-r--r-- | service/test/unit/adapter/test_mailbox.py (renamed from service/test/unit/adapter/mailbox_test.py) | 0 | ||||
-rw-r--r-- | service/test/unit/adapter/test_mailbox_indexer_listener.py (renamed from service/test/unit/adapter/mailbox_indexer_listener_test.py) | 0 | ||||
-rw-r--r-- | service/test/unit/adapter/test_soledad_querier.py (renamed from service/test/unit/adapter/soledad_querier_test.py) | 0 | ||||
-rw-r--r-- | service/test/unit/bitmask_libraries/test_abstract_leap.py (renamed from service/test/unit/bitmask_libraries/abstract_leap_test.py) | 0 | ||||
-rw-r--r-- | service/test/unit/bitmask_libraries/test_certs.py (renamed from service/test/unit/bitmask_libraries/certs_test.py) | 0 | ||||
-rw-r--r-- | service/test/unit/bitmask_libraries/test_leap_srp.py (renamed from service/test/unit/bitmask_libraries/leap_srp_test.py) | 0 | ||||
-rw-r--r-- | service/test/unit/bitmask_libraries/test_nicknym.py (renamed from service/test/unit/bitmask_libraries/nicknym_test.py) | 2 | ||||
-rw-r--r-- | service/test/unit/bitmask_libraries/test_provider.py (renamed from service/test/unit/bitmask_libraries/provider_test.py) | 2 | ||||
-rw-r--r-- | service/test/unit/bitmask_libraries/test_session.py (renamed from service/test/unit/bitmask_libraries/session_test.py) | 10 | ||||
-rw-r--r-- | service/test/unit/bitmask_libraries/test_smtp.py (renamed from service/test/unit/bitmask_libraries/smtp_test.py) | 2 | ||||
-rw-r--r-- | service/test/unit/bitmask_libraries/test_soledad.py (renamed from service/test/unit/bitmask_libraries/soledad_test.py) | 2 | ||||
-rw-r--r-- | service/test/unit/config/test_app_factory.py (renamed from service/test/unit/config/app_factory_test.py) | 0 | ||||
-rw-r--r-- | service/test/unit/controllers/mails_controller_test.py | 126 | ||||
-rw-r--r-- | service/test/unit/resources/__init__.py (renamed from service/test/unit/controllers/__init__.py) | 0 | ||||
-rw-r--r-- | service/test/unit/resources/test_sync_info_controller.py (renamed from service/test/unit/controllers/sync_info_controller_test.py) | 12 | ||||
-rw-r--r-- | service/test/unit/support/test_encrypted_file_storage.py (renamed from service/test/unit/support/encrypted_file_storage_test.py) | 0 | ||||
-rw-r--r-- | service/test/unit/test_runserver.py (renamed from service/test/unit/runserver_test.py) | 0 | ||||
-rw-r--r-- | service/test_requirements.txt | 1 | ||||
-rw-r--r-- | web-ui/app/index.html | 32 | ||||
-rw-r--r-- | web-ui/app/js/main.js | 2 | ||||
-rw-r--r-- | web-ui/app/js/views/i18n.js | 2 | ||||
-rw-r--r-- | web-ui/app/scss/news-cycle.scss | 4 | ||||
-rw-r--r-- | web-ui/app/scss/opensans.scss | 20 |
56 files changed, 494 insertions, 405 deletions
@@ -30,3 +30,4 @@ __pycache__/ .virtualenv # custom config file that can be used with the useragent pixelated.cfg +service/_trial_temp/ diff --git a/Vagrantfile b/Vagrantfile index a8fcc62d..100bcb2a 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,9 +1,13 @@ # -*- mode: ruby -*- # vi: set ft=ruby : +# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! VAGRANTFILE_API_VERSION = "2" Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| + # All Vagrant configuration is done here. The most common configuration + # options are documented and commented below. For a complete reference, + # please see the online documentation at vagrantup.com. # we need a debian testing vagrantbox because # - currently the useragent debian packages depend on python-cryptography which is only @@ -56,5 +60,113 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.provider "virtualbox" do |v| v.memory = 1024 end +>>>>>>> master + # Disable automatic box update checking. If you disable this, then + # boxes will only be checked for updates when the user runs + # `vagrant box outdated`. This is not recommended. + # config.vm.box_check_update = false + + # Create a forwarded port mapping which allows access to a specific port + # within the machine from a port on the host machine. In the example below, + # accessing "localhost:8080" will access port 80 on the guest machine. + # config.vm.network "forwarded_port", guest: 80, host: 8080 + + # Create a private network, which allows host-only access to the machine + # using a specific IP. + # config.vm.network "private_network", ip: "192.168.33.10" + + # Create a public network, which generally matched to bridged network. + # Bridged networks make the machine appear as another physical device on + # your network. + # config.vm.network "public_network" + + # If true, then any SSH connections made will enable agent forwarding. + # Default value: false + # config.ssh.forward_agent = true + + # Share an additional folder to the guest VM. The first argument is + # the path on the host to the actual folder. The second argument is + # the path on the guest to mount the folder. And the optional third + # argument is a set of non-required options. + # config.vm.synced_folder "../data", "/vagrant_data" + + # Provider-specific configuration so you can fine-tune various + # backing providers for Vagrant. These expose provider-specific options. + # Example for VirtualBox: + # + # config.vm.provider "virtualbox" do |vb| + # # Don't boot with headless mode + # vb.gui = true + # + # # Use VBoxManage to customize the VM. For example to change memory: + # vb.customize ["modifyvm", :id, "--memory", "1024"] + # end + # + # View the documentation for the provider you're using for more + # information on available options. + + # Enable provisioning with CFEngine. CFEngine Community packages are + # automatically installed. For example, configure the host as a + # policy server and optionally a policy file to run: + # + # config.vm.provision "cfengine" do |cf| + # cf.am_policy_hub = true + # # cf.run_file = "motd.cf" + # end + # + # You can also configure and bootstrap a client to an existing + # policy server: + # + # config.vm.provision "cfengine" do |cf| + # cf.policy_server_address = "10.0.2.15" + # end + + # Enable provisioning with Puppet stand alone. Puppet manifests + # are contained in a directory path relative to this Vagrantfile. + # You will need to create the manifests directory and a manifest in + # the file default.pp in the manifests_path directory. + # + # config.vm.provision "puppet" do |puppet| + # puppet.manifests_path = "manifests" + # puppet.manifest_file = "default.pp" + # end + + # Enable provisioning with chef solo, specifying a cookbooks path, roles + # path, and data_bags path (all relative to this Vagrantfile), and adding + # some recipes and/or roles. + # + # config.vm.provision "chef_solo" do |chef| + # chef.cookbooks_path = "../my-recipes/cookbooks" + # chef.roles_path = "../my-recipes/roles" + # chef.data_bags_path = "../my-recipes/data_bags" + # chef.add_recipe "mysql" + # chef.add_role "web" + # + # # You may also specify custom JSON attributes: + # chef.json = { mysql_password: "foo" } + # end + + # Enable provisioning with chef server, specifying the chef server URL, + # and the path to the validation key (relative to this Vagrantfile). + # + # The Opscode Platform uses HTTPS. Substitute your organization for + # ORGNAME in the URL and validation key. + # + # If you have your own Chef Server, use the appropriate URL, which may be + # HTTP instead of HTTPS depending on your configuration. Also change the + # validation key to validation.pem. + # + # config.vm.provision "chef_client" do |chef| + # chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME" + # chef.validation_key_path = "ORGNAME-validator.pem" + # end + # + # If you're using the Opscode platform, your validator client is + # ORGNAME-validator, replacing ORGNAME with your organization name. + # + # If you have your own Chef Server, the default validation client name is + # chef-validator, unless you changed the configuration. + # + # chef.validation_client_name = "ORGNAME-validator" end diff --git a/debian/control b/debian/control index 3ae2cec0..2c985f14 100644 --- a/debian/control +++ b/debian/control @@ -11,7 +11,7 @@ X-Python-Version: >= 2.7 Package: pixelated-user-agent Architecture: all -Depends: python (>= 2.7), python (<< 2.8), leap-keymanager, soledad-common, soledad-client, leap-mail, python-srp, python-dirspec, python-u1db, python-whoosh, python-sqlcipher, python-klein, python-flask +Depends: python (>= 2.7), python (<< 2.8), leap-keymanager, soledad-common, soledad-client, leap-mail, python-srp, python-dirspec, python-u1db, python-whoosh, python-sqlcipher Description: API to serve the pixelated front-end requests Pixelated User Agent Service ============================ @@ -12,11 +12,11 @@ function setupjs { } function runIntegrationTests { - nosetests "$*" test/integration + trial --reporter=text $* test.integration } function runUnitTests { - nosetests "$*" test/unit + trial --reporter=text $* test.unit } function runPep8 { diff --git a/service/pixelated/config/app_factory.py b/service/pixelated/config/app_factory.py index 745db937..86816b87 100644 --- a/service/pixelated/config/app_factory.py +++ b/service/pixelated/config/app_factory.py @@ -19,9 +19,9 @@ from OpenSSL import SSL from OpenSSL import crypto from twisted.internet import reactor from twisted.internet import ssl +from pixelated.resources.root_resource import RootResource from twisted.web import resource from twisted.web.util import redirectTo -from pixelated.config.routes import setup_routes from pixelated.adapter.services.mail_service import MailService from pixelated.adapter.model.mail import InputMail from pixelated.adapter.services.mail_sender import MailSender @@ -33,7 +33,6 @@ from pixelated.adapter.listeners.mailbox_indexer_listener import MailboxIndexerL import pixelated.bitmask_libraries.session as LeapSession from pixelated.bitmask_libraries.leap_srp import LeapAuthException from requests.exceptions import ConnectionError -from pixelated.controllers import * from pixelated.adapter.services.tag_service import TagService from leap.common.events import ( register, @@ -100,20 +99,8 @@ def init_app(app, leap_home): MailboxIndexerListener.SEARCH_ENGINE = search_engine InputMail.FROM_EMAIL_ADDRESS = leap_session.account_email() - home_controller = HomeController() - features_controller = FeaturesController() - mails_controller = MailsController(mail_service=mail_service, - draft_service=draft_service, - search_engine=search_engine) - tags_controller = TagsController(search_engine=search_engine) - contacts_controller = ContactsController(search_engine=search_engine) - sync_info_controller = SyncInfoController() - attachments_controller = AttachmentsController(soledad_querier) - - register(signal=proto.SOLEDAD_SYNC_RECEIVE_STATUS, - callback=update_info_sync_and_index_partial(sync_info_controller=sync_info_controller, - search_engine=search_engine, - mail_service=mail_service)) + app.resource.initialize(soledad_querier, search_engine, mail_service, draft_service) + register(signal=proto.SOLEDAD_DONE_DATA_SYNC, callback=init_index_and_remove_dupes(querier=soledad_querier, search_engine=search_engine, @@ -122,21 +109,20 @@ def init_app(app, leap_home): register(signal=proto.SOLEDAD_DONE_DATA_SYNC, uid=CREATE_KEYS_IF_KEYS_DONT_EXISTS_CALLBACK, callback=look_for_user_key_and_create_if_cant_find(leap_session)) - setup_routes(app, home_controller, mails_controller, tags_controller, features_controller, - sync_info_controller, attachments_controller, contacts_controller) - def create_app(app, args): + app.resource = RootResource() if args.sslkey and args.sslcert: listen_with_ssl(app, args) else: listen_without_ssl(app, args) + reactor.suggestThreadPoolSize(20) reactor.callWhenRunning(lambda: init_app(app, args.home)) reactor.run() def listen_without_ssl(app, args): - reactor.listenTCP(args.port, Site(app.resource()), interface=args.host) + reactor.listenTCP(args.port, Site(app.resource), interface=args.host) def _ssl_options(args): @@ -152,7 +138,7 @@ def _ssl_options(args): def listen_with_ssl(app, args): - reactor.listenSSL(args.port, Site(app.resource()), _ssl_options(args), interface=args.host) + reactor.listenSSL(args.port, Site(app.resource), _ssl_options(args), interface=args.host) return reactor diff --git a/service/pixelated/config/routes.py b/service/pixelated/config/routes.py deleted file mode 100644 index 5efbbb28..00000000 --- a/service/pixelated/config/routes.py +++ /dev/null @@ -1,24 +0,0 @@ -def setup_routes(app, home_controller, mails_controller, tags_controller, features_controller, sync_info_controller, - attachments_controller, contacts_controller): - # mails - app.route('/mails', methods=['GET'])(mails_controller.mails) - app.route('/mails/unread', methods=['POST'])(mails_controller.mark_many_mail_unread) - app.route('/mails/read', methods=['POST'])(mails_controller.mark_many_mail_read) - app.route('/mail/<mail_id>', methods=['GET'])(mails_controller.mail) - app.route('/mail/<mail_id>', methods=['DELETE'])(mails_controller.delete_mail) - app.route('/mails/delete', methods=['POST'])(mails_controller.delete_mails) - app.route('/mails', methods=['POST'])(mails_controller.send_mail) - app.route('/mail/<mail_id>/tags', methods=['POST'])(mails_controller.mail_tags) - app.route('/mails', methods=['PUT'])(mails_controller.update_draft) - # tags - app.route('/tags', methods=['GET'])(tags_controller.tags) - # contacts - app.route('/contacts', methods=['GET'])(contacts_controller.contacts) - # features - app.route('/features', methods=['GET'])(features_controller.features) - # sync info - app.route('/sync_info', methods=['GET'])(sync_info_controller.sync_info) - # attachments - app.route('/attachment/<attachment_id>', methods=['GET'])(attachments_controller.attachment) - # static - app.route('/', methods=['GET'], branch=True)(home_controller.home) diff --git a/service/pixelated/controllers/home_controller.py b/service/pixelated/controllers/home_controller.py deleted file mode 100644 index ccdad197..00000000 --- a/service/pixelated/controllers/home_controller.py +++ /dev/null @@ -1,42 +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 os - -from twisted.web.static import File - - -class HomeController: - def __init__(self): - self.static_folder = self._get_static_folder() - pass - - def _get_static_folder(self): - - static_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "web-ui", "app")) - # this is a workaround for packaging - if not os.path.exists(static_folder): - static_folder = os.path.abspath( - os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "app")) - if not os.path.exists(static_folder): - static_folder = os.path.join('/', 'usr', 'share', 'pixelated-user-agent') - return static_folder - - def home(self, request): - request_type = request.requestHeaders.getRawHeaders('accept')[0].split(',')[0] - response_type = request_type if request_type else "text/html" - - request.setHeader('Content-Type', response_type) - return File('%s/' % self.static_folder, defaultType=response_type) diff --git a/service/pixelated/controllers/__init__.py b/service/pixelated/resources/__init__.py index 6bc8e7c2..92a4462f 100644 --- a/service/pixelated/controllers/__init__.py +++ b/service/pixelated/resources/__init__.py @@ -20,6 +20,7 @@ def respond_json(entity, request, status_code=200): request.responseHeaders.addRawHeader(b"content-type", b"application/json") request.code = status_code return json_response + # request.finish() def respond_json_deferred(entity, request, status_code=200): @@ -31,11 +32,3 @@ def respond_json_deferred(entity, request, status_code=200): import json - -from home_controller import HomeController -from mails_controller import MailsController -from tags_controller import TagsController -from features_controller import FeaturesController -from sync_info_controller import SyncInfoController -from attachments_controller import AttachmentsController -from contacts_controller import ContactsController diff --git a/service/pixelated/controllers/attachments_controller.py b/service/pixelated/resources/attachments_resource.py index b3fed903..e0ba1bd1 100644 --- a/service/pixelated/controllers/attachments_controller.py +++ b/service/pixelated/resources/attachments_resource.py @@ -19,31 +19,47 @@ import io import re from twisted.protocols.basic import FileSender from twisted.python.log import err +from twisted.web import server +from twisted.web.resource import Resource -class AttachmentsController: +class AttachmentResource(Resource): - def __init__(self, querier): + isLeaf = True + + def __init__(self, attachment_id, querier): + Resource.__init__(self) + self.attachment_id = attachment_id self.querier = querier - def attachment(self, request, attachment_id): + def render_GET(self, request): encoding = request.args.get('encoding', [None])[0] - filename = request.args.get('filename', [attachment_id])[0] - attachment = self.querier.attachment(attachment_id, encoding) + filename = request.args.get('filename', [self.attachment_id])[0] + attachment = self.querier.attachment(self.attachment_id, encoding) request.setHeader(b'Content-Type', b'application/force-download') request.setHeader(b'Content-Disposition', bytes('attachment; filename=' + filename)) bytes_io = io.BytesIO(attachment['content']) d = FileSender().beginFileTransfer(bytes_io, request) - def cbFinished(ignored): + def cb_finished(_): bytes_io.close() request.finish() - d.addErrback(err).addCallback(cbFinished) + d.addErrback(err).addCallback(cb_finished) - return d + return server.NOT_DONE_YET def _extract_mimetype(self, content_type): match = re.compile('([A-Za-z-]+\/[A-Za-z-]+)').search(content_type) return match.group(1) + + +class AttachmentsResource(Resource): + + def __init__(self, querier): + Resource.__init__(self) + self.querier = querier + + def getChild(self, attachment_id, request): + return AttachmentResource(attachment_id, self.querier) diff --git a/service/pixelated/controllers/contacts_controller.py b/service/pixelated/resources/contacts_resource.py index 5825b563..5ec39761 100644 --- a/service/pixelated/controllers/contacts_controller.py +++ b/service/pixelated/resources/contacts_resource.py @@ -14,18 +14,23 @@ # 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 pixelated.controllers import respond_json_deferred +from pixelated.resources import respond_json_deferred from twisted.internet.threads import deferToThread +from twisted.web import server +from twisted.web.resource import Resource -class ContactsController: +class ContactsResource(Resource): + + isLeaf = True def __init__(self, search_engine): + Resource.__init__(self) self._search_engine = search_engine - def contacts(self, request): + def render_GET(self, request): query = request.args.get('q', [''])[0] d = deferToThread(lambda: self._search_engine.contacts(query)) d.addCallback(lambda tags: respond_json_deferred(tags, request)) - return d + return server.NOT_DONE_YET diff --git a/service/pixelated/controllers/features_controller.py b/service/pixelated/resources/features_resource.py index b91aa183..1784e463 100644 --- a/service/pixelated/controllers/features_controller.py +++ b/service/pixelated/resources/features_resource.py @@ -14,17 +14,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/>. -from pixelated.controllers import respond_json +from pixelated.resources import respond_json import os +from twisted.web.resource import Resource -class FeaturesController: +class FeaturesResource(Resource): DISABLED_FEATURES = ['draftReply', 'encryptionStatus'] - def __init__(self): - pass + isLeaf = True - def features(self, request): + def render_GET(self, request): try: disabled_features = {'logout': os.environ['DISPATCHER_LOGOUT_URL']} except KeyError: diff --git a/service/pixelated/resources/mail_resource.py b/service/pixelated/resources/mail_resource.py new file mode 100644 index 00000000..03873ffb --- /dev/null +++ b/service/pixelated/resources/mail_resource.py @@ -0,0 +1,64 @@ +import json +from pixelated.resources import respond_json +from twisted.web.resource import Resource + + +class MailTags(Resource): + + isLeaf = True + + def __init__(self, mail_id, mail_service, search_engine): + Resource.__init__(self) + self._search_engine = search_engine + self._mail_service = mail_service + self._mail_id = mail_id + + def render_POST(self, request): + content_dict = json.loads(request.content.read()) + new_tags = map(lambda tag: tag.lower(), content_dict['newtags']) + try: + self._mail_service.update_tags(self._mail_id, new_tags) + mail = self._mail_service.mail(self._mail_id) + self._search_engine.index_mail(mail) + except ValueError as ve: + return respond_json(ve.message, request, 403) + return respond_json(mail.as_dict(), request) + + +class Mail(Resource): + + def __init__(self, mail_id, mail_service, search_engine): + Resource.__init__(self) + self.putChild('tags', MailTags(mail_id, mail_service, search_engine)) + + self._search_engine = search_engine + self._mail_id = mail_id + self._mail_service = mail_service + + def render_GET(self, request): + mail = self._mail_service.mail(self._mail_id) + return respond_json(mail.as_dict(), request) + + def render_DELETE(self, request): + self._delete_mail(self._mail_id) + return respond_json(None, request) + + def _delete_mail(self, mail_id): + mail = self._mail_service.mail(mail_id) + if mail.mailbox_name == 'TRASH': + self._mail_service.delete_permanent(mail_id) + self._search_engine.remove_from_index(mail_id) + else: + trashed_mail = self._mail_service.delete_mail(mail_id) + self._search_engine.index_mail(trashed_mail) + + +class MailResource(Resource): + + def __init__(self, mail_service, search_engine): + Resource.__init__(self) + self._mail_service = mail_service + self._search_engine = search_engine + + def getChild(self, mail_id, request): + return Mail(mail_id, self._mail_service, self._search_engine) diff --git a/service/pixelated/controllers/mails_controller.py b/service/pixelated/resources/mails_resource.py index fb9c9adc..a6eb0fe0 100644 --- a/service/pixelated/controllers/mails_controller.py +++ b/service/pixelated/resources/mails_resource.py @@ -1,64 +1,64 @@ -# -# 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.model.mail import InputMail -from pixelated.controllers import respond_json +from pixelated.resources import respond_json +from twisted.web.resource import Resource -class MailsController: - - def __init__(self, mail_service, draft_service, search_engine): - self._mail_service = mail_service - self._draft_service = draft_service - self._search_engine = search_engine +def _format_exception(e): + exception_info = map(str, list(e.args)) + return '\n'.join(exception_info) - def mails(self, request): - mail_ids, total = self._search_engine.search(request.args.get('q')[0], request.args.get('w')[0], request.args.get('p')[0]) - mails = self._mail_service.mails(mail_ids) - response = { - "stats": { - "total": total, - }, - "mails": [mail.as_dict() for mail in mails] - } +class MailsUnreadResource(Resource): - return json.dumps(response) + isLeaf = True - def mail(self, request, mail_id): - mail = self._mail_service.mail(mail_id) - return respond_json(mail.as_dict(), request) + def __init__(self, mail_service, search_engine): + Resource.__init__(self) + self._search_engine = search_engine + self._mail_service = mail_service - def mark_many_mail_unread(self, request): + def render_POST(self, request): content_dict = json.load(request.content) idents = content_dict.get('idents') for ident in idents: mail = self._mail_service.mark_as_unread(ident) self._search_engine.index_mail(mail) - return "" + return respond_json(None, request) + + +class MailsReadResource(Resource): + + isLeaf = True + + def __init__(self, mail_service, search_engine): + Resource.__init__(self) + self._search_engine = search_engine + self._mail_service = mail_service - def mark_many_mail_read(self, request): + def render_POST(self, request): content_dict = json.load(request.content) idents = content_dict.get('idents') for ident in idents: mail = self._mail_service.mark_as_read(ident) self._search_engine.index_mail(mail) - return "" + return respond_json(None, request) + + +class MailsDeleteResource(Resource): + + isLeaf = True + + def __init__(self, mail_service, search_engine): + Resource.__init__(self) + self._mail_service = mail_service + self._search_engine = search_engine + + def render_POST(self, request): + idents = json.loads(request.content.read())['idents'] + for ident in idents: + self._delete_mail(ident) + return respond_json(None, request) def _delete_mail(self, mail_id): mail = self._mail_service.mail(mail_id) @@ -69,17 +69,33 @@ class MailsController: trashed_mail = self._mail_service.delete_mail(mail_id) self._search_engine.index_mail(trashed_mail) - def delete_mail(self, request, mail_id): - self._delete_mail(mail_id) - return respond_json(None, request) - def delete_mails(self, request): - idents = json.loads(request.content.read())['idents'] - for ident in idents: - self._delete_mail(ident) - return respond_json(None, request) +class MailsResource(Resource): + + def __init__(self, search_engine, mail_service, draft_service): + Resource.__init__(self) + self.putChild('delete', MailsDeleteResource(mail_service, search_engine)) + self.putChild('read', MailsReadResource(mail_service, search_engine)) + self.putChild('unread', MailsUnreadResource(mail_service, search_engine)) + + self._draft_service = draft_service + self._mail_service = mail_service + self._search_engine = search_engine - def send_mail(self, request): + def render_GET(self, request): + mail_ids, total = self._search_engine.search(request.args.get('q')[0], request.args.get('w')[0], request.args.get('p')[0]) + mails = self._mail_service.mails(mail_ids) + + response = { + "stats": { + "total": total, + }, + "mails": [mail.as_dict() for mail in mails] + } + + return respond_json(response, request) + + def render_POST(self, request): try: content_dict = json.loads(request.content.read()) _mail = InputMail.from_dict(content_dict) @@ -91,20 +107,9 @@ class MailsController: return respond_json(_mail.as_dict(), request) except Exception as error: - return respond_json({'message': self._format_exception(error)}, request, status_code=422) + return respond_json({'message': _format_exception(error)}, request, status_code=422) - def mail_tags(self, request, mail_id): - content_dict = json.loads(request.content.read()) - new_tags = map(lambda tag: tag.lower(), content_dict['newtags']) - try: - self._mail_service.update_tags(mail_id, new_tags) - mail = self._mail_service.mail(mail_id) - self._search_engine.index_mail(mail) - except ValueError as ve: - return respond_json(ve.message, request, 403) - return respond_json(mail.as_dict(), request) - - def update_draft(self, request): + def render_PUT(self, request): content_dict = json.loads(request.content.read()) _mail = InputMail.from_dict(content_dict) draft_id = content_dict.get('ident') @@ -117,8 +122,5 @@ class MailsController: else: pixelated_mail = self._draft_service.create_draft(_mail) self._search_engine.index_mail(pixelated_mail) - return respond_json({'ident': pixelated_mail.ident}, request) - def _format_exception(self, exception): - exception_info = map(str, list(exception.args)) - return '\n'.join(exception_info) + return respond_json({'ident': pixelated_mail.ident}, request) diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py new file mode 100644 index 00000000..7768472c --- /dev/null +++ b/service/pixelated/resources/root_resource.py @@ -0,0 +1,46 @@ +import os +from pixelated.resources.attachments_resource import AttachmentsResource +from pixelated.resources.contacts_resource import ContactsResource +from pixelated.resources.features_resource import FeaturesResource +from pixelated.resources.mail_resource import MailResource +from pixelated.resources.mails_resource import MailsResource +from pixelated.resources.sync_info_resource import SyncInfoResource +from pixelated.resources.tags_resource import TagsResource +from twisted.web.resource import Resource +from twisted.web.static import File + + +class RootResource(Resource): + + def __init__(self): + Resource.__init__(self) + self._static_folder = self._get_static_folder() + + def getChild(self, path, request): + if path == '': + return self + return Resource.getChild(self, path, request) + + def initialize(self, querier, search_engine, mail_service, draft_service): + self.putChild('assets', File(self._static_folder)) + self.putChild('attachment', AttachmentsResource(querier)) + self.putChild('contacts', ContactsResource(search_engine)) + self.putChild('features', FeaturesResource()) + self.putChild('sync_info', SyncInfoResource()) + self.putChild('tags', TagsResource(search_engine)) + self.putChild('mails', MailsResource(search_engine, mail_service, draft_service)) + self.putChild('mail', MailResource(mail_service, search_engine)) + + def _get_static_folder(self): + + static_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "web-ui", "app")) + # this is a workaround for packaging + if not os.path.exists(static_folder): + static_folder = os.path.abspath( + os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "app")) + if not os.path.exists(static_folder): + static_folder = os.path.join('/', 'usr', 'share', 'pixelated-user-agent') + return static_folder + + def render_GET(self, request): + return open(os.path.join(self._static_folder, 'index.html')).read() diff --git a/service/pixelated/controllers/sync_info_controller.py b/service/pixelated/resources/sync_info_resource.py index 50e53852..5aa94218 100644 --- a/service/pixelated/controllers/sync_info_controller.py +++ b/service/pixelated/resources/sync_info_resource.py @@ -13,11 +13,16 @@ # # 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 pixelated.controllers import respond_json +from pixelated.resources import respond_json +from twisted.web.resource import Resource -class SyncInfoController: +class SyncInfoResource(Resource): + + isLeaf = True + def __init__(self): + Resource.__init__(self) self.current = 0 self.total = 0 @@ -29,7 +34,7 @@ class SyncInfoController: def set_sync_info(self, soledad_sync_status): self.current, self.total = map(int, soledad_sync_status.content.split('/')) - def sync_info(self, request): + def render_GET(self, request): _sync_info = { 'is_syncing': self.current != self.total, 'count': { diff --git a/service/pixelated/controllers/tags_controller.py b/service/pixelated/resources/tags_resource.py index b6741dcc..8a8ab81f 100644 --- a/service/pixelated/controllers/tags_controller.py +++ b/service/pixelated/resources/tags_resource.py @@ -14,20 +14,25 @@ # 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 pixelated.controllers import respond_json_deferred +from pixelated.resources import respond_json_deferred from twisted.internet.threads import deferToThread +from twisted.web.resource import Resource +from twisted.web.server import NOT_DONE_YET -class TagsController: +class TagsResource(Resource): + + isLeaf = True def __init__(self, search_engine): + Resource.__init__(self) self._search_engine = search_engine - def tags(self, request): + def render_GET(self, request): query = request.args.get('q', [''])[0] 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 + return NOT_DONE_YET diff --git a/service/pixelated/runserver.py b/service/pixelated/runserver.py index 37a55582..b6762177 100644 --- a/service/pixelated/runserver.py +++ b/service/pixelated/runserver.py @@ -19,10 +19,6 @@ import logging import json import os -from klein import Klein - - -klein_app = Klein() import ConfigParser from twisted.python import log @@ -36,7 +32,14 @@ import pixelated.support.ext_protobuf # monkey patch for protobuf in OSX import pixelated.support.ext_sqlcipher # monkey patch for sqlcipher in debian -app = Klein() +class App: + + def __init__(self): + self.resource = None + self.config = None + pass + +app = App() app.config = {} diff --git a/service/setup.py b/service/setup.py index 12f3fe9d..8ef66618 100644 --- a/service/setup.py +++ b/service/setup.py @@ -82,23 +82,22 @@ setup(name='pixelated-user-agent', 'pixelated.config', 'pixelated.certificates', 'pixelated.support', - 'pixelated.controllers' + 'pixelated.resources' ], test_suite='nose.collector', install_requires=[ - 'pyasn1==0.1.7', - 'gnupg==1.4.0', - 'Twisted==14.0.2', - 'klein==0.2.3', - 'requests==2.5.0', - 'srp==1.0.5', - 'dirspec==13.10', + 'pyasn1==0.1.3', + 'gnupg==1.2.5', + 'Twisted==12.0.0', + 'requests==2.0.0', + 'srp==1.0.4', + 'dirspec==4.2.0', 'u1db==13.09', 'leap.keymanager==0.3.8', 'leap.soledad.common==0.6.3', 'leap.soledad.client==0.6.3', 'leap.mail==0.3.9-1-gc1f9c92', - 'whoosh==2.6.0' + 'whoosh==2.5.7' ], entry_points={ 'console_scripts': [ diff --git a/service/test/functional/features/environment.py b/service/test/functional/features/environment.py index 72140e40..5e93c840 100644 --- a/service/test/functional/features/environment.py +++ b/service/test/functional/features/environment.py @@ -19,14 +19,14 @@ from test.support.dispatcher.proxy import Proxy from test.support.integration import AppTestClient from selenium import webdriver -from pixelated.controllers.features_controller import FeaturesController +from pixelated.resources.features_resource import FeaturesResource def before_all(context): logging.disable('INFO') client = AppTestClient() proxy = Proxy(proxy_port='8889', app_port='4567') - FeaturesController.DISABLED_FEATURES.append('autoRefresh') + FeaturesResource.DISABLED_FEATURES.append('autoRefresh') context.client = client context.call_to_terminate_proxy = proxy.run_on_a_thread() context.call_to_terminate = client.run_on_a_thread(logfile='/tmp/behave-tests.log') diff --git a/service/test/integration/contacts_test.py b/service/test/integration/test_contacts.py index 925e5e02..925e5e02 100644 --- a/service/test/integration/contacts_test.py +++ b/service/test/integration/test_contacts.py diff --git a/service/test/integration/delete_mail_test.py b/service/test/integration/test_delete_mail.py index 5a3a97fb..5a3a97fb 100644 --- a/service/test/integration/delete_mail_test.py +++ b/service/test/integration/test_delete_mail.py diff --git a/service/test/integration/drafts_test.py b/service/test/integration/test_drafts.py index d4fde099..d4fde099 100644 --- a/service/test/integration/drafts_test.py +++ b/service/test/integration/test_drafts.py diff --git a/service/test/integration/mark_as_read_unread_test.py b/service/test/integration/test_mark_as_read_unread.py index 86a48e62..86a48e62 100644 --- a/service/test/integration/mark_as_read_unread_test.py +++ b/service/test/integration/test_mark_as_read_unread.py diff --git a/service/test/integration/retrieve_attachment_test.py b/service/test/integration/test_retrieve_attachment.py index 5f754a27..d6ad9298 100644 --- a/service/test/integration/retrieve_attachment_test.py +++ b/service/test/integration/test_retrieve_attachment.py @@ -36,6 +36,10 @@ class RetrieveAttachmentTest(SoledadTestBase): self.client.add_document_to_soledad(attachment_dict) - attachment = self.get_attachment(ident, 'base64') + d = self.get_attachment(ident, 'base64') - self.assertEquals('pequeno anexo :D\n', attachment) + def _assert(attachment): + self.assertEquals('pequeno anexo :D\n', attachment) + d.addCallback(_assert) + + return d diff --git a/service/test/integration/search_test.py b/service/test/integration/test_search.py index 464830d2..464830d2 100644 --- a/service/test/integration/search_test.py +++ b/service/test/integration/test_search.py diff --git a/service/test/integration/soledad_querier_test.py b/service/test/integration/test_soledad_querier.py index f8767630..f8767630 100644 --- a/service/test/integration/soledad_querier_test.py +++ b/service/test/integration/test_soledad_querier.py diff --git a/service/test/integration/tags_test.py b/service/test/integration/test_tags.py index 6072392c..6072392c 100644 --- a/service/test/integration/tags_test.py +++ b/service/test/integration/test_tags.py diff --git a/service/test/support/integration/app_test_client.py b/service/test/support/integration/app_test_client.py index eab001c6..860b9c40 100644 --- a/service/test/support/integration/app_test_client.py +++ b/service/test/support/integration/app_test_client.py @@ -18,8 +18,6 @@ import multiprocessing import shutil import time -from pixelated.config.routes import setup_routes -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 @@ -29,16 +27,23 @@ from pixelated.adapter.services.mail_service import MailService from pixelated.adapter.services.mailboxes import Mailboxes from pixelated.adapter.soledad.soledad_querier import SoledadQuerier from pixelated.adapter.services.tag_service import TagService -from pixelated.controllers import FeaturesController, HomeController, MailsController, TagsController, \ - SyncInfoController, AttachmentsController, ContactsController +from pixelated.resources.root_resource import RootResource import pixelated.runserver from pixelated.adapter.model.mail import PixelatedMail from pixelated.adapter.search import SearchEngine from test.support.integration.model import MailBuilder +from test.support.test_helper import request_mock +from twisted.internet import reactor +from twisted.internet.defer import Deferred +from twisted.web.resource import getChildForRequest +from twisted.web.server import Site class AppTestClient: - def __init__(self, soledad_test_folder='soledad-test/test'): + INDEX_KEY = '\xde3?\x87\xff\xd9\xd3\x14\xf0\xa7>\x1f%C{\x16.\\\xae\x8c\x13\xa7\xfb\x04\xd4]+\x8d_\xed\xd1\x8d\x0bI' \ + '\x8a\x0e\xa4tm\xab\xbf\xb4\xa5\x99\x00d\xd5w\x9f\x18\xbc\x1d\xd4_W\xd2\xb6\xe8H\x83\x1b\xd8\x9d\xad' + + def __init__(self, soledad_test_folder='/tmp/soledad-test/test'): self.soledad = initialize_soledad(tempdir=soledad_test_folder) self.mail_address = "test@pixelated.org" @@ -51,7 +56,7 @@ class AppTestClient: self.app = pixelated.runserver.app self.soledad_querier = SoledadQuerier(self.soledad) - self.soledad_querier.get_index_masterkey = lambda: '\xde3?\x87\xff\xd9\xd3\x14\xf0\xa7>\x1f%C{\x16.\\\xae\x8c\x13\xa7\xfb\x04\xd4]+\x8d_\xed\xd1\x8d\x0bI\x8a\x0e\xa4tm\xab\xbf\xb4\xa5\x99\x00d\xd5w\x9f\x18\xbc\x1d\xd4_W\xd2\xb6\xe8H\x83\x1b\xd8\x9d\xad' + self.soledad_querier.get_index_masterkey = lambda: self.INDEX_KEY self.account = SoledadBackedAccount('test', self.soledad, MagicMock()) self.mailboxes = Mailboxes(self.account, self.soledad_querier) @@ -63,55 +68,59 @@ class AppTestClient: 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) - contacts_controller = ContactsController(search_engine=self.search_engine) - sync_info_controller = SyncInfoController() - attachments_controller = AttachmentsController(self.soledad_querier) + self.app.resource = RootResource() - setup_routes(self.app, home_controller, mails_controller, tags_controller, - features_controller, sync_info_controller, attachments_controller, contacts_controller) + self.app.resource.initialize(self.soledad_querier, self.search_engine, self.mail_service, self.draft_service) def _render(self, request, as_json=True): + def get_str(_str): + return json.loads(_str) if as_json else _str + def get_request_written_data(_=None): written_data = request.getWrittenData() if written_data: - return json.loads(written_data) if as_json else written_data + return get_str(written_data) + + resource = getChildForRequest(self.app.resource, request) + result = resource.render(request) + + if isinstance(result, basestring): + return get_str(result), request - d = _render(self.app.resource(), request) if request.finished: - return get_request_written_data(), request + d = Deferred() + d.addCallback(get_request_written_data) + return d, request else: + d = request.notifyFinish() + d.addCallback(lambda _: request) d.addCallback(get_request_written_data) return d, request - def run_on_a_thread(self, logfile='/tmp/app_test_client.log', port=4567, host='localhost'): - worker = lambda: self.app.run(host=host, port=port, logFile=open(logfile, 'w')) - process = multiprocessing.Process(target=worker) + def run_on_a_thread(self, logfile='/tmp/app_test_client.log', port=4567, host='0.0.0.0'): + def _start(): + reactor.listenTCP(port, Site(self.app.resource), interface=host) + reactor.run() + process = multiprocessing.Process(target=_start) process.start() - time.sleep(1) # just let it start + time.sleep(1) return lambda: process.terminate() def get(self, path, get_args, as_json=True): - request = requestMock(path) + request = request_mock(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']}) + request = request_mock(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']}) + request = request_mock(path=path, method="PUT", body=body, headers={'Content-Type': ['application/json']}) return self._render(request) def delete(self, path, body=""): - request = requestMock(path=path, body=body, headers={'Content-Type': ['application/json']}, method="DELETE") + request = request_mock(path=path, body=body, headers={'Content-Type': ['application/json']}, method="DELETE") return self._render(request) def add_document_to_soledad(self, _dict): diff --git a/service/test/support/integration/soledad_test_base.py b/service/test/support/integration/soledad_test_base.py index 4149462c..5892de60 100644 --- a/service/test/support/integration/soledad_test_base.py +++ b/service/test/support/integration/soledad_test_base.py @@ -15,7 +15,7 @@ # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. import unittest -from pixelated.controllers import * +from pixelated.resources import * from test.support.integration.app_test_client import AppTestClient from test.support.integration.model import ResponseMail diff --git a/service/test/support/test_helper.py b/service/test/support/test_helper.py index ff1de64a..54685008 100644 --- a/service/test/support/test_helper.py +++ b/service/test/support/test_helper.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/>. from datetime import datetime +import io from pixelated.adapter.model.mail import InputMail @@ -79,3 +80,26 @@ class TestRequest: def __init__(self, json): self.json = json + + +from twisted.web.test.test_web import DummyRequest + + +class PixRequestMock(DummyRequest): + def __init__(self, path): + DummyRequest.__init__(self, path) + self.content = None + self.code = None + + def getWrittenData(self): + if len(self.written): + return self.written[0] + + +def request_mock(path='', method='GET', body='', headers={}): + dummy = PixRequestMock(path.split('/')) + for name, value in headers.iteritems(): + dummy.setHeader(name, value) + dummy.method = method + dummy.content = io.BytesIO(body) + return dummy diff --git a/service/test/unit/adapter/draft_service_test.py b/service/test/unit/adapter/test_draft_service.py index baa07ce0..baa07ce0 100644 --- a/service/test/unit/adapter/draft_service_test.py +++ b/service/test/unit/adapter/test_draft_service.py diff --git a/service/test/unit/adapter/mail_test.py b/service/test/unit/adapter/test_mail.py index be7b731d..be7b731d 100644 --- a/service/test/unit/adapter/mail_test.py +++ b/service/test/unit/adapter/test_mail.py diff --git a/service/test/unit/adapter/mail_service_test.py b/service/test/unit/adapter/test_mail_service.py index fca6e79b..fca6e79b 100644 --- a/service/test/unit/adapter/mail_service_test.py +++ b/service/test/unit/adapter/test_mail_service.py diff --git a/service/test/unit/adapter/mailbox_test.py b/service/test/unit/adapter/test_mailbox.py index 9725f418..9725f418 100644 --- a/service/test/unit/adapter/mailbox_test.py +++ b/service/test/unit/adapter/test_mailbox.py diff --git a/service/test/unit/adapter/mailbox_indexer_listener_test.py b/service/test/unit/adapter/test_mailbox_indexer_listener.py index 65ba8966..65ba8966 100644 --- a/service/test/unit/adapter/mailbox_indexer_listener_test.py +++ b/service/test/unit/adapter/test_mailbox_indexer_listener.py diff --git a/service/test/unit/adapter/soledad_querier_test.py b/service/test/unit/adapter/test_soledad_querier.py index 2cc23750..2cc23750 100644 --- a/service/test/unit/adapter/soledad_querier_test.py +++ b/service/test/unit/adapter/test_soledad_querier.py diff --git a/service/test/unit/bitmask_libraries/abstract_leap_test.py b/service/test/unit/bitmask_libraries/test_abstract_leap.py index 2634f330..2634f330 100644 --- a/service/test/unit/bitmask_libraries/abstract_leap_test.py +++ b/service/test/unit/bitmask_libraries/test_abstract_leap.py diff --git a/service/test/unit/bitmask_libraries/certs_test.py b/service/test/unit/bitmask_libraries/test_certs.py index 8caafe7e..8caafe7e 100644 --- a/service/test/unit/bitmask_libraries/certs_test.py +++ b/service/test/unit/bitmask_libraries/test_certs.py diff --git a/service/test/unit/bitmask_libraries/leap_srp_test.py b/service/test/unit/bitmask_libraries/test_leap_srp.py index 6d067e5d..6d067e5d 100644 --- a/service/test/unit/bitmask_libraries/leap_srp_test.py +++ b/service/test/unit/bitmask_libraries/test_leap_srp.py diff --git a/service/test/unit/bitmask_libraries/nicknym_test.py b/service/test/unit/bitmask_libraries/test_nicknym.py index 9d564abe..7dec4b2c 100644 --- a/service/test/unit/bitmask_libraries/nicknym_test.py +++ b/service/test/unit/bitmask_libraries/test_nicknym.py @@ -17,7 +17,7 @@ from mock import patch from leap.keymanager import openpgp, KeyNotFound from pixelated.bitmask_libraries.nicknym import NickNym -from abstract_leap_test import AbstractLeapTest +from test_abstract_leap import AbstractLeapTest class NickNymTest(AbstractLeapTest): diff --git a/service/test/unit/bitmask_libraries/provider_test.py b/service/test/unit/bitmask_libraries/test_provider.py index dd57afa0..af8aa291 100644 --- a/service/test/unit/bitmask_libraries/provider_test.py +++ b/service/test/unit/bitmask_libraries/test_provider.py @@ -19,7 +19,7 @@ from httmock import all_requests, HTTMock, urlmatch from requests import HTTPError from pixelated.bitmask_libraries.config import LeapConfig from pixelated.bitmask_libraries.provider import LeapProvider -from abstract_leap_test import AbstractLeapTest +from test_abstract_leap import AbstractLeapTest @all_requests diff --git a/service/test/unit/bitmask_libraries/session_test.py b/service/test/unit/bitmask_libraries/test_session.py index 32d92f25..67722557 100644 --- a/service/test/unit/bitmask_libraries/session_test.py +++ b/service/test/unit/bitmask_libraries/test_session.py @@ -14,12 +14,20 @@ # 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 mock import patch +from mock import MagicMock from pixelated.bitmask_libraries.session import LeapSession -from abstract_leap_test import AbstractLeapTest +from test_abstract_leap import AbstractLeapTest class SessionTest(AbstractLeapTest): + + def setUp(self): + self.mail_fetcher_mock = MagicMock() + + def tearDown(self): + self.mail_fetcher_mock = MagicMock() + def test_background_jobs_are_started(self): self.config.start_background_jobs = True diff --git a/service/test/unit/bitmask_libraries/smtp_test.py b/service/test/unit/bitmask_libraries/test_smtp.py index b00a0af6..4087cbf5 100644 --- a/service/test/unit/bitmask_libraries/smtp_test.py +++ b/service/test/unit/bitmask_libraries/test_smtp.py @@ -17,7 +17,7 @@ import sys import os from mock import MagicMock, patch -from abstract_leap_test import AbstractLeapTest +from test_abstract_leap import AbstractLeapTest from pixelated.bitmask_libraries.smtp import LeapSmtp from httmock import all_requests, HTTMock, urlmatch diff --git a/service/test/unit/bitmask_libraries/soledad_test.py b/service/test/unit/bitmask_libraries/test_soledad.py index c8b45710..a71275e0 100644 --- a/service/test/unit/bitmask_libraries/soledad_test.py +++ b/service/test/unit/bitmask_libraries/test_soledad.py @@ -15,7 +15,7 @@ # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. from mock import patch from pixelated.bitmask_libraries.soledad import SoledadSession -from abstract_leap_test import AbstractLeapTest +from test_abstract_leap import AbstractLeapTest @patch('pixelated.bitmask_libraries.soledad.Soledad') diff --git a/service/test/unit/config/app_factory_test.py b/service/test/unit/config/test_app_factory.py index a42c7c83..a42c7c83 100644 --- a/service/test/unit/config/app_factory_test.py +++ b/service/test/unit/config/test_app_factory.py diff --git a/service/test/unit/controllers/mails_controller_test.py b/service/test/unit/controllers/mails_controller_test.py deleted file mode 100644 index 8108bc19..00000000 --- a/service/test/unit/controllers/mails_controller_test.py +++ /dev/null @@ -1,126 +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 json -import unittest -from io import BytesIO - -from klein.test_resource import requestMock -from mock import MagicMock -from mockito import * -from pixelated.controllers.mails_controller import MailsController - - -class TestMailsController(unittest.TestCase): - - def setUp(self): - self.mail_service = mock() - self.search_engine = mock() - self.dummy_request = MagicMock(spec=['code', 'responseHeaders']) - draft_service = mock() - - self.mails_controller = MailsController(mail_service=self.mail_service, - draft_service=draft_service, - search_engine=self.search_engine) - - self.input_mail = mock() - self.input_mail.json = {'header': {'from': 'a@a.a', 'to': 'b@b.b'}, - 'ident': 1, - 'tags': [], - 'status': [], - 'security_casing': {}, - 'body': 'email body'} - - def tearDown(self): - unstub() - - def test_sending_mail_return_sent_mail_data_when_send_succeeds(self): - self.mail_service.send = self._successfuly_send_mail - request = requestMock('', body=json.dumps(self.input_mail.json)) - - result = self.mails_controller.send_mail(request) - - self.assertEqual(request.code, 200) - self.assertEqual(result, - '{"status": [], "body": "email body", "ident": 1, "tags": [], "header": {"to": "b@b.b", "from": "a@a.a"}, "security_casing": {}}') - - def test_sending_mail_return_error_message_when_send_fails(self): - self.mail_service.send = self._send_that_throws_exception - - request = requestMock('', body=json.dumps(self.input_mail.json)) - result = self.mails_controller.send_mail(request) - - self.assertEqual(request.code, 422) - self.assertEqual(result, - '{"message": "email sending failed\\nmore information of error\\n123\\nthere was a code before this"}') - - def test_fetching_mail_gets_mail_from_mail_service(self): - mail = mock() - mail.as_dict = lambda: {'ident': 1, 'body': 'le mail body'} - when(self.mail_service).mail(1).thenReturn(mail) - - response = self.mails_controller.mail(self.dummy_request, 1) - - verify(self.mail_service).mail(1) - self.assertEqual(response, '{"body": "le mail body", "ident": 1}') - - def test_marking_mail_as_read_set_mail_as_read_on_the_service(self): - mail = mock() - when(self.mail_service).mark_as_read("1").thenReturn(mail) - when(self.search_engine).index_mail(mail).thenReturn(None) - self.dummy_request.content = BytesIO('{"idents":["1"]}') - - self.mails_controller.mark_many_mail_read(self.dummy_request) - - verify(self.mail_service).mark_as_read(u'1') - verify(self.search_engine).index_mail(mail) - - def test_marking_mail_as_unread_set_mail_as_unread_on_the_service(self): - mail = mock() - when(self.mail_service).mark_as_unread("1").thenReturn(mail) - when(self.search_engine).index_mail(mail).thenReturn(None) - self.dummy_request.content = BytesIO('{"idents":["1"]}') - - self.mails_controller.mark_many_mail_unread(self.dummy_request) - - verify(self.mail_service).mark_as_unread(u'1') - verify(self.search_engine).index_mail(mail) - - def test_move_message_to_trash(self): - mail = mock() - mail.mailbox_name = 'INBOX' - when(self.mail_service).mail(1).thenReturn(mail) - when(self.mail_service).delete_mail(1).thenReturn(mail) - - self.mails_controller.delete_mail(self.dummy_request, 1) - - verify(self.search_engine).index_mail(mail) - - def test_delete_permanently_when_mail_in_trash(self): - mail = mock() - mail.mailbox_name = 'TRASH' - when(self.mail_service).mail(1).thenReturn(mail) - self.mails_controller.delete_mail(self.dummy_request, 1) - - verify(self.mail_service).delete_permanent(1) - - def _successfuly_send_mail(self, ident, mail): - sent_mail = mock() - sent_mail.as_dict = lambda: self.input_mail.json - - return sent_mail - - def _send_that_throws_exception(self, ident, mail): - raise Exception('email sending failed', 'more information of error', 123, 'there was a code before this') diff --git a/service/test/unit/controllers/__init__.py b/service/test/unit/resources/__init__.py index e69de29b..e69de29b 100644 --- a/service/test/unit/controllers/__init__.py +++ b/service/test/unit/resources/__init__.py diff --git a/service/test/unit/controllers/sync_info_controller_test.py b/service/test/unit/resources/test_sync_info_controller.py index cd3aeb02..a91dd386 100644 --- a/service/test/unit/controllers/sync_info_controller_test.py +++ b/service/test/unit/resources/test_sync_info_controller.py @@ -16,16 +16,16 @@ import unittest import json -from mock import MagicMock -from pixelated.controllers import SyncInfoController +from test.support.test_helper import request_mock +from pixelated.resources.sync_info_resource import SyncInfoResource from mockito import * -class SyncInfoControllerTest(unittest.TestCase): +class SyncInfoResourceTest(unittest.TestCase): def setUp(self): - self.dummy_request = MagicMock() - self.controller = SyncInfoController() + self.dummy_request = request_mock() + self.controller = SyncInfoResource() def _set_count(self, current, total): soledad_sync_data = mock() @@ -33,7 +33,7 @@ class SyncInfoControllerTest(unittest.TestCase): self.controller.set_sync_info(soledad_sync_data) def get_sync_info(self): - return json.loads(self.controller.sync_info(self.dummy_request)) + return json.loads(self.controller.render_GET(self.dummy_request)) def test_is_not_syncing_if_total_is_equal_to_current(self): self._set_count(total=0, current=0) diff --git a/service/test/unit/support/encrypted_file_storage_test.py b/service/test/unit/support/test_encrypted_file_storage.py index 2a6735c3..2a6735c3 100644 --- a/service/test/unit/support/encrypted_file_storage_test.py +++ b/service/test/unit/support/test_encrypted_file_storage.py diff --git a/service/test/unit/runserver_test.py b/service/test/unit/test_runserver.py index 99b502f1..99b502f1 100644 --- a/service/test/unit/runserver_test.py +++ b/service/test/unit/test_runserver.py diff --git a/service/test_requirements.txt b/service/test_requirements.txt index 5fc2a997..98d86650 100644 --- a/service/test_requirements.txt +++ b/service/test_requirements.txt @@ -1,7 +1,6 @@ PyHamcrest==1.8.1 behave==1.2.4 selenium==2.44.0 -nose==1.3.4 mock==1.0.1 httmock==1.2.2 mockito==0.5.2 diff --git a/web-ui/app/index.html b/web-ui/app/index.html index e4e0f07d..29356e3c 100644 --- a/web-ui/app/index.html +++ b/web-ui/app/index.html @@ -3,16 +3,16 @@ <head> <link rel="icon" type="image/png" - href="/images/Favicon.png"> + href="assets/images/Favicon.png"> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <title>Pixelated Mail</title> <meta name="description" content=""> <meta name="viewport" content="width=device-width"> -<link href="bower_components/font-awesome/css/font-awesome.min.css" rel="stylesheet" type="text/css"> -<link href="css/opensans.css" rel="stylesheet" type="text/css"> -<link href="css/news-cycle.css" rel="stylesheet" type="text/css"/> -<link rel="stylesheet" href="/css/main.css"> +<link href="assets/bower_components/font-awesome/css/font-awesome.min.css" rel="stylesheet" type="text/css"> +<link href="assets/css/opensans.css" rel="stylesheet" type="text/css"> +<link href="assets/css/news-cycle.css" rel="stylesheet" type="text/css"/> +<link rel="stylesheet" href="assets/css/main.css"> </head> <body> @@ -26,7 +26,7 @@ <div class="inner-wrap"> <div id="menu" class="column collapsed-nav no-padding"> <a class="left-off-canvas-logo" href="#"> - <img id="pixelated-logo" src="images/pixelated-logo_symbol_orange.svg" alt="Pixelated"> + <img id="pixelated-logo" src="assets/images/pixelated-logo_symbol_orange.svg" alt="Pixelated"> </a> <a class="left-off-canvas-toggle" href="#"> <i class=" toggle fa fa-navicon"></i> @@ -71,16 +71,16 @@ </div> <!--usemin_start--> -<script src="/bower_components/modernizr/modernizr.js"></script> -<script src="/bower_components/lodash/dist/lodash.js"></script> -<script src="/bower_components/jquery/dist/jquery.js"></script> -<script src="/js/lib/highlightRegex.js"></script> -<script src="/bower_components/handlebars/handlebars.min.js"></script> -<script src="/bower_components/typeahead.js/dist/typeahead.bundle.min.js"></script> -<script src="/bower_components/foundation/js/foundation.js" ></script> -<script src="/bower_components/foundation/js/foundation/foundation.reveal.js" ></script> -<script src="/bower_components/foundation/js/foundation/foundation.offcanvas.js"></script> -<script src="/bower_components/requirejs/require.js" data-main="js/main.js"></script> +<script src="assets/bower_components/modernizr/modernizr.js"></script> +<script src="assets/bower_components/lodash/dist/lodash.js"></script> +<script src="assets/bower_components/jquery/dist/jquery.js"></script> +<script src="assets/js/lib/highlightRegex.js"></script> +<script src="assets/bower_components/handlebars/handlebars.min.js"></script> +<script src="assets/bower_components/typeahead.js/dist/typeahead.bundle.min.js"></script> +<script src="assets/bower_components/foundation/js/foundation.js" ></script> +<script src="assets/bower_components/foundation/js/foundation/foundation.reveal.js" ></script> +<script src="assets/bower_components/foundation/js/foundation/foundation.offcanvas.js"></script> +<script src="assets/bower_components/requirejs/require.js" data-main="assets/js/main.js"></script> <!--usemin_end--> diff --git a/web-ui/app/js/main.js b/web-ui/app/js/main.js index 6f3b3e8c..06eca8dc 100644 --- a/web-ui/app/js/main.js +++ b/web-ui/app/js/main.js @@ -17,7 +17,7 @@ 'use strict'; requirejs.config({ - baseUrl: '../', + baseUrl: '../assets/', paths: { 'mail_list': 'js/mail_list', 'page': 'js/page', diff --git a/web-ui/app/js/views/i18n.js b/web-ui/app/js/views/i18n.js index b09490f5..19327c27 100644 --- a/web-ui/app/js/views/i18n.js +++ b/web-ui/app/js/views/i18n.js @@ -24,7 +24,7 @@ define(['i18next'], function(i18n) { self.get = self; self.init = function(path) { - i18n.init({detectLngQS: 'lang', fallbackLng: 'en', lowerCaseLng: true, getAsync: false, resGetPath: path + 'locales/__lng__/__ns__.json'}); + i18n.init({detectLngQS: 'lang', fallbackLng: 'en', lowerCaseLng: true, getAsync: false, resGetPath: path + 'assets/locales/__lng__/__ns__.json'}); Handlebars.registerHelper('t', self.get.bind(self)); }; diff --git a/web-ui/app/scss/news-cycle.scss b/web-ui/app/scss/news-cycle.scss index 8f813996..ecca383c 100644 --- a/web-ui/app/scss/news-cycle.scss +++ b/web-ui/app/scss/news-cycle.scss @@ -1,13 +1,13 @@ @font-face { font-family: 'News Cycle'; - src: url('/fonts/NewsCycleRegular.ttf') format('truetype'); + src: url('assets/fonts/NewsCycleRegular.ttf') format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'News Cycle'; - src: url('/fonts/NewsCycleBold.ttf') format('truetype'); + src: url('assets/fonts/NewsCycleBold.ttf') format('truetype'); font-weight: bold; font-style: normal; } diff --git a/web-ui/app/scss/opensans.scss b/web-ui/app/scss/opensans.scss index 5d5c7ff5..84529c41 100644 --- a/web-ui/app/scss/opensans.scss +++ b/web-ui/app/scss/opensans.scss @@ -2,60 +2,60 @@ font-family: 'Open Sans'; font-style: normal; font-weight: 300; - src: local('Open Sans Light'), local('OpenSans-Light'), url('/fonts/OpenSans-Light.woff') format('woff'); + src: local('Open Sans Light'), local('OpenSans-Light'), url('/assets/fonts/OpenSans-Light.woff') format('woff'); } @font-face { font-family: 'Open Sans'; font-style: normal; font-weight: 400; - src: local('Open Sans'), local('OpenSans'), url('/fonts/OpenSans.woff') format('woff'); + src: local('Open Sans'), local('OpenSans'), url('/assets/fonts/OpenSans.woff') format('woff'); } @font-face { font-family: 'Open Sans'; font-style: normal; font-weight: 600; - src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url('/fonts/OpenSans-Semibold.woff') format('woff'); + src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url('/assets/fonts/OpenSans-Semibold.woff') format('woff'); } @font-face { font-family: 'Open Sans'; font-style: normal; font-weight: 700; - src: local('Open Sans Bold'), local('OpenSans-Bold'), url('/fonts/OpenSans-Bold.woff') format('woff'); + src: local('Open Sans Bold'), local('OpenSans-Bold'), url('/assets/fonts/OpenSans-Bold.woff') format('woff'); } @font-face { font-family: 'Open Sans'; font-style: normal; font-weight: 800; - src: local('Open Sans Extrabold'), local('OpenSans-Extrabold'), url('/fonts/OpenSans-Extrabold.woff') format('woff'); + src: local('Open Sans Extrabold'), local('OpenSans-Extrabold'), url('/assets/fonts/OpenSans-Extrabold.woff') format('woff'); } @font-face { font-family: 'Open Sans'; font-style: italic; font-weight: 300; - src: local('Open Sans Light Italic'), local('OpenSansLight-Italic'), url('/fonts/OpenSansLight-Italic.woff') format('woff'); + src: local('Open Sans Light Italic'), local('OpenSansLight-Italic'), url('/assets/fonts/OpenSansLight-Italic.woff') format('woff'); } @font-face { font-family: 'Open Sans'; font-style: italic; font-weight: 400; - src: local('Open Sans Italic'), local('OpenSans-Italic'), url('/fonts/OpenSans-Italic.woff') format('woff'); + src: local('Open Sans Italic'), local('OpenSans-Italic'), url('/assets/fonts/OpenSans-Italic.woff') format('woff'); } @font-face { font-family: 'Open Sans'; font-style: italic; font-weight: 600; - src: local('Open Sans Semibold Italic'), local('OpenSans-SemiboldItalic'), url('/fonts/OpenSans-SemiboldItalic.woff + src: local('Open Sans Semibold Italic'), local('OpenSans-SemiboldItalic'), url('/assets/fonts/OpenSans-SemiboldItalic.woff ') format('woff'); } @font-face { font-family: 'Open Sans'; font-style: italic; font-weight: 700; - src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url('/fonts/OpenSans-BoldItalic.woff') format('woff'); + src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url('/assets/fonts/OpenSans-BoldItalic.woff') format('woff'); } @font-face { font-family: 'Open Sans'; font-style: italic; font-weight: 800; - src: local('Open Sans Extrabold Italic'), local('OpenSans-ExtraboldItalic'), url('/fonts/OpenSans-ExtraboldItalic.woff') format('woff'); + src: local('Open Sans Extrabold Italic'), local('OpenSans-ExtraboldItalic'), url('/assets/fonts/OpenSans-ExtraboldItalic.woff') format('woff'); } |