diff options
Diffstat (limited to 'common/src')
| -rw-r--r-- | common/src/leap/soledad/common/__init__.py | 16 | ||||
| -rw-r--r-- | common/src/leap/soledad/common/couch.py | 19 | ||||
| -rw-r--r-- | common/src/leap/soledad/common/errors.py | 70 | ||||
| -rw-r--r-- | common/src/leap/soledad/common/tests/__init__.py | 10 | ||||
| -rw-r--r-- | common/src/leap/soledad/common/tests/test_server.py | 189 | ||||
| -rw-r--r-- | common/src/leap/soledad/common/tests/test_soledad.py | 94 | 
6 files changed, 345 insertions, 53 deletions
diff --git a/common/src/leap/soledad/common/__init__.py b/common/src/leap/soledad/common/__init__.py index 26467740..23d28e76 100644 --- a/common/src/leap/soledad/common/__init__.py +++ b/common/src/leap/soledad/common/__init__.py @@ -21,8 +21,21 @@ Soledad routines common to client and server.  """ +from hashlib import sha256 + +  # -# Assert functions +# Global constants +# + + +SHARED_DB_NAME = 'shared' +SHARED_DB_LOCK_DOC_ID_PREFIX = 'lock-' +USER_DB_PREFIX = 'user-' + + +# +# Global functions  #  # we want to use leap.common.check.leap_assert in case it is available, @@ -63,6 +76,7 @@ except ImportError:                         "Expected type %r instead of %r" %                         (expectedType, type(var))) +  from ._version import get_versions  __version__ = get_versions()['version']  del get_versions diff --git a/common/src/leap/soledad/common/couch.py b/common/src/leap/soledad/common/couch.py index 187d3035..1396f4d7 100644 --- a/common/src/leap/soledad/common/couch.py +++ b/common/src/leap/soledad/common/couch.py @@ -33,6 +33,7 @@ from couchdb.client import Server, Document as CouchDocument  from couchdb.http import ResourceNotFound, Unauthorized +from leap.soledad.common import USER_DB_PREFIX  from leap.soledad.common.objectstore import (      ObjectStoreDatabase,      ObjectStoreSyncTarget, @@ -61,7 +62,7 @@ def persistent_class(cls):                                    dump_method_name, store):          """          Create a persistent method to replace C{old_method_name}. -         +          The new method will load C{key} using C{load_method_name} and stores          it using C{dump_method_name} depending on the value of C{store}.          """ @@ -522,8 +523,7 @@ class CouchServerState(ServerState):      Inteface of the WSGI server with the CouchDB backend.      """ -    def __init__(self, couch_url, shared_db_name, tokens_db_name, -                 user_db_prefix): +    def __init__(self, couch_url, shared_db_name, tokens_db_name):          """          Initialize the couch server state. @@ -533,13 +533,10 @@ class CouchServerState(ServerState):          @type shared_db_name: str          @param tokens_db_name: The name of the tokens database.          @type tokens_db_name: str -        @param user_db_prefix: The prefix for user database names. -        @type user_db_prefix: str          """          self._couch_url = couch_url          self._shared_db_name = shared_db_name          self._tokens_db_name = tokens_db_name -        self._user_db_prefix = user_db_prefix          try:              self._check_couch_permissions()          except NotEnoughCouchPermissions: @@ -553,8 +550,8 @@ class CouchServerState(ServerState):      def _check_couch_permissions(self):          """ -        Assert that Soledad Server has enough permissions on the underlying couch -        database. +        Assert that Soledad Server has enough permissions on the underlying +        couch database.          Soledad Server has to be able to do the following in the couch server: @@ -563,8 +560,8 @@ class CouchServerState(ServerState):              * Read from 'tokens' db.          This function tries to perform the actions above using the "low level" -        couch library to ensure that Soledad Server can do everything it needs on -        the underlying couch database. +        couch library to ensure that Soledad Server can do everything it needs +        on the underlying couch database.          @param couch_url: The URL of the couch database.          @type couch_url: str @@ -593,7 +590,7 @@ class CouchServerState(ServerState):                  _open_couch_db(self._shared_db_name))              # test read/write auth for user-<something> db              _create_delete_test_doc( -                _open_couch_db('%stest-db' % self._user_db_prefix)) +                _open_couch_db('%stest-db' % USER_DB_PREFIX))              # test read auth for tokens db              tokensdb = _open_couch_db(self._tokens_db_name)              tokensdb.info() diff --git a/common/src/leap/soledad/common/errors.py b/common/src/leap/soledad/common/errors.py new file mode 100644 index 00000000..45433627 --- /dev/null +++ b/common/src/leap/soledad/common/errors.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# errors.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +""" +Soledad errors. +""" + + +from u1db import errors +from u1db.remote import http_errors + + +# +# LockResource: a lock based on a document in the shared database. +# + +class InvalidTokenError(errors.U1DBError): +    """ +    Exception raised when trying to unlock shared database with invalid token. +    """ + +    wire_description = "unlock unauthorized" +    status = 401 + + +class NotLockedError(errors.U1DBError): +    """ +    Exception raised when trying to unlock shared database when it is not +    locked. +    """ + +    wire_description = "lock not found" +    status = 404 + + +class AlreadyLockedError(errors.U1DBError): +    """ +    Exception raised when trying to lock shared database but it is already +    locked. +    """ + +    wire_description = "lock is locked" +    status = 403 + +# update u1db "wire description to status" and "wire description to exception" +# maps. +for e in [InvalidTokenError, NotLockedError, AlreadyLockedError]: +    http_errors.wire_description_to_status.update({ +        (e.wire_description, e.status)}) +    errors.wire_description_to_exc.update({ +        (e.wire_description, e)}) + +# u1db error statuses also have to be updated +http_errors.ERROR_STATUSES = set( +    http_errors.wire_description_to_status.values()) diff --git a/common/src/leap/soledad/common/tests/__init__.py b/common/src/leap/soledad/common/tests/__init__.py index 9f47d74a..88f98272 100644 --- a/common/src/leap/soledad/common/tests/__init__.py +++ b/common/src/leap/soledad/common/tests/__init__.py @@ -60,11 +60,12 @@ class BaseSoledadTest(BaseLeapTest):              if os.path.isfile(f):                  os.unlink(f) -    def _soledad_instance(self, user=ADDRESS, passphrase='123', +    def _soledad_instance(self, user=ADDRESS, passphrase=u'123',                            prefix='',                            secrets_path=Soledad.STORAGE_SECRETS_FILE_NAME,                            local_db_path='soledad.u1db', server_url='', -                          cert_file=None, secret_id=None): +                          cert_file=None, secret_id=None, +                          shared_db_class=None):          def _put_doc_side_effect(doc):              self._doc_put = doc @@ -73,10 +74,15 @@ class BaseSoledadTest(BaseLeapTest):              get_doc = Mock(return_value=None)              put_doc = Mock(side_effect=_put_doc_side_effect) +            lock = Mock(return_value=('atoken', 300)) +            unlock = Mock(return_value=True)              def __call__(self):                  return self +        if shared_db_class is not None: +            MockSharedDB = shared_db_class +          Soledad._shared_db = MockSharedDB()          return Soledad(              user, diff --git a/common/src/leap/soledad/common/tests/test_server.py b/common/src/leap/soledad/common/tests/test_server.py index 1ea4d615..83df192b 100644 --- a/common/src/leap/soledad/common/tests/test_server.py +++ b/common/src/leap/soledad/common/tests/test_server.py @@ -24,6 +24,7 @@ import os  import tempfile  import simplejson as json  import mock +import time  from leap.common.testing.basetest import BaseLeapTest @@ -45,7 +46,7 @@ from leap.soledad.client import (      Soledad,      target,  ) -from leap.soledad.server import SoledadApp +from leap.soledad.server import SoledadApp, LockResource  from leap.soledad.server.auth import URLToAuthorization @@ -86,9 +87,8 @@ class ServerAuthorizationTestCase(BaseLeapTest):              /user-db/sync-from/{source}   | GET, PUT, POST          """          uuid = 'myuuid' -        authmap = URLToAuthorization( -            uuid, SoledadApp.SHARED_DB_NAME, SoledadApp.USER_DB_PREFIX) -        dbname = authmap._uuid_dbname(uuid) +        authmap = URLToAuthorization(uuid,) +        dbname = authmap._user_db_name          # test global auth          self.assertTrue(              authmap.is_authorized(self._make_environ('/', 'GET'))) @@ -202,8 +202,7 @@ class ServerAuthorizationTestCase(BaseLeapTest):          Test if authorization fails for a wrong dbname.          """          uuid = 'myuuid' -        authmap = URLToAuthorization( -            uuid, SoledadApp.SHARED_DB_NAME, SoledadApp.USER_DB_PREFIX) +        authmap = URLToAuthorization(uuid)          dbname = 'somedb'          # test wrong-db database resource auth          self.assertFalse( @@ -273,7 +272,7 @@ class EncryptedSyncTestCase(      sync_target = token_leap_sync_target -    def _soledad_instance(self, user='user-uuid', passphrase='123', +    def _soledad_instance(self, user='user-uuid', passphrase=u'123',                            prefix='',                            secrets_path=Soledad.STORAGE_SECRETS_FILE_NAME,                            local_db_path='soledad.u1db', server_url='', @@ -293,6 +292,8 @@ class EncryptedSyncTestCase(              get_doc = mock.Mock(return_value=None)              put_doc = mock.Mock(side_effect=_put_doc_side_effect) +            lock = mock.Mock(return_value=('atoken', 300)) +            unlock = mock.Mock()              def __call__(self):                  return self @@ -310,8 +311,8 @@ class EncryptedSyncTestCase(              secret_id=secret_id)      def make_app(self): -        self.request_state = CouchServerState( -            self._couch_url, 'shared', 'tokens', 'user-') +        self.request_state = CouchServerState(self._couch_url, 'shared', +                                              'tokens')          return self.make_app_with_state(self.request_state)      def setUp(self): @@ -375,3 +376,173 @@ class EncryptedSyncTestCase(          doc2 = doclist[0]          # assert incoming doc is equal to the first sent doc          self.assertEqual(doc1, doc2) + +    def test_encrypted_sym_sync_with_unicode_passphrase(self): +        """ +        Test the complete syncing chain between two soledad dbs using a +        Soledad server backed by a couch database, using an unicode +        passphrase. +        """ +        self.startServer() +        # instantiate soledad and create a document +        sol1 = self._soledad_instance( +            # token is verified in test_target.make_token_soledad_app +            auth_token='auth-token', +            passphrase=u'ãáàäéàëíìïóòöõúùüñç', +        ) +        _, doclist = sol1.get_all_docs() +        self.assertEqual([], doclist) +        doc1 = sol1.create_doc(json.loads(simple_doc)) +        # sync with server +        sol1._server_url = self.getURL() +        sol1.sync() +        # assert doc was sent to couch db +        db = CouchDatabase( +            self._couch_url, +            # the name of the user database is "user-<uuid>". +            'user-user-uuid', +        ) +        _, doclist = db.get_all_docs() +        self.assertEqual(1, len(doclist)) +        couchdoc = doclist[0] +        # assert document structure in couch server +        self.assertEqual(doc1.doc_id, couchdoc.doc_id) +        self.assertEqual(doc1.rev, couchdoc.rev) +        self.assertEqual(6, len(couchdoc.content)) +        self.assertTrue(target.ENC_JSON_KEY in couchdoc.content) +        self.assertTrue(target.ENC_SCHEME_KEY in couchdoc.content) +        self.assertTrue(target.ENC_METHOD_KEY in couchdoc.content) +        self.assertTrue(target.ENC_IV_KEY in couchdoc.content) +        self.assertTrue(target.MAC_KEY in couchdoc.content) +        self.assertTrue(target.MAC_METHOD_KEY in couchdoc.content) +        # instantiate soledad with empty db, but with same secrets path +        sol2 = self._soledad_instance( +            prefix='x', +            auth_token='auth-token', +            passphrase=u'ãáàäéàëíìïóòöõúùüñç', +        ) +        _, doclist = sol2.get_all_docs() +        self.assertEqual([], doclist) +        sol2._secrets_path = sol1.secrets_path +        sol2._load_secrets() +        sol2._set_secret_id(sol1._secret_id) +        # sync the new instance +        sol2._server_url = self.getURL() +        sol2.sync() +        _, doclist = sol2.get_all_docs() +        self.assertEqual(1, len(doclist)) +        doc2 = doclist[0] +        # assert incoming doc is equal to the first sent doc +        self.assertEqual(doc1, doc2) + + +class LockResourceTestCase( +        CouchDBTestCase, TestCaseWithServer): +    """ +    Tests for use of PUT and DELETE on lock resource. +    """ + +    @staticmethod +    def make_app_with_state(state): +        return make_token_soledad_app(state) + +    make_document_for_test = make_leap_document_for_test + +    sync_target = token_leap_sync_target + +    def setUp(self): +        TestCaseWithServer.setUp(self) +        CouchDBTestCase.setUp(self) +        self.tempdir = tempfile.mkdtemp(prefix="leap_tests-") +        self._couch_url = 'http://localhost:' + str(self.wrapper.port) +        self._state = CouchServerState( +            self._couch_url, 'shared', 'tokens') + +    def tearDown(self): +        CouchDBTestCase.tearDown(self) +        TestCaseWithServer.tearDown(self) + +    def test__try_obtain_filesystem_lock(self): +        responder = mock.Mock() +        lr = LockResource('uuid', self._state, responder) +        self.assertFalse(lr._lock.locked) +        self.assertTrue(lr._try_obtain_filesystem_lock()) +        self.assertTrue(lr._lock.locked) +        lr._try_release_filesystem_lock() + +    def test__try_release_filesystem_lock(self): +        responder = mock.Mock() +        lr = LockResource('uuid', self._state, responder) +        lr._try_obtain_filesystem_lock() +        self.assertTrue(lr._lock.locked) +        lr._try_release_filesystem_lock() +        self.assertFalse(lr._lock.locked) + +    def test_put(self): +        responder = mock.Mock() +        lr = LockResource('uuid', self._state, responder) +        # lock! +        lr.put({}, None) +        # assert lock document was correctly written +        lock_doc = lr._shared_db.get_doc('lock-uuid') +        self.assertIsNotNone(lock_doc) +        self.assertTrue(LockResource.TIMESTAMP_KEY in lock_doc.content) +        self.assertTrue(LockResource.LOCK_TOKEN_KEY in lock_doc.content) +        timestamp = lock_doc.content[LockResource.TIMESTAMP_KEY] +        token = lock_doc.content[LockResource.LOCK_TOKEN_KEY] +        self.assertTrue(timestamp < time.time()) +        self.assertTrue(time.time() < timestamp + LockResource.TIMEOUT) +        # assert response to user +        responder.send_response_json.assert_called_with( +            201, token=token, +            timeout=LockResource.TIMEOUT) + +    def test_delete(self): +        responder = mock.Mock() +        lr = LockResource('uuid', self._state, responder) +        # lock! +        lr.put({}, None) +        lock_doc = lr._shared_db.get_doc('lock-uuid') +        token = lock_doc.content[LockResource.LOCK_TOKEN_KEY] +        # unlock! +        lr.delete({'token': token}, None) +        self.assertFalse(lr._lock.locked) +        self.assertIsNone(lr._shared_db.get_doc('lock-uuid')) +        responder.send_response_json.assert_called_with(200) + +    def test_put_while_locked_fails(self): +        responder = mock.Mock() +        lr = LockResource('uuid', self._state, responder) +        # lock! +        lr.put({}, None) +        # try to lock again! +        lr.put({}, None) +        self.assertEqual( +            len(responder.send_response_json.call_args), 2) +        self.assertEqual( +            responder.send_response_json.call_args[0], (403,)) +        self.assertEqual( +            len(responder.send_response_json.call_args[1]), 2) +        self.assertTrue( +            'remaining' in responder.send_response_json.call_args[1]) +        self.assertTrue( +            responder.send_response_json.call_args[1]['remaining'] > 0) + +    def test_unlock_unexisting_lock_fails(self): +        responder = mock.Mock() +        lr = LockResource('uuid', self._state, responder) +        # unlock! +        lr.delete({'token': 'anything'}, None) +        responder.send_response_json.assert_called_with( +            404, error='lock not found') + +    def test_unlock_with_wrong_token_fails(self): +        responder = mock.Mock() +        lr = LockResource('uuid', self._state, responder) +        # lock! +        lr.put({}, None) +        # unlock! +        lr.delete({'token': 'wrongtoken'}, None) +        self.assertIsNotNone(lr._shared_db.get_doc('lock-uuid')) +        responder.send_response_json.assert_called_with( +            401, error='unlock unauthorized') diff --git a/common/src/leap/soledad/common/tests/test_soledad.py b/common/src/leap/soledad/common/tests/test_soledad.py index 0b753647..8970a437 100644 --- a/common/src/leap/soledad/common/tests/test_soledad.py +++ b/common/src/leap/soledad/common/tests/test_soledad.py @@ -90,7 +90,7 @@ class AuxMethodsTestCase(BaseSoledadTest):          """          sol = self._soledad_instance(              'leap@leap.se', -            passphrase='123', +            passphrase=u'123',              secrets_path='value_3',              local_db_path='value_2',              server_url='value_1', @@ -109,25 +109,25 @@ class AuxMethodsTestCase(BaseSoledadTest):          """          sol = self._soledad_instance(              'leap@leap.se', -            passphrase='123', +            passphrase=u'123',              prefix=self.rand_prefix,          )          doc = sol.create_doc({'simple': 'doc'})          doc_id = doc.doc_id          # change the passphrase -        sol.change_passphrase('654321') +        sol.change_passphrase(u'654321')          self.assertRaises(              DatabaseError,              self._soledad_instance, 'leap@leap.se', -            passphrase='123', +            passphrase=u'123',              prefix=self.rand_prefix)          # use new passphrase and retrieve doc          sol2 = self._soledad_instance(              'leap@leap.se', -            passphrase='654321', +            passphrase=u'654321',              prefix=self.rand_prefix)          doc2 = sol2.get_doc(doc_id)          self.assertEqual(doc, doc2) @@ -139,11 +139,11 @@ class AuxMethodsTestCase(BaseSoledadTest):          """          sol = self._soledad_instance(              'leap@leap.se', -            passphrase='123') +            passphrase=u'123')          # check that soledad complains about new passphrase length          self.assertRaises(              PassphraseTooShort, -            sol.change_passphrase, '54321') +            sol.change_passphrase, u'54321')      def test_get_passphrase(self):          """ @@ -161,13 +161,14 @@ class SoledadSharedDBTestCase(BaseSoledadTest):      def setUp(self):          BaseSoledadTest.setUp(self)          self._shared_db = SoledadSharedDatabase( -            'https://provider/', SoledadDocument, None) +            'https://provider/', ADDRESS, document_factory=SoledadDocument, +            creds=None)      def test__get_secrets_from_shared_db(self):          """          Ensure the shared db is queried with the correct doc_id.          """ -        doc_id = self._soledad._uuid_hash() +        doc_id = self._soledad._shared_db_doc_id()          self._soledad._get_secrets_from_shared_db()          self.assertTrue(              self._soledad._shared_db().get_doc.assert_called_with( @@ -178,7 +179,7 @@ class SoledadSharedDBTestCase(BaseSoledadTest):          """          Ensure recovery document is put into shared recover db.          """ -        doc_id = self._soledad._uuid_hash() +        doc_id = self._soledad._shared_db_doc_id()          self._soledad._put_secrets_in_shared_db()          self.assertTrue(              self._soledad._shared_db().get_doc.assert_called_with( @@ -201,9 +202,10 @@ class SoledadSignalingTestCase(BaseSoledadTest):      EVENTS_SERVER_PORT = 8090      def setUp(self): -        BaseSoledadTest.setUp(self)          # mock signaling          soledad.client.signal = Mock() +        # run parent's setUp +        BaseSoledadTest.setUp(self)      def tearDown(self):          pass @@ -213,22 +215,28 @@ class SoledadSignalingTestCase(BaseSoledadTest):          mocked.mock_calls.pop()          mocked.call_args = mocked.call_args_list[-1] -    def test_stage2_bootstrap_signals(self): +    def test_stage3_bootstrap_signals(self):          """          Test that a fresh soledad emits all bootstrap signals. + +        Signals are: +          - downloading keys / done downloading keys. +          - creating keys / done creating keys. +          - downloading keys / done downloading keys. +          - uploading keys / done uploading keys.          """          soledad.client.signal.reset_mock()          # get a fresh instance so it emits all bootstrap signals          sol = self._soledad_instance( -            secrets_path='alternative.json', -            local_db_path='alternative.u1db') +            secrets_path='alternative_stage3.json', +            local_db_path='alternative_stage3.u1db')          # reverse call order so we can verify in the order the signals were          # expected          soledad.client.signal.mock_calls.reverse()          soledad.client.signal.call_args = \              soledad.client.signal.call_args_list[0]          soledad.client.signal.call_args_list.reverse() -        # assert signals +        # downloading keys signals          soledad.client.signal.assert_called_with(              proto.SOLEDAD_DOWNLOADING_KEYS,              ADDRESS, @@ -238,6 +246,7 @@ class SoledadSignalingTestCase(BaseSoledadTest):              proto.SOLEDAD_DONE_DOWNLOADING_KEYS,              ADDRESS,          ) +        # creating keys signals          self._pop_mock_call(soledad.client.signal)          soledad.client.signal.assert_called_with(              proto.SOLEDAD_CREATING_KEYS, @@ -248,6 +257,7 @@ class SoledadSignalingTestCase(BaseSoledadTest):              proto.SOLEDAD_DONE_CREATING_KEYS,              ADDRESS,          ) +        # downloading once more (inside _put_keys_in_shared_db)          self._pop_mock_call(soledad.client.signal)          soledad.client.signal.assert_called_with(              proto.SOLEDAD_DOWNLOADING_KEYS, @@ -258,6 +268,7 @@ class SoledadSignalingTestCase(BaseSoledadTest):              proto.SOLEDAD_DONE_DOWNLOADING_KEYS,              ADDRESS,          ) +        # uploading keys signals          self._pop_mock_call(soledad.client.signal)          soledad.client.signal.assert_called_with(              proto.SOLEDAD_UPLOADING_KEYS, @@ -268,21 +279,45 @@ class SoledadSignalingTestCase(BaseSoledadTest):              proto.SOLEDAD_DONE_UPLOADING_KEYS,              ADDRESS,          ) +        # assert db was locked and unlocked +        sol._shared_db.lock.assert_called_with() +        sol._shared_db.unlock.assert_called_with('atoken') -    def test_stage1_bootstrap_signals(self): +    def test_stage2_bootstrap_signals(self):          """ -        Test that an existent soledad emits some of the bootstrap signals. +        Test that if there are keys in server, soledad will download them and +        emit corresponding signals.          """ -        soledad.client.signal.reset_mock() -        # get an existent instance so it emits only some of bootstrap signals +        # get existing instance so we have access to keys          sol = self._soledad_instance() +        # create a document with secrets +        doc = SoledadDocument(doc_id=sol._shared_db_doc_id()) +        doc.content = sol.export_recovery_document(include_uuid=False) + +        class Stage2MockSharedDB(object): + +            get_doc = Mock(return_value=doc) +            put_doc = Mock() +            lock = Mock(return_value=('atoken', 300)) +            unlock = Mock() + +            def __call__(self): +                return self + +        # reset mock +        soledad.client.signal.reset_mock() +        # get a fresh instance so it emits all bootstrap signals +        sol = self._soledad_instance( +            secrets_path='alternative_stage2.json', +            local_db_path='alternative_stage2.u1db', +            shared_db_class=Stage2MockSharedDB)          # reverse call order so we can verify in the order the signals were          # expected          soledad.client.signal.mock_calls.reverse()          soledad.client.signal.call_args = \              soledad.client.signal.call_args_list[0]          soledad.client.signal.call_args_list.reverse() -        # assert signals +        # assert download keys signals          soledad.client.signal.assert_called_with(              proto.SOLEDAD_DOWNLOADING_KEYS,              ADDRESS, @@ -292,16 +327,15 @@ class SoledadSignalingTestCase(BaseSoledadTest):              proto.SOLEDAD_DONE_DOWNLOADING_KEYS,              ADDRESS,          ) -        self._pop_mock_call(soledad.client.signal) -        soledad.client.signal.assert_called_with( -            proto.SOLEDAD_UPLOADING_KEYS, -            ADDRESS, -        ) -        self._pop_mock_call(soledad.client.signal) -        soledad.client.signal.assert_called_with( -            proto.SOLEDAD_DONE_UPLOADING_KEYS, -            ADDRESS, -        ) + +    def test_stage1_bootstrap_signals(self): +        """ +        Test that if soledad already has a local secret, it emits no signals. +        """ +        soledad.client.signal.reset_mock() +        # get an existent instance so it emits only some of bootstrap signals +        sol = self._soledad_instance() +        self.assertEqual([], soledad.client.signal.mock_calls)      def test_sync_signals(self):          """  | 
