From b915e3d5bd1e37c732b44559af5587f6c6a90fc3 Mon Sep 17 00:00:00 2001 From: drebs Date: Tue, 25 Nov 2014 14:58:31 -0200 Subject: Adapt tests for new api. --- common/src/leap/soledad/common/tests/__init__.py | 284 +------- common/src/leap/soledad/common/tests/test_couch.py | 164 +---- .../tests/test_couch_operations_atomicity.py | 206 +++--- .../src/leap/soledad/common/tests/test_crypto.py | 17 +- common/src/leap/soledad/common/tests/test_http.py | 5 - .../leap/soledad/common/tests/test_http_client.py | 11 +- common/src/leap/soledad/common/tests/test_https.py | 42 +- .../src/leap/soledad/common/tests/test_server.py | 352 ++++----- .../src/leap/soledad/common/tests/test_soledad.py | 166 ++--- .../leap/soledad/common/tests/test_soledad_app.py | 59 ++ .../leap/soledad/common/tests/test_soledad_doc.py | 24 +- .../leap/soledad/common/tests/test_sqlcipher.py | 62 +- .../soledad/common/tests/test_sqlcipher_sync.py | 49 +- common/src/leap/soledad/common/tests/test_sync.py | 153 ++-- .../soledad/common/tests/test_sync_deferred.py | 150 ++-- .../leap/soledad/common/tests/test_sync_target.py | 285 ++++---- .../src/leap/soledad/common/tests/test_target.py | 797 --------------------- .../soledad/common/tests/test_target_soledad.py | 102 --- .../soledad/common/tests/u1db_tests/__init__.py | 4 +- .../soledad/common/tests/u1db_tests/test_https.py | 2 +- common/src/leap/soledad/common/tests/util.py | 352 ++++++++- 21 files changed, 1116 insertions(+), 2170 deletions(-) create mode 100644 common/src/leap/soledad/common/tests/test_soledad_app.py delete mode 100644 common/src/leap/soledad/common/tests/test_target.py delete mode 100644 common/src/leap/soledad/common/tests/test_target_soledad.py (limited to 'common/src') diff --git a/common/src/leap/soledad/common/tests/__init__.py b/common/src/leap/soledad/common/tests/__init__.py index f8253409..acebb77b 100644 --- a/common/src/leap/soledad/common/tests/__init__.py +++ b/common/src/leap/soledad/common/tests/__init__.py @@ -19,291 +19,9 @@ """ Tests to make sure Soledad provides U1DB functionality and more. """ -import os -import random -import string -import u1db -from mock import Mock - - -from leap.soledad.common.document import SoledadDocument -from leap.soledad.common.crypto import ENC_SCHEME_KEY -from leap.soledad.client import Soledad -from leap.soledad.client.crypto import decrypt_doc_dict -from leap.common.testing.basetest import BaseLeapTest - - -#----------------------------------------------------------------------------- -# Some tests inherit from BaseSoledadTest in order to have a working Soledad -# instance in each test. -#----------------------------------------------------------------------------- - -ADDRESS = 'leap@leap.se' - - -class BaseSoledadTest(BaseLeapTest): - """ - Instantiates Soledad for usage in tests. - """ - defer_sync_encryption = False - - def setUp(self): - # config info - self.db1_file = os.path.join(self.tempdir, "db1.u1db") - self.db2_file = os.path.join(self.tempdir, "db2.u1db") - self.email = ADDRESS - # open test dbs - self._db1 = u1db.open(self.db1_file, create=True, - document_factory=SoledadDocument) - self._db2 = u1db.open(self.db2_file, create=True, - document_factory=SoledadDocument) - # get a random prefix for each test, so we do not mess with - # concurrency during initialization and shutting down of - # each local db. - self.rand_prefix = ''.join( - map(lambda x: random.choice(string.ascii_letters), range(6))) - # initialize soledad by hand so we can control keys - self._soledad = self._soledad_instance( - prefix=self.rand_prefix, user=self.email) - - def tearDown(self): - self._db1.close() - self._db2.close() - self._soledad.close() - - # XXX should not access "private" attrs - for f in [self._soledad._local_db_path, self._soledad._secrets_path]: - if os.path.isfile(f): - os.unlink(f) - def get_default_shared_mock(self, put_doc_side_effect): - """ - Get a default class for mocking the shared DB - """ - class defaultMockSharedDB(object): - get_doc = Mock(return_value=None) - put_doc = Mock(side_effect=put_doc_side_effect) - lock = Mock(return_value=('atoken', 300)) - unlock = Mock(return_value=True) - def __call__(self): - return self - return defaultMockSharedDB - - def _soledad_instance(self, user=ADDRESS, passphrase=u'123', - prefix='', - secrets_path='secrets.json', - local_db_path='soledad.u1db', server_url='', - cert_file=None, secret_id=None, - shared_db_class=None): - - def _put_doc_side_effect(doc): - self._doc_put = doc - - if shared_db_class is not None: - MockSharedDB = shared_db_class - else: - MockSharedDB = self.get_default_shared_mock( - _put_doc_side_effect) - - Soledad._shared_db = MockSharedDB() - return Soledad( - user, - passphrase, - secrets_path=os.path.join( - self.tempdir, prefix, secrets_path), - local_db_path=os.path.join( - self.tempdir, prefix, local_db_path), - server_url=server_url, # Soledad will fail if not given an url. - cert_file=cert_file, - secret_id=secret_id, - defer_encryption=self.defer_sync_encryption) - - def assertGetEncryptedDoc( - self, db, doc_id, doc_rev, content, has_conflicts): - """ - Assert that the document in the database looks correct. - """ - exp_doc = self.make_document(doc_id, doc_rev, content, - has_conflicts=has_conflicts) - doc = db.get_doc(doc_id) - - if ENC_SCHEME_KEY in doc.content: - # XXX check for SYM_KEY too - key = self._soledad._crypto.doc_passphrase(doc.doc_id) - secret = self._soledad._crypto.secret - decrypted = decrypt_doc_dict( - doc.content, doc.doc_id, doc.rev, - key, secret) - doc.set_json(decrypted) - self.assertEqual(exp_doc.doc_id, doc.doc_id) - self.assertEqual(exp_doc.rev, doc.rev) - self.assertEqual(exp_doc.has_conflicts, doc.has_conflicts) - self.assertEqual(exp_doc.content, doc.content) - - -# Key material for testing -KEY_FINGERPRINT = "E36E738D69173C13D709E44F2F455E2824D18DDF" -PUBLIC_KEY = """ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -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 -=MuOY ------END PGP PUBLIC KEY BLOCK----- -""" -PRIVATE_KEY = """ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -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 -=JTFu ------END PGP PRIVATE KEY BLOCK----- -""" +import os def load_tests(): diff --git a/common/src/leap/soledad/common/tests/test_couch.py b/common/src/leap/soledad/common/tests/test_couch.py index 10d6c136..d2aef9bb 100644 --- a/common/src/leap/soledad/common/tests/test_couch.py +++ b/common/src/leap/soledad/common/tests/test_couch.py @@ -20,134 +20,21 @@ Test ObjectStore and Couch backend bits. """ -import re -import copy -import shutil -from base64 import b64decode -from mock import Mock -from urlparse import urljoin +import simplejson as json + +from urlparse import urljoin from u1db import errors as u1db_errors from couchdb.client import Server -from leap.common.files import mkdir_p +from testscenarios import TestWithScenarios + +from leap.soledad.common import couch, errors from leap.soledad.common.tests import u1db_tests as tests from leap.soledad.common.tests.u1db_tests import test_backends from leap.soledad.common.tests.u1db_tests import test_sync -from leap.soledad.common import couch, errors -import simplejson as json - - -#----------------------------------------------------------------------------- -# A wrapper for running couchdb locally. -#----------------------------------------------------------------------------- - -import re -import os -import tempfile -import subprocess -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 - testing. - """ - - def start(self): - """ - Start a CouchDB instance for a test. - """ - self.tempdir = tempfile.mkdtemp(suffix='.couch.test') - - path = os.path.join(os.path.dirname(__file__), - 'couchdb.ini.template') - handle = open(path) - conf = handle.read() % { - 'tempdir': self.tempdir, - } - handle.close() - - confPath = os.path.join(self.tempdir, 'test.ini') - handle = open(confPath, 'w') - handle.write(conf) - handle.close() - - # create the dirs from the template - mkdir_p(os.path.join(self.tempdir, 'lib')) - mkdir_p(os.path.join(self.tempdir, 'log')) - args = ['couchdb', '-n', '-a', confPath] - null = open('/dev/null', 'w') - - self.process = subprocess.Popen( - args, env=None, stdout=null.fileno(), stderr=null.fileno(), - close_fds=True) - # find port - logPath = os.path.join(self.tempdir, 'log', 'couch.log') - while not os.path.exists(logPath): - if self.process.poll() is not None: - got_stdout, got_stderr = "", "" - if self.process.stdout is not None: - got_stdout = self.process.stdout.read() - - if self.process.stderr is not None: - got_stderr = self.process.stderr.read() - raise Exception(""" -couchdb exited with code %d. -stdout: -%s -stderr: -%s""" % ( - self.process.returncode, got_stdout, got_stderr)) - time.sleep(0.01) - while os.stat(logPath).st_size == 0: - time.sleep(0.01) - PORT_RE = re.compile( - 'Apache CouchDB has started on http://127.0.0.1:(?P\d+)') - - handle = open(logPath) - line = handle.read() - handle.close() - m = PORT_RE.search(line) - if not m: - self.stop() - raise Exception("Cannot find port in line %s" % line) - self.port = int(m.group('port')) - - def stop(self): - """ - Terminate the CouchDB instance. - """ - self.process.terminate() - self.process.communicate() - shutil.rmtree(self.tempdir) - - -class CouchDBTestCase(unittest.TestCase): - """ - TestCase base class for tests against a real CouchDB server. - """ - - @classmethod - def setUpClass(cls): - """ - Make sure we have a CouchDB instance for a test. - """ - cls.wrapper = CouchDBWrapper() - cls.wrapper.start() - #self.db = self.wrapper.db - - @classmethod - def tearDownClass(cls): - """ - Stop CouchDB instance for test. - """ - cls.wrapper.stop() +from leap.soledad.common.tests.util import CouchDBTestCase #----------------------------------------------------------------------------- @@ -239,7 +126,8 @@ COUCH_SCENARIOS = [ ] -class CouchTests(test_backends.AllDatabaseTests, CouchDBTestCase): +class CouchTests( + TestWithScenarios, test_backends.AllDatabaseTests, CouchDBTestCase): scenarios = COUCH_SCENARIOS @@ -262,7 +150,8 @@ class CouchTests(test_backends.AllDatabaseTests, CouchDBTestCase): test_backends.AllDatabaseTests.tearDown(self) -class CouchDatabaseTests(test_backends.LocalDatabaseTests, CouchDBTestCase): +class CouchDatabaseTests( + TestWithScenarios, test_backends.LocalDatabaseTests, CouchDBTestCase): scenarios = COUCH_SCENARIOS @@ -271,7 +160,7 @@ class CouchDatabaseTests(test_backends.LocalDatabaseTests, CouchDBTestCase): test_backends.LocalDatabaseTests.tearDown(self) -class CouchValidateGenNTransIdTests( +class CouchValidateGenNTransIdTests(TestWithScenarios, test_backends.LocalDatabaseValidateGenNTransIdTests, CouchDBTestCase): scenarios = COUCH_SCENARIOS @@ -281,7 +170,7 @@ class CouchValidateGenNTransIdTests( test_backends.LocalDatabaseValidateGenNTransIdTests.tearDown(self) -class CouchValidateSourceGenTests( +class CouchValidateSourceGenTests(TestWithScenarios, test_backends.LocalDatabaseValidateSourceGenTests, CouchDBTestCase): scenarios = COUCH_SCENARIOS @@ -291,7 +180,7 @@ class CouchValidateSourceGenTests( test_backends.LocalDatabaseValidateSourceGenTests.tearDown(self) -class CouchWithConflictsTests( +class CouchWithConflictsTests(TestWithScenarios, test_backends.LocalDatabaseWithConflictsTests, CouchDBTestCase): scenarios = COUCH_SCENARIOS @@ -325,23 +214,11 @@ simple_doc = tests.simple_doc nested_doc = tests.nested_doc -class CouchDatabaseSyncTargetTests(test_sync.DatabaseSyncTargetTests, - CouchDBTestCase): +class CouchDatabaseSyncTargetTests( + TestWithScenarios, test_sync.DatabaseSyncTargetTests, CouchDBTestCase): scenarios = (tests.multiply_scenarios(COUCH_SCENARIOS, target_scenarios)) - def setUp(self): - # we implement parents' setUp methods here to prevent from launching - # more couch instances then needed. - tests.TestCase.setUp(self) - self.server = self.server_thread = None - self.db, self.st = self.create_db_and_target(self) - self.other_changes = [] - - def tearDown(self): - self.db.delete_database() - test_sync.DatabaseSyncTargetTests.tearDown(self) - def test_sync_exchange_returns_many_new_docs(self): # This test was replicated to allow dictionaries to be compared after # JSON expansion (because one dictionary may have many different @@ -372,7 +249,7 @@ from u1db.backends.inmemory import InMemoryIndex class IndexedCouchDatabase(couch.CouchDatabase): def __init__(self, url, dbname, replica_uid=None, ensure_ddocs=True): - old_class.__init__(self, url, dbname, replica_uid=replica_uid, + old_class.__init__(self, url, dbname, replica_uid=replica_uid, ensure_ddocs=ensure_ddocs) self._indexes = {} @@ -458,7 +335,8 @@ for name, scenario in COUCH_SCENARIOS: scenario = dict(scenario) -class CouchDatabaseSyncTests(test_sync.DatabaseSyncTests, CouchDBTestCase): +class CouchDatabaseSyncTests( + TestWithScenarios, test_sync.DatabaseSyncTests, CouchDBTestCase): scenarios = sync_scenarios @@ -498,6 +376,7 @@ class CouchDatabaseExceptionsTests(CouchDBTestCase): def tearDown(self): self.db.delete_database() self.db.close() + CouchDBTestCase.tearDown(self) def test_missing_design_doc_raises(self): """ @@ -670,6 +549,3 @@ class CouchDatabaseExceptionsTests(CouchDBTestCase): self.assertRaises( errors.MissingDesignDocDeletedError, self.db._do_set_replica_gen_and_trans_id, 1, 2, 3) - - -load_tests = tests.load_with_scenarios diff --git a/common/src/leap/soledad/common/tests/test_couch_operations_atomicity.py b/common/src/leap/soledad/common/tests/test_couch_operations_atomicity.py index 6465eb80..83cee469 100644 --- a/common/src/leap/soledad/common/tests/test_couch_operations_atomicity.py +++ b/common/src/leap/soledad/common/tests/test_couch_operations_atomicity.py @@ -15,26 +15,25 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ -Test atomocity for couch operations. +Test atomicity of couch operations. """ import os -import mock import tempfile import threading - from urlparse import urljoin - +from twisted.internet import defer from leap.soledad.client import Soledad from leap.soledad.common.couch import CouchDatabase, CouchServerState -from leap.soledad.common.tests.test_couch import CouchDBTestCase -from leap.soledad.common.tests.u1db_tests import TestCaseWithServer -from leap.soledad.common.tests.test_sync_target import ( + +from leap.soledad.common.tests.util import ( make_token_soledad_app, - make_leap_document_for_test, - token_leap_sync_target, + make_soledad_document_for_test, + token_soledad_sync_target, ) +from leap.soledad.common.tests.test_couch import CouchDBTestCase +from leap.soledad.common.tests.u1db_tests import TestCaseWithServer from leap.soledad.common.tests.test_server import _couch_ensure_database @@ -52,15 +51,15 @@ class CouchAtomicityTestCase(CouchDBTestCase, TestCaseWithServer): def make_app_after_state(state): return make_token_soledad_app(state) - make_document_for_test = make_leap_document_for_test + make_document_for_test = make_soledad_document_for_test - sync_target = token_leap_sync_target + sync_target = token_soledad_sync_target def _soledad_instance(self, user='user-uuid', passphrase=u'123', prefix='', - secrets_path=Soledad.STORAGE_SECRETS_FILE_NAME, + secrets_path='secrets.json', local_db_path='soledad.u1db', server_url='', - cert_file=None, auth_token=None, secret_id=None): + cert_file=None, auth_token=None): """ Instantiate Soledad. """ @@ -70,19 +69,6 @@ class CouchAtomicityTestCase(CouchDBTestCase, TestCaseWithServer): def _put_doc_side_effect(doc): self._doc_put = doc - # we need a mocked shared db or else Soledad will try to access the - # network to find if there are uploaded secrets. - class MockSharedDB(object): - - get_doc = mock.Mock(return_value=None) - put_doc = mock.Mock(side_effect=_put_doc_side_effect) - lock = mock.Mock(return_value=('atoken', 300)) - unlock = mock.Mock() - - def __call__(self): - return self - - Soledad._shared_db = MockSharedDB() return Soledad( user, passphrase, @@ -92,7 +78,7 @@ class CouchAtomicityTestCase(CouchDBTestCase, TestCaseWithServer): server_url=server_url, cert_file=cert_file, auth_token=auth_token, - secret_id=secret_id) + shared_db=self.get_default_shared_mock(_put_doc_side_effect)) def make_app(self): self.request_state = CouchServerState(self._couch_url, 'shared', @@ -126,7 +112,6 @@ class CouchAtomicityTestCase(CouchDBTestCase, TestCaseWithServer): puts. """ doc = self.db.create_doc({'ops': 0}) - ops = 1 docs = [doc.doc_id] for i in range(0, REPEAT_TIMES): self.assertEqual( @@ -183,24 +168,27 @@ class CouchAtomicityTestCase(CouchDBTestCase, TestCaseWithServer): auth_token='auth-token', server_url=self.getURL()) - def _create_docs_and_sync(sol, syncs): - # create a lot of documents - for i in range(0, REPEAT_TIMES): - sol.create_doc({}) + def _create_docs(results): + deferreds = [] + for i in xrange(0, REPEAT_TIMES): + deferreds.append(sol.create_doc({})) + return defer.DeferredList(deferreds) + + def _assert_transaction_and_sync_logs(results, sync_idx): # assert sizes of transaction and sync logs self.assertEqual( - syncs*REPEAT_TIMES, + sync_idx*REPEAT_TIMES, len(self.db._get_transaction_log())) self.assertEqual( - 1 if syncs > 0 else 0, + 1 if sync_idx > 0 else 0, len(self.db._database.view('syncs/log').rows)) - # sync to the remote db - sol.sync() - gen, docs = self.db.get_all_docs() - self.assertEqual((syncs+1)*REPEAT_TIMES, gen) - self.assertEqual((syncs+1)*REPEAT_TIMES, len(docs)) + + def _assert_sync(results, sync_idx): + gen, docs = results + self.assertEqual((sync_idx+1)*REPEAT_TIMES, gen) + self.assertEqual((sync_idx+1)*REPEAT_TIMES, len(docs)) # assert sizes of transaction and sync logs - self.assertEqual((syncs+1)*REPEAT_TIMES, + self.assertEqual((sync_idx+1)*REPEAT_TIMES, len(self.db._get_transaction_log())) sync_log_rows = self.db._database.view('syncs/log').rows sync_log = sync_log_rows[0].value @@ -210,14 +198,32 @@ class CouchAtomicityTestCase(CouchDBTestCase, TestCaseWithServer): # assert sync_log has exactly 1 row self.assertEqual(1, len(sync_log_rows)) # assert it has the correct replica_uid, gen and trans_id - self.assertEqual(sol._db._replica_uid, replica_uid) - sol_gen, sol_trans_id = sol._db._get_generation_info() + self.assertEqual(sol._dbpool.replica_uid, replica_uid) + conn_key = sol._dbpool._u1dbconnections.keys().pop() + conn = sol._dbpool._u1dbconnections[conn_key] + sol_gen, sol_trans_id = conn._get_generation_info() self.assertEqual(sol_gen, known_gen) self.assertEqual(sol_trans_id, known_trans_id) + + # create some documents + d = _create_docs(None) - _create_docs_and_sync(sol, 0) - _create_docs_and_sync(sol, 1) - sol.close() + # sync first time and assert success + d.addCallback(_assert_transaction_and_sync_logs, 0) + d.addCallback(lambda _: sol.sync()) + d.addCallback(lambda _: sol.get_all_docs()) + d.addCallback(_assert_sync, 0) + + # create more docs, sync second time and assert success + d.addCallback(_create_docs) + d.addCallback(_assert_transaction_and_sync_logs, 1) + d.addCallback(lambda _: sol.sync()) + d.addCallback(lambda _: sol.get_all_docs()) + d.addCallback(_assert_sync, 1) + + d.addCallback(lambda _: sol.close()) + + return d # # Concurrency tests @@ -313,86 +319,76 @@ class CouchAtomicityTestCase(CouchDBTestCase, TestCaseWithServer): """ Assert that the sync_log is correct after concurrent syncs. """ - threads = [] docs = [] - pool = threading.BoundedSemaphore(value=1) + self.startServer() + sol = self._soledad_instance( auth_token='auth-token', server_url=self.getURL()) - def _run_method(self): - # create a lot of documents - doc = self._params['sol'].create_doc({}) - pool.acquire() - docs.append(doc.doc_id) - pool.release() + def _save_doc_ids(results): + for doc in results: + docs.append(doc.doc_id) - # launch threads to create documents in parallel + # create documents in parallel + deferreds = [] for i in range(0, REPEAT_TIMES): - thread = self._WorkerThread( - {'sol': sol, 'syncs': i}, - _run_method) - thread.start() - threads.append(thread) + d = sol.create_doc({}) + deferreds.append(d) - # wait for threads to finish - for thread in threads: - thread.join() + # wait for documents creation and sync + d = defer.gatherResults(deferreds) + d.addCallback(_save_doc_ids) + d.addCallback(lambda _: sol.sync()) - # do the sync! - sol.sync() + def _assert_logs(results): + transaction_log = self.db._get_transaction_log() + self.assertEqual(REPEAT_TIMES, len(transaction_log)) + # assert all documents are in the remote log + self.assertEqual(REPEAT_TIMES, len(docs)) + for doc_id in docs: + self.assertEqual( + 1, + len(filter(lambda t: t[0] == doc_id, transaction_log))) - transaction_log = self.db._get_transaction_log() - self.assertEqual(REPEAT_TIMES, len(transaction_log)) - # assert all documents are in the remote log - self.assertEqual(REPEAT_TIMES, len(docs)) - for doc_id in docs: - self.assertEqual( - 1, - len(filter(lambda t: t[0] == doc_id, transaction_log))) - sol.close() + d.addCallback(_assert_logs) + d.addCallback(lambda _: sol.close()) + + return d def test_concurrent_syncs_do_not_fail(self): """ Assert that concurrent attempts to sync end up being executed sequentially and do not fail. """ - threads = [] docs = [] - pool = threading.BoundedSemaphore(value=1) + self.startServer() + sol = self._soledad_instance( auth_token='auth-token', server_url=self.getURL()) - def _run_method(self): - # create a lot of documents - doc = self._params['sol'].create_doc({}) - # do the sync! - sol.sync() - pool.acquire() - docs.append(doc.doc_id) - pool.release() - - # launch threads to create documents in parallel - for i in range(0, REPEAT_TIMES): - thread = self._WorkerThread( - {'sol': sol, 'syncs': i}, - _run_method) - thread.start() - threads.append(thread) - - # wait for threads to finish - for thread in threads: - thread.join() - - transaction_log = self.db._get_transaction_log() - self.assertEqual(REPEAT_TIMES, len(transaction_log)) - # assert all documents are in the remote log - self.assertEqual(REPEAT_TIMES, len(docs)) - for doc_id in docs: - self.assertEqual( - 1, - len(filter(lambda t: t[0] == doc_id, transaction_log))) - sol.close() + deferreds = [] + for i in xrange(0, REPEAT_TIMES): + d = sol.create_doc({}) + d.addCallback(lambda doc: docs.append(doc.doc_id)) + d.addCallback(lambda _: sol.sync()) + deferreds.append(d) + + def _assert_logs(results): + transaction_log = self.db._get_transaction_log() + self.assertEqual(REPEAT_TIMES, len(transaction_log)) + # assert all documents are in the remote log + self.assertEqual(REPEAT_TIMES, len(docs)) + for doc_id in docs: + self.assertEqual( + 1, + len(filter(lambda t: t[0] == doc_id, transaction_log))) + + d = defer.gatherResults(deferreds) + d.addCallback(_assert_logs) + d.addCallback(lambda _: sol.close()) + + return d diff --git a/common/src/leap/soledad/common/tests/test_crypto.py b/common/src/leap/soledad/common/tests/test_crypto.py index f5fb4b7a..fdad8aac 100644 --- a/common/src/leap/soledad/common/tests/test_crypto.py +++ b/common/src/leap/soledad/common/tests/test_crypto.py @@ -23,7 +23,7 @@ import binascii from leap.soledad.client import crypto from leap.soledad.common.document import SoledadDocument -from leap.soledad.common.tests import BaseSoledadTest +from leap.soledad.common.tests.util import BaseSoledadTest from leap.soledad.common.crypto import WrongMacError from leap.soledad.common.crypto import UnknownMacMethodError from leap.soledad.common.crypto import EncryptionMethods @@ -82,7 +82,7 @@ class RecoveryDocumentTestCase(BaseSoledadTest): rd = self._soledad.secrets._export_recovery_document() s = self._soledad_instance() s.secrets._import_recovery_document(rd) - s.set_secret_id(self._soledad.secrets._secret_id) + s.secrets.set_secret_id(self._soledad.secrets._secret_id) self.assertEqual(self._soledad.storage_secret, s.storage_secret, 'Failed settinng secret for symmetric encryption.') @@ -95,7 +95,7 @@ class SoledadSecretsTestCase(BaseSoledadTest): # instantiate and save secret_id sol = self._soledad_instance(user='user@leap.se') self.assertTrue(len(sol.secrets._secrets) == 1) - secret_id_1 = sol.secret_id + secret_id_1 = sol.secrets.secret_id # assert id is hash of secret self.assertTrue( secret_id_1 == hashlib.sha256(sol.storage_secret).hexdigest()) @@ -104,9 +104,8 @@ class SoledadSecretsTestCase(BaseSoledadTest): self.assertTrue(secret_id_1 != secret_id_2) sol.close() # re-instantiate - sol = self._soledad_instance( - user='user@leap.se', - secret_id=secret_id_1) + sol = self._soledad_instance(user='user@leap.se') + sol.secrets.set_secret_id(secret_id_1) # assert ids are valid self.assertTrue(len(sol.secrets._secrets) == 2) self.assertTrue(secret_id_1 in sol.secrets._secrets) @@ -117,7 +116,7 @@ class SoledadSecretsTestCase(BaseSoledadTest): secret_length = sol.secrets.GEN_SECRET_LENGTH self.assertTrue(len(sol.storage_secret) == secret_length) # assert format of secret 2 - sol.set_secret_id(secret_id_2) + sol.secrets.set_secret_id(secret_id_2) self.assertTrue(sol.storage_secret is not None) self.assertIsInstance(sol.storage_secret, str) self.assertTrue(len(sol.storage_secret) == secret_length) @@ -134,12 +133,12 @@ class SoledadSecretsTestCase(BaseSoledadTest): "Should have a secret at this point") # setting secret id to None should not interfere in the fact we have a # secret. - sol.set_secret_id(None) + sol.secrets.set_secret_id(None) self.assertTrue( sol.secrets._has_secret(), "Should have a secret at this point") # but not being able to decrypt correctly should - sol.secrets._secrets[sol.secret_id] = None + sol.secrets._secrets[sol.secrets.secret_id] = None self.assertFalse(sol.secrets._has_secret()) sol.close() diff --git a/common/src/leap/soledad/common/tests/test_http.py b/common/src/leap/soledad/common/tests/test_http.py index d21470e0..1f661b77 100644 --- a/common/src/leap/soledad/common/tests/test_http.py +++ b/common/src/leap/soledad/common/tests/test_http.py @@ -20,8 +20,6 @@ Test Leap backend bits: test http database from u1db.remote import http_database from leap.soledad.client import auth - -from leap.soledad.common.tests import u1db_tests as tests from leap.soledad.common.tests.u1db_tests import test_http_database @@ -59,6 +57,3 @@ class TestHTTPDatabaseWithCreds( 'token': 'auth-token', }}) self.assertIn('token', db1._creds) - - -load_tests = tests.load_with_scenarios diff --git a/common/src/leap/soledad/common/tests/test_http_client.py b/common/src/leap/soledad/common/tests/test_http_client.py index 3169398b..db731c32 100644 --- a/common/src/leap/soledad/common/tests/test_http_client.py +++ b/common/src/leap/soledad/common/tests/test_http_client.py @@ -21,8 +21,9 @@ import json from u1db.remote import http_client +from testscenarios import TestWithScenarios + from leap.soledad.client import auth -from leap.soledad.common.tests import u1db_tests as tests from leap.soledad.common.tests.u1db_tests import test_http_client from leap.soledad.server.auth import SoledadTokenAuthMiddleware @@ -31,7 +32,9 @@ from leap.soledad.server.auth import SoledadTokenAuthMiddleware # The following tests come from `u1db.tests.test_http_client`. #----------------------------------------------------------------------------- -class TestSoledadClientBase(test_http_client.TestHTTPClientBase): +class TestSoledadClientBase( + TestWithScenarios, + test_http_client.TestHTTPClientBase): """ This class should be used to test Token auth. """ @@ -90,7 +93,7 @@ class TestSoledadClientBase(test_http_client.TestHTTPClientBase): "message": e.message})] uuid, token = encoded.decode('base64').split(':', 1) if uuid != 'user-uuid' and token != 'auth-token': - return unauth_err("Incorrect address or token.") + return Exception("Incorrect address or token.") start_response("200 OK", [('Content-Type', 'application/json')]) return [json.dumps([environ['PATH_INFO'], uuid, token])] @@ -112,5 +115,3 @@ class TestSoledadClientBase(test_http_client.TestHTTPClientBase): res, headers = cli._request('GET', ['doc', 'token']) self.assertEqual( ['/dbase/doc/token', 'user-uuid', 'auth-token'], json.loads(res)) - -load_tests = tests.load_with_scenarios diff --git a/common/src/leap/soledad/common/tests/test_https.py b/common/src/leap/soledad/common/tests/test_https.py index b6288188..4dd55754 100644 --- a/common/src/leap/soledad/common/tests/test_https.py +++ b/common/src/leap/soledad/common/tests/test_https.py @@ -14,30 +14,35 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . + + """ Test Leap backend bits: https """ -from leap.soledad.common.tests import BaseSoledadTest -from leap.soledad.common.tests import test_sync_target as test_st -from leap.soledad.common.tests import u1db_tests as tests -from leap.soledad.common.tests.u1db_tests import test_backends -from leap.soledad.common.tests.u1db_tests import test_https -from leap.soledad import client -from leap.soledad.server import SoledadApp from u1db.remote import http_client +from leap.soledad import client + +from testscenarios import TestWithScenarios + +from leap.soledad.common.tests.u1db_tests import test_backends +from leap.soledad.common.tests.u1db_tests import test_https +from leap.soledad.common.tests.util import ( + BaseSoledadTest, + make_soledad_document_for_test, + make_soledad_app, + make_token_soledad_app, +) -def make_soledad_app(state): - return SoledadApp(state) LEAP_SCENARIOS = [ ('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': test_st.make_leap_document_for_test, - 'make_app_with_state': test_st.make_soledad_app}), + 'make_document_for_test': make_soledad_document_for_test, + 'make_app_with_state': make_soledad_app}), ] @@ -55,14 +60,15 @@ def token_leap_https_sync_target(test, host, path): class TestSoledadSyncTargetHttpsSupport( + TestWithScenarios, test_https.TestHttpSyncTargetHttpsSupport, BaseSoledadTest): scenarios = [ ('token_soledad_https', {'server_def': test_https.https_server_def, - 'make_app_with_state': test_st.make_token_soledad_app, - 'make_document_for_test': test_st.make_leap_document_for_test, + 'make_app_with_state': make_token_soledad_app, + 'make_document_for_test': make_soledad_document_for_test, 'sync_target': token_leap_https_sync_target}), ] @@ -71,8 +77,8 @@ class TestSoledadSyncTargetHttpsSupport( # run smoothly with standard u1db. test_https.TestHttpSyncTargetHttpsSupport.setUp(self) # so here monkey patch again to test our functionality. - http_client._VerifiedHTTPSConnection = client.VerifiedHTTPSConnection - client.SOLEDAD_CERT = http_client.CA_CERTS + http_client._VerifiedHTTPSConnection = client.api.VerifiedHTTPSConnection + client.api.SOLEDAD_CERT = http_client.CA_CERTS def test_working(self): """ @@ -83,7 +89,7 @@ class TestSoledadSyncTargetHttpsSupport( """ self.startServer() db = self.request_state._create_database('test') - self.patch(client, 'SOLEDAD_CERT', self.cacert_pem) + self.patch(client.api, 'SOLEDAD_CERT', self.cacert_pem) remote_target = self.getSyncTarget('localhost', 'test') remote_target.record_sync_info('other-id', 2, 'T-id') self.assertEqual( @@ -99,10 +105,8 @@ class TestSoledadSyncTargetHttpsSupport( """ self.startServer() self.request_state._create_database('test') - self.patch(client, 'SOLEDAD_CERT', self.cacert_pem) + self.patch(client.api, 'SOLEDAD_CERT', self.cacert_pem) remote_target = self.getSyncTarget('127.0.0.1', 'test') self.assertRaises( http_client.CertificateError, remote_target.record_sync_info, 'other-id', 2, 'T-id') - -load_tests = tests.load_with_scenarios diff --git a/common/src/leap/soledad/common/tests/test_server.py b/common/src/leap/soledad/common/tests/test_server.py index acd0a54c..836bd74a 100644 --- a/common/src/leap/soledad/common/tests/test_server.py +++ b/common/src/leap/soledad/common/tests/test_server.py @@ -19,29 +19,28 @@ Tests for server-related functionality. """ import os import tempfile -import simplejson as json import mock import time import binascii from urlparse import urljoin +from twisted.internet import defer -from leap.common.testing.basetest import BaseLeapTest from leap.soledad.common.couch import ( CouchServerState, CouchDatabase, ) -from leap.soledad.common.tests.u1db_tests import ( - TestCaseWithServer, - simple_doc, -) +from leap.soledad.common.tests.u1db_tests import TestCaseWithServer from leap.soledad.common.tests.test_couch import CouchDBTestCase -from leap.soledad.common.tests.test_target_soledad import ( +from leap.soledad.common.tests.util import ( make_token_soledad_app, - make_leap_document_for_test, + make_soledad_document_for_test, + token_soledad_sync_target, + BaseSoledadTest, ) -from leap.soledad.common.tests.test_sync_target import token_leap_sync_target -from leap.soledad.client import Soledad, crypto + +from leap.soledad.common import crypto +from leap.soledad.client import Soledad from leap.soledad.server import LockResource from leap.soledad.server.auth import URLToAuthorization @@ -58,7 +57,7 @@ def _couch_ensure_database(self, dbname): CouchServerState.ensure_database = _couch_ensure_database -class ServerAuthorizationTestCase(BaseLeapTest): +class ServerAuthorizationTestCase(BaseSoledadTest): """ Tests related to Soledad server authorization. """ @@ -272,19 +271,24 @@ class EncryptedSyncTestCase( Tests for encrypted sync using Soledad server backed by a couch database. """ + # increase twisted.trial's timeout because large files syncing might take + # some time to finish. + timeout = 500 + @staticmethod def make_app_with_state(state): return make_token_soledad_app(state) - make_document_for_test = make_leap_document_for_test + make_document_for_test = make_soledad_document_for_test - sync_target = token_leap_sync_target + sync_target = token_soledad_sync_target def _soledad_instance(self, user='user-uuid', passphrase=u'123', prefix='', - secrets_path=Soledad.STORAGE_SECRETS_FILE_NAME, - local_db_path='soledad.u1db', server_url='', - cert_file=None, auth_token=None, secret_id=None): + secrets_path='secrets.json', + local_db_path='soledad.u1db', + server_url='', + cert_file=None, auth_token=None): """ Instantiate Soledad. """ @@ -294,20 +298,15 @@ class EncryptedSyncTestCase( def _put_doc_side_effect(doc): self._doc_put = doc - # we need a mocked shared db or else Soledad will try to access the - # network to find if there are uploaded secrets. - class MockSharedDB(object): - - get_doc = mock.Mock(return_value=None) - put_doc = mock.Mock(side_effect=_put_doc_side_effect) - lock = mock.Mock(return_value=('atoken', 300)) - unlock = mock.Mock() - close = mock.Mock() - - def __call__(self): - return self + if not server_url: + # attempt to find the soledad server url + server_address = None + server = getattr(self, 'server', None) + if server: + server_address = getattr(self.server, 'server_address', None) + if server_address: + server_url = 'http://%s:%d' % (server_address) - Soledad._shared_db = MockSharedDB() return Soledad( user, passphrase, @@ -317,7 +316,7 @@ class EncryptedSyncTestCase( server_url=server_url, cert_file=cert_file, auth_token=auth_token, - secret_id=secret_id) + shared_db=self.get_default_shared_mock(_put_doc_side_effect)) def make_app(self): self.request_state = CouchServerState(self._couch_url, 'shared', @@ -325,70 +324,122 @@ class EncryptedSyncTestCase( return self.make_app_with_state(self.request_state) def setUp(self): - TestCaseWithServer.setUp(self) + # the order of the following initializations is crucial because of + # dependencies. + # XXX explain better CouchDBTestCase.setUp(self) - self.tempdir = tempfile.mkdtemp(prefix="leap_tests-") self._couch_url = 'http://localhost:' + str(self.wrapper.port) + self.tempdir = tempfile.mkdtemp(prefix="leap_tests-") + TestCaseWithServer.setUp(self) def tearDown(self): CouchDBTestCase.tearDown(self) TestCaseWithServer.tearDown(self) - def test_encrypted_sym_sync(self): + def _test_encrypted_sym_sync(self, passphrase=u'123', doc_size=2, + number_of_docs=1): """ Test the complete syncing chain between two soledad dbs using a Soledad server backed by a couch database. """ self.startServer() + # instantiate soledad and create a document sol1 = self._soledad_instance( # token is verified in test_target.make_token_soledad_app - auth_token='auth-token' - ) - _, doclist = sol1.get_all_docs() - self.assertEqual([], doclist) - doc1 = sol1.create_doc(json.loads(simple_doc)) + auth_token='auth-token', + passphrase=passphrase) + + # instantiate another soledad using the same secret as the previous + # one (so we can correctly verify the mac of the synced document) + sol2 = self._soledad_instance( + prefix='x', + auth_token='auth-token', + secrets_path=sol1._secrets_path, + passphrase=passphrase) + # ensure remote db exists before syncing db = CouchDatabase.open_database( urljoin(self._couch_url, 'user-user-uuid'), create=True, ensure_ddocs=True) - # sync with server - sol1._server_url = self.getURL() - sol1.sync() - # assert doc was sent to couch db - _, doclist = db.get_all_docs() - self.assertEqual(1, len(doclist)) - couchdoc = doclist[0] - # assert document structure in couch server - self.assertEqual(doc1.doc_id, couchdoc.doc_id) - self.assertEqual(doc1.rev, couchdoc.rev) - self.assertEqual(6, len(couchdoc.content)) - self.assertTrue(crypto.ENC_JSON_KEY in couchdoc.content) - self.assertTrue(crypto.ENC_SCHEME_KEY in couchdoc.content) - self.assertTrue(crypto.ENC_METHOD_KEY in couchdoc.content) - self.assertTrue(crypto.ENC_IV_KEY in couchdoc.content) - self.assertTrue(crypto.MAC_KEY in couchdoc.content) - self.assertTrue(crypto.MAC_METHOD_KEY in couchdoc.content) - # instantiate soledad with empty db, but with same secrets path - sol2 = self._soledad_instance(prefix='x', auth_token='auth-token') - _, doclist = sol2.get_all_docs() - self.assertEqual([], doclist) - sol2.secrets_path = sol1.secrets_path - sol2.secrets._load_secrets() - sol2.set_secret_id(sol1.secret_id) - # sync the new instance - sol2._server_url = self.getURL() - sol2.sync() - _, doclist = sol2.get_all_docs() - self.assertEqual(1, len(doclist)) - doc2 = doclist[0] - # assert incoming doc is equal to the first sent doc - self.assertEqual(doc1, doc2) - db.delete_database() - db.close() - sol1.close() - sol2.close() + + def _db1AssertEmptyDocList(results): + _, doclist = results + self.assertEqual([], doclist) + + def _db1CreateDocs(results): + deferreds = [] + for i in xrange(number_of_docs): + content = binascii.hexlify(os.urandom(doc_size/2)) + deferreds.append(sol1.create_doc({'data': content})) + return defer.DeferredList(deferreds) + + def _db1AssertDocsSyncedToServer(results): + _, sol_doclist = results + self.assertEqual(number_of_docs, len(sol_doclist)) + # assert doc was sent to couch db + _, couch_doclist = db.get_all_docs() + self.assertEqual(number_of_docs, len(couch_doclist)) + for i in xrange(number_of_docs): + soldoc = sol_doclist.pop() + couchdoc = couch_doclist.pop() + # assert document structure in couch server + self.assertEqual(soldoc.doc_id, couchdoc.doc_id) + self.assertEqual(soldoc.rev, couchdoc.rev) + self.assertEqual(6, len(couchdoc.content)) + self.assertTrue(crypto.ENC_JSON_KEY in couchdoc.content) + self.assertTrue(crypto.ENC_SCHEME_KEY in couchdoc.content) + self.assertTrue(crypto.ENC_METHOD_KEY in couchdoc.content) + self.assertTrue(crypto.ENC_IV_KEY in couchdoc.content) + self.assertTrue(crypto.MAC_KEY in couchdoc.content) + self.assertTrue(crypto.MAC_METHOD_KEY in couchdoc.content) + + d = sol1.get_all_docs() + d.addCallback(_db1AssertEmptyDocList) + d.addCallback(_db1CreateDocs) + d.addCallback(lambda _: sol1.sync()) + d.addCallback(lambda _: sol1.get_all_docs()) + d.addCallback(_db1AssertDocsSyncedToServer) + + def _db2AssertEmptyDocList(results): + _, doclist = results + self.assertEqual([], doclist) + + def _getAllDocsFromBothDbs(results): + d1 = sol1.get_all_docs() + d2 = sol2.get_all_docs() + return defer.DeferredList([d1, d2]) + + d.addCallback(lambda _: sol2.get_all_docs()) + d.addCallback(_db2AssertEmptyDocList) + d.addCallback(lambda _: sol2.sync()) + d.addCallback(_getAllDocsFromBothDbs) + + def _assertDocSyncedFromDb1ToDb2(results): + r1, r2 = results + _, (gen1, doclist1) = r1 + _, (gen2, doclist2) = r2 + self.assertEqual(number_of_docs, gen1) + self.assertEqual(number_of_docs, gen2) + self.assertEqual(number_of_docs, len(doclist1)) + self.assertEqual(number_of_docs, len(doclist2)) + self.assertEqual(doclist1[0], doclist2[0]) + + d.addCallback(_assertDocSyncedFromDb1ToDb2) + + def _cleanUp(results): + db.delete_database() + db.close() + sol1.close() + sol2.close() + + d.addCallback(_cleanUp) + + return d + + def test_encrypted_sym_sync(self): + return self._test_encrypted_sym_sync() def test_encrypted_sym_sync_with_unicode_passphrase(self): """ @@ -396,152 +447,20 @@ class EncryptedSyncTestCase( Soledad server backed by a couch database, using an unicode passphrase. """ - self.startServer() - # instantiate soledad and create a document - sol1 = self._soledad_instance( - # token is verified in test_target.make_token_soledad_app - auth_token='auth-token', - passphrase=u'ãáàäéàëíìïóòöõúùüñç', - ) - _, doclist = sol1.get_all_docs() - self.assertEqual([], doclist) - doc1 = sol1.create_doc(json.loads(simple_doc)) - # ensure remote db exists before syncing - db = CouchDatabase.open_database( - urljoin(self._couch_url, 'user-user-uuid'), - create=True, - ensure_ddocs=True) - # sync with server - sol1._server_url = self.getURL() - sol1.sync() - # assert doc was sent to couch db - _, doclist = db.get_all_docs() - self.assertEqual(1, len(doclist)) - couchdoc = doclist[0] - # assert document structure in couch server - self.assertEqual(doc1.doc_id, couchdoc.doc_id) - self.assertEqual(doc1.rev, couchdoc.rev) - self.assertEqual(6, len(couchdoc.content)) - self.assertTrue(crypto.ENC_JSON_KEY in couchdoc.content) - self.assertTrue(crypto.ENC_SCHEME_KEY in couchdoc.content) - self.assertTrue(crypto.ENC_METHOD_KEY in couchdoc.content) - self.assertTrue(crypto.ENC_IV_KEY in couchdoc.content) - self.assertTrue(crypto.MAC_KEY in couchdoc.content) - self.assertTrue(crypto.MAC_METHOD_KEY in couchdoc.content) - # instantiate soledad with empty db, but with same secrets path - sol2 = self._soledad_instance( - prefix='x', - auth_token='auth-token', - passphrase=u'ãáàäéàëíìïóòöõúùüñç', - ) - _, doclist = sol2.get_all_docs() - self.assertEqual([], doclist) - sol2.secrets_path = sol1.secrets_path - sol2.secrets._load_secrets() - sol2.set_secret_id(sol1.secret_id) - # sync the new instance - sol2._server_url = self.getURL() - sol2.sync() - _, doclist = sol2.get_all_docs() - self.assertEqual(1, len(doclist)) - doc2 = doclist[0] - # assert incoming doc is equal to the first sent doc - self.assertEqual(doc1, doc2) - db.delete_database() - db.close() - sol1.close() - sol2.close() + return self._test_encrypted_sym_sync(passphrase=u'ãáàäéàëíìïóòöõúùüñç') def test_sync_very_large_files(self): """ Test if Soledad can sync very large files. """ - # define the size of the "very large file" length = 100*(10**6) # 100 MB - self.startServer() - # instantiate soledad and create a document - sol1 = self._soledad_instance( - # token is verified in test_target.make_token_soledad_app - auth_token='auth-token' - ) - _, doclist = sol1.get_all_docs() - self.assertEqual([], doclist) - content = binascii.hexlify(os.urandom(length/2)) # len() == length - doc1 = sol1.create_doc({'data': content}) - # ensure remote db exists before syncing - db = CouchDatabase.open_database( - urljoin(self._couch_url, 'user-user-uuid'), - create=True, - ensure_ddocs=True) - # sync with server - sol1._server_url = self.getURL() - sol1.sync() - # instantiate soledad with empty db, but with same secrets path - sol2 = self._soledad_instance(prefix='x', auth_token='auth-token') - _, doclist = sol2.get_all_docs() - self.assertEqual([], doclist) - sol2.secrets_path = sol1.secrets_path - sol2.secrets._load_secrets() - sol2.set_secret_id(sol1.secret_id) - # sync the new instance - sol2._server_url = self.getURL() - sol2.sync() - _, doclist = sol2.get_all_docs() - self.assertEqual(1, len(doclist)) - doc2 = doclist[0] - # assert incoming doc is equal to the first sent doc - self.assertEqual(doc1, doc2) - # delete remote database - db.delete_database() - db.close() - sol1.close() - sol2.close() + return self._test_encrypted_sym_sync(doc_size=length, number_of_docs=1) def test_sync_many_small_files(self): """ Test if Soledad can sync many smallfiles. """ - number_of_docs = 100 - self.startServer() - # instantiate soledad and create a document - sol1 = self._soledad_instance( - # token is verified in test_target.make_token_soledad_app - auth_token='auth-token' - ) - _, doclist = sol1.get_all_docs() - self.assertEqual([], doclist) - # create many small files - for i in range(0, number_of_docs): - sol1.create_doc(json.loads(simple_doc)) - # ensure remote db exists before syncing - db = CouchDatabase.open_database( - urljoin(self._couch_url, 'user-user-uuid'), - create=True, - ensure_ddocs=True) - # sync with server - sol1._server_url = self.getURL() - sol1.sync() - # instantiate soledad with empty db, but with same secrets path - sol2 = self._soledad_instance(prefix='x', auth_token='auth-token') - _, doclist = sol2.get_all_docs() - self.assertEqual([], doclist) - sol2.secrets_path = sol1.secrets_path - sol2.secrets._load_secrets() - sol2.set_secret_id(sol1.secret_id) - # sync the new instance - sol2._server_url = self.getURL() - sol2.sync() - _, doclist = sol2.get_all_docs() - self.assertEqual(number_of_docs, len(doclist)) - # assert incoming docs are equal to sent docs - for doc in doclist: - self.assertEqual(sol1.get_doc(doc.doc_id), doc) - # delete remote database - db.delete_database() - db.close() - sol1.close() - sol2.close() - + return self._test_encrypted_sym_sync(doc_size=2, number_of_docs=100) class LockResourceTestCase( CouchDBTestCase, TestCaseWithServer): @@ -553,15 +472,18 @@ class LockResourceTestCase( def make_app_with_state(state): return make_token_soledad_app(state) - make_document_for_test = make_leap_document_for_test + make_document_for_test = make_soledad_document_for_test - sync_target = token_leap_sync_target + sync_target = token_soledad_sync_target def setUp(self): - TestCaseWithServer.setUp(self) + # the order of the following initializations is crucial because of + # dependencies. + # XXX explain better CouchDBTestCase.setUp(self) - self.tempdir = tempfile.mkdtemp(prefix="leap_tests-") self._couch_url = 'http://localhost:' + str(self.wrapper.port) + self.tempdir = tempfile.mkdtemp(prefix="leap_tests-") + TestCaseWithServer.setUp(self) # create the databases CouchDatabase.open_database( urljoin(self._couch_url, 'shared'), @@ -575,14 +497,14 @@ class LockResourceTestCase( self._couch_url, 'shared', 'tokens') def tearDown(self): - CouchDBTestCase.tearDown(self) - TestCaseWithServer.tearDown(self) # delete remote database db = CouchDatabase.open_database( urljoin(self._couch_url, 'shared'), create=True, ensure_ddocs=True) db.delete_database() + CouchDBTestCase.tearDown(self) + TestCaseWithServer.tearDown(self) def test__try_obtain_filesystem_lock(self): responder = mock.Mock() diff --git a/common/src/leap/soledad/common/tests/test_soledad.py b/common/src/leap/soledad/common/tests/test_soledad.py index 31c02fc4..0b49d9f5 100644 --- a/common/src/leap/soledad/common/tests/test_soledad.py +++ b/common/src/leap/soledad/common/tests/test_soledad.py @@ -20,9 +20,8 @@ Tests for general Soledad functionality. import os from mock import Mock - from leap.common.events import events_pb2 as proto -from leap.soledad.common.tests import ( +from leap.soledad.common.tests.util import ( BaseSoledadTest, ADDRESS, ) @@ -30,10 +29,9 @@ from leap import soledad from leap.soledad.common.document import SoledadDocument from leap.soledad.common.crypto import WrongMacError from leap.soledad.client import Soledad -from leap.soledad.client.sqlcipher import SQLCipherDatabase +from leap.soledad.client.adbapi import U1DBConnectionPool from leap.soledad.client.secrets import PassphraseTooShort from leap.soledad.client.shared_db import SoledadSharedDatabase -from leap.soledad.client.target import SoledadSyncTarget class AuxMethodsTestCase(BaseSoledadTest): @@ -41,18 +39,24 @@ class AuxMethodsTestCase(BaseSoledadTest): def test__init_dirs(self): sol = self._soledad_instance(prefix='_init_dirs') local_db_dir = os.path.dirname(sol.local_db_path) - secrets_path = os.path.dirname(sol.secrets_path) + secrets_path = os.path.dirname(sol.secrets.secrets_path) self.assertTrue(os.path.isdir(local_db_dir)) self.assertTrue(os.path.isdir(secrets_path)) - sol.close() - def test__init_db(self): + def _close_soledad(results): + sol.close() + + d = sol.create_doc({}) + d.addCallback(_close_soledad) + return d + + def test__init_u1db_sqlcipher_backend(self): sol = self._soledad_instance(prefix='_init_db') - self.assertIsInstance(sol._db, SQLCipherDatabase) + self.assertIsInstance(sol._dbpool, U1DBConnectionPool) self.assertTrue(os.path.isfile(sol.local_db_path)) sol.close() - def test__init_config_defaults(self): + def test__init_config_with_defaults(self): """ Test if configuration defaults point to the correct place. """ @@ -62,23 +66,16 @@ class AuxMethodsTestCase(BaseSoledadTest): def __init__(self): pass - # instantiate without initializing so we just test _init_config() + # instantiate without initializing so we just test + # _init_config_with_defaults() sol = SoledadMock() sol._passphrase = u'' - sol._secrets_path = None - sol._local_db_path = None sol._server_url = '' - sol._init_config() - # assert value of secrets_path - self.assertEquals( - os.path.join( - sol.DEFAULT_PREFIX, Soledad.STORAGE_SECRETS_FILE_NAME), - sol._secrets_path) + sol._init_config_with_defaults() # assert value of local_db_path self.assertEquals( - os.path.join(sol.DEFAULT_PREFIX, 'soledad.u1db'), + os.path.join(sol.default_prefix, 'soledad.u1db'), sol.local_db_path) - sol.close() def test__init_config_from_params(self): """ @@ -93,43 +90,56 @@ class AuxMethodsTestCase(BaseSoledadTest): cert_file=None) self.assertEqual( os.path.join(self.tempdir, 'value_3'), - sol.secrets_path) + sol.secrets.secrets_path) self.assertEqual( os.path.join(self.tempdir, 'value_2'), sol.local_db_path) - self.assertEqual('value_1', sol.server_url) + self.assertEqual('value_1', sol._server_url) sol.close() def test_change_passphrase(self): """ Test if passphrase can be changed. """ + prefix = '_change_passphrase' sol = self._soledad_instance( 'leap@leap.se', passphrase=u'123', - prefix=self.rand_prefix, + prefix=prefix, ) - doc = sol.create_doc({'simple': 'doc'}) - doc_id = doc.doc_id - - # change the passphrase - sol.change_passphrase(u'654321') - sol.close() - self.assertRaises( - WrongMacError, - self._soledad_instance, 'leap@leap.se', - passphrase=u'123', - prefix=self.rand_prefix) - - # use new passphrase and retrieve doc - sol2 = self._soledad_instance( - 'leap@leap.se', - passphrase=u'654321', - prefix=self.rand_prefix) - doc2 = sol2.get_doc(doc_id) - self.assertEqual(doc, doc2) - sol2.close() + def _change_passphrase(doc1): + self._doc1 = doc1 + sol.change_passphrase(u'654321') + sol.close() + + def _assert_wrong_password_raises(results): + self.assertRaises( + WrongMacError, + self._soledad_instance, 'leap@leap.se', + passphrase=u'123', + prefix=prefix) + + def _instantiate_with_new_passphrase(results): + sol2 = self._soledad_instance( + 'leap@leap.se', + passphrase=u'654321', + prefix=prefix) + self._sol2 = sol2 + return sol2.get_doc(self._doc1.doc_id) + + def _assert_docs_are_equal(doc2): + self.assertEqual(self._doc1, doc2) + self._sol2.close() + + d = sol.create_doc({'simple': 'doc'}) + d.addCallback(_change_passphrase) + d.addCallback(_assert_wrong_password_raises) + d.addCallback(_instantiate_with_new_passphrase) + d.addCallback(_assert_docs_are_equal) + d.addCallback(lambda _: sol.close()) + + return d def test_change_passphrase_with_short_passphrase_raises(self): """ @@ -150,7 +160,7 @@ class AuxMethodsTestCase(BaseSoledadTest): Assert passphrase getter works fine. """ sol = self._soledad_instance() - self.assertEqual('123', sol.passphrase) + self.assertEqual('123', sol._passphrase) sol.close() @@ -175,7 +185,7 @@ class SoledadSharedDBTestCase(BaseSoledadTest): doc_id = self._soledad.secrets._shared_db_doc_id() self._soledad.secrets._get_secrets_from_shared_db() self.assertTrue( - self._soledad._shared_db().get_doc.assert_called_with( + self._soledad.shared_db.get_doc.assert_called_with( doc_id) is None, 'Wrong doc_id when fetching recovery document.') @@ -186,11 +196,11 @@ class SoledadSharedDBTestCase(BaseSoledadTest): doc_id = self._soledad.secrets._shared_db_doc_id() self._soledad.secrets._put_secrets_in_shared_db() self.assertTrue( - self._soledad._shared_db().get_doc.assert_called_with( + self._soledad.shared_db.get_doc.assert_called_with( doc_id) is None, 'Wrong doc_id when fetching recovery document.') self.assertTrue( - self._soledad._shared_db.put_doc.assert_called_with( + self._soledad.shared_db.put_doc.assert_called_with( self._doc_put) is None, 'Wrong document when putting recovery document.') self.assertTrue( @@ -285,8 +295,8 @@ class SoledadSignalingTestCase(BaseSoledadTest): ADDRESS, ) # assert db was locked and unlocked - sol._shared_db.lock.assert_called_with() - sol._shared_db.unlock.assert_called_with('atoken') + sol.shared_db.lock.assert_called_with() + sol.shared_db.unlock.assert_called_with('atoken') sol.close() def test_stage2_bootstrap_signals(self): @@ -299,25 +309,15 @@ class SoledadSignalingTestCase(BaseSoledadTest): # create a document with secrets doc = SoledadDocument(doc_id=sol.secrets._shared_db_doc_id()) doc.content = sol.secrets._export_recovery_document() - - class Stage2MockSharedDB(object): - - get_doc = Mock(return_value=doc) - put_doc = Mock() - lock = Mock(return_value=('atoken', 300)) - unlock = Mock() - - def __call__(self): - return self - sol.close() # reset mock soledad.client.secrets.events.signal.reset_mock() # get a fresh instance so it emits all bootstrap signals + shared_db = self.get_default_shared_mock(get_doc_return_value=doc) sol = self._soledad_instance( secrets_path='alternative_stage2.json', local_db_path='alternative_stage2.u1db', - shared_db_class=Stage2MockSharedDB) + shared_db_class=shared_db) # reverse call order so we can verify in the order the signals were # expected soledad.client.secrets.events.signal.mock_calls.reverse() @@ -355,33 +355,17 @@ class SoledadSignalingTestCase(BaseSoledadTest): sol = self._soledad_instance() # mock the actual db sync so soledad does not try to connect to the # server - sol._db.sync = Mock() - # do the sync - sol.sync() - # assert the signal has been emitted - soledad.client.signal.assert_called_with( - proto.SOLEDAD_DONE_DATA_SYNC, - ADDRESS, - ) - sol.close() - - def test_need_sync_signals(self): - """ - Test Soledad emits SOLEDAD_CREATING_KEYS signal. - """ - soledad.client.signal.reset_mock() - sol = self._soledad_instance() - # mock the sync target - old_get_sync_info = SoledadSyncTarget.get_sync_info - SoledadSyncTarget.get_sync_info = Mock(return_value=[0, 0, 0, 0, 2]) - # mock our generation so soledad thinks there's new data to sync - sol._db._get_generation = Mock(return_value=1) - # check for new data to sync - sol.need_sync('http://provider/userdb') - # assert the signal has been emitted - soledad.client.signal.assert_called_with( - proto.SOLEDAD_NEW_DATA_TO_SYNC, - ADDRESS, - ) - SoledadSyncTarget.get_sync_info = old_get_sync_info - sol.close() + sol._dbsyncer.sync = Mock() + + def _assert_done_data_sync_signal_emitted(results): + # assert the signal has been emitted + soledad.client.signal.assert_called_with( + proto.SOLEDAD_DONE_DATA_SYNC, + ADDRESS, + ) + sol.close() + + # do the sync and assert signal was emitted + d = sol.sync() + d.addCallback(_assert_done_data_sync_signal_emitted) + return d diff --git a/common/src/leap/soledad/common/tests/test_soledad_app.py b/common/src/leap/soledad/common/tests/test_soledad_app.py new file mode 100644 index 00000000..6efae1d6 --- /dev/null +++ b/common/src/leap/soledad/common/tests/test_soledad_app.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# test_soledad_app.py +# Copyright (C) 2014 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +""" +Test ObjectStore and Couch backend bits. +""" + + +from testscenarios import TestWithScenarios + +from leap.soledad.common.tests.util import BaseSoledadTest +from leap.soledad.common.tests.util import make_soledad_document_for_test +from leap.soledad.common.tests.util import make_soledad_app +from leap.soledad.common.tests.util import make_token_soledad_app +from leap.soledad.common.tests.util import make_token_http_database_for_test +from leap.soledad.common.tests.util import copy_token_http_database_for_test +from leap.soledad.common.tests.u1db_tests import test_backends + + +#----------------------------------------------------------------------------- +# The following tests come from `u1db.tests.test_backends`. +#----------------------------------------------------------------------------- + +LEAP_SCENARIOS = [ + ('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_soledad_document_for_test, + 'make_app_with_state': make_soledad_app}), +] + + +class SoledadTests( + TestWithScenarios, test_backends.AllDatabaseTests, BaseSoledadTest): + + scenarios = LEAP_SCENARIOS + [ + ('token_http', {'make_database_for_test': + make_token_http_database_for_test, + 'copy_database_for_test': + copy_token_http_database_for_test, + 'make_document_for_test': make_soledad_document_for_test, + 'make_app_with_state': make_token_soledad_app, + }) + ] diff --git a/common/src/leap/soledad/common/tests/test_soledad_doc.py b/common/src/leap/soledad/common/tests/test_soledad_doc.py index 0952de6d..4a67f80a 100644 --- a/common/src/leap/soledad/common/tests/test_soledad_doc.py +++ b/common/src/leap/soledad/common/tests/test_soledad_doc.py @@ -17,28 +17,30 @@ """ Test Leap backend bits: soledad docs """ -from leap.soledad.common.tests import BaseSoledadTest +from testscenarios import TestWithScenarios + from leap.soledad.common.tests.u1db_tests import test_document -from leap.soledad.common.tests import u1db_tests as tests -from leap.soledad.common.tests import test_sync_target as st +from leap.soledad.common.tests.util import BaseSoledadTest +from leap.soledad.common.tests.util import make_soledad_document_for_test + #----------------------------------------------------------------------------- # The following tests come from `u1db.tests.test_document`. #----------------------------------------------------------------------------- - -class TestSoledadDocument(test_document.TestDocument, BaseSoledadTest): +class TestSoledadDocument( + TestWithScenarios, + test_document.TestDocument, BaseSoledadTest): scenarios = ([( 'leap', { - 'make_document_for_test': st.make_leap_document_for_test})]) + 'make_document_for_test': make_soledad_document_for_test})]) -class TestSoledadPyDocument(test_document.TestPyDocument, BaseSoledadTest): +class TestSoledadPyDocument( + TestWithScenarios, + test_document.TestPyDocument, BaseSoledadTest): scenarios = ([( 'leap', { - 'make_document_for_test': st.make_leap_document_for_test})]) - - -load_tests = tests.load_with_scenarios + 'make_document_for_test': make_soledad_document_for_test})]) diff --git a/common/src/leap/soledad/common/tests/test_sqlcipher.py b/common/src/leap/soledad/common/tests/test_sqlcipher.py index 78e2f01b..ceb095b8 100644 --- a/common/src/leap/soledad/common/tests/test_sqlcipher.py +++ b/common/src/leap/soledad/common/tests/test_sqlcipher.py @@ -20,10 +20,11 @@ Test sqlcipher backend internals. import os import time import threading - +import tempfile +import shutil from pysqlcipher import dbapi2 - +from testscenarios import TestWithScenarios # u1db stuff. from u1db import ( @@ -34,17 +35,16 @@ from u1db.backends.sqlite_backend import SQLitePartialExpandDatabase # soledad stuff. +from leap.soledad.common import soledad_assert from leap.soledad.common.document import SoledadDocument from leap.soledad.client.sqlcipher import ( SQLCipherDatabase, SQLCipherOptions, DatabaseIsNotEncrypted, - initialize_sqlcipher_db, ) # u1db tests stuff. -from leap.common.testing.basetest import BaseLeapTest from leap.soledad.common.tests import u1db_tests as tests from leap.soledad.common.tests.u1db_tests import test_sqlite_backend from leap.soledad.common.tests.u1db_tests import test_backends @@ -53,12 +53,12 @@ from leap.soledad.common.tests.util import ( make_sqlcipher_database_for_test, copy_sqlcipher_database_for_test, PASSWORD, + BaseSoledadTest, ) def sqlcipher_open(path, passphrase, create=True, document_factory=None): return SQLCipherDatabase( - None, SQLCipherOptions(path, passphrase, create=create)) @@ -93,30 +93,34 @@ SQLCIPHER_SCENARIOS = [ ] -class SQLCipherTests(test_backends.AllDatabaseTests): +class SQLCipherTests(TestWithScenarios, test_backends.AllDatabaseTests): scenarios = SQLCIPHER_SCENARIOS -class SQLCipherDatabaseTests(test_backends.LocalDatabaseTests): +class SQLCipherDatabaseTests(TestWithScenarios, test_backends.LocalDatabaseTests): scenarios = SQLCIPHER_SCENARIOS class SQLCipherValidateGenNTransIdTests( + TestWithScenarios, test_backends.LocalDatabaseValidateGenNTransIdTests): scenarios = SQLCIPHER_SCENARIOS class SQLCipherValidateSourceGenTests( + TestWithScenarios, test_backends.LocalDatabaseValidateSourceGenTests): scenarios = SQLCIPHER_SCENARIOS class SQLCipherWithConflictsTests( + TestWithScenarios, test_backends.LocalDatabaseWithConflictsTests): scenarios = SQLCIPHER_SCENARIOS -class SQLCipherIndexTests(test_backends.DatabaseIndexTests): +class SQLCipherIndexTests( + TestWithScenarios, test_backends.DatabaseIndexTests): scenarios = SQLCIPHER_SCENARIOS @@ -124,7 +128,7 @@ class SQLCipherIndexTests(test_backends.DatabaseIndexTests): # The following tests come from `u1db.tests.test_sqlite_backend`. #----------------------------------------------------------------------------- -class TestSQLCipherDatabase(test_sqlite_backend.TestSQLiteDatabase): +class TestSQLCipherDatabase(TestWithScenarios, test_sqlite_backend.TestSQLiteDatabase): def test_atomic_initialize(self): # This test was modified to ensure that db2.close() is called within @@ -137,11 +141,11 @@ class TestSQLCipherDatabase(test_sqlite_backend.TestSQLiteDatabase): class SQLCipherDatabaseTesting(SQLCipherDatabase): _index_storage_value = "testing" - def __init__(self, soledad_crypto, dbname, ntry): + def __init__(self, dbname, ntry): self._try = ntry self._is_initialized_invocations = 0 SQLCipherDatabase.__init__( - self, soledad_crypto, + self, SQLCipherOptions(dbname, PASSWORD)) def _is_initialized(self, c): @@ -161,15 +165,14 @@ class TestSQLCipherDatabase(test_sqlite_backend.TestSQLiteDatabase): def run(self): try: - db2 = SQLCipherDatabaseTesting(None, dbname, 2) + db2 = SQLCipherDatabaseTesting(dbname, 2) except Exception, e: SecondTry.outcome2.append(e) else: SecondTry.outcome2.append(db2) - self.close() t2 = SecondTry() - db1 = SQLCipherDatabaseTesting(None, dbname, 1) + db1 = SQLCipherDatabaseTesting(dbname, 1) t2.join() self.assertIsInstance(SecondTry.outcome2[0], SQLCipherDatabaseTesting) @@ -368,7 +371,7 @@ class SQLCipherOpen(test_open.TestU1DBOpen): # Tests for actual encryption of the database #----------------------------------------------------------------------------- -class SQLCipherEncryptionTest(BaseLeapTest): +class SQLCipherEncryptionTest(BaseSoledadTest): """ Tests to guarantee SQLCipher is indeed encrypting data when storing. """ @@ -379,11 +382,37 @@ class SQLCipherEncryptionTest(BaseLeapTest): os.unlink(dbfile) def setUp(self): + # the following come from BaseLeapTest.setUpClass, because + # twisted.trial doesn't support such class methods for setting up + # test classes. + self.old_path = os.environ['PATH'] + self.old_home = os.environ['HOME'] + self.tempdir = tempfile.mkdtemp(prefix="leap_tests-") + self.home = self.tempdir + bin_tdir = os.path.join( + self.tempdir, + 'bin') + os.environ["PATH"] = bin_tdir + os.environ["HOME"] = self.tempdir + # this is our own stuff self.DB_FILE = os.path.join(self.tempdir, 'test.db') self._delete_dbfiles() def tearDown(self): self._delete_dbfiles() + # the following come from BaseLeapTest.tearDownClass, because + # twisted.trial doesn't support such class methods for tearing down + # test classes. + os.environ["PATH"] = self.old_path + os.environ["HOME"] = self.old_home + # safety check! please do not wipe my home... + # XXX needs to adapt to non-linuces + soledad_assert( + self.tempdir.startswith('/tmp/leap_tests-') or + self.tempdir.startswith('/var/folder'), + "beware! tried to remove a dir which does not " + "live in temporal folder!") + shutil.rmtree(self.tempdir) def test_try_to_open_encrypted_db_with_sqlite_backend(self): """ @@ -426,6 +455,3 @@ class SQLCipherEncryptionTest(BaseLeapTest): "dbs.") except DatabaseIsNotEncrypted: pass - - -load_tests = tests.load_with_scenarios diff --git a/common/src/leap/soledad/common/tests/test_sqlcipher_sync.py b/common/src/leap/soledad/common/tests/test_sqlcipher_sync.py index ad2a06b3..83c3449e 100644 --- a/common/src/leap/soledad/common/tests/test_sqlcipher_sync.py +++ b/common/src/leap/soledad/common/tests/test_sqlcipher_sync.py @@ -19,12 +19,14 @@ Test sqlcipher backend sync. """ +import os import simplejson as json from u1db import ( sync, vectorclock, ) +from testscenarios import TestWithScenarios from leap.soledad.common.crypto import ENC_SCHEME_KEY from leap.soledad.client.target import SoledadSyncTarget @@ -33,15 +35,12 @@ from leap.soledad.client.sqlcipher import ( SQLCipherDatabase, ) - -from leap.soledad.common.tests import u1db_tests as tests, BaseSoledadTest +from leap.soledad.common.tests import u1db_tests as tests from leap.soledad.common.tests.u1db_tests import test_sync -from leap.soledad.common.tests.test_sqlcipher import ( - SQLCIPHER_SCENARIOS, - make_document_for_test, -) +from leap.soledad.common.tests.test_sqlcipher import SQLCIPHER_SCENARIOS from leap.soledad.common.tests.util import ( make_soledad_app, + BaseSoledadTest, SoledadWithCouchServerMixin, ) @@ -50,15 +49,7 @@ from leap.soledad.common.tests.util import ( # 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, +def sync_via_synchronizer_and_soledad(test, db_source, db_target, trace_hook=None, trace_hook_shallow=None): if trace_hook: test.skipTest("full trace hook unsupported over http") @@ -71,17 +62,16 @@ def sync_via_synchronizer_and_leap(test, db_source, db_target, 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': make_document_for_test, - 'make_app_with_state': tests.test_remote_sync_target.make_http_app, - 'do_sync': test_sync.sync_via_synchronizer, -})) +sync_scenarios = [] +for name, scenario in SQLCIPHER_SCENARIOS: + scenario['do_sync'] = test_sync.sync_via_synchronizer + sync_scenarios.append((name, scenario)) class SQLCipherDatabaseSyncTests( - test_sync.DatabaseSyncTests, BaseSoledadTest): + TestWithScenarios, + test_sync.DatabaseSyncTests, + BaseSoledadTest): """ Test for succesfull sync between SQLCipher and LeapBackend. @@ -92,8 +82,8 @@ class SQLCipherDatabaseSyncTests( scenarios = sync_scenarios - def setUp(self): - test_sync.DatabaseSyncTests.setUp(self) + #def setUp(self): + # test_sync.DatabaseSyncTests.setUp(self) def tearDown(self): test_sync.DatabaseSyncTests.tearDown(self) @@ -112,8 +102,6 @@ class SQLCipherDatabaseSyncTests( and isinstance(self.db3, SQLCipherDatabase): self.db3.close() - - def test_sync_autoresolves(self): """ Test for sync autoresolve remote. @@ -321,12 +309,14 @@ target_scenarios = [ 'create_db_and_target': _make_local_db_and_token_http_target, # 'make_app_with_state': tests.test_remote_sync_target.make_http_app, 'make_app_with_state': make_soledad_app, - 'do_sync': test_sync.sync_via_synchronizer}), + 'do_sync': sync_via_synchronizer_and_soledad}), ] class SQLCipherSyncTargetTests( - SoledadWithCouchServerMixin, test_sync.DatabaseSyncTargetTests): + TestWithScenarios, + SoledadWithCouchServerMixin, + test_sync.DatabaseSyncTargetTests): scenarios = (tests.multiply_scenarios(SQLCIPHER_SCENARIOS, target_scenarios)) @@ -406,4 +396,3 @@ class SQLCipherSyncTargetTests( self.db._last_exchange_log['return'], {'last_gen': 2, 'docs': [(doc.doc_id, doc.rev), (doc2.doc_id, doc2.rev)]}) - diff --git a/common/src/leap/soledad/common/tests/test_sync.py b/common/src/leap/soledad/common/tests/test_sync.py index 0433fac9..893df56b 100644 --- a/common/src/leap/soledad/common/tests/test_sync.py +++ b/common/src/leap/soledad/common/tests/test_sync.py @@ -16,43 +16,35 @@ # along with this program. If not, see . -import mock -import os import json import tempfile import threading import time + from urlparse import urljoin +from twisted.internet import defer + +from testscenarios import TestWithScenarios from leap.soledad.common import couch +from leap.soledad.client import target +from leap.soledad.client import sync +from leap.soledad.server import SoledadApp -from leap.soledad.common.tests import BaseSoledadTest -from leap.soledad.common.tests import test_sync_target from leap.soledad.common.tests import u1db_tests as tests -from leap.soledad.common.tests.u1db_tests import ( - TestCaseWithServer, - simple_doc, - test_backends, - test_sync -) -from leap.soledad.common.tests.test_couch import CouchDBTestCase -from leap.soledad.common.tests.test_target_soledad import ( - make_token_soledad_app, - make_leap_document_for_test, -) -from leap.soledad.common.tests.test_sync_target import token_leap_sync_target -from leap.soledad.client import ( - Soledad, - target, -) +from leap.soledad.common.tests.u1db_tests import TestCaseWithServer +from leap.soledad.common.tests.u1db_tests import simple_doc +from leap.soledad.common.tests.u1db_tests import test_sync +from leap.soledad.common.tests.util import make_token_soledad_app +from leap.soledad.common.tests.util import make_soledad_document_for_test +from leap.soledad.common.tests.util import token_soledad_sync_target +from leap.soledad.common.tests.util import BaseSoledadTest from leap.soledad.common.tests.util import SoledadWithCouchServerMixin -from leap.soledad.client.sync import SoledadSynchronizer -from leap.soledad.server import SoledadApp - +from leap.soledad.common.tests.test_couch import CouchDBTestCase class InterruptableSyncTestCase( - CouchDBTestCase, TestCaseWithServer): + BaseSoledadTest, CouchDBTestCase, TestCaseWithServer): """ Tests for encrypted sync using Soledad server backed by a couch database. """ @@ -61,47 +53,9 @@ class InterruptableSyncTestCase( def make_app_with_state(state): return make_token_soledad_app(state) - make_document_for_test = make_leap_document_for_test - - sync_target = token_leap_sync_target + make_document_for_test = make_soledad_document_for_test - def _soledad_instance(self, user='user-uuid', passphrase=u'123', - prefix='', - secrets_path=Soledad.STORAGE_SECRETS_FILE_NAME, - local_db_path='soledad.u1db', server_url='', - cert_file=None, auth_token=None, secret_id=None): - """ - Instantiate Soledad. - """ - - # this callback ensures we save a document which is sent to the shared - # db. - def _put_doc_side_effect(doc): - self._doc_put = doc - - # we need a mocked shared db or else Soledad will try to access the - # network to find if there are uploaded secrets. - class MockSharedDB(object): - - get_doc = mock.Mock(return_value=None) - put_doc = mock.Mock(side_effect=_put_doc_side_effect) - lock = mock.Mock(return_value=('atoken', 300)) - unlock = mock.Mock() - - def __call__(self): - return self - - Soledad._shared_db = MockSharedDB() - return Soledad( - user, - passphrase, - secrets_path=os.path.join(self.tempdir, prefix, secrets_path), - local_db_path=os.path.join( - self.tempdir, prefix, local_db_path), - server_url=server_url, - cert_file=cert_file, - auth_token=auth_token, - secret_id=secret_id) + sync_target = token_soledad_sync_target def make_app(self): self.request_state = couch.CouchServerState( @@ -135,7 +89,8 @@ class InterruptableSyncTestCase( def run(self): while db._get_generation() < 2: - time.sleep(1) + #print "WAITING %d" % db._get_generation() + time.sleep(0.1) self._soledad.stop_sync() time.sleep(1) @@ -143,16 +98,7 @@ class InterruptableSyncTestCase( self.startServer() # instantiate soledad and create a document - sol = self._soledad_instance( - # token is verified in test_target.make_token_soledad_app - auth_token='auth-token' - ) - _, doclist = sol.get_all_docs() - self.assertEqual([], doclist) - - # create many small files - for i in range(0, number_of_docs): - sol.create_doc(json.loads(simple_doc)) + sol = self._soledad_instance(user='user-uuid', server_url=self.getURL()) # ensure remote db exists before syncing db = couch.CouchDatabase.open_database( @@ -164,21 +110,35 @@ class InterruptableSyncTestCase( t = _SyncInterruptor(sol, db) t.start() - # sync with server - sol._server_url = self.getURL() - sol.sync() # this will be interrupted when couch db gen >= 2 - t.join() + d = sol.get_all_docs() + d.addCallback(lambda results: self.assertEqual([], results[1])) - # recover the sync process - sol.sync() + def _create_docs(results): + # create many small files + deferreds = [] + for i in range(0, number_of_docs): + deferreds.append(sol.create_doc(json.loads(simple_doc))) + return defer.DeferredList(deferreds) - gen, doclist = db.get_all_docs() - self.assertEqual(number_of_docs, len(doclist)) - - # delete remote database - db.delete_database() - db.close() - sol.close() + # sync with server + d.addCallback(_create_docs) + d.addCallback(lambda _: sol.get_all_docs()) + d.addCallback(lambda results: self.assertEqual(number_of_docs, len(results[1]))) + d.addCallback(lambda _: sol.sync()) + d.addCallback(lambda _: t.join()) + d.addCallback(lambda _: db.get_all_docs()) + d.addCallback(lambda results: self.assertNotEqual(number_of_docs, len(results[1]))) + d.addCallback(lambda _: sol.sync()) + d.addCallback(lambda _: db.get_all_docs()) + d.addCallback(lambda results: self.assertEqual(number_of_docs, len(results[1]))) + + def _tear_down(results): + db.delete_database() + db.close() + sol.close() + + d.addCallback(_tear_down) + return d def make_soledad_app(state): @@ -186,6 +146,7 @@ def make_soledad_app(state): class TestSoledadDbSync( + TestWithScenarios, SoledadWithCouchServerMixin, test_sync.TestDbSync): """ @@ -198,7 +159,7 @@ class TestSoledadDbSync( 'make_database_for_test': tests.make_memory_database_for_test, }), ('py-token-http', { - 'make_app_with_state': test_sync_target.make_token_soledad_app, + 'make_app_with_state': make_token_soledad_app, 'make_database_for_test': tests.make_memory_database_for_test, 'token': True }), @@ -211,10 +172,11 @@ class TestSoledadDbSync( """ Need to explicitely invoke inicialization on all bases. """ - tests.TestCaseWithServer.setUp(self) - self.main_test_class = test_sync.TestDbSync + #tests.TestCaseWithServer.setUp(self) + #self.main_test_class = test_sync.TestDbSync SoledadWithCouchServerMixin.setUp(self) self.startServer() + self.db = self.make_database_for_test(self, 'test1') self.db2 = couch.CouchDatabase.open_database( urljoin( 'http://localhost:' + str(self.wrapper.port), 'test'), @@ -227,7 +189,7 @@ class TestSoledadDbSync( """ self.db2.delete_database() SoledadWithCouchServerMixin.tearDown(self) - tests.TestCaseWithServer.tearDown(self) + #tests.TestCaseWithServer.tearDown(self) def do_sync(self, target_name): """ @@ -240,7 +202,7 @@ class TestSoledadDbSync( 'token': 'auth-token', }}) target_url = self.getURL(target_name) - return SoledadSynchronizer( + return sync.SoledadSynchronizer( self.db, target.SoledadSyncTarget( target_url, @@ -254,8 +216,10 @@ class TestSoledadDbSync( Adapted to check for encrypted content. """ + doc1 = self.db.create_doc_from_json(tests.simple_doc) doc2 = self.db2.create_doc_from_json(tests.nested_doc) + local_gen_before_sync = self.do_sync('test') gen, _, changes = self.db.whats_changed(local_gen_before_sync) self.assertEqual(1, len(changes)) @@ -287,6 +251,3 @@ class TestSoledadDbSync( s_gen, _ = db3._get_replica_gen_and_trans_id('test1') self.assertEqual(1, t_gen) self.assertEqual(1, s_gen) - - -load_tests = tests.load_with_scenarios diff --git a/common/src/leap/soledad/common/tests/test_sync_deferred.py b/common/src/leap/soledad/common/tests/test_sync_deferred.py index 07a9742b..26889aff 100644 --- a/common/src/leap/soledad/common/tests/test_sync_deferred.py +++ b/common/src/leap/soledad/common/tests/test_sync_deferred.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # test_sync_deferred.py # Copyright (C) 2014 LEAP # @@ -21,28 +20,31 @@ import time import os import random import string +import shutil + from urlparse import urljoin -from leap.soledad.common.tests import u1db_tests as tests, ADDRESS +from leap.soledad.common import couch +from leap.soledad.client.sqlcipher import ( + SQLCipherOptions, + SQLCipherDatabase, + SQLCipherU1DBSync, +) + +from testscenarios import TestWithScenarios + +from leap.soledad.common.tests import u1db_tests as tests from leap.soledad.common.tests.u1db_tests import test_sync +from leap.soledad.common.tests.util import ADDRESS +from leap.soledad.common.tests.util import SoledadWithCouchServerMixin +from leap.soledad.common.tests.util import make_soledad_app -from leap.soledad.common.document import SoledadDocument -from leap.soledad.common import couch -from leap.soledad.client import target -from leap.soledad.client.sync import SoledadSynchronizer # Just to make clear how this test is different... :) DEFER_DECRYPTION = True WAIT_STEP = 1 MAX_WAIT = 10 - - -from leap.soledad.client.sqlcipher import open as open_sqlcipher -from leap.soledad.common.tests.util import SoledadWithCouchServerMixin -from leap.soledad.common.tests.util import make_soledad_app - - DBPASS = "pass" @@ -54,8 +56,10 @@ class BaseSoledadDeferredEncTest(SoledadWithCouchServerMixin): defer_sync_encryption = True def setUp(self): + SoledadWithCouchServerMixin.setUp(self) # config info self.db1_file = os.path.join(self.tempdir, "db1.u1db") + os.unlink(self.db1_file) self.db_pass = DBPASS self.email = ADDRESS @@ -64,17 +68,21 @@ class BaseSoledadDeferredEncTest(SoledadWithCouchServerMixin): # each local db. self.rand_prefix = ''.join( map(lambda x: random.choice(string.ascii_letters), range(6))) - # initialize soledad by hand so we can control keys - self._soledad = self._soledad_instance( - prefix=self.rand_prefix, user=self.email) - - # open test dbs: db1 will be the local sqlcipher db - # (which instantiates a syncdb) - self.db1 = open_sqlcipher(self.db1_file, DBPASS, create=True, - document_factory=SoledadDocument, - crypto=self._soledad._crypto, - defer_encryption=True, - sync_db_key=DBPASS) + + # open test dbs: db1 will be the local sqlcipher db (which + # instantiates a syncdb). We use the self._soledad instance that was + # already created on some setUp method. + import binascii + tohex = binascii.b2a_hex + key = tohex(self._soledad.secrets.get_local_storage_key()) + sync_db_key = tohex(self._soledad.secrets.get_sync_db_key()) + dbpath = self._soledad._local_db_path + + self.opts = SQLCipherOptions( + dbpath, key, is_raw_key=True, create=False, + defer_encryption=True, sync_db_key=sync_db_key) + self.db1 = SQLCipherDatabase(self.opts) + self.db2 = couch.CouchDatabase.open_database( urljoin( 'http://localhost:' + str(self.wrapper.port), 'test'), @@ -87,20 +95,8 @@ class BaseSoledadDeferredEncTest(SoledadWithCouchServerMixin): self._soledad.close() # XXX should not access "private" attrs - import shutil shutil.rmtree(os.path.dirname(self._soledad._local_db_path)) - - -#SQLCIPHER_SCENARIOS = [ -# ('http', { -# #'make_app_with_state': test_sync_target.make_token_soledad_app, -# 'make_app_with_state': make_soledad_app, -# 'make_database_for_test': ts.make_sqlcipher_database_for_test, -# 'copy_database_for_test': ts.copy_sqlcipher_database_for_test, -# 'make_document_for_test': ts.make_document_for_test, -# 'token': True -# }), -#] + SoledadWithCouchServerMixin.tearDown(self) class SyncTimeoutError(Exception): @@ -111,8 +107,9 @@ class SyncTimeoutError(Exception): class TestSoledadDbSyncDeferredEncDecr( - BaseSoledadDeferredEncTest, - test_sync.TestDbSync): + TestWithScenarios, + test_sync.TestDbSync, + BaseSoledadDeferredEncTest): """ Test db.sync remote sync shortcut. Case with deferred encryption and decryption: using the intermediate @@ -129,13 +126,17 @@ class TestSoledadDbSyncDeferredEncDecr( oauth = False token = True + def make_app(self): + self.request_state = couch.CouchServerState( + self._couch_url, 'shared', 'tokens') + return self.make_app_with_state(self.request_state) + def setUp(self): """ Need to explicitely invoke inicialization on all bases. """ - tests.TestCaseWithServer.setUp(self) - self.main_test_class = test_sync.TestDbSync BaseSoledadDeferredEncTest.setUp(self) + self.server = self.server_thread = None self.startServer() self.syncer = None @@ -143,8 +144,10 @@ class TestSoledadDbSyncDeferredEncDecr( """ Need to explicitely invoke destruction on all bases. """ + dbsyncer = getattr(self, 'dbsyncer', None) + if dbsyncer: + dbsyncer.close() BaseSoledadDeferredEncTest.tearDown(self) - tests.TestCaseWithServer.tearDown(self) def do_sync(self, target_name): """ @@ -152,25 +155,20 @@ class TestSoledadDbSyncDeferredEncDecr( and Token auth. """ if self.token: - extra = dict(creds={'token': { + creds={'token': { 'uuid': 'user-uuid', 'token': 'auth-token', - }}) + }} target_url = self.getURL(target_name) - syncdb = getattr(self.db1, "_sync_db", None) - - syncer = SoledadSynchronizer( - self.db1, - target.SoledadSyncTarget( - target_url, - crypto=self._soledad._crypto, - sync_db=syncdb, - **extra)) - # Keep a reference to be able to know when the sync - # has finished. - self.syncer = syncer - return syncer.sync( - autocreate=True, defer_decryption=DEFER_DECRYPTION) + + # get a u1db syncer + crypto = self._soledad._crypto + replica_uid = self.db1._replica_uid + dbsyncer = SQLCipherU1DBSync(self.opts, crypto, replica_uid, + defer_encryption=True) + self.dbsyncer = dbsyncer + return dbsyncer.sync(target_url, creds=creds, + autocreate=True,defer_decryption=DEFER_DECRYPTION) else: return test_sync.TestDbSync.do_sync(self, target_name) @@ -195,28 +193,30 @@ class TestSoledadDbSyncDeferredEncDecr( """ doc1 = self.db1.create_doc_from_json(tests.simple_doc) doc2 = self.db2.create_doc_from_json(tests.nested_doc) + d = self.do_sync('test') - import time - # need to give time to the encryption to proceed - # TODO should implement a defer list to subscribe to the all-decrypted - # event - time.sleep(2) + def _assert_successful_sync(results): + import time + # need to give time to the encryption to proceed + # TODO should implement a defer list to subscribe to the all-decrypted + # event + time.sleep(2) + local_gen_before_sync = results + self.wait_for_sync() - local_gen_before_sync = self.do_sync('test') - self.wait_for_sync() + gen, _, changes = self.db1.whats_changed(local_gen_before_sync) + self.assertEqual(1, len(changes)) - gen, _, changes = self.db1.whats_changed(local_gen_before_sync) - self.assertEqual(1, len(changes)) + self.assertEqual(doc2.doc_id, changes[0][0]) + self.assertEqual(1, gen - local_gen_before_sync) - self.assertEqual(doc2.doc_id, changes[0][0]) - self.assertEqual(1, gen - local_gen_before_sync) + self.assertGetEncryptedDoc( + self.db2, doc1.doc_id, doc1.rev, tests.simple_doc, False) + self.assertGetEncryptedDoc( + self.db1, doc2.doc_id, doc2.rev, tests.nested_doc, False) - self.assertGetEncryptedDoc( - self.db2, doc1.doc_id, doc1.rev, tests.simple_doc, False) - self.assertGetEncryptedDoc( - self.db1, doc2.doc_id, doc2.rev, tests.nested_doc, False) + d.addCallback(_assert_successful_sync) + return d def test_db_sync_autocreate(self): pass - -load_tests = tests.load_with_scenarios diff --git a/common/src/leap/soledad/common/tests/test_sync_target.py b/common/src/leap/soledad/common/tests/test_sync_target.py index 45009f4e..3792159a 100644 --- a/common/src/leap/soledad/common/tests/test_sync_target.py +++ b/common/src/leap/soledad/common/tests/test_sync_target.py @@ -19,82 +19,38 @@ Test Leap backend bits: sync target """ import cStringIO import os - +import time import simplejson as json import u1db +import random +import string +import shutil + +from testscenarios import TestWithScenarios +from urlparse import urljoin -from u1db.remote import http_database +from leap.soledad.client import target +from leap.soledad.client import crypto +from leap.soledad.client.sqlcipher import SQLCipherU1DBSync +from leap.soledad.client.sqlcipher import SQLCipherOptions +from leap.soledad.client.sqlcipher import SQLCipherDatabase -from leap.soledad.client import ( - target, - auth, - crypto, - sync, -) +from leap.soledad.common import couch from leap.soledad.common.document import SoledadDocument from leap.soledad.common.tests import u1db_tests as tests -from leap.soledad.common.tests import BaseSoledadTest -from leap.soledad.common.tests.util import ( - make_sqlcipher_database_for_test, - make_soledad_app, - make_token_soledad_app, - SoledadWithCouchServerMixin, -) -from leap.soledad.common.tests.u1db_tests import test_backends +from leap.soledad.common.tests.util import make_sqlcipher_database_for_test +from leap.soledad.common.tests.util import make_soledad_app +from leap.soledad.common.tests.util import make_token_soledad_app +from leap.soledad.common.tests.util import make_soledad_document_for_test +from leap.soledad.common.tests.util import token_soledad_sync_target +from leap.soledad.common.tests.util import BaseSoledadTest +from leap.soledad.common.tests.util import SoledadWithCouchServerMixin +from leap.soledad.common.tests.util import ADDRESS from leap.soledad.common.tests.u1db_tests import test_remote_sync_target from leap.soledad.common.tests.u1db_tests import test_sync -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_backends`. -#----------------------------------------------------------------------------- - -def make_leap_document_for_test(test, doc_id, rev, content, - has_conflicts=False): - return SoledadDocument( - doc_id, rev, content, has_conflicts=has_conflicts) - - -LEAP_SCENARIOS = [ - ('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_soledad_app}), -] - - -def make_token_http_database_for_test(test, replica_uid): - test.startServer() - test.request_state._create_database(replica_uid) - - class _HTTPDatabaseWithToken( - http_database.HTTPDatabase, auth.TokenBasedAuth): - - def set_token_credentials(self, uuid, token): - auth.TokenBasedAuth.set_token_credentials(self, uuid, token) - - def _sign_request(self, method, url_query, params): - return auth.TokenBasedAuth._sign_request( - self, method, url_query, params) - - http_db = _HTTPDatabaseWithToken(test.getURL('test')) - http_db.set_token_credentials('user-uuid', 'auth-token') - return http_db - - -def copy_token_http_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. - http_db = test.request_state._copy_database(db) - http_db.set_token_credentials(http_db, 'user-uuid', 'auth-token') - return http_db - - #----------------------------------------------------------------------------- # The following tests come from `u1db.tests.test_remote_sync_target`. #----------------------------------------------------------------------------- @@ -122,12 +78,6 @@ class TestSoledadParsingSyncStream( target. """ - def setUp(self): - test_remote_sync_target.TestParsingSyncStream.setUp(self) - - def tearDown(self): - test_remote_sync_target.TestParsingSyncStream.tearDown(self) - def test_extra_comma(self): """ Test adapted to use encrypted content. @@ -209,17 +159,6 @@ class TestSoledadParsingSyncStream( # functions for TestRemoteSyncTargets # -def leap_sync_target(test, path): - return target.SoledadSyncTarget( - test.getURL(path), crypto=test._soledad._crypto) - - -def token_leap_sync_target(test, path): - st = leap_sync_target(test, path) - st.set_token_credentials('user-uuid', 'auth-token') - return st - - def make_local_db_and_soledad_target(test, path='test'): test.startServer() db = test.request_state._create_database(os.path.basename(path)) @@ -235,32 +174,32 @@ def make_local_db_and_token_soledad_target(test): class TestSoledadSyncTarget( + TestWithScenarios, SoledadWithCouchServerMixin, test_remote_sync_target.TestRemoteSyncTargets): scenarios = [ ('token_soledad', {'make_app_with_state': make_token_soledad_app, - 'make_document_for_test': make_leap_document_for_test, + 'make_document_for_test': make_soledad_document_for_test, 'create_db_and_target': make_local_db_and_token_soledad_target, 'make_database_for_test': make_sqlcipher_database_for_test, - 'sync_target': token_leap_sync_target}), + 'sync_target': token_soledad_sync_target}), ] def setUp(self): - tests.TestCaseWithServer.setUp(self) - self.main_test_class = test_remote_sync_target.TestRemoteSyncTargets + TestWithScenarios.setUp(self) SoledadWithCouchServerMixin.setUp(self) self.startServer() self.db1 = make_sqlcipher_database_for_test(self, 'test1') self.db2 = self.request_state._create_database('test2') def tearDown(self): - SoledadWithCouchServerMixin.tearDown(self) - tests.TestCaseWithServer.tearDown(self) - db2, _ = self.request_state.ensure_database('test2') - db2.delete_database() + #db2, _ = self.request_state.ensure_database('test2') + self.db2.delete_database() self.db1.close() + SoledadWithCouchServerMixin.tearDown(self) + TestWithScenarios.tearDown(self) def test_sync_exchange_send(self): """ @@ -268,7 +207,6 @@ class TestSoledadSyncTarget( This test was adapted to decrypt remote content before assert. """ - self.startServer() db = self.request_state._create_database('test') remote_target = self.getSyncTarget('test') other_docs = [] @@ -289,14 +227,9 @@ class TestSoledadSyncTarget( """ Test for sync exchange failure and retry. - This test was adapted to: - - decrypt remote content before assert. - - not expect a bounced document because soledad has stateful - recoverable sync. + This test was adapted to decrypt remote content before assert. """ - self.startServer() - def blackhole_getstderr(inst): return cStringIO.StringIO() @@ -332,8 +265,9 @@ class TestSoledadSyncTarget( doc2 = self.make_document('doc-here2', 'replica:1', '{"value": "here2"}') - # we do not expect an HTTPError because soledad sync fails gracefully - remote_target.sync_exchange( + self.assertRaises( + u1db.errors.HTTPError, + remote_target.sync_exchange, [(doc1, 10, 'T-sid'), (doc2, 11, 'T-sud')], 'replica', last_known_generation=0, last_known_trans_id=None, return_doc_cb=receive_doc) @@ -364,7 +298,6 @@ class TestSoledadSyncTarget( This test was adapted to decrypt remote content before assert. """ - self.startServer() remote_target = self.getSyncTarget('test') other_docs = [] replica_uid_box = [] @@ -405,7 +338,9 @@ target_scenarios = [ class SoledadDatabaseSyncTargetTests( - SoledadWithCouchServerMixin, test_sync.DatabaseSyncTargetTests): + TestWithScenarios, + SoledadWithCouchServerMixin, + test_sync.DatabaseSyncTargetTests): scenarios = ( tests.multiply_scenarios( @@ -500,8 +435,25 @@ class SoledadDatabaseSyncTargetTests( [(doc.doc_id, doc.rev), (doc2.doc_id, doc2.rev)]}) +# Just to make clear how this test is different... :) +DEFER_DECRYPTION = False + +WAIT_STEP = 1 +MAX_WAIT = 10 +DBPASS = "pass" + + +class SyncTimeoutError(Exception): + """ + Dummy exception to notify timeout during sync. + """ + pass + + class TestSoledadDbSync( - SoledadWithCouchServerMixin, test_sync.TestDbSync): + TestWithScenarios, + SoledadWithCouchServerMixin, + test_sync.TestDbSync): """Test db.sync remote sync shortcut""" scenarios = [ @@ -516,13 +468,67 @@ class TestSoledadDbSync( oauth = False token = False + + def make_app(self): + self.request_state = couch.CouchServerState( + self._couch_url, 'shared', 'tokens') + return self.make_app_with_state(self.request_state) + def setUp(self): - self.main_test_class = test_sync.TestDbSync + """ + Need to explicitely invoke inicialization on all bases. + """ SoledadWithCouchServerMixin.setUp(self) + self.server = self.server_thread = None + self.startServer() + self.syncer = None + + # config info + self.db1_file = os.path.join(self.tempdir, "db1.u1db") + os.unlink(self.db1_file) + self.db_pass = DBPASS + self.email = ADDRESS + + # get a random prefix for each test, so we do not mess with + # concurrency during initialization and shutting down of + # each local db. + self.rand_prefix = ''.join( + map(lambda x: random.choice(string.ascii_letters), range(6))) + + # open test dbs: db1 will be the local sqlcipher db (which + # instantiates a syncdb). We use the self._soledad instance that was + # already created on some setUp method. + import binascii + tohex = binascii.b2a_hex + key = tohex(self._soledad.secrets.get_local_storage_key()) + sync_db_key = tohex(self._soledad.secrets.get_sync_db_key()) + dbpath = self._soledad._local_db_path + + self.opts = SQLCipherOptions( + dbpath, key, is_raw_key=True, create=False, + defer_encryption=True, sync_db_key=sync_db_key) + self.db1 = SQLCipherDatabase(self.opts) + + self.db2 = couch.CouchDatabase.open_database( + urljoin( + 'http://localhost:' + str(self.wrapper.port), 'test'), + create=True, + ensure_ddocs=True) def tearDown(self): + """ + Need to explicitely invoke destruction on all bases. + """ + dbsyncer = getattr(self, 'dbsyncer', None) + if dbsyncer: + dbsyncer.close() + self.db1.close() + self.db2.close() + self._soledad.close() + + # XXX should not access "private" attrs + shutil.rmtree(os.path.dirname(self._soledad._local_db_path)) SoledadWithCouchServerMixin.tearDown(self) - self.db.close() def do_sync(self, target_name): """ @@ -530,44 +536,71 @@ class TestSoledadDbSync( and Token auth. """ if self.token: - extra = dict(creds={'token': { + creds={'token': { 'uuid': 'user-uuid', 'token': 'auth-token', - }}) + }} target_url = self.getURL(target_name) - return sync.SoledadSynchronizer( - self.db, - target.SoledadSyncTarget( - target_url, - crypto=self._soledad._crypto, - **extra)).sync(autocreate=True, - defer_decryption=False) + + # get a u1db syncer + crypto = self._soledad._crypto + replica_uid = self.db1._replica_uid + dbsyncer = SQLCipherU1DBSync(self.opts, crypto, replica_uid, + defer_encryption=True) + self.dbsyncer = dbsyncer + return dbsyncer.sync(target_url, creds=creds, + autocreate=True,defer_decryption=DEFER_DECRYPTION) else: return test_sync.TestDbSync.do_sync(self, target_name) + def wait_for_sync(self): + """ + Wait for sync to finish. + """ + wait = 0 + syncer = self.syncer + if syncer is not None: + while syncer.syncing: + time.sleep(WAIT_STEP) + wait += WAIT_STEP + if wait >= MAX_WAIT: + raise SyncTimeoutError + def test_db_sync(self): """ Test sync. Adapted to check for encrypted content. """ - doc1 = self.db.create_doc_from_json(tests.simple_doc) + doc1 = self.db1.create_doc_from_json(tests.simple_doc) doc2 = self.db2.create_doc_from_json(tests.nested_doc) - local_gen_before_sync = self.do_sync('test2') - gen, _, changes = self.db.whats_changed(local_gen_before_sync) - self.assertEqual(1, len(changes)) - self.assertEqual(doc2.doc_id, changes[0][0]) - self.assertEqual(1, gen - local_gen_before_sync) - self.assertGetEncryptedDoc( - self.db2, doc1.doc_id, doc1.rev, tests.simple_doc, False) - self.assertGetEncryptedDoc( - self.db, doc2.doc_id, doc2.rev, tests.nested_doc, False) + d = self.do_sync('test') + + def _assert_successful_sync(results): + import time + # need to give time to the encryption to proceed + # TODO should implement a defer list to subscribe to the all-decrypted + # event + time.sleep(2) + local_gen_before_sync = results + self.wait_for_sync() + + gen, _, changes = self.db1.whats_changed(local_gen_before_sync) + self.assertEqual(1, len(changes)) + + self.assertEqual(doc2.doc_id, changes[0][0]) + self.assertEqual(1, gen - local_gen_before_sync) + + self.assertGetEncryptedDoc( + self.db2, doc1.doc_id, doc1.rev, tests.simple_doc, False) + self.assertGetEncryptedDoc( + self.db1, doc2.doc_id, doc2.rev, tests.nested_doc, False) + + d.addCallback(_assert_successful_sync) + return d def test_db_sync_autocreate(self): """ We bypass this test because we never need to autocreate databases. """ pass - - -load_tests = tests.load_with_scenarios diff --git a/common/src/leap/soledad/common/tests/test_target.py b/common/src/leap/soledad/common/tests/test_target.py deleted file mode 100644 index eb5e2874..00000000 --- a/common/src/leap/soledad/common/tests/test_target.py +++ /dev/null @@ -1,797 +0,0 @@ -# -*- coding: utf-8 -*- -# test_target.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -""" -Test Leap backend bits. -""" - -import u1db -import os -import simplejson as json -import cStringIO - - -from u1db.sync import Synchronizer -from u1db.remote import ( - http_client, - http_database, -) - -from leap.soledad import client -from leap.soledad.client import ( - target, - auth, - VerifiedHTTPSConnection, -) -from leap.soledad.common.document import SoledadDocument -from leap.soledad.server.auth import SoledadTokenAuthMiddleware - - -from leap.soledad.common.tests import u1db_tests as tests -from leap.soledad.common.tests import BaseSoledadTest -from leap.soledad.common.tests.util import ( - make_sqlcipher_database_for_test, - make_soledad_app, - make_token_soledad_app, - SoledadWithCouchServerMixin, -) -from leap.soledad.common.tests.u1db_tests import test_backends -from leap.soledad.common.tests.u1db_tests import test_http_database -from leap.soledad.common.tests.u1db_tests import test_http_client -from leap.soledad.common.tests.u1db_tests import test_document -from leap.soledad.common.tests.u1db_tests import test_remote_sync_target -from leap.soledad.common.tests.u1db_tests import test_https -from leap.soledad.common.tests.u1db_tests import test_sync - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_backends`. -#----------------------------------------------------------------------------- - -def make_leap_document_for_test(test, doc_id, rev, content, - has_conflicts=False): - return SoledadDocument( - doc_id, rev, content, has_conflicts=has_conflicts) - - -LEAP_SCENARIOS = [ - ('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_soledad_app}), -] - - -def make_token_http_database_for_test(test, replica_uid): - test.startServer() - test.request_state._create_database(replica_uid) - - class _HTTPDatabaseWithToken( - http_database.HTTPDatabase, auth.TokenBasedAuth): - - def set_token_credentials(self, uuid, token): - auth.TokenBasedAuth.set_token_credentials(self, uuid, token) - - def _sign_request(self, method, url_query, params): - return auth.TokenBasedAuth._sign_request( - self, method, url_query, params) - - http_db = _HTTPDatabaseWithToken(test.getURL('test')) - http_db.set_token_credentials('user-uuid', 'auth-token') - return http_db - - -def copy_token_http_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. - http_db = test.request_state._copy_database(db) - http_db.set_token_credentials(http_db, 'user-uuid', 'auth-token') - return http_db - - -class SoledadTests(test_backends.AllDatabaseTests, BaseSoledadTest): - - scenarios = LEAP_SCENARIOS + [ - ('token_http', {'make_database_for_test': - make_token_http_database_for_test, - 'copy_database_for_test': - copy_token_http_database_for_test, - 'make_document_for_test': make_leap_document_for_test, - 'make_app_with_state': make_token_soledad_app, - }) - ] - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_http_client`. -#----------------------------------------------------------------------------- - -class TestSoledadClientBase(test_http_client.TestHTTPClientBase): - """ - This class should be used to test Token auth. - """ - - def getClientWithToken(self, **kwds): - self.startServer() - - class _HTTPClientWithToken( - http_client.HTTPClientBase, auth.TokenBasedAuth): - - def set_token_credentials(self, uuid, token): - auth.TokenBasedAuth.set_token_credentials(self, uuid, token) - - def _sign_request(self, method, url_query, params): - return auth.TokenBasedAuth._sign_request( - self, method, url_query, params) - - return _HTTPClientWithToken(self.getURL('dbase'), **kwds) - - def test_oauth(self): - """ - Suppress oauth test (we test for token auth here). - """ - pass - - def test_oauth_ctr_creds(self): - """ - Suppress oauth test (we test for token auth here). - """ - pass - - def test_oauth_Unauthorized(self): - """ - Suppress oauth test (we test for token auth here). - """ - pass - - def app(self, environ, start_response): - res = test_http_client.TestHTTPClientBase.app( - self, environ, start_response) - if res is not None: - return res - # mime solead application here. - if '/token' in environ['PATH_INFO']: - auth = environ.get(SoledadTokenAuthMiddleware.HTTP_AUTH_KEY) - if not auth: - start_response("401 Unauthorized", - [('Content-Type', 'application/json')]) - return [json.dumps({"error": "unauthorized", - "message": e.message})] - scheme, encoded = auth.split(None, 1) - if scheme.lower() != 'token': - start_response("401 Unauthorized", - [('Content-Type', 'application/json')]) - return [json.dumps({"error": "unauthorized", - "message": e.message})] - uuid, token = encoded.decode('base64').split(':', 1) - if uuid != 'user-uuid' and token != 'auth-token': - return unauth_err("Incorrect address or token.") - start_response("200 OK", [('Content-Type', 'application/json')]) - return [json.dumps([environ['PATH_INFO'], uuid, token])] - - def test_token(self): - """ - Test if token is sent correctly. - """ - cli = self.getClientWithToken() - cli.set_token_credentials('user-uuid', 'auth-token') - res, headers = cli._request('GET', ['doc', 'token']) - self.assertEqual( - ['/dbase/doc/token', 'user-uuid', 'auth-token'], json.loads(res)) - - def test_token_ctr_creds(self): - cli = self.getClientWithToken(creds={'token': { - 'uuid': 'user-uuid', - 'token': 'auth-token', - }}) - res, headers = cli._request('GET', ['doc', 'token']) - self.assertEqual( - ['/dbase/doc/token', 'user-uuid', 'auth-token'], json.loads(res)) - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_document`. -#----------------------------------------------------------------------------- - -class TestSoledadDocument(test_document.TestDocument, BaseSoledadTest): - - scenarios = ([( - 'leap', {'make_document_for_test': make_leap_document_for_test})]) - - -class TestSoledadPyDocument(test_document.TestPyDocument, BaseSoledadTest): - - scenarios = ([( - 'leap', {'make_document_for_test': make_leap_document_for_test})]) - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_remote_sync_target`. -#----------------------------------------------------------------------------- - -class TestSoledadSyncTargetBasics( - test_remote_sync_target.TestHTTPSyncTargetBasics): - """ - Some tests had to be copied to this class so we can instantiate our own - target. - """ - - def test_parse_url(self): - remote_target = target.SoledadSyncTarget('http://127.0.0.1:12345/') - self.assertEqual('http', remote_target._url.scheme) - self.assertEqual('127.0.0.1', remote_target._url.hostname) - self.assertEqual(12345, remote_target._url.port) - self.assertEqual('/', remote_target._url.path) - - -class TestSoledadParsingSyncStream( - test_remote_sync_target.TestParsingSyncStream, - BaseSoledadTest): - """ - Some tests had to be copied to this class so we can instantiate our own - target. - """ - - def setUp(self): - test_remote_sync_target.TestParsingSyncStream.setUp(self) - - def tearDown(self): - test_remote_sync_target.TestParsingSyncStream.tearDown(self) - - def test_extra_comma(self): - """ - Test adapted to use encrypted content. - """ - doc = SoledadDocument('i', rev='r') - doc.content = {} - enc_json = target.encrypt_doc(self._soledad._crypto, doc) - tgt = target.SoledadSyncTarget( - "http://foo/foo", crypto=self._soledad._crypto) - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, "[\r\n{},\r\n]", None) - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, - '[\r\n{},\r\n{"id": "i", "rev": "r", ' - '"content": %s, "gen": 3, "trans_id": "T-sid"}' - ',\r\n]' % json.dumps(enc_json), - lambda doc, gen, trans_id: None) - - def test_wrong_start(self): - tgt = target.SoledadSyncTarget("http://foo/foo") - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, "{}\r\n]", None) - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, "\r\n{}\r\n]", None) - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, "", None) - - def test_wrong_end(self): - tgt = target.SoledadSyncTarget("http://foo/foo") - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, "[\r\n{}", None) - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, "[\r\n", None) - - def test_missing_comma(self): - tgt = target.SoledadSyncTarget("http://foo/foo") - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, - '[\r\n{}\r\n{"id": "i", "rev": "r", ' - '"content": "c", "gen": 3}\r\n]', None) - - def test_no_entries(self): - tgt = target.SoledadSyncTarget("http://foo/foo") - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, "[\r\n]", None) - - def test_error_in_stream(self): - tgt = target.SoledadSyncTarget("http://foo/foo") - - self.assertRaises(u1db.errors.Unavailable, - tgt._parse_sync_stream, - '[\r\n{"new_generation": 0},' - '\r\n{"error": "unavailable"}\r\n', None) - - self.assertRaises(u1db.errors.Unavailable, - tgt._parse_sync_stream, - '[\r\n{"error": "unavailable"}\r\n', None) - - self.assertRaises(u1db.errors.BrokenSyncStream, - tgt._parse_sync_stream, - '[\r\n{"error": "?"}\r\n', None) - - -# -# functions for TestRemoteSyncTargets -# - -def leap_sync_target(test, path): - return target.SoledadSyncTarget( - test.getURL(path), crypto=test._soledad._crypto) - - -def token_leap_sync_target(test, path): - st = leap_sync_target(test, path) - st.set_token_credentials('user-uuid', 'auth-token') - return st - - -def make_local_db_and_soledad_target(test, path='test'): - test.startServer() - db = test.request_state._create_database(os.path.basename(path)) - st = target.SoledadSyncTarget.connect( - test.getURL(path), crypto=test._soledad._crypto) - return db, st - - -def make_local_db_and_token_soledad_target(test): - db, st = make_local_db_and_soledad_target(test, 'test') - st.set_token_credentials('user-uuid', 'auth-token') - return db, st - - -class TestSoledadSyncTarget( - SoledadWithCouchServerMixin, - test_remote_sync_target.TestRemoteSyncTargets): - - scenarios = [ - ('token_soledad', - {'make_app_with_state': make_token_soledad_app, - 'make_document_for_test': make_leap_document_for_test, - 'create_db_and_target': make_local_db_and_token_soledad_target, - 'make_database_for_test': make_sqlcipher_database_for_test, - 'sync_target': token_leap_sync_target}), - ] - - def setUp(self): - tests.TestCaseWithServer.setUp(self) - self.main_test_class = test_remote_sync_target.TestRemoteSyncTargets - SoledadWithCouchServerMixin.setUp(self) - self.startServer() - self.db1 = make_sqlcipher_database_for_test(self, 'test1') - self.db2 = self.request_state._create_database('test2') - - def tearDown(self): - SoledadWithCouchServerMixin.tearDown(self) - tests.TestCaseWithServer.tearDown(self) - db, _ = self.request_state.ensure_database('test2') - db.delete_database() - for i in ['db1', 'db2']: - if hasattr(self, i): - db = getattr(self, i) - db.close() - - def test_sync_exchange_send(self): - """ - Test for sync exchanging send of document. - - This test was adapted to decrypt remote content before assert. - """ - self.startServer() - db = self.request_state._create_database('test') - remote_target = self.getSyncTarget('test') - other_docs = [] - - def receive_doc(doc, gen, trans_id): - other_docs.append((doc.doc_id, doc.rev, doc.get_json())) - - doc = self.make_document('doc-here', 'replica:1', '{"value": "here"}') - new_gen, trans_id = remote_target.sync_exchange( - [(doc, 10, 'T-sid')], 'replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=receive_doc) - self.assertEqual(1, new_gen) - self.assertGetEncryptedDoc( - db, 'doc-here', 'replica:1', '{"value": "here"}', False) - db.close() - - def test_sync_exchange_send_failure_and_retry_scenario(self): - """ - Test for sync exchange failure and retry. - - This test was adapted to: - - decrypt remote content before assert. - - not expect a bounced document because soledad has stateful - recoverable sync. - """ - - self.startServer() - - def blackhole_getstderr(inst): - return cStringIO.StringIO() - - self.patch(self.server.RequestHandlerClass, 'get_stderr', - blackhole_getstderr) - db = self.request_state._create_database('test') - _put_doc_if_newer = db._put_doc_if_newer - trigger_ids = ['doc-here2'] - - def bomb_put_doc_if_newer(self, doc, save_conflict, - replica_uid=None, replica_gen=None, - replica_trans_id=None, number_of_docs=None, - doc_idx=None, sync_id=None): - if doc.doc_id in trigger_ids: - raise Exception - return _put_doc_if_newer(doc, save_conflict=save_conflict, - replica_uid=replica_uid, - replica_gen=replica_gen, - replica_trans_id=replica_trans_id, - number_of_docs=number_of_docs, - doc_idx=doc_idx, - sync_id=sync_id) - from leap.soledad.common.tests.test_couch import IndexedCouchDatabase - self.patch( - IndexedCouchDatabase, '_put_doc_if_newer', bomb_put_doc_if_newer) - remote_target = self.getSyncTarget('test') - other_changes = [] - - def receive_doc(doc, gen, trans_id): - other_changes.append( - (doc.doc_id, doc.rev, doc.get_json(), gen, trans_id)) - - doc1 = self.make_document('doc-here', 'replica:1', '{"value": "here"}') - doc2 = self.make_document('doc-here2', 'replica:1', - '{"value": "here2"}') - # We do not expect an exception here because the sync fails gracefully - remote_target.sync_exchange( - [(doc1, 10, 'T-sid'), (doc2, 11, 'T-sud')], - 'replica', last_known_generation=0, last_known_trans_id=None, - return_doc_cb=receive_doc) - self.assertGetEncryptedDoc( - db, 'doc-here', 'replica:1', '{"value": "here"}', - False) - self.assertEqual( - (10, 'T-sid'), db._get_replica_gen_and_trans_id('replica')) - self.assertEqual([], other_changes) - # retry - trigger_ids = [] - new_gen, trans_id = remote_target.sync_exchange( - [(doc2, 11, 'T-sud')], 'replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=receive_doc) - self.assertGetEncryptedDoc( - db, 'doc-here2', 'replica:1', '{"value": "here2"}', - False) - self.assertEqual( - (11, 'T-sud'), db._get_replica_gen_and_trans_id('replica')) - self.assertEqual(2, new_gen) - self.assertEqual( - ('doc-here', 'replica:1', '{"value": "here"}', 1), - other_changes[0][:-1]) - db.close() - - def test_sync_exchange_send_ensure_callback(self): - """ - Test for sync exchange failure and retry. - - This test was adapted to decrypt remote content before assert. - """ - self.startServer() - remote_target = self.getSyncTarget('test') - other_docs = [] - replica_uid_box = [] - - def receive_doc(doc, gen, trans_id): - other_docs.append((doc.doc_id, doc.rev, doc.get_json())) - - def ensure_cb(replica_uid): - replica_uid_box.append(replica_uid) - - doc = self.make_document('doc-here', 'replica:1', '{"value": "here"}') - new_gen, trans_id = remote_target.sync_exchange( - [(doc, 10, 'T-sid')], 'replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=receive_doc, - ensure_callback=ensure_cb) - self.assertEqual(1, new_gen) - db = self.request_state.open_database('test') - self.assertEqual(1, len(replica_uid_box)) - self.assertEqual(db._replica_uid, replica_uid_box[0]) - self.assertGetEncryptedDoc( - db, 'doc-here', 'replica:1', '{"value": "here"}', False) - db.close() - - def test_sync_exchange_in_stream_error(self): - # we bypass this test because our sync_exchange process does not - # return u1db error 503 "unavailable" for now. - pass - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_https`. -#----------------------------------------------------------------------------- - -def token_leap_https_sync_target(test, host, path): - _, port = test.server.server_address - st = target.SoledadSyncTarget( - 'https://%s:%d/%s' % (host, port, path), - crypto=test._soledad._crypto) - st.set_token_credentials('user-uuid', 'auth-token') - return st - - -class TestSoledadSyncTargetHttpsSupport( - test_https.TestHttpSyncTargetHttpsSupport, - BaseSoledadTest): - - scenarios = [ - ('token_soledad_https', - {'server_def': test_https.https_server_def, - 'make_app_with_state': make_token_soledad_app, - 'make_document_for_test': make_leap_document_for_test, - 'sync_target': token_leap_https_sync_target}), - ] - - def setUp(self): - # the parent constructor undoes our SSL monkey patch to ensure tests - # run smoothly with standard u1db. - test_https.TestHttpSyncTargetHttpsSupport.setUp(self) - # so here monkey patch again to test our functionality. - http_client._VerifiedHTTPSConnection = VerifiedHTTPSConnection - client.SOLEDAD_CERT = http_client.CA_CERTS - - def test_working(self): - """ - Test that SSL connections work well. - - This test was adapted to patch Soledad's HTTPS connection custom class - with the intended CA certificates. - """ - self.startServer() - db = self.request_state._create_database('test') - self.patch(client, 'SOLEDAD_CERT', self.cacert_pem) - remote_target = self.getSyncTarget('localhost', 'test') - remote_target.record_sync_info('other-id', 2, 'T-id') - self.assertEqual( - (2, 'T-id'), db._get_replica_gen_and_trans_id('other-id')) - - def test_host_mismatch(self): - """ - Test that SSL connections to a hostname different than the one in the - certificate raise CertificateError. - - This test was adapted to patch Soledad's HTTPS connection custom class - with the intended CA certificates. - """ - self.startServer() - self.request_state._create_database('test') - self.patch(client, 'SOLEDAD_CERT', self.cacert_pem) - remote_target = self.getSyncTarget('127.0.0.1', 'test') - self.assertRaises( - http_client.CertificateError, remote_target.record_sync_info, - 'other-id', 2, 'T-id') - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_http_database`. -#----------------------------------------------------------------------------- - -class _HTTPDatabase(http_database.HTTPDatabase, auth.TokenBasedAuth): - """ - Wraps our token auth implementation. - """ - - def set_token_credentials(self, uuid, token): - auth.TokenBasedAuth.set_token_credentials(self, uuid, token) - - def _sign_request(self, method, url_query, params): - return auth.TokenBasedAuth._sign_request( - self, method, url_query, params) - - -class TestHTTPDatabaseWithCreds( - test_http_database.TestHTTPDatabaseCtrWithCreds): - - def test_get_sync_target_inherits_token_credentials(self): - # this test was from TestDatabaseSimpleOperations but we put it here - # for convenience. - self.db = _HTTPDatabase('dbase') - self.db.set_token_credentials('user-uuid', 'auth-token') - st = self.db.get_sync_target() - self.assertEqual(self.db._creds, st._creds) - - def test_ctr_with_creds(self): - db1 = _HTTPDatabase('http://dbs/db', creds={'token': { - 'uuid': 'user-uuid', - 'token': 'auth-token', - }}) - self.assertIn('token', db1._creds) - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_sync`. -#----------------------------------------------------------------------------- - -target_scenarios = [ - ('token_leap', {'create_db_and_target': - make_local_db_and_token_soledad_target, - 'make_app_with_state': make_soledad_app}), -] - - -class SoledadDatabaseSyncTargetTests( - SoledadWithCouchServerMixin, test_sync.DatabaseSyncTargetTests): - - scenarios = ( - tests.multiply_scenarios( - tests.DatabaseBaseTests.scenarios, - target_scenarios)) - - whitebox = False - - def setUp(self): - self.main_test_class = test_sync.DatabaseSyncTargetTests - SoledadWithCouchServerMixin.setUp(self) - - def test_sync_exchange(self): - """ - Test sync exchange. - - This test was adapted to decrypt remote content before assert. - """ - sol, _ = make_local_db_and_soledad_target(self) - docs_by_gen = [ - (self.make_document('doc-id', 'replica:1', tests.simple_doc), 10, - 'T-sid')] - new_gen, trans_id = self.st.sync_exchange( - docs_by_gen, 'replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=self.receive_doc) - self.assertGetEncryptedDoc( - self.db, 'doc-id', 'replica:1', tests.simple_doc, False) - self.assertTransactionLog(['doc-id'], self.db) - last_trans_id = self.getLastTransId(self.db) - self.assertEqual(([], 1, last_trans_id), - (self.other_changes, new_gen, last_trans_id)) - self.assertEqual(10, self.st.get_sync_info('replica')[3]) - sol.close() - - def test_sync_exchange_push_many(self): - """ - Test sync exchange. - - This test was adapted to decrypt remote content before assert. - """ - docs_by_gen = [ - (self.make_document( - 'doc-id', 'replica:1', tests.simple_doc), 10, 'T-1'), - (self.make_document( - 'doc-id2', 'replica:1', tests.nested_doc), 11, 'T-2')] - new_gen, trans_id = self.st.sync_exchange( - docs_by_gen, 'replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=self.receive_doc) - self.assertGetEncryptedDoc( - self.db, 'doc-id', 'replica:1', tests.simple_doc, False) - self.assertGetEncryptedDoc( - self.db, 'doc-id2', 'replica:1', tests.nested_doc, False) - self.assertTransactionLog(['doc-id', 'doc-id2'], self.db) - last_trans_id = self.getLastTransId(self.db) - self.assertEqual(([], 2, last_trans_id), - (self.other_changes, new_gen, trans_id)) - self.assertEqual(11, self.st.get_sync_info('replica')[3]) - - def test_sync_exchange_returns_many_new_docs(self): - """ - Test sync exchange. - - This test was adapted to avoid JSON serialization comparison as local - and remote representations might differ. It looks directly at the - doc's contents instead. - """ - doc = self.db.create_doc_from_json(tests.simple_doc) - doc2 = self.db.create_doc_from_json(tests.nested_doc) - self.assertTransactionLog([doc.doc_id, doc2.doc_id], self.db) - new_gen, _ = self.st.sync_exchange( - [], 'other-replica', last_known_generation=0, - last_known_trans_id=None, return_doc_cb=self.receive_doc) - self.assertTransactionLog([doc.doc_id, doc2.doc_id], self.db) - self.assertEqual(2, new_gen) - self.assertEqual( - [(doc.doc_id, doc.rev, 1), - (doc2.doc_id, doc2.rev, 2)], - [c[:-3] + c[-2:-1] for c in self.other_changes]) - self.assertEqual( - json.loads(tests.simple_doc), - json.loads(self.other_changes[0][2])) - self.assertEqual( - json.loads(tests.nested_doc), - json.loads(self.other_changes[1][2])) - if self.whitebox: - self.assertEqual( - self.db._last_exchange_log['return'], - {'last_gen': 2, 'docs': - [(doc.doc_id, doc.rev), (doc2.doc_id, doc2.rev)]}) - - -class TestSoledadDbSync( - SoledadWithCouchServerMixin, test_sync.TestDbSync): - """Test db.sync remote sync shortcut""" - - scenarios = [ - ('py-token-http', { - 'create_db_and_target': make_local_db_and_token_soledad_target, - 'make_app_with_state': make_token_soledad_app, - 'make_database_for_test': make_sqlcipher_database_for_test, - 'token': True - }), - ] - - oauth = False - token = False - - def setUp(self): - self.main_test_class = test_sync.TestDbSync - SoledadWithCouchServerMixin.setUp(self) - - def tearDown(self): - SoledadWithCouchServerMixin.tearDown(self) - self.db.close() - - def do_sync(self, target_name): - """ - Perform sync using SoledadSyncTarget and Token auth. - """ - if self.token: - extra = dict(creds={'token': { - 'uuid': 'user-uuid', - 'token': 'auth-token', - }}) - target_url = self.getURL(target_name) - return Synchronizer( - self.db, - target.SoledadSyncTarget( - target_url, - crypto=self._soledad._crypto, - **extra)).sync(autocreate=True) - else: - return test_sync.TestDbSync.do_sync(self, target_name) - - def test_db_sync(self): - """ - Test sync. - - Adapted to check for encrypted content. - """ - doc1 = self.db.create_doc_from_json(tests.simple_doc) - doc2 = self.db2.create_doc_from_json(tests.nested_doc) - local_gen_before_sync = self.do_sync('test2') - gen, _, changes = self.db.whats_changed(local_gen_before_sync) - self.assertEqual(1, len(changes)) - self.assertEqual(doc2.doc_id, changes[0][0]) - self.assertEqual(1, gen - local_gen_before_sync) - self.assertGetEncryptedDoc( - self.db2, doc1.doc_id, doc1.rev, tests.simple_doc, False) - self.assertGetEncryptedDoc( - self.db, doc2.doc_id, doc2.rev, tests.nested_doc, False) - - def test_db_sync_autocreate(self): - """ - We bypass this test because we never need to autocreate databases. - """ - pass - - -load_tests = tests.load_with_scenarios diff --git a/common/src/leap/soledad/common/tests/test_target_soledad.py b/common/src/leap/soledad/common/tests/test_target_soledad.py deleted file mode 100644 index 899203b8..00000000 --- a/common/src/leap/soledad/common/tests/test_target_soledad.py +++ /dev/null @@ -1,102 +0,0 @@ -from u1db.remote import ( - http_database, -) - -from leap.soledad.client import ( - auth, - VerifiedHTTPSConnection, -) -from leap.soledad.common.document import SoledadDocument -from leap.soledad.server import SoledadApp -from leap.soledad.server.auth import SoledadTokenAuthMiddleware - - -from leap.soledad.common.tests import u1db_tests as tests -from leap.soledad.common.tests import BaseSoledadTest -from leap.soledad.common.tests.u1db_tests import test_backends - - -#----------------------------------------------------------------------------- -# The following tests come from `u1db.tests.test_backends`. -#----------------------------------------------------------------------------- - -def make_leap_document_for_test(test, doc_id, rev, content, - has_conflicts=False): - return SoledadDocument( - doc_id, rev, content, has_conflicts=has_conflicts) - - -def make_soledad_app(state): - return SoledadApp(state) - - -def make_token_soledad_app(state): - app = SoledadApp(state) - - def _verify_authentication_data(uuid, auth_data): - if uuid == 'user-uuid' and auth_data == 'auth-token': - return True - return False - - # we test for action authorization in leap.soledad.common.tests.test_server - def _verify_authorization(uuid, environ): - return True - - application = SoledadTokenAuthMiddleware(app) - application._verify_authentication_data = _verify_authentication_data - application._verify_authorization = _verify_authorization - return application - - -LEAP_SCENARIOS = [ - ('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_soledad_app}), -] - - -def make_token_http_database_for_test(test, replica_uid): - test.startServer() - test.request_state._create_database(replica_uid) - - class _HTTPDatabaseWithToken( - http_database.HTTPDatabase, auth.TokenBasedAuth): - - def set_token_credentials(self, uuid, token): - auth.TokenBasedAuth.set_token_credentials(self, uuid, token) - - def _sign_request(self, method, url_query, params): - return auth.TokenBasedAuth._sign_request( - self, method, url_query, params) - - http_db = _HTTPDatabaseWithToken(test.getURL('test')) - http_db.set_token_credentials('user-uuid', 'auth-token') - return http_db - - -def copy_token_http_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. - http_db = test.request_state._copy_database(db) - http_db.set_token_credentials(http_db, 'user-uuid', 'auth-token') - return http_db - - -class SoledadTests(test_backends.AllDatabaseTests, BaseSoledadTest): - - scenarios = LEAP_SCENARIOS + [ - ('token_http', {'make_database_for_test': - make_token_http_database_for_test, - 'copy_database_for_test': - copy_token_http_database_for_test, - 'make_document_for_test': make_leap_document_for_test, - 'make_app_with_state': make_token_soledad_app, - }) - ] - -load_tests = tests.load_with_scenarios diff --git a/common/src/leap/soledad/common/tests/u1db_tests/__init__.py b/common/src/leap/soledad/common/tests/u1db_tests/__init__.py index ad66fb06..6efeb87f 100644 --- a/common/src/leap/soledad/common/tests/u1db_tests/__init__.py +++ b/common/src/leap/soledad/common/tests/u1db_tests/__init__.py @@ -35,7 +35,7 @@ from pysqlcipher import dbapi2 from StringIO import StringIO import testscenarios -import testtools +from twisted.trial import unittest from u1db import ( errors, @@ -50,7 +50,7 @@ from u1db.remote import ( ) -class TestCase(testtools.TestCase): +class TestCase(unittest.TestCase): def createTempDir(self, prefix='u1db-tmp-'): """Create a temporary directory to do some work in. diff --git a/common/src/leap/soledad/common/tests/u1db_tests/test_https.py b/common/src/leap/soledad/common/tests/u1db_tests/test_https.py index c086fbc0..c5b316ed 100644 --- a/common/src/leap/soledad/common/tests/u1db_tests/test_https.py +++ b/common/src/leap/soledad/common/tests/u1db_tests/test_https.py @@ -75,7 +75,7 @@ class TestHttpSyncTargetHttpsSupport(tests.TestCaseWithServer): # order to maintain the compatibility with u1db default tests, we undo # that replacement here. http_client._VerifiedHTTPSConnection = \ - soledad.client.old__VerifiedHTTPSConnection + soledad.client.api.old__VerifiedHTTPSConnection super(TestHttpSyncTargetHttpsSupport, self).setUp() def getSyncTarget(self, host, path=None): diff --git a/common/src/leap/soledad/common/tests/util.py b/common/src/leap/soledad/common/tests/util.py index b27e6356..d4439ef4 100644 --- a/common/src/leap/soledad/common/tests/util.py +++ b/common/src/leap/soledad/common/tests/util.py @@ -21,24 +21,40 @@ Utilities used by multiple test suites. """ +import os import tempfile import shutil +import random +import string +import u1db +import subprocess +import time +import re + +from mock import Mock from urlparse import urljoin - from StringIO import StringIO from pysqlcipher import dbapi2 + from u1db.errors import DatabaseDoesNotExist +from u1db.remote import http_database + +from twisted.trial import unittest +from leap.common.files import mkdir_p from leap.soledad.common import soledad_assert +from leap.soledad.common.document import SoledadDocument from leap.soledad.common.couch import CouchDatabase, CouchServerState -from leap.soledad.server import SoledadApp -from leap.soledad.server.auth import SoledadTokenAuthMiddleware +from leap.soledad.common.crypto import ENC_SCHEME_KEY +from leap.soledad.client import Soledad +from leap.soledad.client import target +from leap.soledad.client import auth +from leap.soledad.client.crypto import decrypt_doc_dict -from leap.soledad.common.tests import BaseSoledadTest -from leap.soledad.common.tests.test_couch import CouchDBWrapper, CouchDBTestCase - +from leap.soledad.server import SoledadApp +from leap.soledad.server.auth import SoledadTokenAuthMiddleware from leap.soledad.client.sqlcipher import ( SQLCipherDatabase, @@ -47,6 +63,7 @@ from leap.soledad.client.sqlcipher import ( PASSWORD = '123456' +ADDRESS = 'leap@leap.se' def make_sqlcipher_database_for_test(test, replica_uid): @@ -62,7 +79,7 @@ def copy_sqlcipher_database_for_test(test, db): # 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. - new_db = SQLCipherDatabase(':memory:', PASSWORD) + new_db = make_sqlcipher_database_for_test(test, None) tmpfile = StringIO() for line in db._db_handle.iterdump(): if not 'sqlite_sequence' in line: # work around bug in iterdump @@ -98,6 +115,295 @@ def make_token_soledad_app(state): return application +def make_soledad_document_for_test(test, doc_id, rev, content, + has_conflicts=False): + return SoledadDocument( + doc_id, rev, content, has_conflicts=has_conflicts) + + +def make_token_http_database_for_test(test, replica_uid): + test.startServer() + test.request_state._create_database(replica_uid) + + class _HTTPDatabaseWithToken( + http_database.HTTPDatabase, auth.TokenBasedAuth): + + def set_token_credentials(self, uuid, token): + auth.TokenBasedAuth.set_token_credentials(self, uuid, token) + + def _sign_request(self, method, url_query, params): + return auth.TokenBasedAuth._sign_request( + self, method, url_query, params) + + http_db = _HTTPDatabaseWithToken(test.getURL('test')) + http_db.set_token_credentials('user-uuid', 'auth-token') + return http_db + + +def copy_token_http_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. + http_db = test.request_state._copy_database(db) + http_db.set_token_credentials(http_db, 'user-uuid', 'auth-token') + return http_db + + +class MockedSharedDBTest(object): + + def get_default_shared_mock(self, put_doc_side_effect=None, + get_doc_return_value=None): + """ + Get a default class for mocking the shared DB + """ + class defaultMockSharedDB(object): + get_doc = Mock(return_value=get_doc_return_value) + put_doc = Mock(side_effect=put_doc_side_effect) + lock = Mock(return_value=('atoken', 300)) + unlock = Mock(return_value=True) + open = Mock(return_value=None) + syncable = True + + def __call__(self): + return self + return defaultMockSharedDB + + +def soledad_sync_target(test, path): + return target.SoledadSyncTarget( + test.getURL(path), crypto=test._soledad._crypto) + + +def token_soledad_sync_target(test, path): + st = soledad_sync_target(test, path) + st.set_token_credentials('user-uuid', 'auth-token') + return st + + +class BaseSoledadTest(unittest.TestCase, MockedSharedDBTest): + """ + Instantiates Soledad for usage in tests. + """ + defer_sync_encryption = False + + def setUp(self): + # The following snippet comes from BaseLeapTest.setUpClass, but we + # repeat it here because twisted.trial does not work with + # setUpClass/tearDownClass. + self.old_path = os.environ['PATH'] + self.old_home = os.environ['HOME'] + self.tempdir = tempfile.mkdtemp(prefix="leap_tests-") + self.home = self.tempdir + bin_tdir = os.path.join( + self.tempdir, + 'bin') + os.environ["PATH"] = bin_tdir + os.environ["HOME"] = self.tempdir + + # config info + self.db1_file = os.path.join(self.tempdir, "db1.u1db") + self.db2_file = os.path.join(self.tempdir, "db2.u1db") + self.email = ADDRESS + # open test dbs + self._db1 = u1db.open(self.db1_file, create=True, + document_factory=SoledadDocument) + self._db2 = u1db.open(self.db2_file, create=True, + document_factory=SoledadDocument) + # get a random prefix for each test, so we do not mess with + # concurrency during initialization and shutting down of + # each local db. + self.rand_prefix = ''.join( + map(lambda x: random.choice(string.ascii_letters), range(6))) + # initialize soledad by hand so we can control keys + # XXX check if this soledad is actually used + self._soledad = self._soledad_instance( + prefix=self.rand_prefix, user=self.email) + + def tearDown(self): + self._db1.close() + self._db2.close() + self._soledad.close() + + # restore paths + os.environ["PATH"] = self.old_path + os.environ["HOME"] = self.old_home + + def _delete_temporary_dirs(): + # XXX should not access "private" attrs + for f in [self._soledad.local_db_path, + self._soledad.secrets.secrets_path]: + if os.path.isfile(f): + os.unlink(f) + # The following snippet comes from BaseLeapTest.setUpClass, but we + # repeat it here because twisted.trial does not work with + # setUpClass/tearDownClass. + soledad_assert( + self.tempdir.startswith('/tmp/leap_tests-'), + "beware! tried to remove a dir which does not " + "live in temporal folder!") + shutil.rmtree(self.tempdir) + + from twisted.internet import reactor + reactor.addSystemEventTrigger( + "after", "shutdown", _delete_temporary_dirs) + + + def _soledad_instance(self, user=ADDRESS, passphrase=u'123', + prefix='', + secrets_path='secrets.json', + local_db_path='soledad.u1db', + server_url='https://127.0.0.1/', + cert_file=None, + shared_db_class=None, + auth_token='auth-token'): + + def _put_doc_side_effect(doc): + self._doc_put = doc + + if shared_db_class is not None: + MockSharedDB = shared_db_class + else: + MockSharedDB = self.get_default_shared_mock( + _put_doc_side_effect) + + return Soledad( + user, + passphrase, + secrets_path=os.path.join( + self.tempdir, prefix, secrets_path), + local_db_path=os.path.join( + self.tempdir, prefix, local_db_path), + server_url=server_url, # Soledad will fail if not given an url. + cert_file=cert_file, + defer_encryption=self.defer_sync_encryption, + shared_db=MockSharedDB(), + auth_token=auth_token) + + def assertGetEncryptedDoc( + self, db, doc_id, doc_rev, content, has_conflicts): + """ + Assert that the document in the database looks correct. + """ + exp_doc = self.make_document(doc_id, doc_rev, content, + has_conflicts=has_conflicts) + doc = db.get_doc(doc_id) + + if ENC_SCHEME_KEY in doc.content: + # XXX check for SYM_KEY too + key = self._soledad._crypto.doc_passphrase(doc.doc_id) + secret = self._soledad._crypto.secret + decrypted = decrypt_doc_dict( + doc.content, doc.doc_id, doc.rev, + key, secret) + doc.set_json(decrypted) + self.assertEqual(exp_doc.doc_id, doc.doc_id) + self.assertEqual(exp_doc.rev, doc.rev) + self.assertEqual(exp_doc.has_conflicts, doc.has_conflicts) + self.assertEqual(exp_doc.content, doc.content) + + +#----------------------------------------------------------------------------- +# A wrapper for running couchdb locally. +#----------------------------------------------------------------------------- + +# 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 + testing. + """ + + def start(self): + """ + Start a CouchDB instance for a test. + """ + self.tempdir = tempfile.mkdtemp(suffix='.couch.test') + + path = os.path.join(os.path.dirname(__file__), + 'couchdb.ini.template') + handle = open(path) + conf = handle.read() % { + 'tempdir': self.tempdir, + } + handle.close() + + confPath = os.path.join(self.tempdir, 'test.ini') + handle = open(confPath, 'w') + handle.write(conf) + handle.close() + + # create the dirs from the template + mkdir_p(os.path.join(self.tempdir, 'lib')) + mkdir_p(os.path.join(self.tempdir, 'log')) + args = ['/usr/bin/couchdb', '-n', '-a', confPath] + null = open('/dev/null', 'w') + + self.process = subprocess.Popen( + args, env=None, stdout=null.fileno(), stderr=null.fileno(), + close_fds=True) + # find port + logPath = os.path.join(self.tempdir, 'log', 'couch.log') + while not os.path.exists(logPath): + if self.process.poll() is not None: + got_stdout, got_stderr = "", "" + if self.process.stdout is not None: + got_stdout = self.process.stdout.read() + + if self.process.stderr is not None: + got_stderr = self.process.stderr.read() + raise Exception(""" +couchdb exited with code %d. +stdout: +%s +stderr: +%s""" % ( + self.process.returncode, got_stdout, got_stderr)) + time.sleep(0.01) + while os.stat(logPath).st_size == 0: + time.sleep(0.01) + PORT_RE = re.compile( + 'Apache CouchDB has started on http://127.0.0.1:(?P\d+)') + + handle = open(logPath) + line = handle.read() + handle.close() + m = PORT_RE.search(line) + if not m: + self.stop() + raise Exception("Cannot find port in line %s" % line) + self.port = int(m.group('port')) + + def stop(self): + """ + Terminate the CouchDB instance. + """ + self.process.terminate() + self.process.communicate() + shutil.rmtree(self.tempdir) + + +class CouchDBTestCase(unittest.TestCase, MockedSharedDBTest): + """ + TestCase base class for tests against a real CouchDB server. + """ + + def setUp(self): + """ + Make sure we have a CouchDB instance for a test. + """ + self.wrapper = CouchDBWrapper() + self.wrapper.start() + #self.db = self.wrapper.db + + def tearDown(self): + """ + Stop CouchDB instance for test. + """ + self.wrapper.stop() + class CouchServerStateForTests(CouchServerState): """ This is a slightly modified CouchDB server state that allows for creating @@ -126,43 +432,15 @@ class SoledadWithCouchServerMixin( BaseSoledadTest, CouchDBTestCase): - @classmethod - def setUpClass(cls): - """ - Make sure we have a CouchDB instance for a test. - """ - # from BaseLeapTest - cls.tempdir = tempfile.mkdtemp(prefix="leap_tests-") - # from CouchDBTestCase - cls.wrapper = CouchDBWrapper() - cls.wrapper.start() - #self.db = self.wrapper.db - - @classmethod - def tearDownClass(cls): - """ - Stop CouchDB instance for test. - """ - # from BaseLeapTest - soledad_assert( - cls.tempdir.startswith('/tmp/leap_tests-'), - "beware! tried to remove a dir which does not " - "live in temporal folder!") - shutil.rmtree(cls.tempdir) - # from CouchDBTestCase - cls.wrapper.stop() - def setUp(self): - BaseSoledadTest.setUp(self) CouchDBTestCase.setUp(self) + BaseSoledadTest.setUp(self) main_test_class = getattr(self, 'main_test_class', None) if main_test_class is not None: main_test_class.setUp(self) self._couch_url = 'http://localhost:%d' % self.wrapper.port def tearDown(self): - BaseSoledadTest.tearDown(self) - CouchDBTestCase.tearDown(self) main_test_class = getattr(self, 'main_test_class', None) if main_test_class is not None: main_test_class.tearDown(self) @@ -172,6 +450,8 @@ class SoledadWithCouchServerMixin( db.delete_database() except DatabaseDoesNotExist: pass + BaseSoledadTest.tearDown(self) + CouchDBTestCase.tearDown(self) def make_app(self): couch_url = urljoin( -- cgit v1.2.3