diff options
Diffstat (limited to 'src/leap/soledad')
-rw-r--r-- | src/leap/soledad/README | 14 | ||||
-rw-r--r-- | src/leap/soledad/__init__.py | 19 | ||||
-rw-r--r-- | src/leap/soledad/backends/couch.py | 56 | ||||
-rw-r--r-- | src/leap/soledad/backends/leap_backend.py | 57 | ||||
-rw-r--r-- | src/leap/soledad/backends/objectstore.py | 9 | ||||
-rw-r--r-- | src/leap/soledad/backends/openstack.py | 98 | ||||
-rw-r--r-- | src/leap/soledad/backends/sqlcipher.py | 41 | ||||
-rw-r--r-- | src/leap/soledad/server.py | 20 | ||||
-rw-r--r-- | src/leap/soledad/tests/__init__.py | 8 | ||||
-rw-r--r-- | src/leap/soledad/tests/test_couch.py | 13 | ||||
-rw-r--r-- | src/leap/soledad/tests/test_leap_backend.py | 146 | ||||
-rw-r--r-- | src/leap/soledad/tests/test_sqlcipher.py | 74 |
12 files changed, 239 insertions, 316 deletions
diff --git a/src/leap/soledad/README b/src/leap/soledad/README index 3bf62494..b14d5932 100644 --- a/src/leap/soledad/README +++ b/src/leap/soledad/README @@ -9,16 +9,14 @@ Dependencies Soledad depends on the following python libraries: * u1db 0.1.4 [1] - * python-swiftclient 1.2.0 [2] - * python-gnupg 0.3.1 [3] - * CouchDB 0.8 [4] - * hmac 20101005 [5] + * python-gnupg 0.3.1 [2] + * CouchDB 0.8 [3] + * hmac 20101005 [4] [1] http://pypi.python.org/pypi/u1db/0.1.4 -[2] http://pypi.python.org/pypi/python-swiftclient/1.2.0 -[3] http://pypi.python.org/pypi/python-gnupg/0.3.1 -[4] http://pypi.python.org/pypi/CouchDB/0.8 -[5] http://pypi.python.org/pypi/hmac/20101005 +[2] http://pypi.python.org/pypi/python-gnupg/0.3.1 +[3] http://pypi.python.org/pypi/CouchDB/0.8 +[4] http://pypi.python.org/pypi/hmac/20101005 Tests diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py index c83627f0..e11b8319 100644 --- a/src/leap/soledad/__init__.py +++ b/src/leap/soledad/__init__.py @@ -8,7 +8,6 @@ import random import hmac from leap.soledad.backends import sqlcipher from leap.soledad.util import GPGWrapper -import util class Soledad(object): @@ -22,13 +21,17 @@ class Soledad(object): # other configs SECRET_LENGTH = 50 - def __init__(self, user_email, gpghome=None): + def __init__(self, user_email, gpghome=None, initialize=True): self._user_email = user_email if not os.path.isdir(self.PREFIX): os.makedirs(self.PREFIX) if not gpghome: gpghome = self.GNUPG_HOME - self._gpg = util.GPGWrapper(gpghome=gpghome) + self._gpg = GPGWrapper(gpghome=gpghome) + if initialize: + self._initialize() + + def _initialize(self): # load/generate OpenPGP keypair if not self._has_openpgp_keypair(): self._gen_openpgp_keypair() @@ -40,7 +43,11 @@ class Soledad(object): # instantiate u1db # TODO: verify if secret for sqlcipher should be the same as the one # for symmetric encryption. - self._db = sqlcipher.open(self.LOCAL_DB_PATH, True, self._secret) + self._db = sqlcipher.open(self.LOCAL_DB_PATH, True, self._secret, + soledad=self) + + def close(self): + self._db.close() #------------------------------------------------------------------------- # Management of secret for symmetric encryption @@ -64,7 +71,7 @@ class Soledad(object): try: with open(self.SECRET_PATH) as f: self._secret = str(self._gpg.decrypt(f.read())) - except IOError as e: + except IOError: raise IOError('Failed to open secret file %s.' % self.SECRET_PATH) def _gen_secret(self): @@ -207,6 +214,6 @@ class Soledad(object): Synchronize the local encrypted database with LEAP server. """ # TODO: create authentication scheme for sync with server. - return self._db.sync(url, creds=None, autocreate=True, soledad=self) + return self._db.sync(url, creds=None, autocreate=True) __all__ = ['util'] diff --git a/src/leap/soledad/backends/couch.py b/src/leap/soledad/backends/couch.py index c8dadfa8..d349efaf 100644 --- a/src/leap/soledad/backends/couch.py +++ b/src/leap/soledad/backends/couch.py @@ -1,10 +1,17 @@ +# general imports import uuid from base64 import b64encode, b64decode +import re +# u1db from u1db import errors from u1db.sync import LocalSyncTarget from u1db.backends.inmemory import InMemoryIndex +from u1db.remote.server_state import ServerState +from u1db.errors import DatabaseDoesNotExist +# couchdb from couchdb.client import Server, Document as CouchDocument from couchdb.http import ResourceNotFound +# leap from leap.soledad.backends.objectstore import ObjectStore from leap.soledad.backends.leap_backend import LeapDocument @@ -14,9 +21,29 @@ except ImportError: import json # noqa +class InvalidURLError(Exception): + pass + + class CouchDatabase(ObjectStore): """A U1DB implementation that uses Couch as its persistence layer.""" + @classmethod + def open_database(cls, url, create): + # get database from url + m = re.match('(^https?://[^/]+)/(.+)$', url) + if not m: + raise InvalidURLError + url = m.group(1) + dbname = m.group(2) + server = Server(url=url) + try: + server[dbname] + except ResourceNotFound: + if not create: + raise DatabaseDoesNotExist() + return cls(url, dbname) + def __init__(self, url, database, replica_uid=None, full_commit=True, session=None): """Create a new Couch data container.""" @@ -29,13 +56,13 @@ class CouchDatabase(ObjectStore): self._dbname = database # this will ensure that transaction and sync logs exist and are # up-to-date. - self.set_document_factory(LeapDocument) try: self._database = self._server[database] except ResourceNotFound: self._server.create(database) self._database = self._server[database] - super(CouchDatabase, self).__init__(replica_uid=replica_uid) + super(CouchDatabase, self).__init__(replica_uid=replica_uid, + document_factory=LeapDocument) #------------------------------------------------------------------------- # methods from Database @@ -215,3 +242,28 @@ class CouchSyncTarget(LocalSyncTarget): self._db._set_replica_gen_and_trans_id( source_replica_uid, source_replica_generation, source_replica_transaction_id) + + +class CouchServerState(ServerState): + """ + Inteface of the WSGI server with the CouchDB backend. + """ + + def __init__(self, couch_url): + self.couch_url = couch_url + + def open_database(self, dbname): + # TODO: open couch + from leap.soledad.backends.couch import CouchDatabase + return CouchDatabase.open_database(self.couch_url + '/' + dbname, + create=False) + + def ensure_database(self, dbname): + from leap.soledad.backends.couch import CouchDatabase + db = CouchDatabase.open_database(self.couch_url + '/' + dbname, + create=True) + return db, db._replica_uid + + def delete_database(self, dbname): + from leap.soledad.backends.couch import CouchDatabase + CouchDatabase.delete_database(self.couch_url + '/' + dbname) diff --git a/src/leap/soledad/backends/leap_backend.py b/src/leap/soledad/backends/leap_backend.py index f73698f2..c3c52ee6 100644 --- a/src/leap/soledad/backends/leap_backend.py +++ b/src/leap/soledad/backends/leap_backend.py @@ -26,6 +26,8 @@ class DocumentEncryptionFailed(Exception): class LeapDocument(Document): """ + Encryptable and syncable document. + LEAP Documents are standard u1db documents with cabability of returning an encrypted version of the document json string as well as setting document content based on an encrypted version of json string. @@ -41,7 +43,7 @@ class LeapDocument(Document): def get_encrypted_json(self): """ - Returns document's json serialization encrypted with user's public key. + Return document's json serialization encrypted with user's public key. """ if not self._soledad: raise NoSoledadInstance() @@ -71,47 +73,28 @@ class LeapDocument(Document): doc="Determine if document should be synced with server." ) + # Returning the revision as string solves the following exception in + # Twisted web: + # exceptions.TypeError: Can only pass-through bytes on Python 2 + def _get_rev(self): + if self._rev is None: + return None + return str(self._rev) -class LeapDatabase(HTTPDatabase): - """Implement the HTTP remote database API to a Leap server.""" + def _set_rev(self, rev): + self._rev = rev - def __init__(self, url, document_factory=None, creds=None, soledad=None): - super(LeapDatabase, self).__init__(url, creds=creds) - self._soledad = soledad - self._factory = LeapDocument - - @staticmethod - def open_database(url, create): - db = LeapDatabase(url) - db.open(create) - return db - - @staticmethod - def delete_database(url): - db = LeapDatabase(url) - db._delete() - db.close() - - def _allocate_doc_id(self): - """Generate a unique identifier for this document.""" - return 'D-' + uuid.uuid4().hex # 'D-' stands for document - - def get_sync_target(self): - st = LeapSyncTarget(self._url.geturl()) - st._creds = self._creds - return st - - def create_doc_from_json(self, content, doc_id=None): - if doc_id is None: - doc_id = self._allocate_doc_id() - res, headers = self._request_json('PUT', ['doc', doc_id], {}, - content, 'application/json') - new_doc = self._factory(doc_id, res['rev'], content, - soledad=self._soledad) - return new_doc + rev = property( + _get_rev, + _set_rev, + doc="Wrapper to ensure `doc.rev` is always returned as bytes.") class LeapSyncTarget(HTTPSyncTarget): + """ + A SyncTarget that encrypts data before sending and decrypts data after + receiving. + """ def __init__(self, url, creds=None, soledad=None): super(LeapSyncTarget, self).__init__(url, creds) diff --git a/src/leap/soledad/backends/objectstore.py b/src/leap/soledad/backends/objectstore.py index 588fc7a1..199107af 100644 --- a/src/leap/soledad/backends/objectstore.py +++ b/src/leap/soledad/backends/objectstore.py @@ -7,8 +7,13 @@ class ObjectStore(InMemoryDatabase): A backend for storing u1db data in an object store. """ - def __init__(self, replica_uid=None): - super(ObjectStore, self).__init__(replica_uid) + @classmethod + def open_database(cls, url, create, document_factory=None): + raise NotImplementedError(cls.open_database) + + def __init__(self, replica_uid=None, document_factory=None): + super(ObjectStore, self).__init__(replica_uid, + document_factory=document_factory) # sync data in memory with data in object store if not self._get_doc(self.U1DB_DATA_DOC_ID): self._init_u1db_data() diff --git a/src/leap/soledad/backends/openstack.py b/src/leap/soledad/backends/openstack.py deleted file mode 100644 index a9615736..00000000 --- a/src/leap/soledad/backends/openstack.py +++ /dev/null @@ -1,98 +0,0 @@ -# TODO: this backend is not tested yet. -from u1db.remote.http_target import HTTPSyncTarget -import swiftclient -from soledad.backends.objectstore import ObjectStore - - -class OpenStackDatabase(ObjectStore): - """A U1DB implementation that uses OpenStack as its persistence layer.""" - - def __init__(self, auth_url, user, auth_key, container): - """Create a new OpenStack data container.""" - self._auth_url = auth_url - self._user = user - self._auth_key = auth_key - self._container = container - self._connection = swiftclient.Connection(self._auth_url, self._user, - self._auth_key) - self._get_auth() - # this will ensure transaction and sync logs exist and are up-to-date. - super(OpenStackDatabase, self).__init__() - - #------------------------------------------------------------------------- - # implemented methods from Database - #------------------------------------------------------------------------- - - def _get_doc(self, doc_id, check_for_conflicts=False): - """Get just the document content, without fancy handling. - - Conflicts do not happen on server side, so there's no need to check - for them. - """ - try: - response, contents = self._connection.get_object(self._container, - doc_id) - # TODO: change revision to be a dictionary element? - rev = response['x-object-meta-rev'] - return self._factory(doc_id, rev, contents) - except swiftclient.ClientException: - return None - - def get_all_docs(self, include_deleted=False): - """Get all documents from the database.""" - generation = self._get_generation() - results = [] - _, doc_ids = self._connection.get_container(self._container, - full_listing=True) - for doc_id in doc_ids: - doc = self._get_doc(doc_id) - if doc.content is None and not include_deleted: - continue - results.append(doc) - return (generation, results) - - def _put_doc(self, doc, new_rev): - new_rev = self._allocate_doc_rev(doc.rev) - # TODO: change revision to be a dictionary element? - headers = {'X-Object-Meta-Rev': new_rev} - self._connection.put_object(self._container, doc_id, doc.get_json(), - headers=headers) - - def get_sync_target(self): - return OpenStackSyncTarget(self) - - def close(self): - raise NotImplementedError(self.close) - - def sync(self, url, creds=None, autocreate=True): - from u1db.sync import Synchronizer - from u1db.remote.http_target import OpenStackSyncTarget - return Synchronizer(self, OpenStackSyncTarget(url, creds=creds)).sync( - autocreate=autocreate) - - #------------------------------------------------------------------------- - # OpenStack specific methods - #------------------------------------------------------------------------- - - def _get_auth(self): - self._url, self._auth_token = self._connection.get_auth() - return self._url, self.auth_token - - -class OpenStackSyncTarget(HTTPSyncTarget): - - def get_sync_info(self, source_replica_uid): - source_gen, source_trans_id = self._db._get_replica_gen_and_trans_id( - source_replica_uid) - my_gen, my_trans_id = self._db._get_generation_info() - return ( - self._db._replica_uid, my_gen, my_trans_id, source_gen, - source_trans_id) - - def record_sync_info(self, source_replica_uid, source_replica_generation, - source_replica_transaction_id): - if self._trace_hook: - self._trace_hook('record_sync_info') - self._db._set_replica_gen_and_trans_id( - source_replica_uid, source_replica_generation, - source_replica_transaction_id) diff --git a/src/leap/soledad/backends/sqlcipher.py b/src/leap/soledad/backends/sqlcipher.py index 6cebcf7d..9108f73a 100644 --- a/src/leap/soledad/backends/sqlcipher.py +++ b/src/leap/soledad/backends/sqlcipher.py @@ -31,7 +31,7 @@ from u1db import ( from leap.soledad.backends.leap_backend import LeapDocument -def open(path, password, create=True, document_factory=None): +def open(path, password, create=True, document_factory=None, soledad=None): """Open a database at the given location. Will raise u1db.errors.DatabaseDoesNotExist if create=False and the @@ -45,7 +45,8 @@ def open(path, password, create=True, document_factory=None): :return: An instance of Database. """ return SQLCipherDatabase.open_database( - path, password, create=create, document_factory=document_factory) + path, password, create=create, document_factory=document_factory, + soledad=soledad) class DatabaseIsNotEncrypted(Exception): @@ -64,14 +65,23 @@ class SQLCipherDatabase(SQLitePartialExpandDatabase): def set_pragma_key(cls, db_handle, key): db_handle.cursor().execute("PRAGMA key = '%s'" % key) - def __init__(self, sqlite_file, password, document_factory=None): + def __init__(self, sqlite_file, password, document_factory=None, + soledad=None): """Create a new sqlcipher file.""" self._check_if_db_is_encrypted(sqlite_file) self._db_handle = dbapi2.connect(sqlite_file) SQLCipherDatabase.set_pragma_key(self._db_handle, password) self._real_replica_uid = None self._ensure_schema() - self._factory = document_factory or LeapDocument + self._soledad = soledad + + def factory(doc_id=None, rev=None, json='{}', has_conflicts=False, + encrypted_json=None, syncable=True): + return LeapDocument(doc_id=doc_id, rev=rev, json=json, + has_conflicts=has_conflicts, + encrypted_json=encrypted_json, + syncable=syncable, soledad=self._soledad) + self.set_document_factory(factory) def _check_if_db_is_encrypted(self, sqlite_file): if not os.path.exists(sqlite_file): @@ -86,7 +96,8 @@ class SQLCipherDatabase(SQLitePartialExpandDatabase): pass @classmethod - def _open_database(cls, sqlite_file, password, document_factory=None): + def _open_database(cls, sqlite_file, password, document_factory=None, + soledad=None): if not os.path.isfile(sqlite_file): raise errors.DatabaseDoesNotExist() tries = 2 @@ -108,14 +119,16 @@ class SQLCipherDatabase(SQLitePartialExpandDatabase): tries -= 1 time.sleep(cls.WAIT_FOR_PARALLEL_INIT_HALF_INTERVAL) return SQLCipherDatabase._sqlite_registry[v]( - sqlite_file, password, document_factory=document_factory) + sqlite_file, password, document_factory=document_factory, + soledad=soledad) @classmethod def open_database(cls, sqlite_file, password, create, backend_cls=None, - document_factory=None): + document_factory=None, soledad=None): try: return cls._open_database(sqlite_file, password, - document_factory=document_factory) + document_factory=document_factory, + soledad=soledad) except errors.DatabaseDoesNotExist: if not create: raise @@ -123,16 +136,20 @@ class SQLCipherDatabase(SQLitePartialExpandDatabase): # default is SQLCipherPartialExpandDatabase backend_cls = SQLCipherDatabase return backend_cls(sqlite_file, password, - document_factory=document_factory) + document_factory=document_factory, + soledad=soledad) - def sync(self, url, creds=None, autocreate=True, soledad=None): + def sync(self, url, creds=None, autocreate=True): """ Synchronize encrypted documents with remote replica exposed at url. """ from u1db.sync import Synchronizer from leap.soledad.backends.leap_backend import LeapSyncTarget - return Synchronizer(self, LeapSyncTarget(url, creds=creds), - soledad=self._soledad).sync(autocreate=autocreate) + return Synchronizer( + self, + LeapSyncTarget(url, + creds=creds, + soledad=self._soledad)).sync(autocreate=autocreate) def _extra_schema_init(self, c): c.execute( diff --git a/src/leap/soledad/server.py b/src/leap/soledad/server.py new file mode 100644 index 00000000..4fc97be5 --- /dev/null +++ b/src/leap/soledad/server.py @@ -0,0 +1,20 @@ +""" +An u1db server that stores data using couchdb. + +This should be run with: + twistd -n web --wsgi=leap.soledad.server.application +""" + +from twisted.web.wsgi import WSGIResource +from twisted.internet import reactor +from u1db.remote import http_app +from leap.soledad.backends.couch import CouchServerState + +couch_url = 'http://localhost:5984' +state = CouchServerState(couch_url) +# TODO: change working dir to something meaningful +state.set_workingdir('/tmp') +# TODO: write a LeapHTTPApp that will use Couch as backend instead of SQLite +application = http_app.HTTPApp(state) + +resource = WSGIResource(reactor, reactor.getThreadPool(), application) diff --git a/src/leap/soledad/tests/__init__.py b/src/leap/soledad/tests/__init__.py index 890c4d2a..6135e648 100644 --- a/src/leap/soledad/tests/__init__.py +++ b/src/leap/soledad/tests/__init__.py @@ -23,12 +23,16 @@ class BaseSoledadTest(BaseLeapTest): self._db2 = u1db.open(self.db2_file, create=True, document_factory=LeapDocument) # open a soledad instance - self._soledad = Soledad(self.email, gpghome=self.gnupg_home) + self._soledad = Soledad(self.email, gpghome=self.gnupg_home, + initialize=False) self._soledad._gpg.import_keys(PUBLIC_KEY) self._soledad._gpg.import_keys(PRIVATE_KEY) + self._soledad._initialize() def tearDown(self): - pass + self._db1.close() + self._db2.close() + self._soledad.close() # Key material for testing diff --git a/src/leap/soledad/tests/test_couch.py b/src/leap/soledad/tests/test_couch.py index 02399e4c..3482b035 100644 --- a/src/leap/soledad/tests/test_couch.py +++ b/src/leap/soledad/tests/test_couch.py @@ -27,6 +27,8 @@ import time import unittest +# from: https://github.com/smcq/paisley/blob/master/paisley/test/util.py +# TODO: include license of above project. class CouchDBWrapper(object): """ Wrapper for external CouchDB instance which is started and stopped for @@ -42,6 +44,7 @@ class CouchDBWrapper(object): conf = handle.read() % { 'tempdir': self.tempdir, } + handle.close() confPath = os.path.join(self.tempdir, 'test.ini') handle = open(confPath, 'w') @@ -51,10 +54,11 @@ class CouchDBWrapper(object): # create the dirs from the template os.mkdir(os.path.join(self.tempdir, 'lib')) os.mkdir(os.path.join(self.tempdir, 'log')) - argus = ['couchdb', '-n' '-a', confPath] - null = open('/dev/null', 'w') + args = ['couchdb', '-n' '-a', confPath] + #null = open('/dev/null', 'w') self.process = subprocess.Popen( - argus, env=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + args, env=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + close_fds=True) # find port logPath = os.path.join(self.tempdir, 'log', 'couch.log') while not os.path.exists(logPath): @@ -75,6 +79,7 @@ stderr: handle = open(logPath) line = handle.read() + handle.close() m = PORT_RE.search(line) if not m: self.stop() @@ -83,7 +88,7 @@ stderr: def stop(self): self.process.terminate() - + self.process.wait() os.system("rm -rf %s" % self.tempdir) diff --git a/src/leap/soledad/tests/test_leap_backend.py b/src/leap/soledad/tests/test_leap_backend.py index cdd60b10..a061533c 100644 --- a/src/leap/soledad/tests/test_leap_backend.py +++ b/src/leap/soledad/tests/test_leap_backend.py @@ -21,51 +21,9 @@ from leap.soledad.tests.u1db_tests import test_https #----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_common_backend`. -#----------------------------------------------------------------------------- - -class TestLeapBackendImpl(tests.TestCase): - - def test__allocate_doc_id(self): - db = leap_backend.LeapDatabase('test') - doc_id1 = db._allocate_doc_id() - self.assertTrue(doc_id1.startswith('D-')) - self.assertEqual(34, len(doc_id1)) - int(doc_id1[len('D-'):], 16) - self.assertNotEqual(doc_id1, db._allocate_doc_id()) - - -#----------------------------------------------------------------------------- # The following tests come from `u1db.tests.test_backends`. #----------------------------------------------------------------------------- -def make_leap_database_for_test(test, replica_uid, path='test'): - test.startServer() - test.request_state._create_database(replica_uid) - return leap_backend.LeapDatabase(test.getURL(path)) - - -def copy_leap_database_for_test(test, db): - # DO NOT COPY OR REUSE THIS CODE OUTSIDE TESTS: COPYING U1DB DATABASES IS - # THE WRONG THING TO DO, THE ONLY REASON WE DO SO HERE IS TO TEST THAT WE - # CORRECTLY DETECT IT HAPPENING SO THAT WE CAN RAISE ERRORS RATHER THAN - # CORRUPT USER DATA. USE SYNC INSTEAD, OR WE WILL SEND NINJA TO YOUR - # HOUSE. - return test.request_state._copy_database(db) - - -def make_oauth_leap_database_for_test(test, replica_uid): - http_db = make_leap_database_for_test(test, replica_uid, '~/test') - http_db.set_oauth_credentials(tests.consumer1.key, tests.consumer1.secret, - tests.token1.key, tests.token1.secret) - return http_db - - -def make_document_for_test(test, doc_id, rev, content, has_conflicts=False): - return leap_backend.LeapDocument( - doc_id, rev, content, has_conflicts=has_conflicts) - - def make_leap_document_for_test(test, doc_id, rev, content, has_conflicts=False): return leap_backend.LeapDocument( @@ -82,10 +40,11 @@ def make_leap_encrypted_document_for_test(test, doc_id, rev, encrypted_content, LEAP_SCENARIOS = [ - ('http', {'make_database_for_test': make_leap_database_for_test, - 'copy_database_for_test': copy_leap_database_for_test, - 'make_document_for_test': make_leap_document_for_test, - 'make_app_with_state': make_http_app}), + ('http', { + 'make_database_for_test': test_backends.make_http_database_for_test, + 'copy_database_for_test': test_backends.copy_http_database_for_test, + 'make_document_for_test': make_leap_document_for_test, + 'make_app_with_state': make_http_app}), ] @@ -95,101 +54,6 @@ class LeapTests(test_backends.AllDatabaseTests, BaseSoledadTest): #----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_http_database`. -#----------------------------------------------------------------------------- - -class TestLeapDatabaseSimpleOperations( - test_http_database.TestHTTPDatabaseSimpleOperations): - - def setUp(self): - super(test_http_database.TestHTTPDatabaseSimpleOperations, - self).setUp() - self.db = leap_backend.LeapDatabase('dbase') - self.db._conn = object() # crash if used - self.got = None - self.response_val = None - - def _request(method, url_parts, params=None, body=None, - content_type=None): - self.got = method, url_parts, params, body, content_type - if isinstance(self.response_val, Exception): - raise self.response_val - return self.response_val - - def _request_json(method, url_parts, params=None, body=None, - content_type=None): - self.got = method, url_parts, params, body, content_type - if isinstance(self.response_val, Exception): - raise self.response_val - return self.response_val - - self.db._request = _request - self.db._request_json = _request_json - - def test_get_sync_target(self): - st = self.db.get_sync_target() - self.assertIsInstance(st, leap_backend.LeapSyncTarget) - self.assertEqual(st._url, self.db._url) - - -class TestLeapDatabaseCtrWithCreds( - test_http_database.TestHTTPDatabaseCtrWithCreds): - pass - - -class TestLeapDatabaseIntegration( - test_http_database.TestHTTPDatabaseIntegration): - - def test_non_existing_db(self): - db = leap_backend.LeapDatabase(self.getURL('not-there')) - self.assertRaises(u1db.errors.DatabaseDoesNotExist, db.get_doc, 'doc1') - - def test__ensure(self): - db = leap_backend.LeapDatabase(self.getURL('new')) - db._ensure() - self.assertIs(None, db.get_doc('doc1')) - - def test__delete(self): - self.request_state._create_database('db0') - db = leap_backend.LeapDatabase(self.getURL('db0')) - db._delete() - self.assertRaises(u1db.errors.DatabaseDoesNotExist, - self.request_state.check_database, 'db0') - - def test_open_database_existing(self): - self.request_state._create_database('db0') - db = leap_backend.LeapDatabase.open_database(self.getURL('db0'), - create=False) - self.assertIs(None, db.get_doc('doc1')) - - def test_open_database_non_existing(self): - self.assertRaises(u1db.errors.DatabaseDoesNotExist, - leap_backend.LeapDatabase.open_database, - self.getURL('not-there'), - create=False) - - def test_open_database_create(self): - db = leap_backend.LeapDatabase.open_database(self.getURL('new'), - create=True) - self.assertIs(None, db.get_doc('doc1')) - - def test_delete_database_existing(self): - self.request_state._create_database('db0') - leap_backend.LeapDatabase.delete_database(self.getURL('db0')) - self.assertRaises(u1db.errors.DatabaseDoesNotExist, - self.request_state.check_database, 'db0') - - def test_doc_ids_needing_quoting(self): - db0 = self.request_state._create_database('db0') - db = leap_backend.LeapDatabase.open_database(self.getURL('db0'), - create=False) - doc = leap_backend.LeapDocument('%fff', None, '{}') - db.put_doc(doc) - self.assertGetDoc(db0, '%fff', doc.rev, '{}', False) - self.assertGetDoc(db, '%fff', doc.rev, '{}', False) - - -#----------------------------------------------------------------------------- # The following tests come from `u1db.tests.test_http_client`. #----------------------------------------------------------------------------- diff --git a/src/leap/soledad/tests/test_sqlcipher.py b/src/leap/soledad/tests/test_sqlcipher.py index a3ab35b6..09b76b82 100644 --- a/src/leap/soledad/tests/test_sqlcipher.py +++ b/src/leap/soledad/tests/test_sqlcipher.py @@ -3,7 +3,7 @@ import os import time from sqlite3 import dbapi2, DatabaseError -import unittest2 as unittest +import unittest from StringIO import StringIO import threading @@ -11,6 +11,7 @@ import threading from u1db import ( errors, query_parser, + sync, ) from u1db.backends.sqlite_backend import SQLitePartialExpandDatabase @@ -27,6 +28,8 @@ from leap.soledad.tests import u1db_tests as tests from leap.soledad.tests.u1db_tests import test_sqlite_backend from leap.soledad.tests.u1db_tests import test_backends from leap.soledad.tests.u1db_tests import test_open +from leap.soledad.tests.u1db_tests import test_sync +from leap.soledad.backends.leap_backend import LeapSyncTarget PASSWORD = '123456' @@ -233,7 +236,8 @@ class TestSQLCipherPartialExpandDatabase( db2 = SQLCipherDatabase._open_database( path, PASSWORD, document_factory=TestAlternativeDocument) - self.assertEqual(TestAlternativeDocument, db2._factory) + doc = db2.create_doc({}) + self.assertTrue(isinstance(doc, LeapDocument)) def test_open_database_existing(self): temp_dir = self.createTempDir(prefix='u1db-test-') @@ -249,7 +253,8 @@ class TestSQLCipherPartialExpandDatabase( db2 = SQLCipherDatabase.open_database( path, PASSWORD, create=False, document_factory=TestAlternativeDocument) - self.assertEqual(TestAlternativeDocument, db2._factory) + doc = db2.create_doc({}) + self.assertTrue(isinstance(doc, LeapDocument)) def test_create_database_initializes_schema(self): # This test had to be cloned because our implementation of SQLCipher @@ -301,7 +306,8 @@ class SQLCipherOpen(test_open.TestU1DBOpen): db = u1db_open(self.db_path, password=PASSWORD, create=True, document_factory=TestAlternativeDocument) self.addCleanup(db.close) - self.assertEqual(TestAlternativeDocument, db._factory) + doc = db.create_doc({}) + self.assertTrue(isinstance(doc, LeapDocument)) def test_open_existing(self): db = SQLCipherDatabase(self.db_path, PASSWORD) @@ -322,6 +328,63 @@ class SQLCipherOpen(test_open.TestU1DBOpen): #----------------------------------------------------------------------------- +# The following tests come from `u1db.tests.test_sync`. +#----------------------------------------------------------------------------- + +sync_scenarios = [] +for name, scenario in SQLCIPHER_SCENARIOS: + scenario = dict(scenario) + scenario['do_sync'] = test_sync.sync_via_synchronizer + sync_scenarios.append((name, scenario)) + scenario = dict(scenario) + + +def sync_via_synchronizer_and_leap(test, db_source, db_target, + trace_hook=None, trace_hook_shallow=None): + if trace_hook: + test.skipTest("full trace hook unsupported over http") + path = test._http_at[db_target] + target = LeapSyncTarget.connect(test.getURL(path)) + if trace_hook_shallow: + target._set_trace_hook_shallow(trace_hook_shallow) + return sync.Synchronizer(db_source, target).sync() + + +sync_scenarios.append(('pyleap', { + 'make_database_for_test': test_sync.make_database_for_http_test, + 'copy_database_for_test': test_sync.copy_database_for_http_test, + 'make_document_for_test': tests.make_document_for_test, + 'make_app_with_state': tests.test_remote_sync_target.make_http_app, + 'do_sync': sync_via_synchronizer_and_leap, +})) + + +class SQLCipherDatabaseSyncTests(test_sync.DatabaseSyncTests): + + scenarios = sync_scenarios + + +def _make_local_db_and_leap_target(test, path='test'): + test.startServer() + db = test.request_state._create_database(os.path.basename(path)) + st = LeapSyncTarget.connect(test.getURL(path)) + return db, st + + +target_scenarios = [ + ('leap', { + 'create_db_and_target': _make_local_db_and_leap_target, + 'make_app_with_state': tests.test_remote_sync_target.make_http_app}), +] + + +class SQLCipherSyncTargetTests(test_sync.DatabaseSyncTargetTests): + + scenarios = (tests.multiply_scenarios(SQLCIPHER_SCENARIOS, + target_scenarios)) + + +#----------------------------------------------------------------------------- # Tests for actual encryption of the database #----------------------------------------------------------------------------- @@ -372,3 +435,6 @@ class SQLCipherEncryptionTest(unittest.TestCase): "non-encrypted dbs.") except DatabaseIsNotEncrypted: pass + + +load_tests = tests.load_with_scenarios |