summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Vagrantfile112
-rw-r--r--debian/control2
-rwxr-xr-xservice/go4
-rw-r--r--service/pixelated/config/app_factory.py28
-rw-r--r--service/pixelated/config/routes.py24
-rw-r--r--service/pixelated/controllers/home_controller.py42
-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.py64
-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.py46
-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.py13
-rw-r--r--service/setup.py17
-rw-r--r--service/test/functional/features/environment.py4
-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.py67
-rw-r--r--service/test/support/integration/soledad_test_base.py2
-rw-r--r--service/test/support/test_helper.py24
-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.py126
-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.txt1
-rw-r--r--web-ui/app/index.html32
-rw-r--r--web-ui/app/js/main.js2
-rw-r--r--web-ui/app/js/views/i18n.js2
-rw-r--r--web-ui/app/scss/news-cycle.scss4
-rw-r--r--web-ui/app/scss/opensans.scss20
56 files changed, 494 insertions, 405 deletions
diff --git a/.gitignore b/.gitignore
index a9f0d1de..d941ab4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
============================
diff --git a/service/go b/service/go
index bb3eca90..047506df 100755
--- a/service/go
+++ b/service/go
@@ -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');
}