diff options
Diffstat (limited to 'tests/sync')
| -rw-r--r-- | tests/sync/__init__.py | 0 | ||||
| l--------- | tests/sync/test_soledad | 1 | ||||
| -rw-r--r-- | tests/sync/test_sqlcipher_sync.py | 719 | ||||
| -rw-r--r-- | tests/sync/test_sync.py | 233 | ||||
| -rw-r--r-- | tests/sync/test_sync_mutex.py | 133 | ||||
| -rw-r--r-- | tests/sync/test_sync_target.py | 968 | 
6 files changed, 2054 insertions, 0 deletions
| diff --git a/tests/sync/__init__.py b/tests/sync/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/tests/sync/__init__.py diff --git a/tests/sync/test_soledad b/tests/sync/test_soledad new file mode 120000 index 00000000..c1a35d32 --- /dev/null +++ b/tests/sync/test_soledad @@ -0,0 +1 @@ +../test_soledad
\ No newline at end of file diff --git a/tests/sync/test_sqlcipher_sync.py b/tests/sync/test_sqlcipher_sync.py new file mode 100644 index 00000000..26f63a40 --- /dev/null +++ b/tests/sync/test_sqlcipher_sync.py @@ -0,0 +1,719 @@ +# -*- coding: utf-8 -*- +# test_sqlcipher.py +# Copyright (C) 2013-2016 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 <http://www.gnu.org/licenses/>. +""" +Test sqlcipher backend sync. +""" +import os + +from uuid import uuid4 + +from testscenarios import TestWithScenarios + +from leap.soledad.common.l2db import sync +from leap.soledad.common.l2db import vectorclock +from leap.soledad.common.l2db import errors + +from leap.soledad.client.http_target import SoledadHTTPSyncTarget + +from test_soledad import u1db_tests as tests +from test_soledad.util import SQLCIPHER_SCENARIOS +from test_soledad.util import make_soledad_app +from test_soledad.util import soledad_sync_target +from test_soledad.util import BaseSoledadTest + + +# ----------------------------------------------------------------------------- +# The following tests come from `u1db.tests.test_sync`. +# ----------------------------------------------------------------------------- + +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") +    path = test._http_at[db_target] +    target = SoledadHTTPSyncTarget.connect( +        test.getURL(path), test._soledad._crypto) +    target.set_token_credentials('user-uuid', 'auth-token') +    if trace_hook_shallow: +        target._set_trace_hook_shallow(trace_hook_shallow) +    return sync.Synchronizer(db_source, target).sync() + + +def sync_via_synchronizer(test, db_source, db_target, +                          trace_hook=None, +                          trace_hook_shallow=None): +    target = db_target.get_sync_target() +    trace_hook = trace_hook or trace_hook_shallow +    if trace_hook: +        target._set_trace_hook(trace_hook) +    return sync.Synchronizer(db_source, target).sync() + + +sync_scenarios = [] +for name, scenario in SQLCIPHER_SCENARIOS: +    scenario['do_sync'] = sync_via_synchronizer +    sync_scenarios.append((name, scenario)) + + +class SQLCipherDatabaseSyncTests( +        TestWithScenarios, +        tests.DatabaseBaseTests, +        BaseSoledadTest): + +    """ +    Test for succesfull sync between SQLCipher and LeapBackend. + +    Some of the tests in this class had to be adapted because the remote +    backend always receive encrypted content, and so it can not rely on +    document's content comparison to try to autoresolve conflicts. +    """ + +    scenarios = sync_scenarios + +    def setUp(self): +        self._use_tracking = {} +        super(tests.DatabaseBaseTests, self).setUp() + +    def create_database(self, replica_uid, sync_role=None): +        if replica_uid == 'test' and sync_role is None: +            # created up the chain by base class but unused +            return None +        db = self.create_database_for_role(replica_uid, sync_role) +        if sync_role: +            self._use_tracking[db] = (replica_uid, sync_role) +        self.addCleanup(db.close) +        return db + +    def create_database_for_role(self, replica_uid, sync_role): +        # hook point for reuse +        return tests.DatabaseBaseTests.create_database(self, replica_uid) + +    def sync(self, db_from, db_to, trace_hook=None, +             trace_hook_shallow=None): +        from_name, from_sync_role = self._use_tracking[db_from] +        to_name, to_sync_role = self._use_tracking[db_to] +        if from_sync_role not in ('source', 'both'): +            raise Exception("%s marked for %s use but used as source" % +                            (from_name, from_sync_role)) +        if to_sync_role not in ('target', 'both'): +            raise Exception("%s marked for %s use but used as target" % +                            (to_name, to_sync_role)) +        return self.do_sync(self, db_from, db_to, trace_hook, +                            trace_hook_shallow) + +    def assertLastExchangeLog(self, db, expected): +        log = getattr(db, '_last_exchange_log', None) +        if log is None: +            return +        self.assertEqual(expected, log) + +    def copy_database(self, db, sync_role=None): +        # 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. +        db_copy = tests.DatabaseBaseTests.copy_database(self, db) +        name, orig_sync_role = self._use_tracking[db] +        self._use_tracking[db_copy] = (name + '(copy)', sync_role or +                                       orig_sync_role) +        return db_copy + +    def test_sync_tracks_db_generation_of_other(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        self.assertEqual(0, self.sync(self.db1, self.db2)) +        self.assertEqual( +            (0, ''), self.db1._get_replica_gen_and_trans_id('test2')) +        self.assertEqual( +            (0, ''), self.db2._get_replica_gen_and_trans_id('test1')) +        self.assertLastExchangeLog(self.db2, +                                   {'receive': +                                       {'docs': [], 'last_known_gen': 0}, +                                    'return': +                                       {'docs': [], 'last_gen': 0}}) + +    def test_sync_autoresolves(self): +        """ +        Test for sync autoresolve remote. + +        This test was adapted because the remote database receives encrypted +        content and so it can't compare documents contents to autoresolve. +        """ +        # The remote database can't autoresolve conflicts based on magic +        # content convergence, so we modify this test to leave the possibility +        # of the remode document ending up in conflicted state. +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        doc1 = self.db1.create_doc_from_json(tests.simple_doc, doc_id='doc') +        rev1 = doc1.rev +        doc2 = self.db2.create_doc_from_json(tests.simple_doc, doc_id='doc') +        rev2 = doc2.rev +        self.sync(self.db1, self.db2) +        doc = self.db1.get_doc('doc') +        self.assertFalse(doc.has_conflicts) +        # if remote content is in conflicted state, then document revisions +        # will be different. +        # self.assertEqual(doc.rev, self.db2.get_doc('doc').rev) +        v = vectorclock.VectorClockRev(doc.rev) +        self.assertTrue(v.is_newer(vectorclock.VectorClockRev(rev1))) +        self.assertTrue(v.is_newer(vectorclock.VectorClockRev(rev2))) + +    def test_sync_autoresolves_moar(self): +        """ +        Test for sync autoresolve local. + +        This test was adapted to decrypt remote content before assert. +        """ +        # here we test that when a database that has a conflicted document is +        # the source of a sync, and the target database has a revision of the +        # conflicted document that is newer than the source database's, and +        # that target's database's document's content is the same as the +        # source's document's conflict's, the source's document's conflict gets +        # autoresolved, and the source's document's revision bumped. +        # +        # idea is as follows: +        # A          B +        # a1         - +        #   `-------> +        # a1         a1 +        # v          v +        # a2         a1b1 +        #   `-------> +        # a1b1+a2    a1b1 +        #            v +        # a1b1+a2    a1b2 (a1b2 has same content as a2) +        #   `-------> +        # a3b2       a1b2 (autoresolved) +        #   `-------> +        # a3b2       a3b2 +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        self.db1.create_doc_from_json(tests.simple_doc, doc_id='doc') +        self.sync(self.db1, self.db2) +        for db, content in [(self.db1, '{}'), (self.db2, '{"hi": 42}')]: +            doc = db.get_doc('doc') +            doc.set_json(content) +            db.put_doc(doc) +        self.sync(self.db1, self.db2) +        # db1 and db2 now both have a doc of {hi:42}, but db1 has a conflict +        doc = self.db1.get_doc('doc') +        rev1 = doc.rev +        self.assertTrue(doc.has_conflicts) +        # set db2 to have a doc of {} (same as db1 before the conflict) +        doc = self.db2.get_doc('doc') +        doc.set_json('{}') +        self.db2.put_doc(doc) +        rev2 = doc.rev +        # sync it across +        self.sync(self.db1, self.db2) +        # tadaa! +        doc = self.db1.get_doc('doc') +        self.assertFalse(doc.has_conflicts) +        vec1 = vectorclock.VectorClockRev(rev1) +        vec2 = vectorclock.VectorClockRev(rev2) +        vec3 = vectorclock.VectorClockRev(doc.rev) +        self.assertTrue(vec3.is_newer(vec1)) +        self.assertTrue(vec3.is_newer(vec2)) +        # because the conflict is on the source, sync it another time +        self.sync(self.db1, self.db2) +        # make sure db2 now has the exact same thing +        doc1 = self.db1.get_doc('doc') +        self.assertGetEncryptedDoc( +            self.db2, +            doc1.doc_id, doc1.rev, doc1.get_json(), False) + +    def test_sync_autoresolves_moar_backwards(self): +        # here we would test that when a database that has a conflicted +        # document is the target of a sync, and the source database has a +        # revision of the conflicted document that is newer than the target +        # database's, and that source's database's document's content is the +        # same as the target's document's conflict's, the target's document's +        # conflict gets autoresolved, and the document's revision bumped. +        # +        # Despite that, in Soledad we suppose that the server never syncs, so +        # it never has conflicted documents. Also, if it had, convergence +        # would not be possible by checking document's contents because they +        # would be encrypted in server. +        # +        # Therefore we suppress this test. +        pass + +    def test_sync_autoresolves_moar_backwards_three(self): +        # here we would test that when a database that has a conflicted +        # document is the target of a sync, and the source database has a +        # revision of the conflicted document that is newer than the target +        # database's, and that source's database's document's content is the +        # same as the target's document's conflict's, the target's document's +        # conflict gets autoresolved, and the document's revision bumped. +        # +        # We use the same reasoning from the last test to suppress this one. +        pass + +    def test_sync_pulling_doesnt_update_other_if_changed(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        doc = self.db2.create_doc_from_json(tests.simple_doc) +        # After the local side has sent its list of docs, before we start +        # receiving the "targets" response, we update the local database with a +        # new record. +        # When we finish synchronizing, we can notice that something locally +        # was updated, and we cannot tell c2 our new updated generation + +        def before_get_docs(state): +            if state != 'before get_docs': +                return +            self.db1.create_doc_from_json(tests.simple_doc) + +        self.assertEqual(0, self.sync(self.db1, self.db2, +                                      trace_hook=before_get_docs)) +        self.assertLastExchangeLog(self.db2, +                                   {'receive': +                                       {'docs': [], 'last_known_gen': 0}, +                                    'return': +                                       {'docs': [(doc.doc_id, doc.rev)], +                                        'last_gen': 1}}) +        self.assertEqual(1, self.db1._get_replica_gen_and_trans_id('test2')[0]) +        # c2 should not have gotten a '_record_sync_info' call, because the +        # local database had been updated more than just by the messages +        # returned from c2. +        self.assertEqual( +            (0, ''), self.db2._get_replica_gen_and_trans_id('test1')) + +    def test_sync_doesnt_update_other_if_nothing_pulled(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        self.db1.create_doc_from_json(tests.simple_doc) + +        def no_record_sync_info(state): +            if state != 'record_sync_info': +                return +            self.fail('SyncTarget.record_sync_info was called') +        self.assertEqual(1, self.sync(self.db1, self.db2, +                                      trace_hook_shallow=no_record_sync_info)) +        self.assertEqual( +            1, +            self.db2._get_replica_gen_and_trans_id(self.db1._replica_uid)[0]) + +    def test_sync_ignores_convergence(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'both') +        doc = self.db1.create_doc_from_json(tests.simple_doc) +        self.db3 = self.create_database('test3', 'target') +        self.assertEqual(1, self.sync(self.db1, self.db3)) +        self.assertEqual(0, self.sync(self.db2, self.db3)) +        self.assertEqual(1, self.sync(self.db1, self.db2)) +        self.assertLastExchangeLog(self.db2, +                                   {'receive': +                                       {'docs': [(doc.doc_id, doc.rev)], +                                        'source_uid': 'test1', +                                        'source_gen': 1, 'last_known_gen': 0}, +                                    'return': {'docs': [], 'last_gen': 1}}) + +    def test_sync_ignores_superseded(self): +        self.db1 = self.create_database('test1', 'both') +        self.db2 = self.create_database('test2', 'both') +        doc = self.db1.create_doc_from_json(tests.simple_doc) +        doc_rev1 = doc.rev +        self.db3 = self.create_database('test3', 'target') +        self.sync(self.db1, self.db3) +        self.sync(self.db2, self.db3) +        new_content = '{"key": "altval"}' +        doc.set_json(new_content) +        self.db1.put_doc(doc) +        doc_rev2 = doc.rev +        self.sync(self.db2, self.db1) +        self.assertLastExchangeLog(self.db1, +                                   {'receive': +                                       {'docs': [(doc.doc_id, doc_rev1)], +                                        'source_uid': 'test2', +                                        'source_gen': 1, 'last_known_gen': 0}, +                                    'return': +                                       {'docs': [(doc.doc_id, doc_rev2)], +                                        'last_gen': 2}}) +        self.assertGetDoc(self.db1, doc.doc_id, doc_rev2, new_content, False) + +    def test_sync_sees_remote_conflicted(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        doc1 = self.db1.create_doc_from_json(tests.simple_doc) +        doc_id = doc1.doc_id +        doc1_rev = doc1.rev +        self.db1.create_index('test-idx', 'key') +        new_doc = '{"key": "altval"}' +        doc2 = self.db2.create_doc_from_json(new_doc, doc_id=doc_id) +        doc2_rev = doc2.rev +        self.assertTransactionLog([doc1.doc_id], self.db1) +        self.sync(self.db1, self.db2) +        self.assertLastExchangeLog(self.db2, +                                   {'receive': +                                       {'docs': [(doc_id, doc1_rev)], +                                        'source_uid': 'test1', +                                        'source_gen': 1, 'last_known_gen': 0}, +                                    'return': +                                       {'docs': [(doc_id, doc2_rev)], +                                        'last_gen': 1}}) +        self.assertTransactionLog([doc_id, doc_id], self.db1) +        self.assertGetDoc(self.db1, doc_id, doc2_rev, new_doc, True) +        self.assertGetDoc(self.db2, doc_id, doc2_rev, new_doc, False) +        from_idx = self.db1.get_from_index('test-idx', 'altval')[0] +        self.assertEqual(doc2.doc_id, from_idx.doc_id) +        self.assertEqual(doc2.rev, from_idx.rev) +        self.assertTrue(from_idx.has_conflicts) +        self.assertEqual([], self.db1.get_from_index('test-idx', 'value')) + +    def test_sync_sees_remote_delete_conflicted(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        doc1 = self.db1.create_doc_from_json(tests.simple_doc) +        doc_id = doc1.doc_id +        self.db1.create_index('test-idx', 'key') +        self.sync(self.db1, self.db2) +        doc2 = self.make_document(doc1.doc_id, doc1.rev, doc1.get_json()) +        new_doc = '{"key": "altval"}' +        doc1.set_json(new_doc) +        self.db1.put_doc(doc1) +        self.db2.delete_doc(doc2) +        self.assertTransactionLog([doc_id, doc_id], self.db1) +        self.sync(self.db1, self.db2) +        self.assertLastExchangeLog(self.db2, +                                   {'receive': +                                       {'docs': [(doc_id, doc1.rev)], +                                        'source_uid': 'test1', +                                        'source_gen': 2, 'last_known_gen': 1}, +                                    'return': {'docs': [(doc_id, doc2.rev)], +                                               'last_gen': 2}}) +        self.assertTransactionLog([doc_id, doc_id, doc_id], self.db1) +        self.assertGetDocIncludeDeleted(self.db1, doc_id, doc2.rev, None, True) +        self.assertGetDocIncludeDeleted( +            self.db2, doc_id, doc2.rev, None, False) +        self.assertEqual([], self.db1.get_from_index('test-idx', 'value')) + +    def test_sync_local_race_conflicted(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        doc = self.db1.create_doc_from_json(tests.simple_doc) +        doc_id = doc.doc_id +        doc1_rev = doc.rev +        self.db1.create_index('test-idx', 'key') +        self.sync(self.db1, self.db2) +        content1 = '{"key": "localval"}' +        content2 = '{"key": "altval"}' +        doc.set_json(content2) +        self.db2.put_doc(doc) +        doc2_rev2 = doc.rev +        triggered = [] + +        def after_whatschanged(state): +            if state != 'after whats_changed': +                return +            triggered.append(True) +            doc = self.make_document(doc_id, doc1_rev, content1) +            self.db1.put_doc(doc) + +        self.sync(self.db1, self.db2, trace_hook=after_whatschanged) +        self.assertEqual([True], triggered) +        self.assertGetDoc(self.db1, doc_id, doc2_rev2, content2, True) +        from_idx = self.db1.get_from_index('test-idx', 'altval')[0] +        self.assertEqual(doc.doc_id, from_idx.doc_id) +        self.assertEqual(doc.rev, from_idx.rev) +        self.assertTrue(from_idx.has_conflicts) +        self.assertEqual([], self.db1.get_from_index('test-idx', 'value')) +        self.assertEqual([], self.db1.get_from_index('test-idx', 'localval')) + +    def test_sync_propagates_deletes(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'both') +        doc1 = self.db1.create_doc_from_json(tests.simple_doc) +        doc_id = doc1.doc_id +        self.db1.create_index('test-idx', 'key') +        self.sync(self.db1, self.db2) +        self.db2.create_index('test-idx', 'key') +        self.db3 = self.create_database('test3', 'target') +        self.sync(self.db1, self.db3) +        self.db1.delete_doc(doc1) +        deleted_rev = doc1.rev +        self.sync(self.db1, self.db2) +        self.assertLastExchangeLog(self.db2, +                                   {'receive': +                                       {'docs': [(doc_id, deleted_rev)], +                                        'source_uid': 'test1', +                                        'source_gen': 2, 'last_known_gen': 1}, +                                    'return': {'docs': [], 'last_gen': 2}}) +        self.assertGetDocIncludeDeleted( +            self.db1, doc_id, deleted_rev, None, False) +        self.assertGetDocIncludeDeleted( +            self.db2, doc_id, deleted_rev, None, False) +        self.assertEqual([], self.db1.get_from_index('test-idx', 'value')) +        self.assertEqual([], self.db2.get_from_index('test-idx', 'value')) +        self.sync(self.db2, self.db3) +        self.assertLastExchangeLog(self.db3, +                                   {'receive': +                                       {'docs': [(doc_id, deleted_rev)], +                                        'source_uid': 'test2', +                                        'source_gen': 2, +                                        'last_known_gen': 0}, +                                    'return': +                                       {'docs': [], 'last_gen': 2}}) +        self.assertGetDocIncludeDeleted( +            self.db3, doc_id, deleted_rev, None, False) + +    def test_sync_propagates_deletes_2(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        self.db1.create_doc_from_json('{"a": "1"}', doc_id='the-doc') +        self.sync(self.db1, self.db2) +        doc1_2 = self.db2.get_doc('the-doc') +        self.db2.delete_doc(doc1_2) +        self.sync(self.db1, self.db2) +        self.assertGetDocIncludeDeleted( +            self.db1, 'the-doc', doc1_2.rev, None, False) + +    def test_sync_detects_identical_replica_uid(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test1', 'target') +        self.db1.create_doc_from_json(tests.simple_doc, doc_id='doc1') +        self.assertRaises( +            errors.InvalidReplicaUID, self.sync, self.db1, self.db2) + +    def test_optional_sync_preserve_json(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        cont1 = '{  "a":  2  }' +        cont2 = '{ "b":3}' +        self.db1.create_doc_from_json(cont1, doc_id="1") +        self.db2.create_doc_from_json(cont2, doc_id="2") +        self.sync(self.db1, self.db2) +        self.assertEqual(cont1, self.db2.get_doc("1").get_json()) +        self.assertEqual(cont2, self.db1.get_doc("2").get_json()) + +    def test_sync_propagates_resolution(self): +        """ +        Test if synchronization propagates resolution. + +        This test was adapted to decrypt remote content before assert. +        """ +        self.db1 = self.create_database('test1', 'both') +        self.db2 = self.create_database('test2', 'both') +        doc1 = self.db1.create_doc_from_json('{"a": 1}', doc_id='the-doc') +        db3 = self.create_database('test3', 'both') +        self.sync(self.db2, self.db1) +        self.assertEqual( +            self.db1._get_generation_info(), +            self.db2._get_replica_gen_and_trans_id(self.db1._replica_uid)) +        self.assertEqual( +            self.db2._get_generation_info(), +            self.db1._get_replica_gen_and_trans_id(self.db2._replica_uid)) +        self.sync(db3, self.db1) +        # update on 2 +        doc2 = self.make_document('the-doc', doc1.rev, '{"a": 2}') +        self.db2.put_doc(doc2) +        self.sync(self.db2, db3) +        self.assertEqual(db3.get_doc('the-doc').rev, doc2.rev) +        # update on 1 +        doc1.set_json('{"a": 3}') +        self.db1.put_doc(doc1) +        # conflicts +        self.sync(self.db2, self.db1) +        self.sync(db3, self.db1) +        self.assertTrue(self.db2.get_doc('the-doc').has_conflicts) +        self.assertTrue(db3.get_doc('the-doc').has_conflicts) +        # resolve +        conflicts = self.db2.get_doc_conflicts('the-doc') +        doc4 = self.make_document('the-doc', None, '{"a": 4}') +        revs = [doc.rev for doc in conflicts] +        self.db2.resolve_doc(doc4, revs) +        doc2 = self.db2.get_doc('the-doc') +        self.assertEqual(doc4.get_json(), doc2.get_json()) +        self.assertFalse(doc2.has_conflicts) +        self.sync(self.db2, db3) +        doc3 = db3.get_doc('the-doc') + +        self.assertEqual(doc4.get_json(), doc3.get_json()) +        self.assertFalse(doc3.has_conflicts) +        self.db1.close() +        self.db2.close() +        db3.close() + +    def test_sync_puts_changes(self): +        """ +        Test if sync puts changes in remote replica. + +        This test was adapted to decrypt remote content before assert. +        """ +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        doc = self.db1.create_doc_from_json(tests.simple_doc) +        self.assertEqual(1, self.sync(self.db1, self.db2)) +        self.assertGetEncryptedDoc( +            self.db2, doc.doc_id, doc.rev, tests.simple_doc, False) +        self.assertEqual(1, self.db1._get_replica_gen_and_trans_id('test2')[0]) +        self.assertEqual(1, self.db2._get_replica_gen_and_trans_id('test1')[0]) +        self.assertLastExchangeLog( +            self.db2, +            {'receive': {'docs': [(doc.doc_id, doc.rev)], +                         'source_uid': 'test1', +                         'source_gen': 1, 'last_known_gen': 0}, +             'return': {'docs': [], 'last_gen': 1}}) + +    def test_sync_pulls_changes(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        doc = self.db2.create_doc_from_json(tests.simple_doc) +        self.db1.create_index('test-idx', 'key') +        self.assertEqual(0, self.sync(self.db1, self.db2)) +        self.assertGetDoc(self.db1, doc.doc_id, doc.rev, +                          tests.simple_doc, False) +        self.assertEqual(1, self.db1._get_replica_gen_and_trans_id('test2')[0]) +        self.assertEqual(1, self.db2._get_replica_gen_and_trans_id('test1')[0]) +        self.assertLastExchangeLog(self.db2, +                                   {'receive': +                                       {'docs': [], 'last_known_gen': 0}, +                                    'return': +                                       {'docs': [(doc.doc_id, doc.rev)], +                                        'last_gen': 1}}) +        self.assertEqual([doc], self.db1.get_from_index('test-idx', 'value')) + +    def test_sync_supersedes_conflicts(self): +        self.db1 = self.create_database('test1', 'both') +        self.db2 = self.create_database('test2', 'target') +        self.db3 = self.create_database('test3', 'both') +        doc1 = self.db1.create_doc_from_json('{"a": 1}', doc_id='the-doc') +        self.db2.create_doc_from_json('{"b": 1}', doc_id='the-doc') +        self.db3.create_doc_from_json('{"c": 1}', doc_id='the-doc') +        self.sync(self.db3, self.db1) +        self.assertEqual( +            self.db1._get_generation_info(), +            self.db3._get_replica_gen_and_trans_id(self.db1._replica_uid)) +        self.assertEqual( +            self.db3._get_generation_info(), +            self.db1._get_replica_gen_and_trans_id(self.db3._replica_uid)) +        self.sync(self.db3, self.db2) +        self.assertEqual( +            self.db2._get_generation_info(), +            self.db3._get_replica_gen_and_trans_id(self.db2._replica_uid)) +        self.assertEqual( +            self.db3._get_generation_info(), +            self.db2._get_replica_gen_and_trans_id(self.db3._replica_uid)) +        self.assertEqual(3, len(self.db3.get_doc_conflicts('the-doc'))) +        doc1.set_json('{"a": 2}') +        self.db1.put_doc(doc1) +        self.sync(self.db3, self.db1) +        # original doc1 should have been removed from conflicts +        self.assertEqual(3, len(self.db3.get_doc_conflicts('the-doc'))) + +    def test_sync_stops_after_get_sync_info(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        self.db1.create_doc_from_json(tests.simple_doc) +        self.sync(self.db1, self.db2) + +        def put_hook(state): +            self.fail("Tracehook triggered for %s" % (state,)) + +        self.sync(self.db1, self.db2, trace_hook_shallow=put_hook) + +    def test_sync_detects_rollback_in_source(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        self.db1.create_doc_from_json(tests.simple_doc, doc_id='doc1') +        self.sync(self.db1, self.db2) +        self.db1_copy = self.copy_database(self.db1) +        self.db1.create_doc_from_json(tests.simple_doc, doc_id='doc2') +        self.sync(self.db1, self.db2) +        self.assertRaises( +            errors.InvalidGeneration, self.sync, self.db1_copy, self.db2) + +    def test_sync_detects_rollback_in_target(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        self.db1.create_doc_from_json(tests.simple_doc, doc_id="divergent") +        self.sync(self.db1, self.db2) +        self.db2_copy = self.copy_database(self.db2) +        self.db2.create_doc_from_json(tests.simple_doc, doc_id='doc2') +        self.sync(self.db1, self.db2) +        self.assertRaises( +            errors.InvalidGeneration, self.sync, self.db1, self.db2_copy) + +    def test_sync_detects_diverged_source(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        self.db3 = self.copy_database(self.db1) +        self.db1.create_doc_from_json(tests.simple_doc, doc_id="divergent") +        self.db3.create_doc_from_json(tests.simple_doc, doc_id="divergent") +        self.sync(self.db1, self.db2) +        self.assertRaises( +            errors.InvalidTransactionId, self.sync, self.db3, self.db2) + +    def test_sync_detects_diverged_target(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        self.db3 = self.copy_database(self.db2) +        self.db3.create_doc_from_json(tests.nested_doc, doc_id="divergent") +        self.db1.create_doc_from_json(tests.simple_doc, doc_id="divergent") +        self.sync(self.db1, self.db2) +        self.assertRaises( +            errors.InvalidTransactionId, self.sync, self.db1, self.db3) + +    def test_sync_detects_rollback_and_divergence_in_source(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        self.db1.create_doc_from_json(tests.simple_doc, doc_id='doc1') +        self.sync(self.db1, self.db2) +        self.db1_copy = self.copy_database(self.db1) +        self.db1.create_doc_from_json(tests.simple_doc, doc_id='doc2') +        self.db1.create_doc_from_json(tests.simple_doc, doc_id='doc3') +        self.sync(self.db1, self.db2) +        self.db1_copy.create_doc_from_json(tests.simple_doc, doc_id='doc2') +        self.db1_copy.create_doc_from_json(tests.simple_doc, doc_id='doc3') +        self.assertRaises( +            errors.InvalidTransactionId, self.sync, self.db1_copy, self.db2) + +    def test_sync_detects_rollback_and_divergence_in_target(self): +        self.db1 = self.create_database('test1', 'source') +        self.db2 = self.create_database('test2', 'target') +        self.db1.create_doc_from_json(tests.simple_doc, doc_id="divergent") +        self.sync(self.db1, self.db2) +        self.db2_copy = self.copy_database(self.db2) +        self.db2.create_doc_from_json(tests.simple_doc, doc_id='doc2') +        self.db2.create_doc_from_json(tests.simple_doc, doc_id='doc3') +        self.sync(self.db1, self.db2) +        self.db2_copy.create_doc_from_json(tests.simple_doc, doc_id='doc2') +        self.db2_copy.create_doc_from_json(tests.simple_doc, doc_id='doc3') +        self.assertRaises( +            errors.InvalidTransactionId, self.sync, self.db1, self.db2_copy) + + +def make_local_db_and_soledad_target( +        test, path='test', +        source_replica_uid=uuid4().hex): +    test.startTwistedServer() +    replica_uid = os.path.basename(path) +    db = test.request_state._create_database(replica_uid) +    st = soledad_sync_target( +        test, db._dbname, +        source_replica_uid=source_replica_uid) +    return db, st + + +target_scenarios = [ +    ('leap', { +        'create_db_and_target': make_local_db_and_soledad_target, +        'make_app_with_state': make_soledad_app, +        'do_sync': sync_via_synchronizer_and_soledad}), +] diff --git a/tests/sync/test_sync.py b/tests/sync/test_sync.py new file mode 100644 index 00000000..fb9a0245 --- /dev/null +++ b/tests/sync/test_sync.py @@ -0,0 +1,233 @@ +# -*- coding: utf-8 -*- +# test_sync.py +# Copyright (C) 2013, 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 <http://www.gnu.org/licenses/>. +import json +import pytest +import threading +import time + +from six.moves.urllib.parse import urljoin +from mock import Mock +from twisted.internet import defer + +from testscenarios import TestWithScenarios + +from leap.soledad.common import couch +from leap.soledad.client import sync + +from test_soledad import u1db_tests as tests +from test_soledad.u1db_tests import TestCaseWithServer +from test_soledad.u1db_tests import simple_doc +from test_soledad.util import make_token_soledad_app +from test_soledad.util import make_soledad_document_for_test +from test_soledad.util import soledad_sync_target +from test_soledad.util import BaseSoledadTest +from test_soledad.util import SoledadWithCouchServerMixin +from test_soledad.util import CouchDBTestCase + + +class InterruptableSyncTestCase( +        BaseSoledadTest, CouchDBTestCase, TestCaseWithServer): + +    """ +    Tests for encrypted sync using Soledad server backed by a couch database. +    """ + +    @staticmethod +    def make_app_with_state(state): +        return make_token_soledad_app(state) + +    make_document_for_test = make_soledad_document_for_test + +    sync_target = soledad_sync_target + +    def make_app(self): +        self.request_state = couch.CouchServerState(self.couch_url) +        return self.make_app_with_state(self.request_state) + +    def setUp(self): +        TestCaseWithServer.setUp(self) +        CouchDBTestCase.setUp(self) + +    def tearDown(self): +        CouchDBTestCase.tearDown(self) +        TestCaseWithServer.tearDown(self) + +    def test_interruptable_sync(self): +        """ +        Test if Soledad can sync many smallfiles. +        """ + +        self.skipTest("Sync is currently not interruptable.") + +        class _SyncInterruptor(threading.Thread): + +            """ +            A thread meant to interrupt the sync process. +            """ + +            def __init__(self, soledad, couchdb): +                self._soledad = soledad +                self._couchdb = couchdb +                threading.Thread.__init__(self) + +            def run(self): +                while db._get_generation() < 2: +                    # print "WAITING %d" % db._get_generation() +                    time.sleep(0.1) +                self._soledad.stop_sync() +                time.sleep(1) + +        number_of_docs = 10 +        self.startServer() + +        # instantiate soledad and create a document +        sol = self._soledad_instance( +            user='user-uuid', server_url=self.getURL()) + +        # ensure remote db exists before syncing +        db = couch.CouchDatabase.open_database( +            urljoin(self.couch_url, 'user-user-uuid'), +            create=True) + +        # create interruptor thread +        t = _SyncInterruptor(sol, db) +        t.start() + +        d = sol.get_all_docs() +        d.addCallback(lambda results: self.assertEqual([], results[1])) + +        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) + +        # 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 + + +@pytest.mark.needs_couch +class TestSoledadDbSync( +        TestWithScenarios, +        SoledadWithCouchServerMixin, +        tests.TestCaseWithServer): + +    """ +    Test db.sync remote sync shortcut +    """ + +    scenarios = [ +        ('py-token-http', { +            'make_app_with_state': make_token_soledad_app, +            'make_database_for_test': tests.make_memory_database_for_test, +            'token': True +        }), +    ] + +    oauth = False +    token = False + +    def setUp(self): +        """ +        Need to explicitely invoke inicialization on all bases. +        """ +        SoledadWithCouchServerMixin.setUp(self) +        self.startTwistedServer() +        self.db = self.make_database_for_test(self, 'test1') +        self.db2 = self.request_state._create_database(replica_uid='test') + +    def tearDown(self): +        """ +        Need to explicitely invoke destruction on all bases. +        """ +        SoledadWithCouchServerMixin.tearDown(self) +        # tests.TestCaseWithServer.tearDown(self) + +    def do_sync(self): +        """ +        Perform sync using SoledadSynchronizer, SoledadSyncTarget +        and Token auth. +        """ +        target = soledad_sync_target( +            self, self.db2._dbname, +            source_replica_uid=self._soledad._dbpool.replica_uid) +        return sync.SoledadSynchronizer( +            self.db, +            target).sync() + +    @defer.inlineCallbacks +    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 = yield self.do_sync() +        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) + +    # TODO: add u1db.tests.test_sync.TestRemoteSyncIntegration + + +class TestSoledadSynchronizer(BaseSoledadTest): + +    def setUp(self): +        BaseSoledadTest.setUp(self) +        self.db = Mock() +        self.target = Mock() +        self.synchronizer = sync.SoledadSynchronizer( +            self.db, +            self.target) + +    def test_docs_by_gen_includes_deleted(self): +        changes = [('id', 'gen', 'trans')] +        docs_by_gen = self.synchronizer._docs_by_gen_from_changes(changes) +        f, args, kwargs = docs_by_gen[0][0] +        self.assertIn('include_deleted', kwargs) +        self.assertTrue(kwargs['include_deleted']) diff --git a/tests/sync/test_sync_mutex.py b/tests/sync/test_sync_mutex.py new file mode 100644 index 00000000..fdd2aacd --- /dev/null +++ b/tests/sync/test_sync_mutex.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# test_sync_mutex.py +# Copyright (C) 2013, 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 <http://www.gnu.org/licenses/>. + + +""" +Test that synchronization is a critical section and, as such, there might not +be two concurrent synchronization processes at the same time. +""" + + +import pytest +import time +import uuid + +from six.moves.urllib.parse import urljoin + +from twisted.internet import defer + +from leap.soledad.client.sync import SoledadSynchronizer + +from leap.soledad.common.couch.state import CouchServerState +from leap.soledad.common.couch import CouchDatabase +from test_soledad.u1db_tests import TestCaseWithServer + +from test_soledad.util import CouchDBTestCase +from test_soledad.util import BaseSoledadTest +from test_soledad.util import make_token_soledad_app +from test_soledad.util import make_soledad_document_for_test +from test_soledad.util import soledad_sync_target + + +# monkey-patch the soledad synchronizer so it stores start and finish times + +_old_sync = SoledadSynchronizer.sync + + +def _timed_sync(self): +    t = time.time() + +    sync_id = uuid.uuid4() + +    if not getattr(self.source, 'sync_times', False): +        self.source.sync_times = {} + +    self.source.sync_times[sync_id] = {'start': t} + +    def _store_finish_time(passthrough): +        t = time.time() +        self.source.sync_times[sync_id]['end'] = t +        return passthrough + +    d = _old_sync(self) +    d.addBoth(_store_finish_time) +    return d + + +SoledadSynchronizer.sync = _timed_sync + +# -- end of monkey-patching + + +@pytest.mark.needs_couch +class TestSyncMutex( +        BaseSoledadTest, CouchDBTestCase, TestCaseWithServer): + +    @staticmethod +    def make_app_with_state(state): +        return make_token_soledad_app(state) + +    make_document_for_test = make_soledad_document_for_test + +    sync_target = soledad_sync_target + +    def make_app(self): +        self.request_state = CouchServerState(self.couch_url) +        return self.make_app_with_state(self.request_state) + +    def setUp(self): +        TestCaseWithServer.setUp(self) +        CouchDBTestCase.setUp(self) +        self.user = ('user-%s' % uuid.uuid4().hex) + +    def tearDown(self): +        CouchDBTestCase.tearDown(self) +        TestCaseWithServer.tearDown(self) + +    def test_two_concurrent_syncs_do_not_overlap_no_docs(self): +        self.startServer() + +        # ensure remote db exists before syncing +        db = CouchDatabase.open_database( +            urljoin(self.couch_url, 'user-' + self.user), +            create=True) + +        sol = self._soledad_instance( +            user=self.user, server_url=self.getURL()) + +        d1 = sol.sync() +        d2 = sol.sync() + +        def _assert_syncs_do_not_overlap(thearg): +            # recover sync times +            sync_times = [] +            for key in sol._dbsyncer.sync_times: +                sync_times.append(sol._dbsyncer.sync_times[key]) +            sync_times.sort(key=lambda s: s['start']) + +            self.assertTrue( +                (sync_times[0]['start'] < sync_times[0]['end'] and +                 sync_times[0]['end'] < sync_times[1]['start'] and +                 sync_times[1]['start'] < sync_times[1]['end'])) + +            db.delete_database() +            db.close() +            sol.close() + +        d = defer.gatherResults([d1, d2]) +        d.addBoth(_assert_syncs_do_not_overlap) +        return d diff --git a/tests/sync/test_sync_target.py b/tests/sync/test_sync_target.py new file mode 100644 index 00000000..712f0d3f --- /dev/null +++ b/tests/sync/test_sync_target.py @@ -0,0 +1,968 @@ +# -*- coding: utf-8 -*- +# test_sync_target.py +# Copyright (C) 2013, 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 <http://www.gnu.org/licenses/>. +""" +Test Leap backend bits: sync target +""" +import os +import time +import json +import pytest +import random +import string +import shutil + +from six import StringIO as cStringIO +from uuid import uuid4 + +from testscenarios import TestWithScenarios +from twisted.internet import defer + +from leap.soledad.client import http_target as target +from leap.soledad.client.http_target.fetch_protocol import DocStreamReceiver +from leap.soledad.client._db.sqlcipher import SQLCipherU1DBSync +from leap.soledad.client._db.sqlcipher import SQLCipherOptions +from leap.soledad.client._db.sqlcipher import SQLCipherDatabase +from leap.soledad.client import _crypto + +from leap.soledad.common import l2db + +from leap.soledad.common.document import SoledadDocument +from test_soledad import u1db_tests as tests +from test_soledad.util import make_sqlcipher_database_for_test +from test_soledad.util import make_soledad_app +from test_soledad.util import make_token_soledad_app +from test_soledad.util import make_soledad_document_for_test +from test_soledad.util import soledad_sync_target +from twisted.trial import unittest +from test_soledad.util import SoledadWithCouchServerMixin +from test_soledad.util import ADDRESS +from test_soledad.util import SQLCIPHER_SCENARIOS + + +# ----------------------------------------------------------------------------- +# The following tests come from `u1db.tests.test_remote_sync_target`. +# ----------------------------------------------------------------------------- + +class TestSoledadParseReceivedDocResponse(unittest.TestCase): + +    """ +    Some tests had to be copied to this class so we can instantiate our own +    target. +    """ + +    def parse(self, stream): +        parser = DocStreamReceiver(None, defer.Deferred(), +                                   lambda *_: defer.succeed(42)) +        parser.dataReceived(stream) +        parser.finish() + +    def test_extra_comma(self): +        doc = SoledadDocument('i', rev='r') +        doc.content = {'a': 'b'} + +        encrypted_docstr = _crypto.SoledadCrypto('safe').encrypt_doc(doc) + +        with self.assertRaises(l2db.errors.BrokenSyncStream): +            self.parse("[\r\n{},\r\n]") + +        with self.assertRaises(l2db.errors.BrokenSyncStream): +            self.parse( +                ('[\r\n{},\r\n{"id": "i", "rev": "r", ' + +                 '"gen": 3, "trans_id": "T-sid"},\r\n' + +                 '%s,\r\n]') % encrypted_docstr) + +    def test_wrong_start(self): +        with self.assertRaises(l2db.errors.BrokenSyncStream): +            self.parse("{}\r\n]") + +        with self.assertRaises(l2db.errors.BrokenSyncStream): +            self.parse("\r\n{}\r\n]") + +    def test_wrong_end(self): +        with self.assertRaises(l2db.errors.BrokenSyncStream): +            self.parse("[\r\n{}") + +        with self.assertRaises(l2db.errors.BrokenSyncStream): +            self.parse("[\r\n") + +    def test_missing_comma(self): +        with self.assertRaises(l2db.errors.BrokenSyncStream): +            self.parse( +                '[\r\n{}\r\n{"id": "i", "rev": "r", ' +                '"content": "c", "gen": 3}\r\n]') + +    def test_no_entries(self): +        with self.assertRaises(l2db.errors.BrokenSyncStream): +            self.parse("[\r\n]") + +    def test_error_in_stream(self): +        with self.assertRaises(l2db.errors.BrokenSyncStream): +            self.parse( +                '[\r\n{"new_generation": 0},' +                '\r\n{"error": "unavailable"}\r\n') + +        with self.assertRaises(l2db.errors.BrokenSyncStream): +            self.parse( +                '[\r\n{"error": "unavailable"}\r\n') + +        with self.assertRaises(l2db.errors.BrokenSyncStream): +            self.parse('[\r\n{"error": "?"}\r\n') + +# +# functions for TestRemoteSyncTargets +# + + +def make_local_db_and_soledad_target( +        test, path='test', +        source_replica_uid=uuid4().hex): +    test.startTwistedServer() +    replica_uid = os.path.basename(path) +    db = test.request_state._create_database(replica_uid) +    st = soledad_sync_target( +        test, db._dbname, +        source_replica_uid=source_replica_uid) +    return db, st + + +def make_local_db_and_token_soledad_target( +        test, +        source_replica_uid=uuid4().hex): +    db, st = make_local_db_and_soledad_target( +        test, path='test', +        source_replica_uid=source_replica_uid) +    st.set_token_credentials('user-uuid', 'auth-token') +    return db, st + + +@pytest.mark.needs_couch +class TestSoledadSyncTarget( +        TestWithScenarios, +        SoledadWithCouchServerMixin, +        tests.TestCaseWithServer): + +    scenarios = [ +        ('token_soledad', +            {'make_app_with_state': make_token_soledad_app, +             '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': soledad_sync_target}), +    ] + +    def getSyncTarget(self, path=None, source_replica_uid=uuid4().hex): +        if self.port is None: +            self.startTwistedServer() +        if path is None: +            path = self.db2._dbname +        target = self.sync_target( +            self, path, +            source_replica_uid=source_replica_uid) +        return target + +    def setUp(self): +        TestWithScenarios.setUp(self) +        SoledadWithCouchServerMixin.setUp(self) +        self.startTwistedServer() +        self.db1 = make_sqlcipher_database_for_test(self, 'test1') +        self.db2 = self.request_state._create_database('test') + +    def tearDown(self): +        # db2, _ = self.request_state.ensure_database('test2') +        self.delete_db(self.db2._dbname) +        self.db1.close() +        SoledadWithCouchServerMixin.tearDown(self) +        TestWithScenarios.tearDown(self) + +    @defer.inlineCallbacks +    def test_sync_exchange_send(self): +        """ +        Test for sync exchanging send of document. + +        This test was adapted to decrypt remote content before assert. +        """ +        db = self.db2 +        remote_target = self.getSyncTarget() +        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"}') +        get_doc = (lambda _: doc, (1,), {}) +        new_gen, trans_id = yield remote_target.sync_exchange( +            [(get_doc, 10, 'T-sid')], 'replica', last_known_generation=0, +            last_known_trans_id=None, insert_doc_cb=receive_doc) +        self.assertEqual(1, new_gen) +        self.assertGetEncryptedDoc( +            db, 'doc-here', 'replica:1', '{"value": "here"}', False) + +    @defer.inlineCallbacks +    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. +        """ + +        def blackhole_getstderr(inst): +            return cStringIO.StringIO() + +        db = self.db2 +        _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 l2db.errors.U1DBError +            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.backend import SoledadBackend +        self.patch( +            SoledadBackend, '_put_doc_if_newer', bomb_put_doc_if_newer) +        remote_target = self.getSyncTarget( +            source_replica_uid='replica') +        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"}') +        get_doc1 = (lambda _: doc1, (1,), {}) +        get_doc2 = (lambda _: doc2, (2,), {}) + +        with self.assertRaises(l2db.errors.U1DBError): +            yield remote_target.sync_exchange( +                [(get_doc1, 10, 'T-sid'), (get_doc2, 11, 'T-sud')], +                'replica', +                last_known_generation=0, +                last_known_trans_id=None, +                insert_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 = yield remote_target.sync_exchange( +            [(get_doc2, 11, 'T-sud')], 'replica', last_known_generation=0, +            last_known_trans_id=None, insert_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]) + +    @defer.inlineCallbacks +    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. +        """ +        remote_target = self.getSyncTarget() +        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"}') +        get_doc = (lambda _: doc, (1,), {}) +        new_gen, trans_id = yield remote_target.sync_exchange( +            [(get_doc, 10, 'T-sid')], 'replica', last_known_generation=0, +            last_known_trans_id=None, insert_doc_cb=receive_doc, +            ensure_callback=ensure_cb) +        self.assertEqual(1, new_gen) +        db = self.db2 +        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) + +    @defer.inlineCallbacks +    def test_sync_exchange_send_events(self): +        """ +        Test for sync exchange's SOLEDAD_SYNC_SEND_STATUS event. +        """ +        remote_target = self.getSyncTarget() +        uuid = remote_target.uuid +        events = [] + +        def mocked_events(*args): +            events.append((args)) +        self.patch( +            target.send, '_emit_send_status', mocked_events) + +        doc = self.make_document('doc-here', 'replica:1', '{"value": "here"}') +        doc2 = self.make_document('doc-here', 'replica:1', '{"value": "here"}') +        doc3 = self.make_document('doc-here', 'replica:1', '{"value": "here"}') +        get_doc = (lambda _: doc, (1,), {}) +        get_doc2 = (lambda _: doc2, (1,), {}) +        get_doc3 = (lambda _: doc3, (1,), {}) +        docs = [(get_doc, 10, 'T-sid'), +                (get_doc2, 11, 'T-sid2'), (get_doc3, 12, 'T-sid3')] +        new_gen, trans_id = yield remote_target.sync_exchange( +            docs, 'replica', last_known_generation=0, +            last_known_trans_id=None, insert_doc_cb=lambda _: 1, +            ensure_callback=lambda _: 1) +        self.assertEqual(1, new_gen) +        self.assertEqual(4, len(events)) +        self.assertEquals([(uuid, 0, 3), (uuid, 1, 3), (uuid, 2, 3), +                           (uuid, 3, 3)], events) + +    def test_sync_exchange_in_stream_error(self): +        self.skipTest("bypass this test because our sync_exchange process " +                      "does not return u1db error 503 \"unavailable\" for " +                      "now") + +    @defer.inlineCallbacks +    def test_get_sync_info(self): +        db = self.db2 +        db._set_replica_gen_and_trans_id('other-id', 1, 'T-transid') +        remote_target = self.getSyncTarget( +            source_replica_uid='other-id') +        sync_info = yield remote_target.get_sync_info('other-id') +        self.assertEqual( +            ('test', 0, '', 1, 'T-transid'), +            sync_info) + +    @defer.inlineCallbacks +    def test_record_sync_info(self): +        remote_target = self.getSyncTarget( +            source_replica_uid='other-id') +        yield remote_target.record_sync_info('other-id', 2, 'T-transid') +        self.assertEqual((2, 'T-transid'), +                         self.db2._get_replica_gen_and_trans_id('other-id')) + +    @defer.inlineCallbacks +    def test_sync_exchange_receive(self): +        db = self.db2 +        doc = db.create_doc_from_json('{"value": "there"}') +        remote_target = self.getSyncTarget() +        other_changes = [] + +        def receive_doc(doc, gen, trans_id): +            other_changes.append( +                (doc.doc_id, doc.rev, doc.get_json(), gen, trans_id)) + +        new_gen, trans_id = yield remote_target.sync_exchange( +            [], 'replica', last_known_generation=0, last_known_trans_id=None, +            insert_doc_cb=receive_doc) +        self.assertEqual(1, new_gen) +        self.assertEqual( +            (doc.doc_id, doc.rev, '{"value": "there"}', 1), +            other_changes[0][:-1]) + + +# ----------------------------------------------------------------------------- +# The following tests come from `u1db.tests.test_sync`. +# ----------------------------------------------------------------------------- + +target_scenarios = [ +    ('mem,token_soledad', +     {'create_db_and_target': make_local_db_and_token_soledad_target, +      'make_app_with_state': make_soledad_app, +      'make_database_for_test': tests.make_memory_database_for_test, +      'copy_database_for_test': tests.copy_memory_database_for_test, +      'make_document_for_test': tests.make_document_for_test}) +] + + +@pytest.mark.needs_couch +class SoledadDatabaseSyncTargetTests( +        TestWithScenarios, +        SoledadWithCouchServerMixin, +        tests.DatabaseBaseTests, +        tests.TestCaseWithServer): +    """ +    Adaptation of u1db.tests.test_sync.DatabaseSyncTargetTests. +    """ + +    # TODO: implement _set_trace_hook(_shallow) in SoledadHTTPSyncTarget so +    #       skipped tests can be succesfully executed. + +    scenarios = target_scenarios + +    whitebox = False + +    def setUp(self): +        tests.TestCaseWithServer.setUp(self) +        self.other_changes = [] +        SoledadWithCouchServerMixin.setUp(self) +        self.db, self.st = make_local_db_and_soledad_target(self) + +    def tearDown(self): +        self.db.close() +        tests.TestCaseWithServer.tearDown(self) +        SoledadWithCouchServerMixin.tearDown(self) + +    def set_trace_hook(self, callback, shallow=False): +        setter = (self.st._set_trace_hook if not shallow else +                  self.st._set_trace_hook_shallow) +        try: +            setter(callback) +        except NotImplementedError: +            self.skipTest("%s does not implement _set_trace_hook" +                          % (self.st.__class__.__name__,)) + +    @defer.inlineCallbacks +    def test_sync_exchange(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-sid')] +        new_gen, trans_id = yield self.st.sync_exchange( +            docs_by_gen, 'replica', last_known_generation=0, +            last_known_trans_id=None, insert_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)) +        sync_info = yield self.st.get_sync_info('replica') +        self.assertEqual(10, sync_info[3]) + +    @defer.inlineCallbacks +    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 = yield self.st.sync_exchange( +            docs_by_gen, 'replica', last_known_generation=0, +            last_known_trans_id=None, insert_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)) +        sync_info = yield self.st.get_sync_info('replica') +        self.assertEqual(11, sync_info[3]) + +    @defer.inlineCallbacks +    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, _ = yield self.st.sync_exchange( +            [], 'other-replica', last_known_generation=0, +            last_known_trans_id=None, insert_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)]}) + +    def receive_doc(self, doc, gen, trans_id): +        self.other_changes.append( +            (doc.doc_id, doc.rev, doc.get_json(), gen, trans_id)) + +    def test_get_sync_target(self): +        self.assertIsNot(None, self.st) + +    @defer.inlineCallbacks +    def test_get_sync_info(self): +        sync_info = yield self.st.get_sync_info('other') +        self.assertEqual( +            ('test', 0, '', 0, ''), sync_info) + +    @defer.inlineCallbacks +    def test_create_doc_updates_sync_info(self): +        sync_info = yield self.st.get_sync_info('other') +        self.assertEqual( +            ('test', 0, '', 0, ''), sync_info) +        self.db.create_doc_from_json(tests.simple_doc) +        sync_info = yield self.st.get_sync_info('other') +        self.assertEqual(1, sync_info[1]) + +    @defer.inlineCallbacks +    def test_record_sync_info(self): +        yield self.st.record_sync_info('replica', 10, 'T-transid') +        sync_info = yield self.st.get_sync_info('replica') +        self.assertEqual( +            ('test', 0, '', 10, 'T-transid'), sync_info) + +    @defer.inlineCallbacks +    def test_sync_exchange_deleted(self): +        doc = self.db.create_doc_from_json('{}') +        edit_rev = 'replica:1|' + doc.rev +        docs_by_gen = [ +            ((self.make_document, (doc.doc_id, edit_rev, None), {}), +             10, 'T-sid')] +        new_gen, trans_id = yield self.st.sync_exchange( +            docs_by_gen, 'replica', last_known_generation=0, +            last_known_trans_id=None, insert_doc_cb=self.receive_doc) +        self.assertGetDocIncludeDeleted( +            self.db, doc.doc_id, edit_rev, None, False) +        self.assertTransactionLog([doc.doc_id, doc.doc_id], self.db) +        last_trans_id = self.getLastTransId(self.db) +        self.assertEqual(([], 2, last_trans_id), +                         (self.other_changes, new_gen, trans_id)) +        sync_info = yield self.st.get_sync_info('replica') +        self.assertEqual(10, sync_info[3]) + +    @defer.inlineCallbacks +    def test_sync_exchange_refuses_conflicts(self): +        doc = self.db.create_doc_from_json(tests.simple_doc) +        self.assertTransactionLog([doc.doc_id], self.db) +        new_doc = '{"key": "altval"}' +        docs_by_gen = [ +            ((self.make_document, (doc.doc_id, 'replica:1', new_doc), {}), 10, +             'T-sid')] +        new_gen, _ = yield self.st.sync_exchange( +            docs_by_gen, 'replica', last_known_generation=0, +            last_known_trans_id=None, insert_doc_cb=self.receive_doc) +        self.assertTransactionLog([doc.doc_id], self.db) +        self.assertEqual( +            (doc.doc_id, doc.rev, tests.simple_doc, 1), +            self.other_changes[0][:-1]) +        self.assertEqual(1, new_gen) +        if self.whitebox: +            self.assertEqual(self.db._last_exchange_log['return'], +                             {'last_gen': 1, 'docs': [(doc.doc_id, doc.rev)]}) + +    @defer.inlineCallbacks +    def test_sync_exchange_ignores_convergence(self): +        doc = self.db.create_doc_from_json(tests.simple_doc) +        self.assertTransactionLog([doc.doc_id], self.db) +        gen, txid = self.db._get_generation_info() +        docs_by_gen = [ +            ((self.make_document, (doc.doc_id, doc.rev, tests.simple_doc), {}), +             10, 'T-sid')] +        new_gen, _ = yield self.st.sync_exchange( +            docs_by_gen, 'replica', last_known_generation=gen, +            last_known_trans_id=txid, insert_doc_cb=self.receive_doc) +        self.assertTransactionLog([doc.doc_id], self.db) +        self.assertEqual(([], 1), (self.other_changes, new_gen)) + +    @defer.inlineCallbacks +    def test_sync_exchange_returns_new_docs(self): +        doc = self.db.create_doc_from_json(tests.simple_doc) +        self.assertTransactionLog([doc.doc_id], self.db) +        new_gen, _ = yield self.st.sync_exchange( +            [], 'other-replica', last_known_generation=0, +            last_known_trans_id=None, insert_doc_cb=self.receive_doc) +        self.assertTransactionLog([doc.doc_id], self.db) +        self.assertEqual( +            (doc.doc_id, doc.rev, tests.simple_doc, 1), +            self.other_changes[0][:-1]) +        self.assertEqual(1, new_gen) +        if self.whitebox: +            self.assertEqual(self.db._last_exchange_log['return'], +                             {'last_gen': 1, 'docs': [(doc.doc_id, doc.rev)]}) + +    @defer.inlineCallbacks +    def test_sync_exchange_returns_deleted_docs(self): +        doc = self.db.create_doc_from_json(tests.simple_doc) +        self.db.delete_doc(doc) +        self.assertTransactionLog([doc.doc_id, doc.doc_id], self.db) +        new_gen, _ = yield self.st.sync_exchange( +            [], 'other-replica', last_known_generation=0, +            last_known_trans_id=None, insert_doc_cb=self.receive_doc) +        self.assertTransactionLog([doc.doc_id, doc.doc_id], self.db) +        self.assertEqual(2, new_gen) +        self.assertEqual( +            (doc.doc_id, doc.rev, None, 2), self.other_changes[0][:-1]) +        if self.whitebox: +            self.assertEqual(self.db._last_exchange_log['return'], +                             {'last_gen': 2, 'docs': [(doc.doc_id, doc.rev)]}) + +    @defer.inlineCallbacks +    def test_sync_exchange_getting_newer_docs(self): +        doc = self.db.create_doc_from_json(tests.simple_doc) +        self.assertTransactionLog([doc.doc_id], self.db) +        new_doc = '{"key": "altval"}' +        docs_by_gen = [ +            ((self.make_document, (doc.doc_id, 'test:1|z:2', new_doc), {}), 10, +             'T-sid')] +        new_gen, _ = yield self.st.sync_exchange( +            docs_by_gen, 'other-replica', last_known_generation=0, +            last_known_trans_id=None, insert_doc_cb=self.receive_doc) +        self.assertTransactionLog([doc.doc_id, doc.doc_id], self.db) +        self.assertEqual(([], 2), (self.other_changes, new_gen)) + +    @defer.inlineCallbacks +    def test_sync_exchange_with_concurrent_updates_of_synced_doc(self): +        expected = [] + +        def before_whatschanged_cb(state): +            if state != 'before whats_changed': +                return +            cont = '{"key": "cuncurrent"}' +            conc_rev = self.db.put_doc( +                self.make_document(doc.doc_id, 'test:1|z:2', cont)) +            expected.append((doc.doc_id, conc_rev, cont, 3)) + +        self.set_trace_hook(before_whatschanged_cb) +        doc = self.db.create_doc_from_json(tests.simple_doc) +        self.assertTransactionLog([doc.doc_id], self.db) +        new_doc = '{"key": "altval"}' +        docs_by_gen = [ +            ((self.make_document, (doc.doc_id, 'test:1|z:2', new_doc), {}), 10, +             'T-sid')] +        new_gen, _ = yield self.st.sync_exchange( +            docs_by_gen, 'other-replica', last_known_generation=0, +            last_known_trans_id=None, insert_doc_cb=self.receive_doc) +        self.assertEqual(expected, [c[:-1] for c in self.other_changes]) +        self.assertEqual(3, new_gen) + +    @defer.inlineCallbacks +    def test_sync_exchange_with_concurrent_updates(self): + +        def after_whatschanged_cb(state): +            if state != 'after whats_changed': +                return +            self.db.create_doc_from_json('{"new": "doc"}') + +        self.set_trace_hook(after_whatschanged_cb) +        doc = self.db.create_doc_from_json(tests.simple_doc) +        self.assertTransactionLog([doc.doc_id], self.db) +        new_doc = '{"key": "altval"}' +        docs_by_gen = [ +            ((self.make_document, (doc.doc_id, 'test:1|z:2', new_doc), {}), 10, +             'T-sid')] +        new_gen, _ = yield self.st.sync_exchange( +            docs_by_gen, 'other-replica', last_known_generation=0, +            last_known_trans_id=None, insert_doc_cb=self.receive_doc) +        self.assertEqual(([], 2), (self.other_changes, new_gen)) + +    @defer.inlineCallbacks +    def test_sync_exchange_converged_handling(self): +        doc = self.db.create_doc_from_json(tests.simple_doc) +        docs_by_gen = [ +            ((self.make_document, ('new', 'other:1', '{}'), {}), 4, 'T-foo'), +            ((self.make_document, (doc.doc_id, doc.rev, doc.get_json()), {}), +                5, 'T-bar')] +        new_gen, _ = yield self.st.sync_exchange( +            docs_by_gen, 'other-replica', last_known_generation=0, +            last_known_trans_id=None, insert_doc_cb=self.receive_doc) +        self.assertEqual(([], 2), (self.other_changes, new_gen)) + +    @defer.inlineCallbacks +    def test_sync_exchange_detect_incomplete_exchange(self): +        def before_get_docs_explode(state): +            if state != 'before get_docs': +                return +            raise l2db.errors.U1DBError("fail") +        self.set_trace_hook(before_get_docs_explode) +        # suppress traceback printing in the wsgiref server +        # self.patch(simple_server.ServerHandler, +        #           'log_exception', lambda h, exc_info: None) +        doc = self.db.create_doc_from_json(tests.simple_doc) +        self.assertTransactionLog([doc.doc_id], self.db) +        self.assertRaises( +            (l2db.errors.U1DBError, l2db.errors.BrokenSyncStream), +            self.st.sync_exchange, [], 'other-replica', +            last_known_generation=0, last_known_trans_id=None, +            insert_doc_cb=self.receive_doc) + +    @defer.inlineCallbacks +    def test_sync_exchange_doc_ids(self): +        sync_exchange_doc_ids = getattr(self.st, 'sync_exchange_doc_ids', None) +        if sync_exchange_doc_ids is None: +            self.skipTest("sync_exchange_doc_ids not implemented") +        db2 = self.create_database('test2') +        doc = db2.create_doc_from_json(tests.simple_doc) +        new_gen, trans_id = yield sync_exchange_doc_ids( +            db2, [(doc.doc_id, 10, 'T-sid')], 0, None, +            insert_doc_cb=self.receive_doc) +        self.assertGetDoc(self.db, doc.doc_id, doc.rev, +                          tests.simple_doc, False) +        self.assertTransactionLog([doc.doc_id], self.db) +        last_trans_id = self.getLastTransId(self.db) +        self.assertEqual(([], 1, last_trans_id), +                         (self.other_changes, new_gen, trans_id)) +        self.assertEqual(10, self.st.get_sync_info(db2._replica_uid)[3]) + +    @defer.inlineCallbacks +    def test__set_trace_hook(self): +        called = [] + +        def cb(state): +            called.append(state) + +        self.set_trace_hook(cb) +        yield self.st.sync_exchange([], 'replica', 0, None, self.receive_doc) +        yield self.st.record_sync_info('replica', 0, 'T-sid') +        self.assertEqual(['before whats_changed', +                          'after whats_changed', +                          'before get_docs', +                          'record_sync_info', +                          ], +                         called) + +    @defer.inlineCallbacks +    def test__set_trace_hook_shallow(self): +        if (self.st._set_trace_hook_shallow == self.st._set_trace_hook or +            self.st._set_trace_hook_shallow.im_func == +                target.SoledadHTTPSyncTarget._set_trace_hook_shallow.im_func): +            # shallow same as full +            expected = ['before whats_changed', +                        'after whats_changed', +                        'before get_docs', +                        'record_sync_info', +                        ] +        else: +            expected = ['sync_exchange', 'record_sync_info'] + +        called = [] + +        def cb(state): +            called.append(state) + +        self.set_trace_hook(cb, shallow=True) +        yield self.st.sync_exchange([], 'replica', 0, None, self.receive_doc) +        yield self.st.record_sync_info('replica', 0, 'T-sid') +        self.assertEqual(expected, called) + + +WAIT_STEP = 1 +MAX_WAIT = 10 +DBPASS = "pass" + + +class SyncTimeoutError(Exception): + +    """ +    Dummy exception to notify timeout during sync. +    """ +    pass + + +@pytest.mark.needs_couch +class TestSoledadDbSync( +        TestWithScenarios, +        SoledadWithCouchServerMixin, +        tests.TestCaseWithServer): + +    """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): +        """ +        Need to explicitely invoke inicialization on all bases. +        """ +        SoledadWithCouchServerMixin.setUp(self) +        self.server = self.server_thread = None +        self.startTwistedServer() +        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.local_key) +        dbpath = self._soledad._local_db_path + +        self.opts = SQLCipherOptions( +            dbpath, key, is_raw_key=True, create=False) +        self.db1 = SQLCipherDatabase(self.opts) + +        self.db2 = self.request_state._create_database(replica_uid='test') + +    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) + +    def do_sync(self, target_name): +        """ +        Perform sync using SoledadSynchronizer, SoledadSyncTarget +        and Token auth. +        """ +        if self.token: +            creds = {'token': { +                'uuid': 'user-uuid', +                'token': 'auth-token', +            }} +            target_url = self.getURL(self.db2._dbname) + +            # get a u1db syncer +            crypto = self._soledad._crypto +            replica_uid = self.db1._replica_uid +            dbsyncer = SQLCipherU1DBSync( +                self.opts, +                crypto, +                replica_uid, +                None) +            self.dbsyncer = dbsyncer +            return dbsyncer.sync(target_url, +                                 creds=creds) +        else: +            return self._do_sync(self, target_name) + +    def _do_sync(self, target_name): +        if self.oauth: +            path = '~/' + target_name +            extra = dict(creds={'oauth': { +                'consumer_key': tests.consumer1.key, +                'consumer_secret': tests.consumer1.secret, +                'token_key': tests.token1.key, +                'token_secret': tests.token1.secret, +            }}) +        else: +            path = target_name +            extra = {} +        target_url = self.getURL(path) +        return self.db.sync(target_url, **extra) + +    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.db1.create_doc_from_json(tests.simple_doc) +        doc2 = self.db2.create_doc_from_json(tests.nested_doc) +        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 + + +@pytest.mark.needs_couch +class SQLCipherSyncTargetTests(SoledadDatabaseSyncTargetTests): + +    # TODO: implement _set_trace_hook(_shallow) in SoledadHTTPSyncTarget so +    #       skipped tests can be succesfully executed. + +    scenarios = (tests.multiply_scenarios(SQLCIPHER_SCENARIOS, +                                          target_scenarios)) + +    whitebox = False | 
