summaryrefslogtreecommitdiff
path: root/src/leap/soledad/tests/test_couch.py
diff options
context:
space:
mode:
authordrebs <drebs@leap.se>2013-03-07 17:23:05 -0300
committerdrebs <drebs@leap.se>2013-03-07 17:23:05 -0300
commitd89c1849d551de68d26a1f56798ee5084dca6556 (patch)
tree25a54c87b22fca5ac494621a0ccbcca9e181cf76 /src/leap/soledad/tests/test_couch.py
parent1b1def113e6ed9b8af6897e16f0d9b4c96bbfa6b (diff)
Move source files to proper subdirectory.
Diffstat (limited to 'src/leap/soledad/tests/test_couch.py')
-rw-r--r--src/leap/soledad/tests/test_couch.py407
1 files changed, 407 insertions, 0 deletions
diff --git a/src/leap/soledad/tests/test_couch.py b/src/leap/soledad/tests/test_couch.py
new file mode 100644
index 00000000..c02d485b
--- /dev/null
+++ b/src/leap/soledad/tests/test_couch.py
@@ -0,0 +1,407 @@
+"""Test ObjectStore backend bits.
+
+For these tests to run, a couch server has to be running on (default) port
+5984.
+"""
+
+import re
+import copy
+from base64 import b64decode
+from leap.soledad.backends import couch
+from leap.soledad.tests import u1db_tests as tests
+from leap.soledad.tests.u1db_tests import test_backends
+from leap.soledad.tests.u1db_tests import test_sync
+try:
+ import simplejson as json
+except ImportError:
+ import json # noqa
+
+
+#-----------------------------------------------------------------------------
+# 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
+ os.mkdir(os.path.join(self.tempdir, 'lib'))
+ os.mkdir(os.path.join(self.tempdir, 'log'))
+ args = ['couchdb', '-n' '-a', confPath]
+ #null = open('/dev/null', 'w')
+ self.process = subprocess.Popen(
+ args, env=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ close_fds=True)
+ # find port
+ logPath = os.path.join(self.tempdir, 'log', 'couch.log')
+ while not os.path.exists(logPath):
+ if self.process.poll() is not None:
+ raise Exception("""
+couchdb exited with code %d.
+stdout:
+%s
+stderr:
+%s""" % (
+ self.process.returncode, self.process.stdout.read(),
+ self.process.stderr.read()))
+ 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<port>\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()
+ os.system("rm -rf %s" % self.tempdir)
+
+
+class CouchDBTestCase(unittest.TestCase):
+ """
+ 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
+ super(CouchDBTestCase, self).setUp()
+
+ def tearDown(self):
+ """
+ Stop CouchDB instance for test.
+ """
+ self.wrapper.stop()
+ super(CouchDBTestCase, self).tearDown()
+
+
+#-----------------------------------------------------------------------------
+# The following tests come from `u1db.tests.test_common_backend`.
+#-----------------------------------------------------------------------------
+
+class TestCouchBackendImpl(CouchDBTestCase):
+
+ def test__allocate_doc_id(self):
+ db = couch.CouchDatabase('http://localhost:' + str(self.wrapper.port),
+ 'u1db_tests')
+ doc_id1 = db._allocate_doc_id()
+ self.assertTrue(doc_id1.startswith('D-'))
+ self.assertEqual(34, len(doc_id1))
+ int(doc_id1[len('D-'):], 16)
+ self.assertNotEqual(doc_id1, db._allocate_doc_id())
+
+
+#-----------------------------------------------------------------------------
+# The following tests come from `u1db.tests.test_backends`.
+#-----------------------------------------------------------------------------
+
+def make_couch_database_for_test(test, replica_uid):
+ port = str(test.wrapper.port)
+ return couch.CouchDatabase('http://localhost:' + port, replica_uid,
+ replica_uid=replica_uid or 'test')
+
+
+def copy_couch_database_for_test(test, db):
+ port = str(test.wrapper.port)
+ new_db = couch.CouchDatabase('http://localhost:' + port,
+ db._replica_uid + '_copy',
+ replica_uid=db._replica_uid or 'test')
+ gen, docs = db.get_all_docs(include_deleted=True)
+ for doc in docs:
+ new_db._put_doc(doc)
+ new_db._transaction_log = copy.deepcopy(db._transaction_log)
+ new_db._conflicts = copy.deepcopy(db._conflicts)
+ new_db._other_generations = copy.deepcopy(db._other_generations)
+ new_db._indexes = copy.deepcopy(db._indexes)
+ new_db._store_u1db_data()
+ return new_db
+
+
+COUCH_SCENARIOS = [
+ ('couch', {'make_database_for_test': make_couch_database_for_test,
+ 'copy_database_for_test': copy_couch_database_for_test,
+ 'make_document_for_test': tests.make_document_for_test, }),
+]
+
+
+class CouchTests(test_backends.AllDatabaseTests, CouchDBTestCase):
+
+ scenarios = COUCH_SCENARIOS
+
+ def tearDown(self):
+ self.db.delete_database()
+ super(CouchTests, self).tearDown()
+
+
+class CouchDatabaseTests(test_backends.LocalDatabaseTests, CouchDBTestCase):
+
+ scenarios = COUCH_SCENARIOS
+
+ def tearDown(self):
+ self.db.delete_database()
+ super(CouchDatabaseTests, self).tearDown()
+
+
+class CouchValidateGenNTransIdTests(
+ test_backends.LocalDatabaseValidateGenNTransIdTests, CouchDBTestCase):
+
+ scenarios = COUCH_SCENARIOS
+
+ def tearDown(self):
+ self.db.delete_database()
+ super(CouchValidateGenNTransIdTests, self).tearDown()
+
+
+class CouchValidateSourceGenTests(
+ test_backends.LocalDatabaseValidateSourceGenTests, CouchDBTestCase):
+
+ scenarios = COUCH_SCENARIOS
+
+ def tearDown(self):
+ self.db.delete_database()
+ super(CouchValidateSourceGenTests, self).tearDown()
+
+
+class CouchWithConflictsTests(
+ test_backends.LocalDatabaseWithConflictsTests, CouchDBTestCase):
+
+ scenarios = COUCH_SCENARIOS
+
+ def tearDown(self):
+ self.db.delete_database()
+ super(CouchWithConflictsTests, self).tearDown()
+
+
+# Notice: the CouchDB backend is currently used for storing encrypted data in
+# the server, so indexing makes no sense. Thus, we ignore index testing for
+# now.
+
+class CouchIndexTests(test_backends.DatabaseIndexTests, CouchDBTestCase):
+
+ scenarios = COUCH_SCENARIOS
+
+ def tearDown(self):
+ self.db.delete_database()
+ super(CouchIndexTests, self).tearDown()
+
+
+#-----------------------------------------------------------------------------
+# The following tests come from `u1db.tests.test_sync`.
+#-----------------------------------------------------------------------------
+
+target_scenarios = [
+ ('local', {'create_db_and_target': test_sync._make_local_db_and_target}), ]
+
+
+simple_doc = tests.simple_doc
+nested_doc = tests.nested_doc
+
+
+class CouchDatabaseSyncTargetTests(test_sync.DatabaseSyncTargetTests,
+ CouchDBTestCase):
+
+ scenarios = (tests.multiply_scenarios(COUCH_SCENARIOS, target_scenarios))
+
+ def tearDown(self):
+ self.db.delete_database()
+ super(CouchDatabaseSyncTargetTests, self).tearDown()
+
+ 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
+ # serialized representations).
+ doc = self.db.create_doc_from_json(simple_doc)
+ doc2 = self.db.create_doc_from_json(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, json.loads(simple_doc), 1),
+ (doc2.doc_id, doc2.rev, json.loads(nested_doc), 2)],
+ [c[:-3] + (json.loads(c[-3]), c[-2]) for c in self.other_changes])
+ 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)]})
+
+
+sync_scenarios = []
+for name, scenario in COUCH_SCENARIOS:
+ scenario = dict(scenario)
+ scenario['do_sync'] = test_sync.sync_via_synchronizer
+ sync_scenarios.append((name, scenario))
+ scenario = dict(scenario)
+
+
+class CouchDatabaseSyncTests(test_sync.DatabaseSyncTests, CouchDBTestCase):
+
+ scenarios = sync_scenarios
+
+ def setUp(self):
+ self.db = None
+ self.db1 = None
+ self.db2 = None
+ self.db3 = None
+ super(CouchDatabaseSyncTests, self).setUp()
+
+ def tearDown(self):
+ self.db and self.db.delete_database()
+ self.db1 and self.db1.delete_database()
+ self.db2 and self.db2.delete_database()
+ self.db3 and self.db3.delete_database()
+ db = self.create_database('test1_copy', 'source')
+ db.delete_database()
+ db = self.create_database('test2_copy', 'target')
+ db.delete_database()
+ db = self.create_database('test3', 'target')
+ db.delete_database()
+ super(CouchDatabaseSyncTests, self).tearDown()
+
+
+#-----------------------------------------------------------------------------
+# The following tests test extra functionality introduced by our backends
+#-----------------------------------------------------------------------------
+
+class CouchDatabaseStorageTests(CouchDBTestCase):
+
+ def _listify(self, l):
+ if type(l) is dict:
+ return {
+ self._listify(a): self._listify(b) for a, b in l.iteritems()}
+ if hasattr(l, '__iter__'):
+ return [self._listify(i) for i in l]
+ return l
+
+ def _fetch_u1db_data(self, db):
+ cdoc = db._database.get(db.U1DB_DATA_DOC_ID)
+ jsonstr = db._database.get_attachment(cdoc, 'u1db_json').getvalue()
+ return json.loads(jsonstr)
+
+ def test_transaction_log_storage_after_put(self):
+ db = couch.CouchDatabase('http://localhost:' + str(self.wrapper.port),
+ 'u1db_tests')
+ db.create_doc({'simple': 'doc'})
+ content = self._fetch_u1db_data(db)
+ self.assertEqual(
+ self._listify(db._transaction_log),
+ self._listify(content['transaction_log']))
+
+ def test_conflict_log_storage_after_put_if_newer(self):
+ db = couch.CouchDatabase('http://localhost:' + str(self.wrapper.port),
+ 'u1db_tests')
+ doc = db.create_doc({'simple': 'doc'})
+ doc.set_json(nested_doc)
+ doc.rev = db._replica_uid + ':2'
+ db._force_doc_sync_conflict(doc)
+ content = self._fetch_u1db_data(db)
+ self.assertEqual(
+ self._listify(db._conflicts),
+ self._listify(json.loads(b64decode(content['conflicts']))))
+
+ def test_other_gens_storage_after_set(self):
+ db = couch.CouchDatabase('http://localhost:' + str(self.wrapper.port),
+ 'u1db_tests')
+ doc = db.create_doc({'simple': 'doc'})
+ db._set_replica_gen_and_trans_id('a', 'b', 'c')
+ content = self._fetch_u1db_data(db)
+ self.assertEqual(
+ self._listify(db._other_generations),
+ self._listify(content['other_generations']))
+
+ def test_index_storage_after_create(self):
+ db = couch.CouchDatabase('http://localhost:' + str(self.wrapper.port),
+ 'u1db_tests')
+ doc = db.create_doc({'name': 'john'})
+ db.create_index('myindex', 'name')
+ content = self._fetch_u1db_data(db)
+ myind = db._indexes['myindex']
+ index = {
+ 'myindex': {
+ 'definition': myind._definition,
+ 'name': myind._name,
+ 'values': myind._values,
+ }
+ }
+ self.assertEqual(self._listify(index),
+ self._listify(
+ json.loads(b64decode(content['indexes']))))
+
+ def test_index_storage_after_delete(self):
+ db = couch.CouchDatabase('http://localhost:' + str(self.wrapper.port),
+ 'u1db_tests')
+ doc = db.create_doc({'name': 'john'})
+ db.create_index('myindex', 'name')
+ db.create_index('myindex2', 'name')
+ db.delete_index('myindex')
+ content = self._fetch_u1db_data(db)
+ myind = db._indexes['myindex2']
+ index = {
+ 'myindex2': {
+ 'definition': myind._definition,
+ 'name': myind._name,
+ 'values': myind._values,
+ }
+ }
+ self.assertEqual(self._listify(index),
+ self._listify(
+ json.loads(b64decode(content['indexes']))))
+
+ def test_replica_uid_storage_after_db_creation(self):
+ db = couch.CouchDatabase('http://localhost:' + str(self.wrapper.port),
+ 'u1db_tests')
+ content = self._fetch_u1db_data(db)
+ self.assertEqual(db._replica_uid, content['replica_uid'])
+
+
+load_tests = tests.load_with_scenarios