summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorErik Johnston <erikj@matrix.org>2015-11-19 13:21:10 +0000
committerErik Johnston <erikj@matrix.org>2015-11-19 13:21:10 +0000
commiteb3677f58ffeddc534bc8ceb2adb060d0794b817 (patch)
tree4377eb0dc5e221862489bdcc802e50e2f1f41cb1 /tests
Imported Upstream version 0.11.0
Diffstat (limited to 'tests')
-rw-r--r--tests/__init__.py15
-rw-r--r--tests/api/__init__.py0
-rw-r--r--tests/api/test_auth.py295
-rw-r--r--tests/api/test_filtering.py507
-rw-r--r--tests/api/test_ratelimiting.py39
-rw-r--r--tests/appservice/__init__.py14
-rw-r--r--tests/appservice/test_appservice.py229
-rw-r--r--tests/appservice/test_scheduler.py252
-rw-r--r--tests/crypto/__init__.py15
-rw-r--r--tests/crypto/test_event_signing.py114
-rw-r--r--tests/events/__init__.py0
-rw-r--r--tests/events/test_utils.py115
-rw-r--r--tests/federation/__init__.py0
-rw-r--r--tests/federation/test_federation.py301
-rw-r--r--tests/handlers/__init__.py0
-rw-r--r--tests/handlers/test_appservice.py139
-rw-r--r--tests/handlers/test_auth.py70
-rw-r--r--tests/handlers/test_directory.py110
-rw-r--r--tests/handlers/test_federation.py130
-rw-r--r--tests/handlers/test_presence.py1328
-rw-r--r--tests/handlers/test_presencelike.py311
-rw-r--r--tests/handlers/test_profile.py145
-rw-r--r--tests/handlers/test_room.py404
-rw-r--r--tests/handlers/test_typing.py414
-rw-r--r--tests/metrics/__init__.py0
-rw-r--r--tests/metrics/test_metric.py161
-rw-r--r--tests/rest/__init__.py14
-rw-r--r--tests/rest/client/__init__.py14
-rw-r--r--tests/rest/client/v1/__init__.py15
-rw-r--r--tests/rest/client/v1/test_events.py217
-rw-r--r--tests/rest/client/v1/test_presence.py411
-rw-r--r--tests/rest/client/v1/test_profile.py148
-rw-r--r--tests/rest/client/v1/test_rooms.py1052
-rw-r--r--tests/rest/client/v1/test_typing.py162
-rw-r--r--tests/rest/client/v1/utils.py131
-rw-r--r--tests/rest/client/v2_alpha/__init__.py62
-rw-r--r--tests/rest/client/v2_alpha/test_filter.py95
-rw-r--r--tests/rest/client/v2_alpha/test_register.py135
-rw-r--r--tests/storage/__init__.py0
-rw-r--r--tests/storage/event_injector.py81
-rw-r--r--tests/storage/test__base.py199
-rw-r--r--tests/storage/test_appservice.py407
-rw-r--r--tests/storage/test_background_update.py76
-rw-r--r--tests/storage/test_base.py200
-rw-r--r--tests/storage/test_directory.py79
-rw-r--r--tests/storage/test_events.py116
-rw-r--r--tests/storage/test_presence.py161
-rw-r--r--tests/storage/test_profile.py64
-rw-r--r--tests/storage/test_redaction.py254
-rw-r--r--tests/storage/test_registration.py130
-rw-r--r--tests/storage/test_room.py155
-rw-r--r--tests/storage/test_roommember.py160
-rw-r--r--tests/storage/test_stream.py185
-rw-r--r--tests/test_distributor.py118
-rw-r--r--tests/test_state.py641
-rw-r--r--tests/test_test_utils.py70
-rw-r--r--tests/test_types.py63
-rw-r--r--tests/unittest.py92
-rw-r--r--tests/util/__init__.py15
-rw-r--r--tests/util/test_dict_cache.py101
-rw-r--r--tests/util/test_log_context.py43
-rw-r--r--tests/util/test_lrucache.py54
-rw-r--r--tests/utils.py479
63 files changed, 11507 insertions, 0 deletions
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 00000000..9bff9ec1
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
diff --git a/tests/api/__init__.py b/tests/api/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/api/__init__.py
diff --git a/tests/api/test_auth.py b/tests/api/test_auth.py
new file mode 100644
index 00000000..70d928de
--- /dev/null
+++ b/tests/api/test_auth.py
@@ -0,0 +1,295 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from tests import unittest
+from twisted.internet import defer
+
+from mock import Mock
+
+from synapse.api.auth import Auth
+from synapse.api.errors import AuthError
+from synapse.types import UserID
+from tests.utils import setup_test_homeserver
+
+import pymacaroons
+
+
+class AuthTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.state_handler = Mock()
+ self.store = Mock()
+
+ self.hs = yield setup_test_homeserver(handlers=None)
+ self.hs.get_datastore = Mock(return_value=self.store)
+ self.auth = Auth(self.hs)
+
+ self.test_user = "@foo:bar"
+ self.test_token = "_test_token_"
+
+ @defer.inlineCallbacks
+ def test_get_user_by_req_user_valid_token(self):
+ self.store.get_app_service_by_token = Mock(return_value=None)
+ user_info = {
+ "name": self.test_user,
+ "token_id": "ditto",
+ }
+ self.store.get_user_by_access_token = Mock(return_value=user_info)
+
+ request = Mock(args={})
+ request.args["access_token"] = [self.test_token]
+ request.requestHeaders.getRawHeaders = Mock(return_value=[""])
+ (user, _, _) = yield self.auth.get_user_by_req(request)
+ self.assertEquals(user.to_string(), self.test_user)
+
+ def test_get_user_by_req_user_bad_token(self):
+ self.store.get_app_service_by_token = Mock(return_value=None)
+ self.store.get_user_by_access_token = Mock(return_value=None)
+
+ request = Mock(args={})
+ request.args["access_token"] = [self.test_token]
+ request.requestHeaders.getRawHeaders = Mock(return_value=[""])
+ d = self.auth.get_user_by_req(request)
+ self.failureResultOf(d, AuthError)
+
+ def test_get_user_by_req_user_missing_token(self):
+ self.store.get_app_service_by_token = Mock(return_value=None)
+ user_info = {
+ "name": self.test_user,
+ "token_id": "ditto",
+ }
+ self.store.get_user_by_access_token = Mock(return_value=user_info)
+
+ request = Mock(args={})
+ request.requestHeaders.getRawHeaders = Mock(return_value=[""])
+ d = self.auth.get_user_by_req(request)
+ self.failureResultOf(d, AuthError)
+
+ @defer.inlineCallbacks
+ def test_get_user_by_req_appservice_valid_token(self):
+ app_service = Mock(token="foobar", url="a_url", sender=self.test_user)
+ self.store.get_app_service_by_token = Mock(return_value=app_service)
+ self.store.get_user_by_access_token = Mock(return_value=None)
+
+ request = Mock(args={})
+ request.args["access_token"] = [self.test_token]
+ request.requestHeaders.getRawHeaders = Mock(return_value=[""])
+ (user, _, _) = yield self.auth.get_user_by_req(request)
+ self.assertEquals(user.to_string(), self.test_user)
+
+ def test_get_user_by_req_appservice_bad_token(self):
+ self.store.get_app_service_by_token = Mock(return_value=None)
+ self.store.get_user_by_access_token = Mock(return_value=None)
+
+ request = Mock(args={})
+ request.args["access_token"] = [self.test_token]
+ request.requestHeaders.getRawHeaders = Mock(return_value=[""])
+ d = self.auth.get_user_by_req(request)
+ self.failureResultOf(d, AuthError)
+
+ def test_get_user_by_req_appservice_missing_token(self):
+ app_service = Mock(token="foobar", url="a_url", sender=self.test_user)
+ self.store.get_app_service_by_token = Mock(return_value=app_service)
+ self.store.get_user_by_access_token = Mock(return_value=None)
+
+ request = Mock(args={})
+ request.requestHeaders.getRawHeaders = Mock(return_value=[""])
+ d = self.auth.get_user_by_req(request)
+ self.failureResultOf(d, AuthError)
+
+ @defer.inlineCallbacks
+ def test_get_user_by_req_appservice_valid_token_valid_user_id(self):
+ masquerading_user_id = "@doppelganger:matrix.org"
+ app_service = Mock(token="foobar", url="a_url", sender=self.test_user)
+ app_service.is_interested_in_user = Mock(return_value=True)
+ self.store.get_app_service_by_token = Mock(return_value=app_service)
+ self.store.get_user_by_access_token = Mock(return_value=None)
+
+ request = Mock(args={})
+ request.args["access_token"] = [self.test_token]
+ request.args["user_id"] = [masquerading_user_id]
+ request.requestHeaders.getRawHeaders = Mock(return_value=[""])
+ (user, _, _) = yield self.auth.get_user_by_req(request)
+ self.assertEquals(user.to_string(), masquerading_user_id)
+
+ def test_get_user_by_req_appservice_valid_token_bad_user_id(self):
+ masquerading_user_id = "@doppelganger:matrix.org"
+ app_service = Mock(token="foobar", url="a_url", sender=self.test_user)
+ app_service.is_interested_in_user = Mock(return_value=False)
+ self.store.get_app_service_by_token = Mock(return_value=app_service)
+ self.store.get_user_by_access_token = Mock(return_value=None)
+
+ request = Mock(args={})
+ request.args["access_token"] = [self.test_token]
+ request.args["user_id"] = [masquerading_user_id]
+ request.requestHeaders.getRawHeaders = Mock(return_value=[""])
+ d = self.auth.get_user_by_req(request)
+ self.failureResultOf(d, AuthError)
+
+ @defer.inlineCallbacks
+ def test_get_user_from_macaroon(self):
+ # TODO(danielwh): Remove this mock when we remove the
+ # get_user_by_access_token fallback.
+ self.store.get_user_by_access_token = Mock(
+ return_value={"name": "@baldrick:matrix.org"}
+ )
+
+ user_id = "@baldrick:matrix.org"
+ macaroon = pymacaroons.Macaroon(
+ location=self.hs.config.server_name,
+ identifier="key",
+ key=self.hs.config.macaroon_secret_key)
+ macaroon.add_first_party_caveat("gen = 1")
+ macaroon.add_first_party_caveat("type = access")
+ macaroon.add_first_party_caveat("user_id = %s" % (user_id,))
+ user_info = yield self.auth._get_user_from_macaroon(macaroon.serialize())
+ user = user_info["user"]
+ self.assertEqual(UserID.from_string(user_id), user)
+
+ @defer.inlineCallbacks
+ def test_get_guest_user_from_macaroon(self):
+ user_id = "@baldrick:matrix.org"
+ macaroon = pymacaroons.Macaroon(
+ location=self.hs.config.server_name,
+ identifier="key",
+ key=self.hs.config.macaroon_secret_key)
+ macaroon.add_first_party_caveat("gen = 1")
+ macaroon.add_first_party_caveat("type = access")
+ macaroon.add_first_party_caveat("user_id = %s" % (user_id,))
+ macaroon.add_first_party_caveat("guest = true")
+ serialized = macaroon.serialize()
+
+ user_info = yield self.auth._get_user_from_macaroon(serialized)
+ user = user_info["user"]
+ is_guest = user_info["is_guest"]
+ self.assertEqual(UserID.from_string(user_id), user)
+ self.assertTrue(is_guest)
+
+ @defer.inlineCallbacks
+ def test_get_user_from_macaroon_user_db_mismatch(self):
+ self.store.get_user_by_access_token = Mock(
+ return_value={"name": "@percy:matrix.org"}
+ )
+
+ user = "@baldrick:matrix.org"
+ macaroon = pymacaroons.Macaroon(
+ location=self.hs.config.server_name,
+ identifier="key",
+ key=self.hs.config.macaroon_secret_key)
+ macaroon.add_first_party_caveat("gen = 1")
+ macaroon.add_first_party_caveat("type = access")
+ macaroon.add_first_party_caveat("user_id = %s" % (user,))
+ with self.assertRaises(AuthError) as cm:
+ yield self.auth._get_user_from_macaroon(macaroon.serialize())
+ self.assertEqual(401, cm.exception.code)
+ self.assertIn("User mismatch", cm.exception.msg)
+
+ @defer.inlineCallbacks
+ def test_get_user_from_macaroon_missing_caveat(self):
+ # TODO(danielwh): Remove this mock when we remove the
+ # get_user_by_access_token fallback.
+ self.store.get_user_by_access_token = Mock(
+ return_value={"name": "@baldrick:matrix.org"}
+ )
+
+ macaroon = pymacaroons.Macaroon(
+ location=self.hs.config.server_name,
+ identifier="key",
+ key=self.hs.config.macaroon_secret_key)
+ macaroon.add_first_party_caveat("gen = 1")
+ macaroon.add_first_party_caveat("type = access")
+
+ with self.assertRaises(AuthError) as cm:
+ yield self.auth._get_user_from_macaroon(macaroon.serialize())
+ self.assertEqual(401, cm.exception.code)
+ self.assertIn("No user caveat", cm.exception.msg)
+
+ @defer.inlineCallbacks
+ def test_get_user_from_macaroon_wrong_key(self):
+ # TODO(danielwh): Remove this mock when we remove the
+ # get_user_by_access_token fallback.
+ self.store.get_user_by_access_token = Mock(
+ return_value={"name": "@baldrick:matrix.org"}
+ )
+
+ user = "@baldrick:matrix.org"
+ macaroon = pymacaroons.Macaroon(
+ location=self.hs.config.server_name,
+ identifier="key",
+ key=self.hs.config.macaroon_secret_key + "wrong")
+ macaroon.add_first_party_caveat("gen = 1")
+ macaroon.add_first_party_caveat("type = access")
+ macaroon.add_first_party_caveat("user_id = %s" % (user,))
+
+ with self.assertRaises(AuthError) as cm:
+ yield self.auth._get_user_from_macaroon(macaroon.serialize())
+ self.assertEqual(401, cm.exception.code)
+ self.assertIn("Invalid macaroon", cm.exception.msg)
+
+ @defer.inlineCallbacks
+ def test_get_user_from_macaroon_unknown_caveat(self):
+ # TODO(danielwh): Remove this mock when we remove the
+ # get_user_by_access_token fallback.
+ self.store.get_user_by_access_token = Mock(
+ return_value={"name": "@baldrick:matrix.org"}
+ )
+
+ user = "@baldrick:matrix.org"
+ macaroon = pymacaroons.Macaroon(
+ location=self.hs.config.server_name,
+ identifier="key",
+ key=self.hs.config.macaroon_secret_key)
+ macaroon.add_first_party_caveat("gen = 1")
+ macaroon.add_first_party_caveat("type = access")
+ macaroon.add_first_party_caveat("user_id = %s" % (user,))
+ macaroon.add_first_party_caveat("cunning > fox")
+
+ with self.assertRaises(AuthError) as cm:
+ yield self.auth._get_user_from_macaroon(macaroon.serialize())
+ self.assertEqual(401, cm.exception.code)
+ self.assertIn("Invalid macaroon", cm.exception.msg)
+
+ @defer.inlineCallbacks
+ def test_get_user_from_macaroon_expired(self):
+ # TODO(danielwh): Remove this mock when we remove the
+ # get_user_by_access_token fallback.
+ self.store.get_user_by_access_token = Mock(
+ return_value={"name": "@baldrick:matrix.org"}
+ )
+
+ self.store.get_user_by_access_token = Mock(
+ return_value={"name": "@baldrick:matrix.org"}
+ )
+
+ user = "@baldrick:matrix.org"
+ macaroon = pymacaroons.Macaroon(
+ location=self.hs.config.server_name,
+ identifier="key",
+ key=self.hs.config.macaroon_secret_key)
+ macaroon.add_first_party_caveat("gen = 1")
+ macaroon.add_first_party_caveat("type = access")
+ macaroon.add_first_party_caveat("user_id = %s" % (user,))
+ macaroon.add_first_party_caveat("time < 1") # ms
+
+ self.hs.clock.now = 5000 # seconds
+
+ yield self.auth._get_user_from_macaroon(macaroon.serialize())
+ # TODO(daniel): Turn on the check that we validate expiration, when we
+ # validate expiration (and remove the above line, which will start
+ # throwing).
+ # with self.assertRaises(AuthError) as cm:
+ # yield self.auth._get_user_from_macaroon(macaroon.serialize())
+ # self.assertEqual(401, cm.exception.code)
+ # self.assertIn("Invalid macaroon", cm.exception.msg)
diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py
new file mode 100644
index 00000000..9f9af2d7
--- /dev/null
+++ b/tests/api/test_filtering.py
@@ -0,0 +1,507 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from collections import namedtuple
+from tests import unittest
+from twisted.internet import defer
+
+from mock import Mock, NonCallableMock
+from tests.utils import (
+ MockHttpResource, DeferredMockCallable, setup_test_homeserver
+)
+
+from synapse.types import UserID
+from synapse.api.filtering import FilterCollection, Filter
+
+user_localpart = "test_user"
+# MockEvent = namedtuple("MockEvent", "sender type room_id")
+
+
+def MockEvent(**kwargs):
+ ev = NonCallableMock(spec_set=kwargs.keys())
+ ev.configure_mock(**kwargs)
+ return ev
+
+
+class FilteringTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_federation_resource = MockHttpResource()
+
+ self.mock_http_client = Mock(spec=[])
+ self.mock_http_client.put_json = DeferredMockCallable()
+
+ hs = yield setup_test_homeserver(
+ handlers=None,
+ http_client=self.mock_http_client,
+ keyring=Mock(),
+ )
+
+ self.filtering = hs.get_filtering()
+
+ self.datastore = hs.get_datastore()
+
+ def test_definition_types_works_with_literals(self):
+ definition = {
+ "types": ["m.room.message", "org.matrix.foo.bar"]
+ }
+ event = MockEvent(
+ sender="@foo:bar",
+ type="m.room.message",
+ room_id="!foo:bar"
+ )
+
+ self.assertTrue(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_types_works_with_wildcards(self):
+ definition = {
+ "types": ["m.*", "org.matrix.foo.bar"]
+ }
+ event = MockEvent(
+ sender="@foo:bar",
+ type="m.room.message",
+ room_id="!foo:bar"
+ )
+ self.assertTrue(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_types_works_with_unknowns(self):
+ definition = {
+ "types": ["m.room.message", "org.matrix.foo.bar"]
+ }
+ event = MockEvent(
+ sender="@foo:bar",
+ type="now.for.something.completely.different",
+ room_id="!foo:bar"
+ )
+ self.assertFalse(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_not_types_works_with_literals(self):
+ definition = {
+ "not_types": ["m.room.message", "org.matrix.foo.bar"]
+ }
+ event = MockEvent(
+ sender="@foo:bar",
+ type="m.room.message",
+ room_id="!foo:bar"
+ )
+ self.assertFalse(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_not_types_works_with_wildcards(self):
+ definition = {
+ "not_types": ["m.room.message", "org.matrix.*"]
+ }
+ event = MockEvent(
+ sender="@foo:bar",
+ type="org.matrix.custom.event",
+ room_id="!foo:bar"
+ )
+ self.assertFalse(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_not_types_works_with_unknowns(self):
+ definition = {
+ "not_types": ["m.*", "org.*"]
+ }
+ event = MockEvent(
+ sender="@foo:bar",
+ type="com.nom.nom.nom",
+ room_id="!foo:bar"
+ )
+ self.assertTrue(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_not_types_takes_priority_over_types(self):
+ definition = {
+ "not_types": ["m.*", "org.*"],
+ "types": ["m.room.message", "m.room.topic"]
+ }
+ event = MockEvent(
+ sender="@foo:bar",
+ type="m.room.topic",
+ room_id="!foo:bar"
+ )
+ self.assertFalse(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_senders_works_with_literals(self):
+ definition = {
+ "senders": ["@flibble:wibble"]
+ }
+ event = MockEvent(
+ sender="@flibble:wibble",
+ type="com.nom.nom.nom",
+ room_id="!foo:bar"
+ )
+ self.assertTrue(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_senders_works_with_unknowns(self):
+ definition = {
+ "senders": ["@flibble:wibble"]
+ }
+ event = MockEvent(
+ sender="@challenger:appears",
+ type="com.nom.nom.nom",
+ room_id="!foo:bar"
+ )
+ self.assertFalse(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_not_senders_works_with_literals(self):
+ definition = {
+ "not_senders": ["@flibble:wibble"]
+ }
+ event = MockEvent(
+ sender="@flibble:wibble",
+ type="com.nom.nom.nom",
+ room_id="!foo:bar"
+ )
+ self.assertFalse(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_not_senders_works_with_unknowns(self):
+ definition = {
+ "not_senders": ["@flibble:wibble"]
+ }
+ event = MockEvent(
+ sender="@challenger:appears",
+ type="com.nom.nom.nom",
+ room_id="!foo:bar"
+ )
+ self.assertTrue(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_not_senders_takes_priority_over_senders(self):
+ definition = {
+ "not_senders": ["@misspiggy:muppets"],
+ "senders": ["@kermit:muppets", "@misspiggy:muppets"]
+ }
+ event = MockEvent(
+ sender="@misspiggy:muppets",
+ type="m.room.topic",
+ room_id="!foo:bar"
+ )
+ self.assertFalse(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_rooms_works_with_literals(self):
+ definition = {
+ "rooms": ["!secretbase:unknown"]
+ }
+ event = MockEvent(
+ sender="@foo:bar",
+ type="m.room.message",
+ room_id="!secretbase:unknown"
+ )
+ self.assertTrue(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_rooms_works_with_unknowns(self):
+ definition = {
+ "rooms": ["!secretbase:unknown"]
+ }
+ event = MockEvent(
+ sender="@foo:bar",
+ type="m.room.message",
+ room_id="!anothersecretbase:unknown"
+ )
+ self.assertFalse(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_not_rooms_works_with_literals(self):
+ definition = {
+ "not_rooms": ["!anothersecretbase:unknown"]
+ }
+ event = MockEvent(
+ sender="@foo:bar",
+ type="m.room.message",
+ room_id="!anothersecretbase:unknown"
+ )
+ self.assertFalse(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_not_rooms_works_with_unknowns(self):
+ definition = {
+ "not_rooms": ["!secretbase:unknown"]
+ }
+ event = MockEvent(
+ sender="@foo:bar",
+ type="m.room.message",
+ room_id="!anothersecretbase:unknown"
+ )
+ self.assertTrue(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_not_rooms_takes_priority_over_rooms(self):
+ definition = {
+ "not_rooms": ["!secretbase:unknown"],
+ "rooms": ["!secretbase:unknown"]
+ }
+ event = MockEvent(
+ sender="@foo:bar",
+ type="m.room.message",
+ room_id="!secretbase:unknown"
+ )
+ self.assertFalse(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_combined_event(self):
+ definition = {
+ "not_senders": ["@misspiggy:muppets"],
+ "senders": ["@kermit:muppets"],
+ "rooms": ["!stage:unknown"],
+ "not_rooms": ["!piggyshouse:muppets"],
+ "types": ["m.room.message", "muppets.kermit.*"],
+ "not_types": ["muppets.misspiggy.*"]
+ }
+ event = MockEvent(
+ sender="@kermit:muppets", # yup
+ type="m.room.message", # yup
+ room_id="!stage:unknown" # yup
+ )
+ self.assertTrue(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_combined_event_bad_sender(self):
+ definition = {
+ "not_senders": ["@misspiggy:muppets"],
+ "senders": ["@kermit:muppets"],
+ "rooms": ["!stage:unknown"],
+ "not_rooms": ["!piggyshouse:muppets"],
+ "types": ["m.room.message", "muppets.kermit.*"],
+ "not_types": ["muppets.misspiggy.*"]
+ }
+ event = MockEvent(
+ sender="@misspiggy:muppets", # nope
+ type="m.room.message", # yup
+ room_id="!stage:unknown" # yup
+ )
+ self.assertFalse(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_combined_event_bad_room(self):
+ definition = {
+ "not_senders": ["@misspiggy:muppets"],
+ "senders": ["@kermit:muppets"],
+ "rooms": ["!stage:unknown"],
+ "not_rooms": ["!piggyshouse:muppets"],
+ "types": ["m.room.message", "muppets.kermit.*"],
+ "not_types": ["muppets.misspiggy.*"]
+ }
+ event = MockEvent(
+ sender="@kermit:muppets", # yup
+ type="m.room.message", # yup
+ room_id="!piggyshouse:muppets" # nope
+ )
+ self.assertFalse(
+ Filter(definition).check(event)
+ )
+
+ def test_definition_combined_event_bad_type(self):
+ definition = {
+ "not_senders": ["@misspiggy:muppets"],
+ "senders": ["@kermit:muppets"],
+ "rooms": ["!stage:unknown"],
+ "not_rooms": ["!piggyshouse:muppets"],
+ "types": ["m.room.message", "muppets.kermit.*"],
+ "not_types": ["muppets.misspiggy.*"]
+ }
+ event = MockEvent(
+ sender="@kermit:muppets", # yup
+ type="muppets.misspiggy.kisses", # nope
+ room_id="!stage:unknown" # yup
+ )
+ self.assertFalse(
+ Filter(definition).check(event)
+ )
+
+ @defer.inlineCallbacks
+ def test_filter_presence_match(self):
+ user_filter_json = {
+ "presence": {
+ "types": ["m.*"]
+ }
+ }
+ user = UserID.from_string("@" + user_localpart + ":test")
+ filter_id = yield self.datastore.add_user_filter(
+ user_localpart=user_localpart,
+ user_filter=user_filter_json,
+ )
+ event = MockEvent(
+ sender="@foo:bar",
+ type="m.profile",
+ )
+ events = [event]
+
+ user_filter = yield self.filtering.get_user_filter(
+ user_localpart=user_localpart,
+ filter_id=filter_id,
+ )
+
+ results = user_filter.filter_presence(events=events)
+ self.assertEquals(events, results)
+
+ @defer.inlineCallbacks
+ def test_filter_presence_no_match(self):
+ user_filter_json = {
+ "presence": {
+ "types": ["m.*"]
+ }
+ }
+ user = UserID.from_string("@" + user_localpart + ":test")
+ filter_id = yield self.datastore.add_user_filter(
+ user_localpart=user_localpart,
+ user_filter=user_filter_json,
+ )
+ event = MockEvent(
+ sender="@foo:bar",
+ type="custom.avatar.3d.crazy",
+ )
+ events = [event]
+
+ user_filter = yield self.filtering.get_user_filter(
+ user_localpart=user_localpart,
+ filter_id=filter_id,
+ )
+
+ results = user_filter.filter_presence(events=events)
+ self.assertEquals([], results)
+
+ @defer.inlineCallbacks
+ def test_filter_room_state_match(self):
+ user_filter_json = {
+ "room": {
+ "state": {
+ "types": ["m.*"]
+ }
+ }
+ }
+ user = UserID.from_string("@" + user_localpart + ":test")
+ filter_id = yield self.datastore.add_user_filter(
+ user_localpart=user_localpart,
+ user_filter=user_filter_json,
+ )
+ event = MockEvent(
+ sender="@foo:bar",
+ type="m.room.topic",
+ room_id="!foo:bar"
+ )
+ events = [event]
+
+ user_filter = yield self.filtering.get_user_filter(
+ user_localpart=user_localpart,
+ filter_id=filter_id,
+ )
+
+ results = user_filter.filter_room_state(events=events)
+ self.assertEquals(events, results)
+
+ @defer.inlineCallbacks
+ def test_filter_room_state_no_match(self):
+ user_filter_json = {
+ "room": {
+ "state": {
+ "types": ["m.*"]
+ }
+ }
+ }
+ user = UserID.from_string("@" + user_localpart + ":test")
+ filter_id = yield self.datastore.add_user_filter(
+ user_localpart=user_localpart,
+ user_filter=user_filter_json,
+ )
+ event = MockEvent(
+ sender="@foo:bar",
+ type="org.matrix.custom.event",
+ room_id="!foo:bar"
+ )
+ events = [event]
+
+ user_filter = yield self.filtering.get_user_filter(
+ user_localpart=user_localpart,
+ filter_id=filter_id,
+ )
+
+ results = user_filter.filter_room_state(events)
+ self.assertEquals([], results)
+
+ @defer.inlineCallbacks
+ def test_add_filter(self):
+ user_filter_json = {
+ "room": {
+ "state": {
+ "types": ["m.*"]
+ }
+ }
+ }
+
+ filter_id = yield self.filtering.add_user_filter(
+ user_localpart=user_localpart,
+ user_filter=user_filter_json,
+ )
+
+ self.assertEquals(filter_id, 0)
+ self.assertEquals(user_filter_json,
+ (yield self.datastore.get_user_filter(
+ user_localpart=user_localpart,
+ filter_id=0,
+ ))
+ )
+
+ @defer.inlineCallbacks
+ def test_get_filter(self):
+ user_filter_json = {
+ "room": {
+ "state": {
+ "types": ["m.*"]
+ }
+ }
+ }
+
+ filter_id = yield self.datastore.add_user_filter(
+ user_localpart=user_localpart,
+ user_filter=user_filter_json,
+ )
+
+ filter = yield self.filtering.get_user_filter(
+ user_localpart=user_localpart,
+ filter_id=filter_id,
+ )
+
+ self.assertEquals(filter.filter_json, user_filter_json)
diff --git a/tests/api/test_ratelimiting.py b/tests/api/test_ratelimiting.py
new file mode 100644
index 00000000..dd0bc19e
--- /dev/null
+++ b/tests/api/test_ratelimiting.py
@@ -0,0 +1,39 @@
+from synapse.api.ratelimiting import Ratelimiter
+
+from tests import unittest
+
+class TestRatelimiter(unittest.TestCase):
+
+ def test_allowed(self):
+ limiter = Ratelimiter()
+ allowed, time_allowed = limiter.send_message(
+ user_id="test_id", time_now_s=0, msg_rate_hz=0.1, burst_count=1,
+ )
+ self.assertTrue(allowed)
+ self.assertEquals(10., time_allowed)
+
+ allowed, time_allowed = limiter.send_message(
+ user_id="test_id", time_now_s=5, msg_rate_hz=0.1, burst_count=1,
+ )
+ self.assertFalse(allowed)
+ self.assertEquals(10., time_allowed)
+
+ allowed, time_allowed = limiter.send_message(
+ user_id="test_id", time_now_s=10, msg_rate_hz=0.1, burst_count=1
+ )
+ self.assertTrue(allowed)
+ self.assertEquals(20., time_allowed)
+
+ def test_pruning(self):
+ limiter = Ratelimiter()
+ allowed, time_allowed = limiter.send_message(
+ user_id="test_id_1", time_now_s=0, msg_rate_hz=0.1, burst_count=1,
+ )
+
+ self.assertIn("test_id_1", limiter.message_counts)
+
+ allowed, time_allowed = limiter.send_message(
+ user_id="test_id_2", time_now_s=10, msg_rate_hz=0.1, burst_count=1
+ )
+
+ self.assertNotIn("test_id_1", limiter.message_counts)
diff --git a/tests/appservice/__init__.py b/tests/appservice/__init__.py
new file mode 100644
index 00000000..1a84d94c
--- /dev/null
+++ b/tests/appservice/__init__.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/appservice/test_appservice.py b/tests/appservice/test_appservice.py
new file mode 100644
index 00000000..8ce8dc0a
--- /dev/null
+++ b/tests/appservice/test_appservice.py
@@ -0,0 +1,229 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from synapse.appservice import ApplicationService
+
+from mock import Mock, PropertyMock
+from tests import unittest
+
+
+def _regex(regex, exclusive=True):
+ return {
+ "regex": regex,
+ "exclusive": exclusive
+ }
+
+
+class ApplicationServiceTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.service = ApplicationService(
+ url="some_url",
+ token="some_token",
+ namespaces={
+ ApplicationService.NS_USERS: [],
+ ApplicationService.NS_ROOMS: [],
+ ApplicationService.NS_ALIASES: []
+ }
+ )
+ self.event = Mock(
+ type="m.something", room_id="!foo:bar", sender="@someone:somewhere"
+ )
+
+ def test_regex_user_id_prefix_match(self):
+ self.service.namespaces[ApplicationService.NS_USERS].append(
+ _regex("@irc_.*")
+ )
+ self.event.sender = "@irc_foobar:matrix.org"
+ self.assertTrue(self.service.is_interested(self.event))
+
+ def test_regex_user_id_prefix_no_match(self):
+ self.service.namespaces[ApplicationService.NS_USERS].append(
+ _regex("@irc_.*")
+ )
+ self.event.sender = "@someone_else:matrix.org"
+ self.assertFalse(self.service.is_interested(self.event))
+
+ def test_regex_room_member_is_checked(self):
+ self.service.namespaces[ApplicationService.NS_USERS].append(
+ _regex("@irc_.*")
+ )
+ self.event.sender = "@someone_else:matrix.org"
+ self.event.type = "m.room.member"
+ self.event.state_key = "@irc_foobar:matrix.org"
+ self.assertTrue(self.service.is_interested(self.event))
+
+ def test_regex_room_id_match(self):
+ self.service.namespaces[ApplicationService.NS_ROOMS].append(
+ _regex("!some_prefix.*some_suffix:matrix.org")
+ )
+ self.event.room_id = "!some_prefixs0m3th1nGsome_suffix:matrix.org"
+ self.assertTrue(self.service.is_interested(self.event))
+
+ def test_regex_room_id_no_match(self):
+ self.service.namespaces[ApplicationService.NS_ROOMS].append(
+ _regex("!some_prefix.*some_suffix:matrix.org")
+ )
+ self.event.room_id = "!XqBunHwQIXUiqCaoxq:matrix.org"
+ self.assertFalse(self.service.is_interested(self.event))
+
+ def test_regex_alias_match(self):
+ self.service.namespaces[ApplicationService.NS_ALIASES].append(
+ _regex("#irc_.*:matrix.org")
+ )
+ self.assertTrue(self.service.is_interested(
+ self.event,
+ aliases_for_event=["#irc_foobar:matrix.org", "#athing:matrix.org"]
+ ))
+
+ def test_non_exclusive_alias(self):
+ self.service.namespaces[ApplicationService.NS_ALIASES].append(
+ _regex("#irc_.*:matrix.org", exclusive=False)
+ )
+ self.assertFalse(self.service.is_exclusive_alias(
+ "#irc_foobar:matrix.org"
+ ))
+
+ def test_non_exclusive_room(self):
+ self.service.namespaces[ApplicationService.NS_ROOMS].append(
+ _regex("!irc_.*:matrix.org", exclusive=False)
+ )
+ self.assertFalse(self.service.is_exclusive_room(
+ "!irc_foobar:matrix.org"
+ ))
+
+ def test_non_exclusive_user(self):
+ self.service.namespaces[ApplicationService.NS_USERS].append(
+ _regex("@irc_.*:matrix.org", exclusive=False)
+ )
+ self.assertFalse(self.service.is_exclusive_user(
+ "@irc_foobar:matrix.org"
+ ))
+
+ def test_exclusive_alias(self):
+ self.service.namespaces[ApplicationService.NS_ALIASES].append(
+ _regex("#irc_.*:matrix.org", exclusive=True)
+ )
+ self.assertTrue(self.service.is_exclusive_alias(
+ "#irc_foobar:matrix.org"
+ ))
+
+ def test_exclusive_user(self):
+ self.service.namespaces[ApplicationService.NS_USERS].append(
+ _regex("@irc_.*:matrix.org", exclusive=True)
+ )
+ self.assertTrue(self.service.is_exclusive_user(
+ "@irc_foobar:matrix.org"
+ ))
+
+ def test_exclusive_room(self):
+ self.service.namespaces[ApplicationService.NS_ROOMS].append(
+ _regex("!irc_.*:matrix.org", exclusive=True)
+ )
+ self.assertTrue(self.service.is_exclusive_room(
+ "!irc_foobar:matrix.org"
+ ))
+
+ def test_regex_alias_no_match(self):
+ self.service.namespaces[ApplicationService.NS_ALIASES].append(
+ _regex("#irc_.*:matrix.org")
+ )
+ self.assertFalse(self.service.is_interested(
+ self.event,
+ aliases_for_event=["#xmpp_foobar:matrix.org", "#athing:matrix.org"]
+ ))
+
+ def test_regex_multiple_matches(self):
+ self.service.namespaces[ApplicationService.NS_ALIASES].append(
+ _regex("#irc_.*:matrix.org")
+ )
+ self.service.namespaces[ApplicationService.NS_USERS].append(
+ _regex("@irc_.*")
+ )
+ self.event.sender = "@irc_foobar:matrix.org"
+ self.assertTrue(self.service.is_interested(
+ self.event,
+ aliases_for_event=["#irc_barfoo:matrix.org"]
+ ))
+
+ def test_restrict_to_rooms(self):
+ self.service.namespaces[ApplicationService.NS_ROOMS].append(
+ _regex("!flibble_.*:matrix.org")
+ )
+ self.service.namespaces[ApplicationService.NS_USERS].append(
+ _regex("@irc_.*")
+ )
+ self.event.sender = "@irc_foobar:matrix.org"
+ self.event.room_id = "!wibblewoo:matrix.org"
+ self.assertFalse(self.service.is_interested(
+ self.event,
+ restrict_to=ApplicationService.NS_ROOMS
+ ))
+
+ def test_restrict_to_aliases(self):
+ self.service.namespaces[ApplicationService.NS_ALIASES].append(
+ _regex("#xmpp_.*:matrix.org")
+ )
+ self.service.namespaces[ApplicationService.NS_USERS].append(
+ _regex("@irc_.*")
+ )
+ self.event.sender = "@irc_foobar:matrix.org"
+ self.assertFalse(self.service.is_interested(
+ self.event,
+ restrict_to=ApplicationService.NS_ALIASES,
+ aliases_for_event=["#irc_barfoo:matrix.org"]
+ ))
+
+ def test_restrict_to_senders(self):
+ self.service.namespaces[ApplicationService.NS_ALIASES].append(
+ _regex("#xmpp_.*:matrix.org")
+ )
+ self.service.namespaces[ApplicationService.NS_USERS].append(
+ _regex("@irc_.*")
+ )
+ self.event.sender = "@xmpp_foobar:matrix.org"
+ self.assertFalse(self.service.is_interested(
+ self.event,
+ restrict_to=ApplicationService.NS_USERS,
+ aliases_for_event=["#xmpp_barfoo:matrix.org"]
+ ))
+
+ def test_interested_in_self(self):
+ # make sure invites get through
+ self.service.sender = "@appservice:name"
+ self.service.namespaces[ApplicationService.NS_USERS].append(
+ _regex("@irc_.*")
+ )
+ self.event.type = "m.room.member"
+ self.event.content = {
+ "membership": "invite"
+ }
+ self.event.state_key = self.service.sender
+ self.assertTrue(self.service.is_interested(self.event))
+
+ def test_member_list_match(self):
+ self.service.namespaces[ApplicationService.NS_USERS].append(
+ _regex("@irc_.*")
+ )
+ join_list = [
+ "@alice:here",
+ "@irc_fo:here", # AS user
+ "@bob:here",
+ ]
+
+ self.event.sender = "@xmpp_foobar:matrix.org"
+ self.assertTrue(self.service.is_interested(
+ event=self.event,
+ member_list=join_list
+ ))
diff --git a/tests/appservice/test_scheduler.py b/tests/appservice/test_scheduler.py
new file mode 100644
index 00000000..82a59650
--- /dev/null
+++ b/tests/appservice/test_scheduler.py
@@ -0,0 +1,252 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from synapse.appservice import ApplicationServiceState, AppServiceTransaction
+from synapse.appservice.scheduler import (
+ _ServiceQueuer, _TransactionController, _Recoverer
+)
+from twisted.internet import defer
+from ..utils import MockClock
+from mock import Mock
+from tests import unittest
+
+
+class ApplicationServiceSchedulerTransactionCtrlTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.clock = MockClock()
+ self.store = Mock()
+ self.as_api = Mock()
+ self.recoverer = Mock()
+ self.recoverer_fn = Mock(return_value=self.recoverer)
+ self.txnctrl = _TransactionController(
+ clock=self.clock, store=self.store, as_api=self.as_api,
+ recoverer_fn=self.recoverer_fn
+ )
+
+ def test_single_service_up_txn_sent(self):
+ # Test: The AS is up and the txn is successfully sent.
+ service = Mock()
+ events = [Mock(), Mock()]
+ txn_id = "foobar"
+ txn = Mock(id=txn_id, service=service, events=events)
+
+ # mock methods
+ self.store.get_appservice_state = Mock(
+ return_value=defer.succeed(ApplicationServiceState.UP)
+ )
+ txn.send = Mock(return_value=defer.succeed(True))
+ self.store.create_appservice_txn = Mock(
+ return_value=defer.succeed(txn)
+ )
+
+ # actual call
+ self.txnctrl.send(service, events)
+
+ self.store.create_appservice_txn.assert_called_once_with(
+ service=service, events=events # txn made and saved
+ )
+ self.assertEquals(0, len(self.txnctrl.recoverers)) # no recoverer made
+ txn.complete.assert_called_once_with(self.store) # txn completed
+
+ def test_single_service_down(self):
+ # Test: The AS is down so it shouldn't push; Recoverers will do it.
+ # It should still make a transaction though.
+ service = Mock()
+ events = [Mock(), Mock()]
+
+ txn = Mock(id="idhere", service=service, events=events)
+ self.store.get_appservice_state = Mock(
+ return_value=defer.succeed(ApplicationServiceState.DOWN)
+ )
+ self.store.create_appservice_txn = Mock(
+ return_value=defer.succeed(txn)
+ )
+
+ # actual call
+ self.txnctrl.send(service, events)
+
+ self.store.create_appservice_txn.assert_called_once_with(
+ service=service, events=events # txn made and saved
+ )
+ self.assertEquals(0, txn.send.call_count) # txn not sent though
+ self.assertEquals(0, txn.complete.call_count) # or completed
+
+ def test_single_service_up_txn_not_sent(self):
+ # Test: The AS is up and the txn is not sent. A Recoverer is made and
+ # started.
+ service = Mock()
+ events = [Mock(), Mock()]
+ txn_id = "foobar"
+ txn = Mock(id=txn_id, service=service, events=events)
+
+ # mock methods
+ self.store.get_appservice_state = Mock(
+ return_value=defer.succeed(ApplicationServiceState.UP)
+ )
+ self.store.set_appservice_state = Mock(return_value=defer.succeed(True))
+ txn.send = Mock(return_value=defer.succeed(False)) # fails to send
+ self.store.create_appservice_txn = Mock(
+ return_value=defer.succeed(txn)
+ )
+
+ # actual call
+ self.txnctrl.send(service, events)
+
+ self.store.create_appservice_txn.assert_called_once_with(
+ service=service, events=events
+ )
+ self.assertEquals(1, self.recoverer_fn.call_count) # recoverer made
+ self.assertEquals(1, self.recoverer.recover.call_count) # and invoked
+ self.assertEquals(1, len(self.txnctrl.recoverers)) # and stored
+ self.assertEquals(0, txn.complete.call_count) # txn not completed
+ self.store.set_appservice_state.assert_called_once_with(
+ service, ApplicationServiceState.DOWN # service marked as down
+ )
+
+
+class ApplicationServiceSchedulerRecovererTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.clock = MockClock()
+ self.as_api = Mock()
+ self.store = Mock()
+ self.service = Mock()
+ self.callback = Mock()
+ self.recoverer = _Recoverer(
+ clock=self.clock,
+ as_api=self.as_api,
+ store=self.store,
+ service=self.service,
+ callback=self.callback,
+ )
+
+ def test_recover_single_txn(self):
+ txn = Mock()
+ # return one txn to send, then no more old txns
+ txns = [txn, None]
+
+ def take_txn(*args, **kwargs):
+ return defer.succeed(txns.pop(0))
+ self.store.get_oldest_unsent_txn = Mock(side_effect=take_txn)
+
+ self.recoverer.recover()
+ # shouldn't have called anything prior to waiting for exp backoff
+ self.assertEquals(0, self.store.get_oldest_unsent_txn.call_count)
+ txn.send = Mock(return_value=True)
+ # wait for exp backoff
+ self.clock.advance_time(2)
+ self.assertEquals(1, txn.send.call_count)
+ self.assertEquals(1, txn.complete.call_count)
+ # 2 because it needs to get None to know there are no more txns
+ self.assertEquals(2, self.store.get_oldest_unsent_txn.call_count)
+ self.callback.assert_called_once_with(self.recoverer)
+ self.assertEquals(self.recoverer.service, self.service)
+
+ def test_recover_retry_txn(self):
+ txn = Mock()
+ txns = [txn, None]
+ pop_txn = False
+
+ def take_txn(*args, **kwargs):
+ if pop_txn:
+ return defer.succeed(txns.pop(0))
+ else:
+ return defer.succeed(txn)
+ self.store.get_oldest_unsent_txn = Mock(side_effect=take_txn)
+
+ self.recoverer.recover()
+ self.assertEquals(0, self.store.get_oldest_unsent_txn.call_count)
+ txn.send = Mock(return_value=False)
+ self.clock.advance_time(2)
+ self.assertEquals(1, txn.send.call_count)
+ self.assertEquals(0, txn.complete.call_count)
+ self.assertEquals(0, self.callback.call_count)
+ self.clock.advance_time(4)
+ self.assertEquals(2, txn.send.call_count)
+ self.assertEquals(0, txn.complete.call_count)
+ self.assertEquals(0, self.callback.call_count)
+ self.clock.advance_time(8)
+ self.assertEquals(3, txn.send.call_count)
+ self.assertEquals(0, txn.complete.call_count)
+ self.assertEquals(0, self.callback.call_count)
+ txn.send = Mock(return_value=True) # successfully send the txn
+ pop_txn = True # returns the txn the first time, then no more.
+ self.clock.advance_time(16)
+ self.assertEquals(1, txn.send.call_count) # new mock reset call count
+ self.assertEquals(1, txn.complete.call_count)
+ self.callback.assert_called_once_with(self.recoverer)
+
+
+class ApplicationServiceSchedulerQueuerTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.txn_ctrl = Mock()
+ self.queuer = _ServiceQueuer(self.txn_ctrl)
+
+ def test_send_single_event_no_queue(self):
+ # Expect the event to be sent immediately.
+ service = Mock(id=4)
+ event = Mock()
+ self.queuer.enqueue(service, event)
+ self.txn_ctrl.send.assert_called_once_with(service, [event])
+
+ def test_send_single_event_with_queue(self):
+ d = defer.Deferred()
+ self.txn_ctrl.send = Mock(return_value=d)
+ service = Mock(id=4)
+ event = Mock(event_id="first")
+ event2 = Mock(event_id="second")
+ event3 = Mock(event_id="third")
+ # Send an event and don't resolve it just yet.
+ self.queuer.enqueue(service, event)
+ # Send more events: expect send() to NOT be called multiple times.
+ self.queuer.enqueue(service, event2)
+ self.queuer.enqueue(service, event3)
+ self.txn_ctrl.send.assert_called_with(service, [event])
+ self.assertEquals(1, self.txn_ctrl.send.call_count)
+ # Resolve the send event: expect the queued events to be sent
+ d.callback(service)
+ self.txn_ctrl.send.assert_called_with(service, [event2, event3])
+ self.assertEquals(2, self.txn_ctrl.send.call_count)
+
+ def test_multiple_service_queues(self):
+ # Tests that each service has its own queue, and that they don't block
+ # on each other.
+ srv1 = Mock(id=4)
+ srv_1_defer = defer.Deferred()
+ srv_1_event = Mock(event_id="srv1a")
+ srv_1_event2 = Mock(event_id="srv1b")
+
+ srv2 = Mock(id=6)
+ srv_2_defer = defer.Deferred()
+ srv_2_event = Mock(event_id="srv2a")
+ srv_2_event2 = Mock(event_id="srv2b")
+
+ send_return_list = [srv_1_defer, srv_2_defer]
+ self.txn_ctrl.send = Mock(side_effect=lambda x,y: send_return_list.pop(0))
+
+ # send events for different ASes and make sure they are sent
+ self.queuer.enqueue(srv1, srv_1_event)
+ self.queuer.enqueue(srv1, srv_1_event2)
+ self.txn_ctrl.send.assert_called_with(srv1, [srv_1_event])
+ self.queuer.enqueue(srv2, srv_2_event)
+ self.queuer.enqueue(srv2, srv_2_event2)
+ self.txn_ctrl.send.assert_called_with(srv2, [srv_2_event])
+
+ # make sure callbacks for a service only send queued events for THAT
+ # service
+ srv_2_defer.callback(srv2)
+ self.txn_ctrl.send.assert_called_with(srv2, [srv_2_event2])
+ self.assertEquals(3, self.txn_ctrl.send.call_count)
diff --git a/tests/crypto/__init__.py b/tests/crypto/__init__.py
new file mode 100644
index 00000000..9bff9ec1
--- /dev/null
+++ b/tests/crypto/__init__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
diff --git a/tests/crypto/test_event_signing.py b/tests/crypto/test_event_signing.py
new file mode 100644
index 00000000..79134729
--- /dev/null
+++ b/tests/crypto/test_event_signing.py
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+
+from synapse.events.builder import EventBuilder
+from synapse.crypto.event_signing import add_hashes_and_signatures
+
+from unpaddedbase64 import decode_base64
+
+import nacl.signing
+
+
+# Perform these tests using given secret key so we get entirely deterministic
+# signatures output that we can test against.
+SIGNING_KEY_SEED = decode_base64(
+ "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA1"
+)
+
+KEY_ALG = "ed25519"
+KEY_VER = 1
+KEY_NAME = "%s:%d" % (KEY_ALG, KEY_VER)
+
+HOSTNAME = "domain"
+
+
+class EventSigningTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.signing_key = nacl.signing.SigningKey(SIGNING_KEY_SEED)
+ self.signing_key.alg = KEY_ALG
+ self.signing_key.version = KEY_VER
+
+ def test_sign_minimal(self):
+ builder = EventBuilder(
+ {
+ 'event_id': "$0:domain",
+ 'origin': "domain",
+ 'origin_server_ts': 1000000,
+ 'signatures': {},
+ 'type': "X",
+ 'unsigned': {'age_ts': 1000000},
+ },
+ )
+
+ add_hashes_and_signatures(builder, HOSTNAME, self.signing_key)
+
+ event = builder.build()
+
+ self.assertTrue(hasattr(event, 'hashes'))
+ self.assertIn('sha256', event.hashes)
+ self.assertEquals(
+ event.hashes['sha256'],
+ "6tJjLpXtggfke8UxFhAKg82QVkJzvKOVOOSjUDK4ZSI",
+ )
+
+ self.assertTrue(hasattr(event, 'signatures'))
+ self.assertIn(HOSTNAME, event.signatures)
+ self.assertIn(KEY_NAME, event.signatures["domain"])
+ self.assertEquals(
+ event.signatures[HOSTNAME][KEY_NAME],
+ "2Wptgo4CwmLo/Y8B8qinxApKaCkBG2fjTWB7AbP5Uy+"
+ "aIbygsSdLOFzvdDjww8zUVKCmI02eP9xtyJxc/cLiBA",
+ )
+
+ def test_sign_message(self):
+ builder = EventBuilder(
+ {
+ 'content': {
+ 'body': "Here is the message content",
+ },
+ 'event_id': "$0:domain",
+ 'origin': "domain",
+ 'origin_server_ts': 1000000,
+ 'type': "m.room.message",
+ 'room_id': "!r:domain",
+ 'sender': "@u:domain",
+ 'signatures': {},
+ 'unsigned': {'age_ts': 1000000},
+ }
+ )
+
+ add_hashes_and_signatures(builder, HOSTNAME, self.signing_key)
+
+ event = builder.build()
+
+ self.assertTrue(hasattr(event, 'hashes'))
+ self.assertIn('sha256', event.hashes)
+ self.assertEquals(
+ event.hashes['sha256'],
+ "onLKD1bGljeBWQhWZ1kaP9SorVmRQNdN5aM2JYU2n/g",
+ )
+
+ self.assertTrue(hasattr(event, 'signatures'))
+ self.assertIn(HOSTNAME, event.signatures)
+ self.assertIn(KEY_NAME, event.signatures["domain"])
+ self.assertEquals(
+ event.signatures[HOSTNAME][KEY_NAME],
+ "Wm+VzmOUOz08Ds+0NTWb1d4CZrVsJSikkeRxh6aCcUw"
+ "u6pNC78FunoD7KNWzqFn241eYHYMGCA5McEiVPdhzBA"
+ )
diff --git a/tests/events/__init__.py b/tests/events/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/events/__init__.py
diff --git a/tests/events/test_utils.py b/tests/events/test_utils.py
new file mode 100644
index 00000000..16179921
--- /dev/null
+++ b/tests/events/test_utils.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the 'License');
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an 'AS IS' BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from .. import unittest
+
+from synapse.events import FrozenEvent
+from synapse.events.utils import prune_event
+
+class PruneEventTestCase(unittest.TestCase):
+ """ Asserts that a new event constructed with `evdict` will look like
+ `matchdict` when it is redacted. """
+ def run_test(self, evdict, matchdict):
+ self.assertEquals(
+ prune_event(FrozenEvent(evdict)).get_dict(),
+ matchdict
+ )
+
+ def test_minimal(self):
+ self.run_test(
+ {'type': 'A'},
+ {
+ 'type': 'A',
+ 'content': {},
+ 'signatures': {},
+ 'unsigned': {},
+ }
+ )
+
+ def test_basic_keys(self):
+ self.run_test(
+ {
+ 'type': 'A',
+ 'room_id': '!1:domain',
+ 'sender': '@2:domain',
+ 'event_id': '$3:domain',
+ 'origin': 'domain',
+ },
+ {
+ 'type': 'A',
+ 'room_id': '!1:domain',
+ 'sender': '@2:domain',
+ 'event_id': '$3:domain',
+ 'origin': 'domain',
+ 'content': {},
+ 'signatures': {},
+ 'unsigned': {},
+ }
+ )
+
+ def test_unsigned_age_ts(self):
+ self.run_test(
+ {
+ 'type': 'B',
+ 'unsigned': {'age_ts': 20},
+ },
+ {
+ 'type': 'B',
+ 'content': {},
+ 'signatures': {},
+ 'unsigned': {'age_ts': 20},
+ }
+ )
+
+ self.run_test(
+ {
+ 'type': 'B',
+ 'unsigned': {'other_key': 'here'},
+ },
+ {
+ 'type': 'B',
+ 'content': {},
+ 'signatures': {},
+ 'unsigned': {},
+ }
+ )
+
+ def test_content(self):
+ self.run_test(
+ {
+ 'type': 'C',
+ 'content': {'things': 'here'},
+ },
+ {
+ 'type': 'C',
+ 'content': {},
+ 'signatures': {},
+ 'unsigned': {},
+ }
+ )
+
+ self.run_test(
+ {
+ 'type': 'm.room.create',
+ 'content': {'creator': '@2:domain', 'other_field': 'here'},
+ },
+ {
+ 'type': 'm.room.create',
+ 'content': {'creator': '@2:domain'},
+ 'signatures': {},
+ 'unsigned': {},
+ }
+ )
diff --git a/tests/federation/__init__.py b/tests/federation/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/federation/__init__.py
diff --git a/tests/federation/test_federation.py b/tests/federation/test_federation.py
new file mode 100644
index 00000000..a4ef60b9
--- /dev/null
+++ b/tests/federation/test_federation.py
@@ -0,0 +1,301 @@
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# trial imports
+from twisted.internet import defer
+from tests import unittest
+
+# python imports
+from mock import Mock, ANY
+
+from ..utils import MockHttpResource, MockClock, setup_test_homeserver
+
+from synapse.federation import initialize_http_replication
+from synapse.events import FrozenEvent
+
+
+def make_pdu(prev_pdus=[], **kwargs):
+ """Provide some default fields for making a PduTuple."""
+ pdu_fields = {
+ "state_key": None,
+ "prev_events": prev_pdus,
+ }
+ pdu_fields.update(kwargs)
+
+ return FrozenEvent(pdu_fields)
+
+
+class FederationTestCase(unittest.TestCase):
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_resource = MockHttpResource()
+ self.mock_http_client = Mock(spec=[
+ "get_json",
+ "put_json",
+ ])
+ self.mock_persistence = Mock(spec=[
+ "prep_send_transaction",
+ "delivered_txn",
+ "get_received_txn_response",
+ "set_received_txn_response",
+ "get_destination_retry_timings",
+ "get_auth_chain",
+ ])
+ self.mock_persistence.get_received_txn_response.return_value = (
+ defer.succeed(None)
+ )
+
+ retry_timings_res = {
+ "destination": "",
+ "retry_last_ts": 0,
+ "retry_interval": 0,
+ }
+ self.mock_persistence.get_destination_retry_timings.return_value = (
+ defer.succeed(retry_timings_res)
+ )
+ self.mock_persistence.get_auth_chain.return_value = []
+ self.clock = MockClock()
+ hs = yield setup_test_homeserver(
+ resource_for_federation=self.mock_resource,
+ http_client=self.mock_http_client,
+ datastore=self.mock_persistence,
+ clock=self.clock,
+ keyring=Mock(),
+ )
+ self.federation = initialize_http_replication(hs)
+ self.distributor = hs.get_distributor()
+
+ @defer.inlineCallbacks
+ def test_get_state(self):
+ mock_handler = Mock(spec=[
+ "get_state_for_pdu",
+ ])
+
+ self.federation.set_handler(mock_handler)
+
+ mock_handler.get_state_for_pdu.return_value = defer.succeed([])
+
+ # Empty context initially
+ (code, response) = yield self.mock_resource.trigger(
+ "GET",
+ "/_matrix/federation/v1/state/my-context/",
+ None
+ )
+ self.assertEquals(200, code)
+ self.assertFalse(response["pdus"])
+
+ # Now lets give the context some state
+ mock_handler.get_state_for_pdu.return_value = (
+ defer.succeed([
+ make_pdu(
+ event_id="the-pdu-id",
+ origin="red",
+ user_id="@a:red",
+ room_id="my-context",
+ type="m.topic",
+ origin_server_ts=123456789000,
+ depth=1,
+ content={"topic": "The topic"},
+ state_key="",
+ power_level=1000,
+ prev_state="last-pdu-id",
+ ),
+ ])
+ )
+
+ (code, response) = yield self.mock_resource.trigger(
+ "GET",
+ "/_matrix/federation/v1/state/my-context/",
+ None
+ )
+ self.assertEquals(200, code)
+ self.assertEquals(1, len(response["pdus"]))
+
+ @defer.inlineCallbacks
+ def test_get_pdu(self):
+ mock_handler = Mock(spec=[
+ "get_persisted_pdu",
+ ])
+
+ self.federation.set_handler(mock_handler)
+
+ mock_handler.get_persisted_pdu.return_value = (
+ defer.succeed(None)
+ )
+
+ (code, response) = yield self.mock_resource.trigger(
+ "GET",
+ "/_matrix/federation/v1/event/abc123def456/",
+ None
+ )
+ self.assertEquals(404, code)
+
+ # Now insert such a PDU
+ mock_handler.get_persisted_pdu.return_value = (
+ defer.succeed(
+ make_pdu(
+ event_id="abc123def456",
+ origin="red",
+ user_id="@a:red",
+ room_id="my-context",
+ type="m.text",
+ origin_server_ts=123456789001,
+ depth=1,
+ content={"text": "Here is the message"},
+ )
+ )
+ )
+
+ (code, response) = yield self.mock_resource.trigger(
+ "GET",
+ "/_matrix/federation/v1/event/abc123def456/",
+ None
+ )
+ self.assertEquals(200, code)
+ self.assertEquals(1, len(response["pdus"]))
+ self.assertEquals("m.text", response["pdus"][0]["type"])
+
+ @defer.inlineCallbacks
+ def test_send_pdu(self):
+ self.mock_http_client.put_json.return_value = defer.succeed(
+ (200, "OK")
+ )
+
+ pdu = make_pdu(
+ event_id="abc123def456",
+ origin="red",
+ user_id="@a:red",
+ room_id="my-context",
+ type="m.text",
+ origin_server_ts=123456789001,
+ depth=1,
+ content={"text": "Here is the message"},
+ )
+
+ yield self.federation.send_pdu(pdu, ["remote"])
+
+ self.mock_http_client.put_json.assert_called_with(
+ "remote",
+ path="/_matrix/federation/v1/send/1000000/",
+ data={
+ "origin_server_ts": 1000000,
+ "origin": "test",
+ "pdus": [
+ pdu.get_pdu_json(),
+ ],
+ 'pdu_failures': [],
+ },
+ json_data_callback=ANY,
+ )
+
+ @defer.inlineCallbacks
+ def test_send_edu(self):
+ self.mock_http_client.put_json.return_value = defer.succeed(
+ (200, "OK")
+ )
+
+ yield self.federation.send_edu(
+ destination="remote",
+ edu_type="m.test",
+ content={"testing": "content here"},
+ )
+
+ # MockClock ensures we can guess these timestamps
+ self.mock_http_client.put_json.assert_called_with(
+ "remote",
+ path="/_matrix/federation/v1/send/1000000/",
+ data={
+ "origin": "test",
+ "origin_server_ts": 1000000,
+ "pdus": [],
+ "edus": [
+ {
+ "edu_type": "m.test",
+ "content": {"testing": "content here"},
+ }
+ ],
+ 'pdu_failures': [],
+ },
+ json_data_callback=ANY,
+ )
+
+ @defer.inlineCallbacks
+ def test_recv_edu(self):
+ recv_observer = Mock()
+ recv_observer.return_value = defer.succeed(())
+
+ self.federation.register_edu_handler("m.test", recv_observer)
+
+ yield self.mock_resource.trigger(
+ "PUT",
+ "/_matrix/federation/v1/send/1001000/",
+ """{
+ "origin": "remote",
+ "origin_server_ts": 1001000,
+ "pdus": [],
+ "edus": [
+ {
+ "origin": "remote",
+ "destination": "test",
+ "edu_type": "m.test",
+ "content": {"testing": "reply here"}
+ }
+ ]
+ }"""
+ )
+
+ recv_observer.assert_called_with(
+ "remote", {"testing": "reply here"}
+ )
+
+ @defer.inlineCallbacks
+ def test_send_query(self):
+ self.mock_http_client.get_json.return_value = defer.succeed(
+ {"your": "response"}
+ )
+
+ response = yield self.federation.make_query(
+ destination="remote",
+ query_type="a-question",
+ args={"one": "1", "two": "2"},
+ )
+
+ self.assertEquals({"your": "response"}, response)
+
+ self.mock_http_client.get_json.assert_called_with(
+ destination="remote",
+ path="/_matrix/federation/v1/query/a-question",
+ args={"one": "1", "two": "2"},
+ retry_on_dns_fail=True,
+ )
+
+ @defer.inlineCallbacks
+ def test_recv_query(self):
+ recv_handler = Mock()
+ recv_handler.return_value = defer.succeed({"another": "response"})
+
+ self.federation.register_query_handler("a-question", recv_handler)
+
+ code, response = yield self.mock_resource.trigger(
+ "GET",
+ "/_matrix/federation/v1/query/a-question?three=3&four=4",
+ None
+ )
+
+ self.assertEquals(200, code)
+ self.assertEquals({"another": "response"}, response)
+
+ recv_handler.assert_called_with(
+ {"three": "3", "four": "4"}
+ )
diff --git a/tests/handlers/__init__.py b/tests/handlers/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/handlers/__init__.py
diff --git a/tests/handlers/test_appservice.py b/tests/handlers/test_appservice.py
new file mode 100644
index 00000000..9e95d1e5
--- /dev/null
+++ b/tests/handlers/test_appservice.py
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from twisted.internet import defer
+from .. import unittest
+
+from synapse.handlers.appservice import ApplicationServicesHandler
+
+from mock import Mock
+
+
+class AppServiceHandlerTestCase(unittest.TestCase):
+ """ Tests the ApplicationServicesHandler. """
+
+ def setUp(self):
+ self.mock_store = Mock()
+ self.mock_as_api = Mock()
+ self.mock_scheduler = Mock()
+ hs = Mock()
+ hs.get_datastore = Mock(return_value=self.mock_store)
+ self.handler = ApplicationServicesHandler(
+ hs, self.mock_as_api, self.mock_scheduler
+ )
+
+ @defer.inlineCallbacks
+ def test_notify_interested_services(self):
+ interested_service = self._mkservice(is_interested=True)
+ services = [
+ self._mkservice(is_interested=False),
+ interested_service,
+ self._mkservice(is_interested=False)
+ ]
+
+ self.mock_store.get_app_services = Mock(return_value=services)
+ self.mock_store.get_user_by_id = Mock(return_value=[])
+
+ event = Mock(
+ sender="@someone:anywhere",
+ type="m.room.message",
+ room_id="!foo:bar"
+ )
+ self.mock_as_api.push = Mock()
+ yield self.handler.notify_interested_services(event)
+ self.mock_scheduler.submit_event_for_as.assert_called_once_with(
+ interested_service, event
+ )
+
+ @defer.inlineCallbacks
+ def test_query_user_exists_unknown_user(self):
+ user_id = "@someone:anywhere"
+ services = [self._mkservice(is_interested=True)]
+ services[0].is_interested_in_user = Mock(return_value=True)
+ self.mock_store.get_app_services = Mock(return_value=services)
+ self.mock_store.get_user_by_id = Mock(return_value=None)
+
+ event = Mock(
+ sender=user_id,
+ type="m.room.message",
+ room_id="!foo:bar"
+ )
+ self.mock_as_api.push = Mock()
+ self.mock_as_api.query_user = Mock()
+ yield self.handler.notify_interested_services(event)
+ self.mock_as_api.query_user.assert_called_once_with(
+ services[0], user_id
+ )
+
+ @defer.inlineCallbacks
+ def test_query_user_exists_known_user(self):
+ user_id = "@someone:anywhere"
+ services = [self._mkservice(is_interested=True)]
+ services[0].is_interested_in_user = Mock(return_value=True)
+ self.mock_store.get_app_services = Mock(return_value=services)
+ self.mock_store.get_user_by_id = Mock(return_value={
+ "name": user_id
+ })
+
+ event = Mock(
+ sender=user_id,
+ type="m.room.message",
+ room_id="!foo:bar"
+ )
+ self.mock_as_api.push = Mock()
+ self.mock_as_api.query_user = Mock()
+ yield self.handler.notify_interested_services(event)
+ self.assertFalse(
+ self.mock_as_api.query_user.called,
+ "query_user called when it shouldn't have been."
+ )
+
+ @defer.inlineCallbacks
+ def test_query_room_alias_exists(self):
+ room_alias_str = "#foo:bar"
+ room_alias = Mock()
+ room_alias.to_string = Mock(return_value=room_alias_str)
+
+ room_id = "!alpha:bet"
+ servers = ["aperture"]
+ interested_service = self._mkservice(is_interested=True)
+ services = [
+ self._mkservice(is_interested=False),
+ interested_service,
+ self._mkservice(is_interested=False)
+ ]
+
+ self.mock_store.get_app_services = Mock(return_value=services)
+ self.mock_store.get_association_from_room_alias = Mock(
+ return_value=Mock(room_id=room_id, servers=servers)
+ )
+
+ result = yield self.handler.query_room_alias_exists(room_alias)
+
+ self.mock_as_api.query_alias.assert_called_once_with(
+ interested_service,
+ room_alias_str
+ )
+ self.assertEquals(result.room_id, room_id)
+ self.assertEquals(result.servers, servers)
+
+
+
+ def _mkservice(self, is_interested):
+ service = Mock()
+ service.is_interested = Mock(return_value=is_interested)
+ service.token = "mock_service_token"
+ service.url = "mock_service_url"
+ return service
diff --git a/tests/handlers/test_auth.py b/tests/handlers/test_auth.py
new file mode 100644
index 00000000..978e4d0d
--- /dev/null
+++ b/tests/handlers/test_auth.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pymacaroons
+
+from mock import Mock, NonCallableMock
+from synapse.handlers.auth import AuthHandler
+from tests import unittest
+from tests.utils import setup_test_homeserver
+from twisted.internet import defer
+
+
+class AuthHandlers(object):
+ def __init__(self, hs):
+ self.auth_handler = AuthHandler(hs)
+
+
+class AuthTestCase(unittest.TestCase):
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.hs = yield setup_test_homeserver(handlers=None)
+ self.hs.handlers = AuthHandlers(self.hs)
+
+ def test_token_is_a_macaroon(self):
+ self.hs.config.macaroon_secret_key = "this key is a huge secret"
+
+ token = self.hs.handlers.auth_handler.generate_access_token("some_user")
+ # Check that we can parse the thing with pymacaroons
+ macaroon = pymacaroons.Macaroon.deserialize(token)
+ # The most basic of sanity checks
+ if "some_user" not in macaroon.inspect():
+ self.fail("some_user was not in %s" % macaroon.inspect())
+
+ def test_macaroon_caveats(self):
+ self.hs.config.macaroon_secret_key = "this key is a massive secret"
+ self.hs.clock.now = 5000
+
+ token = self.hs.handlers.auth_handler.generate_access_token("a_user")
+ macaroon = pymacaroons.Macaroon.deserialize(token)
+
+ def verify_gen(caveat):
+ return caveat == "gen = 1"
+
+ def verify_user(caveat):
+ return caveat == "user_id = a_user"
+
+ def verify_type(caveat):
+ return caveat == "type = access"
+
+ def verify_expiry(caveat):
+ return caveat == "time < 8600000"
+
+ v = pymacaroons.Verifier()
+ v.satisfy_general(verify_gen)
+ v.satisfy_general(verify_user)
+ v.satisfy_general(verify_type)
+ v.satisfy_general(verify_expiry)
+ v.verify(macaroon, self.hs.config.macaroon_secret_key)
diff --git a/tests/handlers/test_directory.py b/tests/handlers/test_directory.py
new file mode 100644
index 00000000..27306ba4
--- /dev/null
+++ b/tests/handlers/test_directory.py
@@ -0,0 +1,110 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from mock import Mock
+
+from synapse.handlers.directory import DirectoryHandler
+from synapse.types import RoomAlias
+
+from tests.utils import setup_test_homeserver
+
+
+class DirectoryHandlers(object):
+ def __init__(self, hs):
+ self.directory_handler = DirectoryHandler(hs)
+
+
+class DirectoryTestCase(unittest.TestCase):
+ """ Tests the directory service. """
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_federation = Mock(spec=[
+ "make_query",
+ ])
+
+ self.query_handlers = {}
+
+ def register_query_handler(query_type, handler):
+ self.query_handlers[query_type] = handler
+ self.mock_federation.register_query_handler = register_query_handler
+
+ hs = yield setup_test_homeserver(
+ http_client=None,
+ resource_for_federation=Mock(),
+ replication_layer=self.mock_federation,
+ )
+ hs.handlers = DirectoryHandlers(hs)
+
+ self.handler = hs.get_handlers().directory_handler
+
+ self.store = hs.get_datastore()
+
+ self.my_room = RoomAlias.from_string("#my-room:test")
+ self.your_room = RoomAlias.from_string("#your-room:test")
+ self.remote_room = RoomAlias.from_string("#another:remote")
+
+ @defer.inlineCallbacks
+ def test_get_local_association(self):
+ yield self.store.create_room_alias_association(
+ self.my_room, "!8765qwer:test", ["test"]
+ )
+
+ result = yield self.handler.get_association(self.my_room)
+
+ self.assertEquals({
+ "room_id": "!8765qwer:test",
+ "servers": ["test"],
+ }, result)
+
+ @defer.inlineCallbacks
+ def test_get_remote_association(self):
+ self.mock_federation.make_query.return_value = defer.succeed(
+ {"room_id": "!8765qwer:test", "servers": ["test", "remote"]}
+ )
+
+ result = yield self.handler.get_association(self.remote_room)
+
+ self.assertEquals({
+ "room_id": "!8765qwer:test",
+ "servers": ["test", "remote"],
+ }, result)
+ self.mock_federation.make_query.assert_called_with(
+ destination="remote",
+ query_type="directory",
+ args={
+ "room_alias": "#another:remote",
+ },
+ retry_on_dns_fail=False,
+ )
+
+ @defer.inlineCallbacks
+ def test_incoming_fed_query(self):
+ yield self.store.create_room_alias_association(
+ self.your_room, "!8765asdf:test", ["test"]
+ )
+
+ response = yield self.query_handlers["directory"](
+ {"room_alias": "#your-room:test"}
+ )
+
+ self.assertEquals({
+ "room_id": "!8765asdf:test",
+ "servers": ["test"],
+ }, response)
diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py
new file mode 100644
index 00000000..d392c230
--- /dev/null
+++ b/tests/handlers/test_federation.py
@@ -0,0 +1,130 @@
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from twisted.internet import defer
+from tests import unittest
+
+from synapse.api.constants import EventTypes
+from synapse.events import FrozenEvent
+from synapse.handlers.federation import FederationHandler
+
+from mock import NonCallableMock, ANY, Mock
+
+from ..utils import setup_test_homeserver
+
+
+class FederationTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+
+ self.state_handler = NonCallableMock(spec_set=[
+ "compute_event_context",
+ ])
+
+ self.auth = NonCallableMock(spec_set=[
+ "check",
+ "check_host_in_room",
+ ])
+
+ self.hostname = "test"
+ hs = yield setup_test_homeserver(
+ self.hostname,
+ datastore=NonCallableMock(spec_set=[
+ "persist_event",
+ "store_room",
+ "get_room",
+ "get_destination_retry_timings",
+ "set_destination_retry_timings",
+ "have_events",
+ ]),
+ resource_for_federation=NonCallableMock(),
+ http_client=NonCallableMock(spec_set=[]),
+ notifier=NonCallableMock(spec_set=["on_new_room_event"]),
+ handlers=NonCallableMock(spec_set=[
+ "room_member_handler",
+ "federation_handler",
+ ]),
+ auth=self.auth,
+ state_handler=self.state_handler,
+ keyring=Mock(),
+ )
+
+ self.datastore = hs.get_datastore()
+ self.handlers = hs.get_handlers()
+ self.notifier = hs.get_notifier()
+ self.hs = hs
+
+ self.handlers.federation_handler = FederationHandler(self.hs)
+
+ @defer.inlineCallbacks
+ def test_msg(self):
+ pdu = FrozenEvent({
+ "type": EventTypes.Message,
+ "room_id": "foo",
+ "content": {"msgtype": u"fooo"},
+ "origin_server_ts": 0,
+ "event_id": "$a:b",
+ "user_id":"@a:b",
+ "origin": "b",
+ "auth_events": [],
+ "hashes": {"sha256":"AcLrgtUIqqwaGoHhrEvYG1YLDIsVPYJdSRGhkp3jJp8"},
+ })
+
+ self.datastore.persist_event.return_value = defer.succeed((1,1))
+ self.datastore.get_room.return_value = defer.succeed(True)
+ self.auth.check_host_in_room.return_value = defer.succeed(True)
+
+ retry_timings_res = {
+ "destination": "",
+ "retry_last_ts": 0,
+ "retry_interval": 0,
+ }
+ self.datastore.get_destination_retry_timings.return_value = (
+ defer.succeed(retry_timings_res)
+ )
+
+ def have_events(event_ids):
+ return defer.succeed({})
+ self.datastore.have_events.side_effect = have_events
+
+ def annotate(ev, old_state=None, outlier=False):
+ context = Mock()
+ context.current_state = {}
+ context.auth_events = {}
+ return defer.succeed(context)
+ self.state_handler.compute_event_context.side_effect = annotate
+
+ yield self.handlers.federation_handler.on_receive_pdu(
+ "fo", pdu, False
+ )
+
+ self.datastore.persist_event.assert_called_once_with(
+ ANY,
+ is_new_state=True,
+ backfilled=False,
+ current_state=None,
+ context=ANY,
+ )
+
+ self.state_handler.compute_event_context.assert_called_once_with(
+ ANY, old_state=None, outlier=False
+ )
+
+ self.auth.check.assert_called_once_with(ANY, auth_events={})
+
+ self.notifier.on_new_room_event.assert_called_once_with(
+ ANY, 1, 1, extra_users=[]
+ )
diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py
new file mode 100644
index 00000000..10d4482c
--- /dev/null
+++ b/tests/handlers/test_presence.py
@@ -0,0 +1,1328 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer, reactor
+
+from mock import Mock, call, ANY, NonCallableMock
+import json
+
+from tests.utils import (
+ MockHttpResource, MockClock, DeferredMockCallable, setup_test_homeserver
+)
+
+from synapse.api.constants import PresenceState
+from synapse.api.errors import SynapseError
+from synapse.handlers.presence import PresenceHandler, UserPresenceCache
+from synapse.streams.config import SourcePaginationConfig
+from synapse.storage.transactions import DestinationsTable
+from synapse.types import UserID
+
+OFFLINE = PresenceState.OFFLINE
+UNAVAILABLE = PresenceState.UNAVAILABLE
+ONLINE = PresenceState.ONLINE
+
+
+def _expect_edu(destination, edu_type, content, origin="test"):
+ return {
+ "origin": origin,
+ "origin_server_ts": 1000000,
+ "pdus": [],
+ "edus": [
+ {
+ "edu_type": edu_type,
+ "content": content,
+ }
+ ],
+ "pdu_failures": [],
+ }
+
+def _make_edu_json(origin, edu_type, content):
+ return json.dumps(_expect_edu("test", edu_type, content, origin=origin))
+
+
+class JustPresenceHandlers(object):
+ def __init__(self, hs):
+ self.presence_handler = PresenceHandler(hs)
+
+
+class PresenceTestCase(unittest.TestCase):
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.clock = MockClock()
+
+ self.mock_federation_resource = MockHttpResource()
+
+ self.mock_http_client = Mock(spec=[])
+ self.mock_http_client.put_json = DeferredMockCallable()
+
+ hs_kwargs = {}
+ if hasattr(self, "make_datastore_mock"):
+ hs_kwargs["datastore"] = self.make_datastore_mock()
+
+ hs = yield setup_test_homeserver(
+ clock=self.clock,
+ handlers=None,
+ resource_for_federation=self.mock_federation_resource,
+ http_client=self.mock_http_client,
+ keyring=Mock(),
+ **hs_kwargs
+ )
+ hs.handlers = JustPresenceHandlers(hs)
+
+ self.datastore = hs.get_datastore()
+
+ self.setUp_roommemberhandler_mocks(hs.handlers)
+
+ self.handler = hs.get_handlers().presence_handler
+ self.event_source = hs.get_event_sources().sources["presence"]
+
+ self.distributor = hs.get_distributor()
+ self.distributor.declare("user_joined_room")
+
+ yield self.setUp_users(hs)
+
+ def setUp_roommemberhandler_mocks(self, handlers):
+ self.room_id = "a-room"
+ self.room_members = []
+
+ room_member_handler = handlers.room_member_handler = Mock(spec=[
+ "get_joined_rooms_for_user",
+ "get_room_members",
+ "fetch_room_distributions_into",
+ ])
+ self.room_member_handler = room_member_handler
+
+ def get_rooms_for_user(user):
+ if user in self.room_members:
+ return defer.succeed([self.room_id])
+ else:
+ return defer.succeed([])
+ room_member_handler.get_joined_rooms_for_user = get_rooms_for_user
+
+ def get_room_members(room_id):
+ if room_id == self.room_id:
+ return defer.succeed(self.room_members)
+ else:
+ return defer.succeed([])
+ room_member_handler.get_room_members = get_room_members
+
+ @defer.inlineCallbacks
+ def fetch_room_distributions_into(room_id, localusers=None,
+ remotedomains=None, ignore_user=None):
+
+ members = yield get_room_members(room_id)
+ for member in members:
+ if ignore_user is not None and member == ignore_user:
+ continue
+
+ if member.is_mine:
+ if localusers is not None:
+ localusers.add(member)
+ else:
+ if remotedomains is not None:
+ remotedomains.add(member.domain)
+ room_member_handler.fetch_room_distributions_into = (
+ fetch_room_distributions_into)
+
+ self.setUp_datastore_room_mocks(self.datastore)
+
+ def setUp_datastore_room_mocks(self, datastore):
+ def get_room_hosts(room_id):
+ if room_id == self.room_id:
+ hosts = set([u.domain for u in self.room_members])
+ return defer.succeed(hosts)
+ else:
+ return defer.succeed([])
+ datastore.get_joined_hosts_for_room = get_room_hosts
+
+ def user_rooms_intersect(userlist):
+ room_member_ids = map(lambda u: u.to_string(), self.room_members)
+
+ shared = all(map(lambda i: i in room_member_ids, userlist))
+ return defer.succeed(shared)
+ datastore.user_rooms_intersect = user_rooms_intersect
+
+ @defer.inlineCallbacks
+ def setUp_users(self, hs):
+ # Some local users to test with
+ self.u_apple = UserID.from_string("@apple:test")
+ self.u_banana = UserID.from_string("@banana:test")
+ self.u_clementine = UserID.from_string("@clementine:test")
+
+ for u in self.u_apple, self.u_banana, self.u_clementine:
+ yield self.datastore.create_presence(u.localpart)
+
+ yield self.datastore.set_presence_state(
+ self.u_apple.localpart, {"state": ONLINE, "status_msg": "Online"}
+ )
+
+ # ID of a local user that does not exist
+ self.u_durian = UserID.from_string("@durian:test")
+
+ # A remote user
+ self.u_cabbage = UserID.from_string("@cabbage:elsewhere")
+
+
+class MockedDatastorePresenceTestCase(PresenceTestCase):
+ def make_datastore_mock(self):
+ datastore = Mock(spec=[
+ # Bits that Federation needs
+ "prep_send_transaction",
+ "delivered_txn",
+ "get_received_txn_response",
+ "set_received_txn_response",
+ "get_destination_retry_timings",
+ ])
+
+ self.setUp_datastore_federation_mocks(datastore)
+ self.setUp_datastore_presence_mocks(datastore)
+
+ return datastore
+
+ def setUp_datastore_federation_mocks(self, datastore):
+ retry_timings_res = {
+ "destination": "",
+ "retry_last_ts": 0,
+ "retry_interval": 0,
+ }
+ datastore.get_destination_retry_timings.return_value = (
+ defer.succeed(retry_timings_res)
+ )
+
+ def get_received_txn_response(*args):
+ return defer.succeed(None)
+ datastore.get_received_txn_response = get_received_txn_response
+
+ def setUp_datastore_presence_mocks(self, datastore):
+ self.current_user_state = {
+ "apple": OFFLINE,
+ "banana": OFFLINE,
+ "clementine": OFFLINE,
+ "fig": OFFLINE,
+ }
+
+ def get_presence_state(user_localpart):
+ return defer.succeed(
+ {"state": self.current_user_state[user_localpart],
+ "status_msg": None,
+ "mtime": 123456000}
+ )
+ datastore.get_presence_state = get_presence_state
+
+ def set_presence_state(user_localpart, new_state):
+ was = self.current_user_state[user_localpart]
+ self.current_user_state[user_localpart] = new_state["state"]
+ return defer.succeed({"state": was})
+ datastore.set_presence_state = set_presence_state
+
+ def get_presence_list(user_localpart, accepted):
+ if not user_localpart in self.PRESENCE_LIST:
+ return defer.succeed([])
+ return defer.succeed([
+ {"observed_user_id": u, "accepted": accepted} for u in
+ self.PRESENCE_LIST[user_localpart]])
+ datastore.get_presence_list = get_presence_list
+
+ def is_presence_visible(observed_localpart, observer_userid):
+ return True
+ datastore.is_presence_visible = is_presence_visible
+
+ @defer.inlineCallbacks
+ def setUp_users(self, hs):
+ # Some local users to test with
+ self.u_apple = UserID.from_string("@apple:test")
+ self.u_banana = UserID.from_string("@banana:test")
+ self.u_clementine = UserID.from_string("@clementine:test")
+ self.u_durian = UserID.from_string("@durian:test")
+ self.u_elderberry = UserID.from_string("@elderberry:test")
+ self.u_fig = UserID.from_string("@fig:test")
+
+ # Remote user
+ self.u_onion = UserID.from_string("@onion:farm")
+ self.u_potato = UserID.from_string("@potato:remote")
+
+ yield
+
+
+class PresenceStateTestCase(PresenceTestCase):
+ """ Tests presence management. """
+ @defer.inlineCallbacks
+ def setUp(self):
+ yield super(PresenceStateTestCase, self).setUp()
+
+ self.mock_start = Mock()
+ self.mock_stop = Mock()
+
+ self.handler.start_polling_presence = self.mock_start
+ self.handler.stop_polling_presence = self.mock_stop
+
+ @defer.inlineCallbacks
+ def test_get_my_state(self):
+ state = yield self.handler.get_state(
+ target_user=self.u_apple, auth_user=self.u_apple
+ )
+
+ self.assertEquals(
+ {"presence": ONLINE, "status_msg": "Online"},
+ state
+ )
+
+ @defer.inlineCallbacks
+ def test_get_allowed_state(self):
+ yield self.datastore.allow_presence_visible(
+ observed_localpart=self.u_apple.localpart,
+ observer_userid=self.u_banana.to_string(),
+ )
+
+ state = yield self.handler.get_state(
+ target_user=self.u_apple, auth_user=self.u_banana
+ )
+
+ self.assertEquals(
+ {"presence": ONLINE, "status_msg": "Online"},
+ state
+ )
+
+ @defer.inlineCallbacks
+ def test_get_same_room_state(self):
+ self.room_members = [self.u_apple, self.u_clementine]
+
+ state = yield self.handler.get_state(
+ target_user=self.u_apple, auth_user=self.u_clementine
+ )
+
+ self.assertEquals(
+ {"presence": ONLINE, "status_msg": "Online"},
+ state
+ )
+
+ @defer.inlineCallbacks
+ def test_get_disallowed_state(self):
+ self.room_members = []
+
+ yield self.assertFailure(
+ self.handler.get_state(
+ target_user=self.u_apple, auth_user=self.u_clementine
+ ),
+ SynapseError
+ )
+
+ @defer.inlineCallbacks
+ def test_set_my_state(self):
+ yield self.handler.set_state(
+ target_user=self.u_apple, auth_user=self.u_apple,
+ state={"presence": UNAVAILABLE, "status_msg": "Away"})
+
+ self.assertEquals(
+ {"state": UNAVAILABLE,
+ "status_msg": "Away",
+ "mtime": 1000000},
+ (yield self.datastore.get_presence_state(self.u_apple.localpart))
+ )
+
+ self.mock_start.assert_called_with(self.u_apple,
+ state={
+ "presence": UNAVAILABLE,
+ "status_msg": "Away",
+ "last_active": 1000000, # MockClock
+ })
+
+ yield self.handler.set_state(
+ target_user=self.u_apple, auth_user=self.u_apple,
+ state={"presence": OFFLINE})
+
+ self.mock_stop.assert_called_with(self.u_apple)
+
+
+class PresenceInvitesTestCase(PresenceTestCase):
+ """ Tests presence management. """
+ @defer.inlineCallbacks
+ def setUp(self):
+ yield super(PresenceInvitesTestCase, self).setUp()
+
+ self.mock_start = Mock()
+ self.mock_stop = Mock()
+
+ self.handler.start_polling_presence = self.mock_start
+ self.handler.stop_polling_presence = self.mock_stop
+
+ @defer.inlineCallbacks
+ def test_invite_local(self):
+ # TODO(paul): This test will likely break if/when real auth permissions
+ # are added; for now the HS will always accept any invite
+
+ yield self.handler.send_invite(
+ observer_user=self.u_apple, observed_user=self.u_banana)
+
+ self.assertEquals(
+ [{"observed_user_id": "@banana:test", "accepted": 1}],
+ (yield self.datastore.get_presence_list(self.u_apple.localpart))
+ )
+ self.assertTrue(
+ (yield self.datastore.is_presence_visible(
+ observed_localpart=self.u_banana.localpart,
+ observer_userid=self.u_apple.to_string(),
+ ))
+ )
+
+ self.mock_start.assert_called_with(
+ self.u_apple, target_user=self.u_banana)
+
+ @defer.inlineCallbacks
+ def test_invite_local_nonexistant(self):
+ yield self.handler.send_invite(
+ observer_user=self.u_apple, observed_user=self.u_durian)
+
+ self.assertEquals(
+ [],
+ (yield self.datastore.get_presence_list(self.u_apple.localpart))
+ )
+
+ @defer.inlineCallbacks
+ def test_invite_remote(self):
+ # Use a different destination, otherwise retry logic might fail the
+ # request
+ u_rocket = UserID.from_string("@rocket:there")
+
+ put_json = self.mock_http_client.put_json
+ put_json.expect_call_and_return(
+ call("there",
+ path="/_matrix/federation/v1/send/1000000/",
+ data=_expect_edu("there", "m.presence_invite",
+ content={
+ "observer_user": "@apple:test",
+ "observed_user": "@rocket:there",
+ }
+ ),
+ json_data_callback=ANY,
+ ),
+ defer.succeed((200, "OK"))
+ )
+
+ yield self.handler.send_invite(
+ observer_user=self.u_apple, observed_user=u_rocket)
+
+ self.assertEquals(
+ [{"observed_user_id": "@rocket:there", "accepted": 0}],
+ (yield self.datastore.get_presence_list(self.u_apple.localpart))
+ )
+
+ yield put_json.await_calls()
+
+ @defer.inlineCallbacks
+ def test_accept_remote(self):
+ # TODO(paul): This test will likely break if/when real auth permissions
+ # are added; for now the HS will always accept any invite
+
+ # Use a different destination, otherwise retry logic might fail the
+ # request
+ u_rocket = UserID.from_string("@rocket:moon")
+
+ put_json = self.mock_http_client.put_json
+ put_json.expect_call_and_return(
+ call("moon",
+ path="/_matrix/federation/v1/send/1000000/",
+ data=_expect_edu("moon", "m.presence_accept",
+ content={
+ "observer_user": "@rocket:moon",
+ "observed_user": "@apple:test",
+ }
+ ),
+ json_data_callback=ANY,
+ ),
+ defer.succeed((200, "OK"))
+ )
+
+ yield self.mock_federation_resource.trigger("PUT",
+ "/_matrix/federation/v1/send/1000000/",
+ _make_edu_json("elsewhere", "m.presence_invite",
+ content={
+ "observer_user": "@rocket:moon",
+ "observed_user": "@apple:test",
+ }
+ )
+ )
+
+ self.assertTrue(
+ (yield self.datastore.is_presence_visible(
+ observed_localpart=self.u_apple.localpart,
+ observer_userid=u_rocket.to_string(),
+ ))
+ )
+
+ yield put_json.await_calls()
+
+ @defer.inlineCallbacks
+ def test_invited_remote_nonexistant(self):
+ # Use a different destination, otherwise retry logic might fail the
+ # request
+ u_rocket = UserID.from_string("@rocket:sun")
+
+ put_json = self.mock_http_client.put_json
+ put_json.expect_call_and_return(
+ call("sun",
+ path="/_matrix/federation/v1/send/1000000/",
+ data=_expect_edu("sun", "m.presence_deny",
+ content={
+ "observer_user": "@rocket:sun",
+ "observed_user": "@durian:test",
+ }
+ ),
+ json_data_callback=ANY,
+ ),
+ defer.succeed((200, "OK"))
+ )
+
+ yield self.mock_federation_resource.trigger("PUT",
+ "/_matrix/federation/v1/send/1000000/",
+ _make_edu_json("sun", "m.presence_invite",
+ content={
+ "observer_user": "@rocket:sun",
+ "observed_user": "@durian:test",
+ }
+ )
+ )
+
+ yield put_json.await_calls()
+
+ @defer.inlineCallbacks
+ def test_accepted_remote(self):
+ yield self.datastore.add_presence_list_pending(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_cabbage.to_string(),
+ )
+
+ yield self.mock_federation_resource.trigger("PUT",
+ "/_matrix/federation/v1/send/1000000/",
+ _make_edu_json("elsewhere", "m.presence_accept",
+ content={
+ "observer_user": "@apple:test",
+ "observed_user": "@cabbage:elsewhere",
+ }
+ )
+ )
+
+ self.assertEquals(
+ [{"observed_user_id": "@cabbage:elsewhere", "accepted": 1}],
+ (yield self.datastore.get_presence_list(self.u_apple.localpart))
+ )
+
+ self.mock_start.assert_called_with(
+ self.u_apple, target_user=self.u_cabbage)
+
+ @defer.inlineCallbacks
+ def test_denied_remote(self):
+ yield self.datastore.add_presence_list_pending(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid="@eggplant:elsewhere",
+ )
+
+ yield self.mock_federation_resource.trigger("PUT",
+ "/_matrix/federation/v1/send/1000000/",
+ _make_edu_json("elsewhere", "m.presence_deny",
+ content={
+ "observer_user": "@apple:test",
+ "observed_user": "@eggplant:elsewhere",
+ }
+ )
+ )
+
+ self.assertEquals(
+ [],
+ (yield self.datastore.get_presence_list(self.u_apple.localpart))
+ )
+
+ @defer.inlineCallbacks
+ def test_drop_local(self):
+ yield self.datastore.add_presence_list_pending(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_banana.to_string(),
+ )
+ yield self.datastore.set_presence_list_accepted(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_banana.to_string(),
+ )
+
+ yield self.handler.drop(
+ observer_user=self.u_apple,
+ observed_user=self.u_banana,
+ )
+
+ self.assertEquals(
+ [],
+ (yield self.datastore.get_presence_list(self.u_apple.localpart))
+ )
+
+ self.mock_stop.assert_called_with(
+ self.u_apple, target_user=self.u_banana)
+
+ @defer.inlineCallbacks
+ def test_drop_remote(self):
+ yield self.datastore.add_presence_list_pending(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_cabbage.to_string(),
+ )
+ yield self.datastore.set_presence_list_accepted(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_cabbage.to_string(),
+ )
+
+ yield self.handler.drop(
+ observer_user=self.u_apple,
+ observed_user=self.u_cabbage,
+ )
+
+ self.assertEquals(
+ [],
+ (yield self.datastore.get_presence_list(self.u_apple.localpart))
+ )
+
+ @defer.inlineCallbacks
+ def test_get_presence_list(self):
+ yield self.datastore.add_presence_list_pending(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_banana.to_string(),
+ )
+ yield self.datastore.set_presence_list_accepted(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_banana.to_string(),
+ )
+
+ presence = yield self.handler.get_presence_list(
+ observer_user=self.u_apple)
+
+ self.assertEquals([
+ {"observed_user": self.u_banana,
+ "presence": OFFLINE,
+ "accepted": 1},
+ ], presence)
+
+
+class PresencePushTestCase(MockedDatastorePresenceTestCase):
+ """ Tests steady-state presence status updates.
+
+ They assert that presence state update messages are pushed around the place
+ when users change state, presuming that the watches are all established.
+
+ These tests are MASSIVELY fragile currently as they poke internals of the
+ presence handler; namely the _local_pushmap and _remote_recvmap.
+ BE WARNED...
+ """
+ PRESENCE_LIST = {
+ 'apple': [ "@banana:test", "@clementine:test" ],
+ 'banana': [ "@apple:test" ],
+ }
+
+ @defer.inlineCallbacks
+ def test_push_local(self):
+ self.room_members = [self.u_apple, self.u_elderberry]
+
+ self.datastore.set_presence_state.return_value = defer.succeed(
+ {"state": ONLINE}
+ )
+
+ # TODO(paul): Gut-wrenching
+ self.handler._user_cachemap[self.u_apple] = UserPresenceCache()
+ self.handler._user_cachemap[self.u_apple].update(
+ {"presence": OFFLINE}, serial=0
+ )
+ apple_set = self.handler._local_pushmap.setdefault("apple", set())
+ apple_set.add(self.u_banana)
+ apple_set.add(self.u_clementine)
+
+ self.assertEquals(self.event_source.get_current_key(), 0)
+
+ yield self.handler.set_state(self.u_apple, self.u_apple,
+ {"presence": ONLINE}
+ )
+
+ # Apple sees self-reflection even without room_id
+ (events, _) = yield self.event_source.get_new_events(
+ user=self.u_apple,
+ from_key=0,
+ )
+
+ self.assertEquals(self.event_source.get_current_key(), 1)
+ self.assertEquals(events,
+ [
+ {"type": "m.presence",
+ "content": {
+ "user_id": "@apple:test",
+ "presence": ONLINE,
+ "last_active_ago": 0,
+ }},
+ ],
+ msg="Presence event should be visible to self-reflection"
+ )
+
+ # Apple sees self-reflection
+ (events, _) = yield self.event_source.get_new_events(
+ user=self.u_apple,
+ from_key=0,
+ room_ids=[self.room_id],
+ )
+
+ self.assertEquals(self.event_source.get_current_key(), 1)
+ self.assertEquals(events,
+ [
+ {"type": "m.presence",
+ "content": {
+ "user_id": "@apple:test",
+ "presence": ONLINE,
+ "last_active_ago": 0,
+ }},
+ ],
+ msg="Presence event should be visible to self-reflection"
+ )
+
+ config = SourcePaginationConfig(from_key=1, to_key=0)
+ (chunk, _) = yield self.event_source.get_pagination_rows(
+ self.u_apple, config, None
+ )
+ self.assertEquals(chunk,
+ [
+ {"type": "m.presence",
+ "content": {
+ "user_id": "@apple:test",
+ "presence": ONLINE,
+ "last_active_ago": 0,
+ }},
+ ]
+ )
+
+ # Banana sees it because of presence subscription
+ (events, _) = yield self.event_source.get_new_events(
+ user=self.u_banana,
+ from_key=0,
+ room_ids=[self.room_id],
+ )
+
+ self.assertEquals(self.event_source.get_current_key(), 1)
+ self.assertEquals(events,
+ [
+ {"type": "m.presence",
+ "content": {
+ "user_id": "@apple:test",
+ "presence": ONLINE,
+ "last_active_ago": 0,
+ }},
+ ],
+ msg="Presence event should be visible to explicit subscribers"
+ )
+
+ # Elderberry sees it because of same room
+ (events, _) = yield self.event_source.get_new_events(
+ user=self.u_elderberry,
+ from_key=0,
+ room_ids=[self.room_id],
+ )
+
+ self.assertEquals(self.event_source.get_current_key(), 1)
+ self.assertEquals(events,
+ [
+ {"type": "m.presence",
+ "content": {
+ "user_id": "@apple:test",
+ "presence": ONLINE,
+ "last_active_ago": 0,
+ }},
+ ],
+ msg="Presence event should be visible to other room members"
+ )
+
+ # Durian is not in the room, should not see this event
+ (events, _) = yield self.event_source.get_new_events(
+ user=self.u_durian,
+ from_key=0,
+ room_ids=[],
+ )
+
+ self.assertEquals(self.event_source.get_current_key(), 1)
+ self.assertEquals(events, [],
+ msg="Presence event should not be visible to others"
+ )
+
+ presence = yield self.handler.get_presence_list(
+ observer_user=self.u_apple, accepted=True)
+
+ self.assertEquals(
+ [
+ {"observed_user": self.u_banana,
+ "presence": OFFLINE,
+ "accepted": True},
+ {"observed_user": self.u_clementine,
+ "presence": OFFLINE,
+ "accepted": True},
+ ],
+ presence
+ )
+
+ # TODO(paul): Gut-wrenching
+ banana_set = self.handler._local_pushmap.setdefault("banana", set())
+ banana_set.add(self.u_apple)
+
+ yield self.handler.set_state(self.u_banana, self.u_banana,
+ {"presence": ONLINE}
+ )
+
+ self.clock.advance_time(2)
+
+ presence = yield self.handler.get_presence_list(
+ observer_user=self.u_apple, accepted=True)
+
+ self.assertEquals([
+ {"observed_user": self.u_banana,
+ "presence": ONLINE,
+ "last_active_ago": 2000,
+ "accepted": True},
+ {"observed_user": self.u_clementine,
+ "presence": OFFLINE,
+ "accepted": True},
+ ], presence)
+
+ (events, _) = yield self.event_source.get_new_events(
+ user=self.u_apple,
+ from_key=1,
+ )
+
+ self.assertEquals(self.event_source.get_current_key(), 2)
+ self.assertEquals(events,
+ [
+ {"type": "m.presence",
+ "content": {
+ "user_id": "@banana:test",
+ "presence": ONLINE,
+ "last_active_ago": 2000
+ }},
+ ]
+ )
+
+ @defer.inlineCallbacks
+ def test_push_remote(self):
+ put_json = self.mock_http_client.put_json
+ put_json.expect_call_and_return(
+ call("farm",
+ path=ANY, # Can't guarantee which txn ID will be which
+ data=_expect_edu("farm", "m.presence",
+ content={
+ "push": [
+ {"user_id": "@apple:test",
+ "presence": u"online",
+ "last_active_ago": 0},
+ ],
+ }
+ ),
+ json_data_callback=ANY,
+ ),
+ defer.succeed((200, "OK"))
+ )
+ put_json.expect_call_and_return(
+ call("remote",
+ path=ANY, # Can't guarantee which txn ID will be which
+ data=_expect_edu("remote", "m.presence",
+ content={
+ "push": [
+ {"user_id": "@apple:test",
+ "presence": u"online",
+ "last_active_ago": 0},
+ ],
+ }
+ ),
+ json_data_callback=ANY,
+ ),
+ defer.succeed((200, "OK"))
+ )
+
+ self.room_members = [self.u_apple, self.u_onion]
+
+ self.datastore.set_presence_state.return_value = defer.succeed(
+ {"state": ONLINE}
+ )
+
+ # TODO(paul): Gut-wrenching
+ self.handler._user_cachemap[self.u_apple] = UserPresenceCache()
+ self.handler._user_cachemap[self.u_apple].update(
+ {"presence": OFFLINE}, serial=0
+ )
+ apple_set = self.handler._remote_sendmap.setdefault("apple", set())
+ apple_set.add(self.u_potato.domain)
+
+ yield self.handler.set_state(self.u_apple, self.u_apple,
+ {"presence": ONLINE}
+ )
+
+ yield put_json.await_calls()
+
+ @defer.inlineCallbacks
+ def test_recv_remote(self):
+ self.room_members = [self.u_apple, self.u_banana, self.u_potato]
+
+ self.assertEquals(self.event_source.get_current_key(), 0)
+
+ yield self.mock_federation_resource.trigger("PUT",
+ "/_matrix/federation/v1/send/1000000/",
+ _make_edu_json("elsewhere", "m.presence",
+ content={
+ "push": [
+ {"user_id": "@potato:remote",
+ "presence": "online",
+ "last_active_ago": 1000},
+ ],
+ }
+ )
+ )
+
+ (events, _) = yield self.event_source.get_new_events(
+ user=self.u_apple,
+ from_key=0,
+ room_ids=[self.room_id],
+ )
+
+ self.assertEquals(self.event_source.get_current_key(), 1)
+ self.assertEquals(events,
+ [
+ {"type": "m.presence",
+ "content": {
+ "user_id": "@potato:remote",
+ "presence": ONLINE,
+ "last_active_ago": 1000,
+ }}
+ ]
+ )
+
+ self.clock.advance_time(2)
+
+ state = yield self.handler.get_state(self.u_potato, self.u_apple)
+
+ self.assertEquals(
+ {"presence": ONLINE, "last_active_ago": 3000},
+ state
+ )
+
+ @defer.inlineCallbacks
+ def test_recv_remote_offline(self):
+ """ Various tests relating to SYN-261 """
+
+ self.room_members = [self.u_apple, self.u_banana, self.u_potato]
+
+ self.assertEquals(self.event_source.get_current_key(), 0)
+
+ yield self.mock_federation_resource.trigger("PUT",
+ "/_matrix/federation/v1/send/1000000/",
+ _make_edu_json("elsewhere", "m.presence",
+ content={
+ "push": [
+ {"user_id": "@potato:remote",
+ "presence": "offline"},
+ ],
+ }
+ )
+ )
+
+ self.assertEquals(self.event_source.get_current_key(), 1)
+
+ (events, _) = yield self.event_source.get_new_events(
+ user=self.u_apple,
+ from_key=0,
+ room_ids=[self.room_id,]
+ )
+ self.assertEquals(events,
+ [
+ {"type": "m.presence",
+ "content": {
+ "user_id": "@potato:remote",
+ "presence": OFFLINE,
+ }}
+ ]
+ )
+
+ yield self.mock_federation_resource.trigger("PUT",
+ "/_matrix/federation/v1/send/1000001/",
+ _make_edu_json("elsewhere", "m.presence",
+ content={
+ "push": [
+ {"user_id": "@potato:remote",
+ "presence": "online"},
+ ],
+ }
+ )
+ )
+
+ self.assertEquals(self.event_source.get_current_key(), 2)
+
+ (events, _) = yield self.event_source.get_new_events(
+ user=self.u_apple,
+ from_key=0,
+ room_ids=[self.room_id,]
+ )
+ self.assertEquals(events,
+ [
+ {"type": "m.presence",
+ "content": {
+ "user_id": "@potato:remote",
+ "presence": ONLINE,
+ }}
+ ]
+ )
+
+ @defer.inlineCallbacks
+ def test_join_room_local(self):
+ self.room_members = [self.u_apple, self.u_banana]
+
+ self.assertEquals(self.event_source.get_current_key(), 0)
+
+ # TODO(paul): Gut-wrenching
+ self.handler._user_cachemap[self.u_clementine] = UserPresenceCache()
+ self.handler._user_cachemap[self.u_clementine].update(
+ {
+ "presence": PresenceState.ONLINE,
+ "last_active": self.clock.time_msec(),
+ }, self.u_clementine
+ )
+
+ yield self.distributor.fire("user_joined_room", self.u_clementine,
+ self.room_id
+ )
+
+ self.room_members.append(self.u_clementine)
+
+ (events, _) = yield self.event_source.get_new_events(
+ user=self.u_apple,
+ from_key=0,
+ )
+
+ self.assertEquals(self.event_source.get_current_key(), 1)
+ self.assertEquals(events,
+ [
+ {"type": "m.presence",
+ "content": {
+ "user_id": "@clementine:test",
+ "presence": ONLINE,
+ "last_active_ago": 0,
+ }}
+ ]
+ )
+
+ @defer.inlineCallbacks
+ def test_join_room_remote(self):
+ ## Sending local user state to a newly-joined remote user
+ put_json = self.mock_http_client.put_json
+ put_json.expect_call_and_return(
+ call("remote",
+ path=ANY, # Can't guarantee which txn ID will be which
+ data=_expect_edu("remote", "m.presence",
+ content={
+ "push": [
+ {"user_id": "@apple:test",
+ "presence": "online"},
+ ],
+ }
+ ),
+ json_data_callback=ANY,
+ ),
+ defer.succeed((200, "OK"))
+ )
+ put_json.expect_call_and_return(
+ call("remote",
+ path=ANY, # Can't guarantee which txn ID will be which
+ data=_expect_edu("remote", "m.presence",
+ content={
+ "push": [
+ {"user_id": "@banana:test",
+ "presence": "offline"},
+ ],
+ }
+ ),
+ json_data_callback=ANY,
+ ),
+ defer.succeed((200, "OK"))
+ )
+
+ # TODO(paul): Gut-wrenching
+ self.handler._user_cachemap[self.u_apple] = UserPresenceCache()
+ self.handler._user_cachemap[self.u_apple].update(
+ {"presence": PresenceState.ONLINE}, self.u_apple)
+ self.room_members = [self.u_apple, self.u_banana]
+
+ yield self.distributor.fire("user_joined_room", self.u_potato,
+ self.room_id
+ )
+
+ yield put_json.await_calls()
+
+ ## Sending newly-joined local user state to remote users
+
+ put_json.expect_call_and_return(
+ call("remote",
+ path="/_matrix/federation/v1/send/1000002/",
+ data=_expect_edu("remote", "m.presence",
+ content={
+ "push": [
+ {"user_id": "@clementine:test",
+ "presence": "online"},
+ ],
+ }
+ ),
+ json_data_callback=ANY,
+ ),
+ defer.succeed((200, "OK"))
+ )
+
+ self.handler._user_cachemap[self.u_clementine] = UserPresenceCache()
+ self.handler._user_cachemap[self.u_clementine].update(
+ {"presence": ONLINE}, self.u_clementine)
+ self.room_members.append(self.u_potato)
+
+ yield self.distributor.fire("user_joined_room", self.u_clementine,
+ self.room_id
+ )
+
+ put_json.await_calls()
+
+
+class PresencePollingTestCase(MockedDatastorePresenceTestCase):
+ """ Tests presence status polling. """
+
+ # For this test, we have three local users; apple is watching and is
+ # watched by the other two, but the others don't watch each other.
+ # Additionally clementine is watching a remote user.
+ PRESENCE_LIST = {
+ 'apple': [ "@banana:test", "@clementine:test" ],
+ 'banana': [ "@apple:test" ],
+ 'clementine': [ "@apple:test", "@potato:remote" ],
+ 'fig': [ "@potato:remote" ],
+ }
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ yield super(PresencePollingTestCase, self).setUp()
+
+ self.mock_update_client = Mock()
+
+ def update(*args,**kwargs):
+ return defer.succeed(None)
+ self.mock_update_client.side_effect = update
+
+ self.handler.push_update_to_clients = self.mock_update_client
+
+ @defer.inlineCallbacks
+ def test_push_local(self):
+ # apple goes online
+ yield self.handler.set_state(
+ target_user=self.u_apple, auth_user=self.u_apple,
+ state={"presence": ONLINE}
+ )
+
+ # apple should see both banana and clementine currently offline
+ self.mock_update_client.assert_has_calls([
+ call(users_to_push=[self.u_apple]),
+ call(users_to_push=[self.u_apple]),
+ ], any_order=True)
+
+ # Gut-wrenching tests
+ self.assertTrue("banana" in self.handler._local_pushmap)
+ self.assertTrue(self.u_apple in self.handler._local_pushmap["banana"])
+ self.assertTrue("clementine" in self.handler._local_pushmap)
+ self.assertTrue(self.u_apple in self.handler._local_pushmap["clementine"])
+
+ self.mock_update_client.reset_mock()
+
+ # banana goes online
+ yield self.handler.set_state(
+ target_user=self.u_banana, auth_user=self.u_banana,
+ state={"presence": ONLINE}
+ )
+
+ # apple and banana should now both see each other online
+ self.mock_update_client.assert_has_calls([
+ call(users_to_push=set([self.u_apple]), room_ids=[]),
+ call(users_to_push=[self.u_banana]),
+ ], any_order=True)
+
+ self.assertTrue("apple" in self.handler._local_pushmap)
+ self.assertTrue(self.u_banana in self.handler._local_pushmap["apple"])
+
+ self.mock_update_client.reset_mock()
+
+ # apple goes offline
+ yield self.handler.set_state(
+ target_user=self.u_apple, auth_user=self.u_apple,
+ state={"presence": OFFLINE}
+ )
+
+ # banana should now be told apple is offline
+ self.mock_update_client.assert_has_calls([
+ call(users_to_push=set([self.u_banana, self.u_apple]), room_ids=[]),
+ ], any_order=True)
+
+ self.assertFalse("banana" in self.handler._local_pushmap)
+ self.assertFalse("clementine" in self.handler._local_pushmap)
+
+ @defer.inlineCallbacks
+ def test_remote_poll_send(self):
+ put_json = self.mock_http_client.put_json
+ put_json.expect_call_and_return(
+ call("remote",
+ path=ANY,
+ data=_expect_edu("remote", "m.presence",
+ content={
+ "poll": [ "@potato:remote" ],
+ },
+ ),
+ json_data_callback=ANY,
+ ),
+ defer.succeed((200, "OK"))
+ )
+
+ put_json.expect_call_and_return(
+ call("remote",
+ path=ANY,
+ data=_expect_edu("remote", "m.presence",
+ content={
+ "push": [ {
+ "user_id": "@clementine:test",
+ "presence": OFFLINE,
+ }],
+ },
+ ),
+ json_data_callback=ANY,
+ ),
+ defer.succeed((200, "OK"))
+ )
+
+ # clementine goes online
+ yield self.handler.set_state(
+ target_user=self.u_clementine, auth_user=self.u_clementine,
+ state={"presence": ONLINE}
+ )
+
+ yield put_json.await_calls()
+
+ # Gut-wrenching tests
+ self.assertTrue(self.u_potato in self.handler._remote_recvmap,
+ msg="expected potato to be in _remote_recvmap"
+ )
+ self.assertTrue(self.u_clementine in
+ self.handler._remote_recvmap[self.u_potato])
+
+
+ put_json.expect_call_and_return(
+ call("remote",
+ path=ANY,
+ data=_expect_edu("remote", "m.presence",
+ content={
+ "push": [ {
+ "user_id": "@fig:test",
+ "presence": OFFLINE,
+ }],
+ },
+ ),
+ json_data_callback=ANY,
+ ),
+ defer.succeed((200, "OK"))
+ )
+
+ # fig goes online; shouldn't send a second poll
+ yield self.handler.set_state(
+ target_user=self.u_fig, auth_user=self.u_fig,
+ state={"presence": ONLINE}
+ )
+
+ # reactor.iterate(delay=0)
+
+ yield put_json.await_calls()
+
+ # fig goes offline
+ yield self.handler.set_state(
+ target_user=self.u_fig, auth_user=self.u_fig,
+ state={"presence": OFFLINE}
+ )
+
+ reactor.iterate(delay=0)
+
+ put_json.assert_had_no_calls()
+
+ put_json.expect_call_and_return(
+ call("remote",
+ path=ANY,
+ data=_expect_edu("remote", "m.presence",
+ content={
+ "unpoll": [ "@potato:remote" ],
+ },
+ ),
+ json_data_callback=ANY,
+ ),
+ defer.succeed((200, "OK"))
+ )
+
+ # clementine goes offline
+ yield self.handler.set_state(
+ target_user=self.u_clementine, auth_user=self.u_clementine,
+ state={"presence": OFFLINE}
+ )
+
+ yield put_json.await_calls()
+
+ self.assertFalse(self.u_potato in self.handler._remote_recvmap,
+ msg="expected potato not to be in _remote_recvmap"
+ )
+
+ @defer.inlineCallbacks
+ def test_remote_poll_receive(self):
+ put_json = self.mock_http_client.put_json
+ put_json.expect_call_and_return(
+ call("remote",
+ path="/_matrix/federation/v1/send/1000000/",
+ data=_expect_edu("remote", "m.presence",
+ content={
+ "push": [
+ {"user_id": "@banana:test",
+ "presence": "offline",
+ "status_msg": None},
+ ],
+ },
+ ),
+ json_data_callback=ANY,
+ ),
+ defer.succeed((200, "OK"))
+ )
+
+ yield self.mock_federation_resource.trigger("PUT",
+ "/_matrix/federation/v1/send/1000000/",
+ _make_edu_json("remote", "m.presence",
+ content={
+ "poll": [ "@banana:test" ],
+ },
+ )
+ )
+
+ yield put_json.await_calls()
+
+ # Gut-wrenching tests
+ self.assertTrue(self.u_banana in self.handler._remote_sendmap)
+
+ yield self.mock_federation_resource.trigger("PUT",
+ "/_matrix/federation/v1/send/1000001/",
+ _make_edu_json("remote", "m.presence",
+ content={
+ "unpoll": [ "@banana:test" ],
+ }
+ )
+ )
+
+ # Gut-wrenching tests
+ self.assertFalse(self.u_banana in self.handler._remote_sendmap)
diff --git a/tests/handlers/test_presencelike.py b/tests/handlers/test_presencelike.py
new file mode 100644
index 00000000..19107cae
--- /dev/null
+++ b/tests/handlers/test_presencelike.py
@@ -0,0 +1,311 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This file contains tests of the "presence-like" data that is shared between
+presence and profiles; namely, the displayname and avatar_url."""
+
+from tests import unittest
+from twisted.internet import defer
+
+from mock import Mock, call, ANY, NonCallableMock
+
+from ..utils import MockClock, setup_test_homeserver
+
+from synapse.api.constants import PresenceState
+from synapse.handlers.presence import PresenceHandler
+from synapse.handlers.profile import ProfileHandler
+from synapse.types import UserID
+
+
+OFFLINE = PresenceState.OFFLINE
+UNAVAILABLE = PresenceState.UNAVAILABLE
+ONLINE = PresenceState.ONLINE
+
+
+class MockReplication(object):
+ def __init__(self):
+ self.edu_handlers = {}
+
+ def register_edu_handler(self, edu_type, handler):
+ self.edu_handlers[edu_type] = handler
+
+ def register_query_handler(self, query_type, handler):
+ pass
+
+ def received_edu(self, origin, edu_type, content):
+ self.edu_handlers[edu_type](origin, content)
+
+
+class PresenceAndProfileHandlers(object):
+ def __init__(self, hs):
+ self.presence_handler = PresenceHandler(hs)
+ self.profile_handler = ProfileHandler(hs)
+
+
+class PresenceProfilelikeDataTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ hs = yield setup_test_homeserver(
+ clock=MockClock(),
+ datastore=Mock(spec=[
+ "set_presence_state",
+ "is_presence_visible",
+ "set_profile_displayname",
+ "get_rooms_for_user",
+ ]),
+ handlers=None,
+ resource_for_federation=Mock(),
+ http_client=None,
+ replication_layer=MockReplication(),
+ ratelimiter=NonCallableMock(spec_set=[
+ "send_message",
+ ]),
+ )
+ self.ratelimiter = hs.get_ratelimiter()
+ self.ratelimiter.send_message.return_value = (True, 0)
+ hs.handlers = PresenceAndProfileHandlers(hs)
+
+ self.datastore = hs.get_datastore()
+
+ self.replication = hs.get_replication_layer()
+ self.replication.send_edu = Mock()
+
+ def send_edu(*args, **kwargs):
+ # print "send_edu: %s, %s" % (args, kwargs)
+ return defer.succeed((200, "OK"))
+ self.replication.send_edu.side_effect = send_edu
+
+ def get_profile_displayname(user_localpart):
+ return defer.succeed("Frank")
+ self.datastore.get_profile_displayname = get_profile_displayname
+
+ def is_presence_visible(*args, **kwargs):
+ return defer.succeed(False)
+ self.datastore.is_presence_visible = is_presence_visible
+
+ def get_profile_avatar_url(user_localpart):
+ return defer.succeed("http://foo")
+ self.datastore.get_profile_avatar_url = get_profile_avatar_url
+
+ self.presence_list = [
+ {"observed_user_id": "@banana:test", "accepted": True},
+ {"observed_user_id": "@clementine:test", "accepted": True},
+ ]
+ def get_presence_list(user_localpart, accepted=None):
+ return defer.succeed(self.presence_list)
+ self.datastore.get_presence_list = get_presence_list
+
+ def user_rooms_intersect(userlist):
+ return defer.succeed(False)
+ self.datastore.user_rooms_intersect = user_rooms_intersect
+
+ self.handlers = hs.get_handlers()
+
+ self.mock_update_client = Mock()
+ def update(*args, **kwargs):
+ # print "mock_update_client: %s, %s" %(args, kwargs)
+ return defer.succeed(None)
+ self.mock_update_client.side_effect = update
+
+ self.handlers.presence_handler.push_update_to_clients = (
+ self.mock_update_client)
+
+ hs.handlers.room_member_handler = Mock(spec=[
+ "get_joined_rooms_for_user",
+ ])
+ hs.handlers.room_member_handler.get_joined_rooms_for_user = (
+ lambda u: defer.succeed([]))
+
+ # Some local users to test with
+ self.u_apple = UserID.from_string("@apple:test")
+ self.u_banana = UserID.from_string("@banana:test")
+ self.u_clementine = UserID.from_string("@clementine:test")
+
+ # Remote user
+ self.u_potato = UserID.from_string("@potato:remote")
+
+ self.mock_get_joined = (
+ self.datastore.get_rooms_for_user
+ )
+
+ @defer.inlineCallbacks
+ def test_set_my_state(self):
+ self.presence_list = [
+ {"observed_user_id": "@banana:test", "accepted": True},
+ {"observed_user_id": "@clementine:test", "accepted": True},
+ ]
+
+ mocked_set = self.datastore.set_presence_state
+ mocked_set.return_value = defer.succeed({"state": OFFLINE})
+
+ yield self.handlers.presence_handler.set_state(
+ target_user=self.u_apple, auth_user=self.u_apple,
+ state={"presence": UNAVAILABLE, "status_msg": "Away"})
+
+ mocked_set.assert_called_with("apple",
+ {"state": UNAVAILABLE, "status_msg": "Away"}
+ )
+
+ @defer.inlineCallbacks
+ def test_push_local(self):
+ def get_joined(*args):
+ return defer.succeed([])
+
+ self.mock_get_joined.side_effect = get_joined
+
+ self.presence_list = [
+ {"observed_user_id": "@banana:test", "accepted": True},
+ {"observed_user_id": "@clementine:test", "accepted": True},
+ ]
+
+ self.datastore.set_presence_state.return_value = defer.succeed(
+ {"state": ONLINE}
+ )
+
+ # TODO(paul): Gut-wrenching
+ from synapse.handlers.presence import UserPresenceCache
+ self.handlers.presence_handler._user_cachemap[self.u_apple] = (
+ UserPresenceCache()
+ )
+ self.handlers.presence_handler._user_cachemap[self.u_apple].update(
+ {"presence": OFFLINE}, serial=0
+ )
+ apple_set = self.handlers.presence_handler._local_pushmap.setdefault(
+ "apple", set())
+ apple_set.add(self.u_banana)
+ apple_set.add(self.u_clementine)
+
+ yield self.handlers.presence_handler.set_state(self.u_apple,
+ self.u_apple, {"presence": ONLINE}
+ )
+ yield self.handlers.presence_handler.set_state(self.u_banana,
+ self.u_banana, {"presence": ONLINE}
+ )
+
+ presence = yield self.handlers.presence_handler.get_presence_list(
+ observer_user=self.u_apple, accepted=True)
+
+ self.assertEquals([
+ {"observed_user": self.u_banana,
+ "presence": ONLINE,
+ "last_active_ago": 0,
+ "displayname": "Frank",
+ "avatar_url": "http://foo",
+ "accepted": True},
+ {"observed_user": self.u_clementine,
+ "presence": OFFLINE,
+ "accepted": True}
+ ], presence)
+
+ self.mock_update_client.assert_has_calls([
+ call(
+ users_to_push={self.u_apple, self.u_banana, self.u_clementine},
+ room_ids=[]
+ ),
+ ], any_order=True)
+
+ self.mock_update_client.reset_mock()
+
+ self.datastore.set_profile_displayname.return_value = defer.succeed(
+ None)
+
+ yield self.handlers.profile_handler.set_displayname(self.u_apple,
+ self.u_apple, "I am an Apple")
+
+ self.mock_update_client.assert_has_calls([
+ call(
+ users_to_push={self.u_apple, self.u_banana, self.u_clementine},
+ room_ids=[],
+ ),
+ ], any_order=True)
+
+ @defer.inlineCallbacks
+ def test_push_remote(self):
+ self.presence_list = [
+ {"observed_user_id": "@potato:remote", "accepted": True},
+ ]
+
+ self.datastore.set_presence_state.return_value = defer.succeed(
+ {"state": ONLINE}
+ )
+
+ # TODO(paul): Gut-wrenching
+ from synapse.handlers.presence import UserPresenceCache
+ self.handlers.presence_handler._user_cachemap[self.u_apple] = (
+ UserPresenceCache()
+ )
+ self.handlers.presence_handler._user_cachemap[self.u_apple].update(
+ {"presence": OFFLINE}, serial=0
+ )
+ apple_set = self.handlers.presence_handler._remote_sendmap.setdefault(
+ "apple", set())
+ apple_set.add(self.u_potato.domain)
+
+ yield self.handlers.presence_handler.set_state(self.u_apple,
+ self.u_apple, {"presence": ONLINE}
+ )
+
+ self.replication.send_edu.assert_called_with(
+ destination="remote",
+ edu_type="m.presence",
+ content={
+ "push": [
+ {"user_id": "@apple:test",
+ "presence": "online",
+ "last_active_ago": 0,
+ "displayname": "Frank",
+ "avatar_url": "http://foo"},
+ ],
+ },
+ )
+
+ @defer.inlineCallbacks
+ def test_recv_remote(self):
+ self.presence_list = [
+ {"observed_user_id": "@banana:test"},
+ {"observed_user_id": "@clementine:test"},
+ ]
+
+ # TODO(paul): Gut-wrenching
+ potato_set = self.handlers.presence_handler._remote_recvmap.setdefault(
+ self.u_potato, set()
+ )
+ potato_set.add(self.u_apple)
+
+ yield self.replication.received_edu(
+ "remote", "m.presence", {
+ "push": [
+ {"user_id": "@potato:remote",
+ "presence": "online",
+ "displayname": "Frank",
+ "avatar_url": "http://foo"},
+ ],
+ }
+ )
+
+ self.mock_update_client.assert_called_with(
+ users_to_push=set([self.u_apple]),
+ room_ids=[],
+ )
+
+ state = yield self.handlers.presence_handler.get_state(self.u_potato,
+ self.u_apple)
+
+ self.assertEquals(
+ {"presence": ONLINE,
+ "displayname": "Frank",
+ "avatar_url": "http://foo"},
+ state)
diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py
new file mode 100644
index 00000000..31f03d73
--- /dev/null
+++ b/tests/handlers/test_profile.py
@@ -0,0 +1,145 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from mock import Mock, NonCallableMock
+
+from synapse.api.errors import AuthError
+from synapse.handlers.profile import ProfileHandler
+from synapse.types import UserID
+
+from tests.utils import setup_test_homeserver
+
+
+class ProfileHandlers(object):
+ def __init__(self, hs):
+ self.profile_handler = ProfileHandler(hs)
+
+
+class ProfileTestCase(unittest.TestCase):
+ """ Tests profile management. """
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_federation = Mock(spec=[
+ "make_query",
+ ])
+
+ self.query_handlers = {}
+ def register_query_handler(query_type, handler):
+ self.query_handlers[query_type] = handler
+ self.mock_federation.register_query_handler = register_query_handler
+
+ hs = yield setup_test_homeserver(
+ http_client=None,
+ handlers=None,
+ resource_for_federation=Mock(),
+ replication_layer=self.mock_federation,
+ ratelimiter=NonCallableMock(spec_set=[
+ "send_message",
+ ])
+ )
+
+ self.ratelimiter = hs.get_ratelimiter()
+ self.ratelimiter.send_message.return_value = (True, 0)
+
+ hs.handlers = ProfileHandlers(hs)
+
+ self.store = hs.get_datastore()
+
+ self.frank = UserID.from_string("@1234ABCD:test")
+ self.bob = UserID.from_string("@4567:test")
+ self.alice = UserID.from_string("@alice:remote")
+
+ yield self.store.create_profile(self.frank.localpart)
+
+ self.handler = hs.get_handlers().profile_handler
+
+ # TODO(paul): Icky signal declarings.. booo
+ hs.get_distributor().declare("changed_presencelike_data")
+
+ @defer.inlineCallbacks
+ def test_get_my_name(self):
+ yield self.store.set_profile_displayname(
+ self.frank.localpart, "Frank"
+ )
+
+ displayname = yield self.handler.get_displayname(self.frank)
+
+ self.assertEquals("Frank", displayname)
+
+ @defer.inlineCallbacks
+ def test_set_my_name(self):
+ yield self.handler.set_displayname(self.frank, self.frank, "Frank Jr.")
+
+ self.assertEquals(
+ (yield self.store.get_profile_displayname(self.frank.localpart)),
+ "Frank Jr."
+ )
+
+ @defer.inlineCallbacks
+ def test_set_my_name_noauth(self):
+ d = self.handler.set_displayname(self.frank, self.bob, "Frank Jr.")
+
+ yield self.assertFailure(d, AuthError)
+
+ @defer.inlineCallbacks
+ def test_get_other_name(self):
+ self.mock_federation.make_query.return_value = defer.succeed(
+ {"displayname": "Alice"}
+ )
+
+ displayname = yield self.handler.get_displayname(self.alice)
+
+ self.assertEquals(displayname, "Alice")
+ self.mock_federation.make_query.assert_called_with(
+ destination="remote",
+ query_type="profile",
+ args={"user_id": "@alice:remote", "field": "displayname"}
+ )
+
+ @defer.inlineCallbacks
+ def test_incoming_fed_query(self):
+ yield self.store.create_profile("caroline")
+ yield self.store.set_profile_displayname("caroline", "Caroline")
+
+ response = yield self.query_handlers["profile"](
+ {"user_id": "@caroline:test", "field": "displayname"}
+ )
+
+ self.assertEquals({"displayname": "Caroline"}, response)
+
+ @defer.inlineCallbacks
+ def test_get_my_avatar(self):
+ yield self.store.set_profile_avatar_url(
+ self.frank.localpart, "http://my.server/me.png"
+ )
+
+ avatar_url = yield self.handler.get_avatar_url(self.frank)
+
+ self.assertEquals("http://my.server/me.png", avatar_url)
+
+ @defer.inlineCallbacks
+ def test_set_my_avatar(self):
+ yield self.handler.set_avatar_url(self.frank, self.frank,
+ "http://my.server/pic.gif")
+
+ self.assertEquals(
+ (yield self.store.get_profile_avatar_url(self.frank.localpart)),
+ "http://my.server/pic.gif"
+ )
diff --git a/tests/handlers/test_room.py b/tests/handlers/test_room.py
new file mode 100644
index 00000000..2a7553f9
--- /dev/null
+++ b/tests/handlers/test_room.py
@@ -0,0 +1,404 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from twisted.internet import defer
+from .. import unittest
+
+from synapse.api.constants import EventTypes, Membership
+from synapse.handlers.room import RoomMemberHandler, RoomCreationHandler
+from synapse.handlers.profile import ProfileHandler
+from synapse.types import UserID
+from ..utils import setup_test_homeserver
+
+from mock import Mock, NonCallableMock
+
+
+class RoomMemberHandlerTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.hostname = "red"
+ hs = yield setup_test_homeserver(
+ self.hostname,
+ ratelimiter=NonCallableMock(spec_set=[
+ "send_message",
+ ]),
+ datastore=NonCallableMock(spec_set=[
+ "persist_event",
+ "get_room_member",
+ "get_room",
+ "store_room",
+ "get_latest_events_in_room",
+ "add_event_hashes",
+ ]),
+ resource_for_federation=NonCallableMock(),
+ http_client=NonCallableMock(spec_set=[]),
+ notifier=NonCallableMock(spec_set=["on_new_room_event"]),
+ handlers=NonCallableMock(spec_set=[
+ "room_member_handler",
+ "profile_handler",
+ "federation_handler",
+ ]),
+ auth=NonCallableMock(spec_set=[
+ "check",
+ "add_auth_events",
+ "check_host_in_room",
+ ]),
+ state_handler=NonCallableMock(spec_set=[
+ "compute_event_context",
+ "get_current_state",
+ ]),
+ )
+
+ self.federation = NonCallableMock(spec_set=[
+ "handle_new_event",
+ "send_invite",
+ "get_state_for_room",
+ ])
+
+ self.datastore = hs.get_datastore()
+ self.handlers = hs.get_handlers()
+ self.notifier = hs.get_notifier()
+ self.state_handler = hs.get_state_handler()
+ self.distributor = hs.get_distributor()
+ self.auth = hs.get_auth()
+ self.hs = hs
+
+ self.handlers.federation_handler = self.federation
+
+ self.distributor.declare("collect_presencelike_data")
+
+ self.handlers.room_member_handler = RoomMemberHandler(self.hs)
+ self.handlers.profile_handler = ProfileHandler(self.hs)
+ self.room_member_handler = self.handlers.room_member_handler
+
+ self.ratelimiter = hs.get_ratelimiter()
+ self.ratelimiter.send_message.return_value = (True, 0)
+
+ self.datastore.persist_event.return_value = (1,1)
+ self.datastore.add_event_hashes.return_value = []
+
+ @defer.inlineCallbacks
+ def test_invite(self):
+ room_id = "!foo:red"
+ user_id = "@bob:red"
+ target_user_id = "@red:blue"
+ content = {"membership": Membership.INVITE}
+
+ builder = self.hs.get_event_builder_factory().new({
+ "type": EventTypes.Member,
+ "sender": user_id,
+ "state_key": target_user_id,
+ "room_id": room_id,
+ "content": content,
+ })
+
+ self.datastore.get_latest_events_in_room.return_value = (
+ defer.succeed([])
+ )
+
+ def annotate(_):
+ ctx = Mock()
+ ctx.current_state = {
+ (EventTypes.Member, "@alice:green"): self._create_member(
+ user_id="@alice:green",
+ room_id=room_id,
+ ),
+ (EventTypes.Member, "@bob:red"): self._create_member(
+ user_id="@bob:red",
+ room_id=room_id,
+ ),
+ }
+ ctx.prev_state_events = []
+
+ return defer.succeed(ctx)
+
+ self.state_handler.compute_event_context.side_effect = annotate
+
+ def add_auth(_, ctx):
+ ctx.auth_events = ctx.current_state[
+ (EventTypes.Member, "@bob:red")
+ ]
+
+ return defer.succeed(True)
+ self.auth.add_auth_events.side_effect = add_auth
+
+ def send_invite(domain, event):
+ return defer.succeed(event)
+
+ self.federation.send_invite.side_effect = send_invite
+
+ room_handler = self.room_member_handler
+ event, context = yield room_handler._create_new_client_event(
+ builder
+ )
+
+ yield room_handler.change_membership(event, context)
+
+ self.state_handler.compute_event_context.assert_called_once_with(
+ builder
+ )
+
+ self.auth.add_auth_events.assert_called_once_with(
+ builder, context
+ )
+
+ self.federation.send_invite.assert_called_once_with(
+ "blue", event,
+ )
+
+ self.datastore.persist_event.assert_called_once_with(
+ event, context=context,
+ )
+ self.notifier.on_new_room_event.assert_called_once_with(
+ event, 1, 1, extra_users=[UserID.from_string(target_user_id)]
+ )
+ self.assertFalse(self.datastore.get_room.called)
+ self.assertFalse(self.datastore.store_room.called)
+ self.assertFalse(self.federation.get_state_for_room.called)
+
+ @defer.inlineCallbacks
+ def test_simple_join(self):
+ room_id = "!foo:red"
+ user_id = "@bob:red"
+ user = UserID.from_string(user_id)
+
+ join_signal_observer = Mock()
+ self.distributor.observe("user_joined_room", join_signal_observer)
+
+ builder = self.hs.get_event_builder_factory().new({
+ "type": EventTypes.Member,
+ "sender": user_id,
+ "state_key": user_id,
+ "room_id": room_id,
+ "content": {"membership": Membership.JOIN},
+ })
+
+ self.datastore.get_latest_events_in_room.return_value = (
+ defer.succeed([])
+ )
+
+ def annotate(_):
+ ctx = Mock()
+ ctx.current_state = {
+ (EventTypes.Member, "@bob:red"): self._create_member(
+ user_id="@bob:red",
+ room_id=room_id,
+ membership=Membership.INVITE
+ ),
+ }
+ ctx.prev_state_events = []
+
+ return defer.succeed(ctx)
+
+ self.state_handler.compute_event_context.side_effect = annotate
+
+ def add_auth(_, ctx):
+ ctx.auth_events = ctx.current_state[
+ (EventTypes.Member, "@bob:red")
+ ]
+
+ return defer.succeed(True)
+ self.auth.add_auth_events.side_effect = add_auth
+
+ room_handler = self.room_member_handler
+ event, context = yield room_handler._create_new_client_event(
+ builder
+ )
+
+ # Actual invocation
+ yield room_handler.change_membership(event, context)
+
+ self.federation.handle_new_event.assert_called_once_with(
+ event, destinations=set()
+ )
+
+ self.datastore.persist_event.assert_called_once_with(
+ event, context=context
+ )
+ self.notifier.on_new_room_event.assert_called_once_with(
+ event, 1, 1, extra_users=[user]
+ )
+
+ join_signal_observer.assert_called_with(
+ user=user, room_id=room_id
+ )
+
+ def _create_member(self, user_id, room_id, membership=Membership.JOIN):
+ builder = self.hs.get_event_builder_factory().new({
+ "type": EventTypes.Member,
+ "sender": user_id,
+ "state_key": user_id,
+ "room_id": room_id,
+ "content": {"membership": membership},
+ })
+
+ return builder.build()
+
+ @defer.inlineCallbacks
+ def test_simple_leave(self):
+ room_id = "!foo:red"
+ user_id = "@bob:red"
+ user = UserID.from_string(user_id)
+
+ builder = self.hs.get_event_builder_factory().new({
+ "type": EventTypes.Member,
+ "sender": user_id,
+ "state_key": user_id,
+ "room_id": room_id,
+ "content": {"membership": Membership.LEAVE},
+ })
+
+ self.datastore.get_latest_events_in_room.return_value = (
+ defer.succeed([])
+ )
+
+ def annotate(_):
+ ctx = Mock()
+ ctx.current_state = {
+ (EventTypes.Member, "@bob:red"): self._create_member(
+ user_id="@bob:red",
+ room_id=room_id,
+ membership=Membership.JOIN
+ ),
+ }
+ ctx.prev_state_events = []
+
+ return defer.succeed(ctx)
+
+ self.state_handler.compute_event_context.side_effect = annotate
+
+ def add_auth(_, ctx):
+ ctx.auth_events = ctx.current_state[
+ (EventTypes.Member, "@bob:red")
+ ]
+
+ return defer.succeed(True)
+ self.auth.add_auth_events.side_effect = add_auth
+
+ room_handler = self.room_member_handler
+ event, context = yield room_handler._create_new_client_event(
+ builder
+ )
+
+ leave_signal_observer = Mock()
+ self.distributor.observe("user_left_room", leave_signal_observer)
+
+ # Actual invocation
+ yield room_handler.change_membership(event, context)
+
+ self.federation.handle_new_event.assert_called_once_with(
+ event, destinations=set(['red'])
+ )
+
+ self.datastore.persist_event.assert_called_once_with(
+ event, context=context
+ )
+ self.notifier.on_new_room_event.assert_called_once_with(
+ event, 1, 1, extra_users=[user]
+ )
+
+ leave_signal_observer.assert_called_with(
+ user=user, room_id=room_id
+ )
+
+
+class RoomCreationTest(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.hostname = "red"
+
+ hs = yield setup_test_homeserver(
+ self.hostname,
+ datastore=NonCallableMock(spec_set=[
+ "store_room",
+ "snapshot_room",
+ "persist_event",
+ "get_joined_hosts_for_room",
+ ]),
+ http_client=NonCallableMock(spec_set=[]),
+ notifier=NonCallableMock(spec_set=["on_new_room_event"]),
+ handlers=NonCallableMock(spec_set=[
+ "room_creation_handler",
+ "message_handler",
+ ]),
+ auth=NonCallableMock(spec_set=["check", "add_auth_events"]),
+ ratelimiter=NonCallableMock(spec_set=[
+ "send_message",
+ ]),
+ )
+
+ self.federation = NonCallableMock(spec_set=[
+ "handle_new_event",
+ ])
+
+ self.handlers = hs.get_handlers()
+
+ self.handlers.room_creation_handler = RoomCreationHandler(hs)
+ self.room_creation_handler = self.handlers.room_creation_handler
+
+ self.message_handler = self.handlers.message_handler
+
+ self.ratelimiter = hs.get_ratelimiter()
+ self.ratelimiter.send_message.return_value = (True, 0)
+
+ @defer.inlineCallbacks
+ def test_room_creation(self):
+ user_id = "@foo:red"
+ room_id = "!bobs_room:red"
+ config = {"visibility": "private"}
+
+ yield self.room_creation_handler.create_room(
+ user_id=user_id,
+ room_id=room_id,
+ config=config,
+ )
+
+ self.assertTrue(self.message_handler.create_and_send_event.called)
+
+ event_dicts = [
+ e[0][0]
+ for e in self.message_handler.create_and_send_event.call_args_list
+ ]
+
+ self.assertTrue(len(event_dicts) > 3)
+
+ self.assertDictContainsSubset(
+ {
+ "type": EventTypes.Create,
+ "sender": user_id,
+ "room_id": room_id,
+ },
+ event_dicts[0]
+ )
+
+ self.assertEqual(user_id, event_dicts[0]["content"]["creator"])
+
+ self.assertDictContainsSubset(
+ {
+ "type": EventTypes.Member,
+ "sender": user_id,
+ "room_id": room_id,
+ "state_key": user_id,
+ },
+ event_dicts[1]
+ )
+
+ self.assertEqual(
+ Membership.JOIN,
+ event_dicts[1]["content"]["membership"]
+ )
diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py
new file mode 100644
index 00000000..2d7ba435
--- /dev/null
+++ b/tests/handlers/test_typing.py
@@ -0,0 +1,414 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from mock import Mock, call, ANY
+import json
+
+from ..utils import (
+ MockHttpResource, MockClock, DeferredMockCallable, setup_test_homeserver
+)
+
+from synapse.api.errors import AuthError
+from synapse.handlers.typing import TypingNotificationHandler
+
+from synapse.storage.transactions import DestinationsTable
+from synapse.types import UserID
+
+
+def _expect_edu(destination, edu_type, content, origin="test"):
+ return {
+ "origin": origin,
+ "origin_server_ts": 1000000,
+ "pdus": [],
+ "edus": [
+ {
+ "edu_type": edu_type,
+ "content": content,
+ }
+ ],
+ "pdu_failures": [],
+ }
+
+
+def _make_edu_json(origin, edu_type, content):
+ return json.dumps(_expect_edu("test", edu_type, content, origin=origin))
+
+
+class JustTypingNotificationHandlers(object):
+ def __init__(self, hs):
+ self.typing_notification_handler = TypingNotificationHandler(hs)
+
+
+class TypingNotificationsTestCase(unittest.TestCase):
+ """Tests typing notifications to rooms."""
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.clock = MockClock()
+
+ self.mock_http_client = Mock(spec=[])
+ self.mock_http_client.put_json = DeferredMockCallable()
+
+ self.mock_federation_resource = MockHttpResource()
+
+ mock_notifier = Mock(spec=["on_new_event"])
+ self.on_new_event = mock_notifier.on_new_event
+
+ self.auth = Mock(spec=[])
+
+ hs = yield setup_test_homeserver(
+ auth=self.auth,
+ clock=self.clock,
+ datastore=Mock(spec=[
+ # Bits that Federation needs
+ "prep_send_transaction",
+ "delivered_txn",
+ "get_received_txn_response",
+ "set_received_txn_response",
+ "get_destination_retry_timings",
+ ]),
+ handlers=None,
+ notifier=mock_notifier,
+ resource_for_client=Mock(),
+ resource_for_federation=self.mock_federation_resource,
+ http_client=self.mock_http_client,
+ keyring=Mock(),
+ )
+ hs.handlers = JustTypingNotificationHandlers(hs)
+
+ self.handler = hs.get_handlers().typing_notification_handler
+
+ self.event_source = hs.get_event_sources().sources["typing"]
+
+ self.datastore = hs.get_datastore()
+ retry_timings_res = {
+ "destination": "",
+ "retry_last_ts": 0,
+ "retry_interval": 0,
+ }
+ self.datastore.get_destination_retry_timings.return_value = (
+ defer.succeed(retry_timings_res)
+ )
+
+ def get_received_txn_response(*args):
+ return defer.succeed(None)
+ self.datastore.get_received_txn_response = get_received_txn_response
+
+ self.room_id = "a-room"
+
+ # Mock the RoomMemberHandler
+ hs.handlers.room_member_handler = Mock(spec=[])
+ self.room_member_handler = hs.handlers.room_member_handler
+
+ self.room_members = []
+
+ def get_rooms_for_user(user):
+ if user in self.room_members:
+ return defer.succeed([self.room_id])
+ else:
+ return defer.succeed([])
+ self.room_member_handler.get_rooms_for_user = get_rooms_for_user
+
+ def get_room_members(room_id):
+ if room_id == self.room_id:
+ return defer.succeed(self.room_members)
+ else:
+ return defer.succeed([])
+ self.room_member_handler.get_room_members = get_room_members
+
+ def get_joined_rooms_for_user(user):
+ if user in self.room_members:
+ return defer.succeed([self.room_id])
+ else:
+ return defer.succeed([])
+ self.room_member_handler.get_joined_rooms_for_user = get_joined_rooms_for_user
+
+ @defer.inlineCallbacks
+ def fetch_room_distributions_into(room_id, localusers=None,
+ remotedomains=None, ignore_user=None):
+
+ members = yield get_room_members(room_id)
+ for member in members:
+ if ignore_user is not None and member == ignore_user:
+ continue
+
+ if hs.is_mine(member):
+ if localusers is not None:
+ localusers.add(member)
+ else:
+ if remotedomains is not None:
+ remotedomains.add(member.domain)
+ self.room_member_handler.fetch_room_distributions_into = (
+ fetch_room_distributions_into)
+
+ def check_joined_room(room_id, user_id):
+ if user_id not in [u.to_string() for u in self.room_members]:
+ raise AuthError(401, "User is not in the room")
+
+ self.auth.check_joined_room = check_joined_room
+
+ # Some local users to test with
+ self.u_apple = UserID.from_string("@apple:test")
+ self.u_banana = UserID.from_string("@banana:test")
+
+ # Remote user
+ self.u_onion = UserID.from_string("@onion:farm")
+
+ @defer.inlineCallbacks
+ def test_started_typing_local(self):
+ self.room_members = [self.u_apple, self.u_banana]
+
+ self.assertEquals(self.event_source.get_current_key(), 0)
+
+ yield self.handler.started_typing(
+ target_user=self.u_apple,
+ auth_user=self.u_apple,
+ room_id=self.room_id,
+ timeout=20000,
+ )
+
+ self.on_new_event.assert_has_calls([
+ call('typing_key', 1, rooms=[self.room_id]),
+ ])
+
+ self.assertEquals(self.event_source.get_current_key(), 1)
+ events = yield self.event_source.get_new_events(
+ room_ids=[self.room_id],
+ from_key=0,
+ )
+ self.assertEquals(
+ events[0],
+ [
+ {"type": "m.typing",
+ "room_id": self.room_id,
+ "content": {
+ "user_ids": [self.u_apple.to_string()],
+ }},
+ ]
+ )
+
+ @defer.inlineCallbacks
+ def test_started_typing_remote_send(self):
+ self.room_members = [self.u_apple, self.u_onion]
+
+ put_json = self.mock_http_client.put_json
+ put_json.expect_call_and_return(
+ call("farm",
+ path="/_matrix/federation/v1/send/1000000/",
+ data=_expect_edu("farm", "m.typing",
+ content={
+ "room_id": self.room_id,
+ "user_id": self.u_apple.to_string(),
+ "typing": True,
+ }
+ ),
+ json_data_callback=ANY,
+ ),
+ defer.succeed((200, "OK"))
+ )
+
+ yield self.handler.started_typing(
+ target_user=self.u_apple,
+ auth_user=self.u_apple,
+ room_id=self.room_id,
+ timeout=20000,
+ )
+
+ yield put_json.await_calls()
+
+ @defer.inlineCallbacks
+ def test_started_typing_remote_recv(self):
+ self.room_members = [self.u_apple, self.u_onion]
+
+ self.assertEquals(self.event_source.get_current_key(), 0)
+
+ yield self.mock_federation_resource.trigger("PUT",
+ "/_matrix/federation/v1/send/1000000/",
+ _make_edu_json("farm", "m.typing",
+ content={
+ "room_id": self.room_id,
+ "user_id": self.u_onion.to_string(),
+ "typing": True,
+ }
+ )
+ )
+
+ self.on_new_event.assert_has_calls([
+ call('typing_key', 1, rooms=[self.room_id]),
+ ])
+
+ self.assertEquals(self.event_source.get_current_key(), 1)
+ events = yield self.event_source.get_new_events(
+ room_ids=[self.room_id],
+ from_key=0
+ )
+ self.assertEquals(
+ events[0],
+ [
+ {"type": "m.typing",
+ "room_id": self.room_id,
+ "content": {
+ "user_ids": [self.u_onion.to_string()],
+ }},
+ ]
+ )
+
+ @defer.inlineCallbacks
+ def test_stopped_typing(self):
+ self.room_members = [self.u_apple, self.u_banana, self.u_onion]
+
+ put_json = self.mock_http_client.put_json
+ put_json.expect_call_and_return(
+ call("farm",
+ path="/_matrix/federation/v1/send/1000000/",
+ data=_expect_edu("farm", "m.typing",
+ content={
+ "room_id": self.room_id,
+ "user_id": self.u_apple.to_string(),
+ "typing": False,
+ }
+ ),
+ json_data_callback=ANY,
+ ),
+ defer.succeed((200, "OK"))
+ )
+
+ # Gut-wrenching
+ from synapse.handlers.typing import RoomMember
+ member = RoomMember(self.room_id, self.u_apple)
+ self.handler._member_typing_until[member] = 1002000
+ self.handler._member_typing_timer[member] = (
+ self.clock.call_later(1002, lambda: 0)
+ )
+ self.handler._room_typing[self.room_id] = set((self.u_apple,))
+
+ self.assertEquals(self.event_source.get_current_key(), 0)
+
+ yield self.handler.stopped_typing(
+ target_user=self.u_apple,
+ auth_user=self.u_apple,
+ room_id=self.room_id,
+ )
+
+ self.on_new_event.assert_has_calls([
+ call('typing_key', 1, rooms=[self.room_id]),
+ ])
+
+ yield put_json.await_calls()
+
+ self.assertEquals(self.event_source.get_current_key(), 1)
+ events = yield self.event_source.get_new_events(
+ room_ids=[self.room_id],
+ from_key=0,
+ )
+ self.assertEquals(
+ events[0],
+ [
+ {"type": "m.typing",
+ "room_id": self.room_id,
+ "content": {
+ "user_ids": [],
+ }},
+ ]
+ )
+
+ @defer.inlineCallbacks
+ def test_typing_timeout(self):
+ self.room_members = [self.u_apple, self.u_banana]
+
+ self.assertEquals(self.event_source.get_current_key(), 0)
+
+ yield self.handler.started_typing(
+ target_user=self.u_apple,
+ auth_user=self.u_apple,
+ room_id=self.room_id,
+ timeout=10000,
+ )
+
+ self.on_new_event.assert_has_calls([
+ call('typing_key', 1, rooms=[self.room_id]),
+ ])
+ self.on_new_event.reset_mock()
+
+ self.assertEquals(self.event_source.get_current_key(), 1)
+ events = yield self.event_source.get_new_events(
+ room_ids=[self.room_id],
+ from_key=0,
+ )
+ self.assertEquals(
+ events[0],
+ [
+ {"type": "m.typing",
+ "room_id": self.room_id,
+ "content": {
+ "user_ids": [self.u_apple.to_string()],
+ }},
+ ]
+ )
+
+ self.clock.advance_time(11)
+
+ self.on_new_event.assert_has_calls([
+ call('typing_key', 2, rooms=[self.room_id]),
+ ])
+
+ self.assertEquals(self.event_source.get_current_key(), 2)
+ events = yield self.event_source.get_new_events(
+ room_ids=[self.room_id],
+ from_key=1,
+ )
+ self.assertEquals(
+ events[0],
+ [
+ {"type": "m.typing",
+ "room_id": self.room_id,
+ "content": {
+ "user_ids": [],
+ }},
+ ]
+ )
+
+ # SYN-230 - see if we can still set after timeout
+
+ yield self.handler.started_typing(
+ target_user=self.u_apple,
+ auth_user=self.u_apple,
+ room_id=self.room_id,
+ timeout=10000,
+ )
+
+ self.on_new_event.assert_has_calls([
+ call('typing_key', 3, rooms=[self.room_id]),
+ ])
+ self.on_new_event.reset_mock()
+
+ self.assertEquals(self.event_source.get_current_key(), 3)
+ events = yield self.event_source.get_new_events(
+ room_ids=[self.room_id],
+ from_key=0,
+ )
+ self.assertEquals(
+ events[0],
+ [
+ {"type": "m.typing",
+ "room_id": self.room_id,
+ "content": {
+ "user_ids": [self.u_apple.to_string()],
+ }},
+ ]
+ )
diff --git a/tests/metrics/__init__.py b/tests/metrics/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/metrics/__init__.py
diff --git a/tests/metrics/test_metric.py b/tests/metrics/test_metric.py
new file mode 100644
index 00000000..60090142
--- /dev/null
+++ b/tests/metrics/test_metric.py
@@ -0,0 +1,161 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from tests import unittest
+
+from synapse.metrics.metric import (
+ CounterMetric, CallbackMetric, DistributionMetric, CacheMetric
+)
+
+
+class CounterMetricTestCase(unittest.TestCase):
+
+ def test_scalar(self):
+ counter = CounterMetric("scalar")
+
+ self.assertEquals(counter.render(), [
+ 'scalar 0',
+ ])
+
+ counter.inc()
+
+ self.assertEquals(counter.render(), [
+ 'scalar 1',
+ ])
+
+ counter.inc_by(2)
+
+ self.assertEquals(counter.render(), [
+ 'scalar 3'
+ ])
+
+ def test_vector(self):
+ counter = CounterMetric("vector", labels=["method"])
+
+ # Empty counter doesn't yet know what values it has
+ self.assertEquals(counter.render(), [])
+
+ counter.inc("GET")
+
+ self.assertEquals(counter.render(), [
+ 'vector{method="GET"} 1',
+ ])
+
+ counter.inc("GET")
+ counter.inc("PUT")
+
+ self.assertEquals(counter.render(), [
+ 'vector{method="GET"} 2',
+ 'vector{method="PUT"} 1',
+ ])
+
+
+class CallbackMetricTestCase(unittest.TestCase):
+
+ def test_scalar(self):
+ d = dict()
+
+ metric = CallbackMetric("size", lambda: len(d))
+
+ self.assertEquals(metric.render(), [
+ 'size 0',
+ ])
+
+ d["key"] = "value"
+
+ self.assertEquals(metric.render(), [
+ 'size 1',
+ ])
+
+ def test_vector(self):
+ vals = dict()
+
+ metric = CallbackMetric("values", lambda: vals, labels=["type"])
+
+ self.assertEquals(metric.render(), [])
+
+ # Keys have to be tuples, even if they're 1-element
+ vals[("foo",)] = 1
+ vals[("bar",)] = 2
+
+ self.assertEquals(metric.render(), [
+ 'values{type="bar"} 2',
+ 'values{type="foo"} 1',
+ ])
+
+
+class DistributionMetricTestCase(unittest.TestCase):
+
+ def test_scalar(self):
+ metric = DistributionMetric("thing")
+
+ self.assertEquals(metric.render(), [
+ 'thing:count 0',
+ 'thing:total 0',
+ ])
+
+ metric.inc_by(500)
+
+ self.assertEquals(metric.render(), [
+ 'thing:count 1',
+ 'thing:total 500',
+ ])
+
+ def test_vector(self):
+ metric = DistributionMetric("queries", labels=["verb"])
+
+ self.assertEquals(metric.render(), [])
+
+ metric.inc_by(300, "SELECT")
+ metric.inc_by(200, "SELECT")
+ metric.inc_by(800, "INSERT")
+
+ self.assertEquals(metric.render(), [
+ 'queries:count{verb="INSERT"} 1',
+ 'queries:count{verb="SELECT"} 2',
+ 'queries:total{verb="INSERT"} 800',
+ 'queries:total{verb="SELECT"} 500',
+ ])
+
+
+class CacheMetricTestCase(unittest.TestCase):
+
+ def test_cache(self):
+ d = dict()
+
+ metric = CacheMetric("cache", lambda: len(d))
+
+ self.assertEquals(metric.render(), [
+ 'cache:hits 0',
+ 'cache:total 0',
+ 'cache:size 0',
+ ])
+
+ metric.inc_misses()
+ d["key"] = "value"
+
+ self.assertEquals(metric.render(), [
+ 'cache:hits 0',
+ 'cache:total 1',
+ 'cache:size 1',
+ ])
+
+ metric.inc_hits()
+
+ self.assertEquals(metric.render(), [
+ 'cache:hits 1',
+ 'cache:total 2',
+ 'cache:size 1',
+ ])
diff --git a/tests/rest/__init__.py b/tests/rest/__init__.py
new file mode 100644
index 00000000..1a84d94c
--- /dev/null
+++ b/tests/rest/__init__.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/rest/client/__init__.py b/tests/rest/client/__init__.py
new file mode 100644
index 00000000..1a84d94c
--- /dev/null
+++ b/tests/rest/client/__init__.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/rest/client/v1/__init__.py b/tests/rest/client/v1/__init__.py
new file mode 100644
index 00000000..9bff9ec1
--- /dev/null
+++ b/tests/rest/client/v1/__init__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
diff --git a/tests/rest/client/v1/test_events.py b/tests/rest/client/v1/test_events.py
new file mode 100644
index 00000000..ac3b0b58
--- /dev/null
+++ b/tests/rest/client/v1/test_events.py
@@ -0,0 +1,217 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+""" Tests REST events for /events paths."""
+from tests import unittest
+
+# twisted imports
+from twisted.internet import defer
+
+import synapse.rest.client.v1.events
+import synapse.rest.client.v1.register
+import synapse.rest.client.v1.room
+
+
+from ....utils import MockHttpResource, setup_test_homeserver
+from .utils import RestTestCase
+
+from mock import Mock, NonCallableMock
+
+
+PATH_PREFIX = "/_matrix/client/api/v1"
+
+
+class EventStreamPaginationApiTestCase(unittest.TestCase):
+ """ Tests event streaming query parameters and start/end keys used in the
+ Pagination stream API. """
+ user_id = "sid1"
+
+ def setUp(self):
+ # configure stream and inject items
+ pass
+
+ def tearDown(self):
+ pass
+
+ def TODO_test_long_poll(self):
+ # stream from 'end' key, send (self+other) message, expect message.
+
+ # stream from 'END', send (self+other) message, expect message.
+
+ # stream from 'end' key, send (self+other) topic, expect topic.
+
+ # stream from 'END', send (self+other) topic, expect topic.
+
+ # stream from 'end' key, send (self+other) invite, expect invite.
+
+ # stream from 'END', send (self+other) invite, expect invite.
+
+ pass
+
+ def TODO_test_stream_forward(self):
+ # stream from START, expect injected items
+
+ # stream from 'start' key, expect same content
+
+ # stream from 'end' key, expect nothing
+
+ # stream from 'END', expect nothing
+
+ # The following is needed for cases where content is removed e.g. you
+ # left a room, so the token you're streaming from is > the one that
+ # would be returned naturally from START>END.
+ # stream from very new token (higher than end key), expect same token
+ # returned as end key
+ pass
+
+ def TODO_test_limits(self):
+ # stream from a key, expect limit_num items
+
+ # stream from START, expect limit_num items
+
+ pass
+
+ def TODO_test_range(self):
+ # stream from key to key, expect X items
+
+ # stream from key to END, expect X items
+
+ # stream from START to key, expect X items
+
+ # stream from START to END, expect all items
+ pass
+
+ def TODO_test_direction(self):
+ # stream from END to START and fwds, expect newest first
+
+ # stream from END to START and bwds, expect oldest first
+
+ # stream from START to END and fwds, expect oldest first
+
+ # stream from START to END and bwds, expect newest first
+
+ pass
+
+
+class EventStreamPermissionsTestCase(RestTestCase):
+ """ Tests event streaming (GET /events). """
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
+
+ hs = yield setup_test_homeserver(
+ http_client=None,
+ replication_layer=Mock(),
+ ratelimiter=NonCallableMock(spec_set=[
+ "send_message",
+ ]),
+ )
+ self.ratelimiter = hs.get_ratelimiter()
+ self.ratelimiter.send_message.return_value = (True, 0)
+ hs.config.enable_registration_captcha = False
+ hs.config.disable_registration = False
+
+ hs.get_handlers().federation_handler = Mock()
+
+ synapse.rest.client.v1.register.register_servlets(hs, self.mock_resource)
+ synapse.rest.client.v1.events.register_servlets(hs, self.mock_resource)
+ synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
+
+ # register an account
+ self.user_id = "sid1"
+ response = yield self.register(self.user_id)
+ self.token = response["access_token"]
+ self.user_id = response["user_id"]
+
+ # register a 2nd account
+ self.other_user = "other1"
+ response = yield self.register(self.other_user)
+ self.other_token = response["access_token"]
+ self.other_user = response["user_id"]
+
+ def tearDown(self):
+ pass
+
+ @defer.inlineCallbacks
+ def test_stream_basic_permissions(self):
+ # invalid token, expect 403
+ (code, response) = yield self.mock_resource.trigger_get(
+ "/events?access_token=%s" % ("invalid" + self.token, )
+ )
+ self.assertEquals(403, code, msg=str(response))
+
+ # valid token, expect content
+ (code, response) = yield self.mock_resource.trigger_get(
+ "/events?access_token=%s&timeout=0" % (self.token,)
+ )
+ self.assertEquals(200, code, msg=str(response))
+ self.assertTrue("chunk" in response)
+ self.assertTrue("start" in response)
+ self.assertTrue("end" in response)
+
+ @defer.inlineCallbacks
+ def test_stream_room_permissions(self):
+ room_id = yield self.create_room_as(
+ self.other_user,
+ tok=self.other_token
+ )
+ yield self.send(room_id, tok=self.other_token)
+
+ # invited to room (expect no content for room)
+ yield self.invite(
+ room_id,
+ src=self.other_user,
+ targ=self.user_id,
+ tok=self.other_token
+ )
+
+ (code, response) = yield self.mock_resource.trigger_get(
+ "/events?access_token=%s&timeout=0" % (self.token,)
+ )
+ self.assertEquals(200, code, msg=str(response))
+
+ # We may get a presence event for ourselves down
+ self.assertEquals(
+ 0,
+ len([
+ c for c in response["chunk"]
+ if not (
+ c.get("type") == "m.presence"
+ and c["content"].get("user_id") == self.user_id
+ )
+ ])
+ )
+
+ # joined room (expect all content for room)
+ yield self.join(room=room_id, user=self.user_id, tok=self.token)
+
+ # left to room (expect no content for room)
+
+ def TODO_test_stream_items(self):
+ # new user, no content
+
+ # join room, expect 1 item (join)
+
+ # send message, expect 2 items (join,send)
+
+ # set topic, expect 3 items (join,send,topic)
+
+ # someone else join room, expect 4 (join,send,topic,join)
+
+ # someone else send message, expect 5 (join,send.topic,join,send)
+
+ # someone else set topic, expect 6 (join,send,topic,join,send,topic)
+ pass
diff --git a/tests/rest/client/v1/test_presence.py b/tests/rest/client/v1/test_presence.py
new file mode 100644
index 00000000..8581796f
--- /dev/null
+++ b/tests/rest/client/v1/test_presence.py
@@ -0,0 +1,411 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests REST events for /presence paths."""
+
+from tests import unittest
+from twisted.internet import defer
+
+from mock import Mock
+
+from ....utils import MockHttpResource, setup_test_homeserver
+
+from synapse.api.constants import PresenceState
+from synapse.handlers.presence import PresenceHandler
+from synapse.rest.client.v1 import presence
+from synapse.rest.client.v1 import events
+from synapse.types import UserID
+from synapse.util.async import run_on_reactor
+
+from collections import namedtuple
+
+
+OFFLINE = PresenceState.OFFLINE
+UNAVAILABLE = PresenceState.UNAVAILABLE
+ONLINE = PresenceState.ONLINE
+
+
+myid = "@apple:test"
+PATH_PREFIX = "/_matrix/client/api/v1"
+
+
+class NullSource(object):
+ """This event source never yields any events and its token remains at
+ zero. It may be useful for unit-testing."""
+ def __init__(self, hs):
+ pass
+
+ def get_new_events(
+ self,
+ user,
+ from_key,
+ room_ids=None,
+ limit=None,
+ is_guest=None
+ ):
+ return defer.succeed(([], from_key))
+
+ def get_current_key(self, direction='f'):
+ return defer.succeed(0)
+
+ def get_pagination_rows(self, user, pagination_config, key):
+ return defer.succeed(([], pagination_config.from_key))
+
+
+class JustPresenceHandlers(object):
+ def __init__(self, hs):
+ self.presence_handler = PresenceHandler(hs)
+
+
+class PresenceStateTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
+ hs = yield setup_test_homeserver(
+ datastore=Mock(spec=[
+ "get_presence_state",
+ "set_presence_state",
+ "insert_client_ip",
+ ]),
+ http_client=None,
+ resource_for_client=self.mock_resource,
+ resource_for_federation=self.mock_resource,
+ )
+ hs.handlers = JustPresenceHandlers(hs)
+
+ self.datastore = hs.get_datastore()
+ self.datastore.get_app_service_by_token = Mock(return_value=None)
+
+ def get_presence_list(*a, **kw):
+ return defer.succeed([])
+ self.datastore.get_presence_list = get_presence_list
+
+ def _get_user_by_access_token(token=None, allow_guest=False):
+ return {
+ "user": UserID.from_string(myid),
+ "token_id": 1,
+ "is_guest": False,
+ }
+
+ hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
+
+ room_member_handler = hs.handlers.room_member_handler = Mock(
+ spec=[
+ "get_joined_rooms_for_user",
+ ]
+ )
+
+ def get_rooms_for_user(user):
+ return defer.succeed([])
+ room_member_handler.get_joined_rooms_for_user = get_rooms_for_user
+
+ presence.register_servlets(hs, self.mock_resource)
+
+ self.u_apple = UserID.from_string(myid)
+
+ @defer.inlineCallbacks
+ def test_get_my_status(self):
+ mocked_get = self.datastore.get_presence_state
+ mocked_get.return_value = defer.succeed(
+ {"state": ONLINE, "status_msg": "Available"}
+ )
+
+ (code, response) = yield self.mock_resource.trigger("GET",
+ "/presence/%s/status" % (myid), None)
+
+ self.assertEquals(200, code)
+ self.assertEquals(
+ {"presence": ONLINE, "status_msg": "Available"},
+ response
+ )
+ mocked_get.assert_called_with("apple")
+
+ @defer.inlineCallbacks
+ def test_set_my_status(self):
+ mocked_set = self.datastore.set_presence_state
+ mocked_set.return_value = defer.succeed({"state": OFFLINE})
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ "/presence/%s/status" % (myid),
+ '{"presence": "unavailable", "status_msg": "Away"}')
+
+ self.assertEquals(200, code)
+ mocked_set.assert_called_with("apple",
+ {"state": UNAVAILABLE, "status_msg": "Away"}
+ )
+
+
+class PresenceListTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
+
+ hs = yield setup_test_homeserver(
+ datastore=Mock(spec=[
+ "has_presence_state",
+ "get_presence_state",
+ "allow_presence_visible",
+ "is_presence_visible",
+ "add_presence_list_pending",
+ "set_presence_list_accepted",
+ "del_presence_list",
+ "get_presence_list",
+ "insert_client_ip",
+ ]),
+ http_client=None,
+ resource_for_client=self.mock_resource,
+ resource_for_federation=self.mock_resource,
+ )
+ hs.handlers = JustPresenceHandlers(hs)
+
+ self.datastore = hs.get_datastore()
+ self.datastore.get_app_service_by_token = Mock(return_value=None)
+
+ def has_presence_state(user_localpart):
+ return defer.succeed(
+ user_localpart in ("apple", "banana",)
+ )
+ self.datastore.has_presence_state = has_presence_state
+
+ def _get_user_by_access_token(token=None, allow_guest=False):
+ return {
+ "user": UserID.from_string(myid),
+ "token_id": 1,
+ "is_guest": False,
+ }
+
+ hs.handlers.room_member_handler = Mock(
+ spec=[
+ "get_joined_rooms_for_user",
+ ]
+ )
+
+ hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
+
+ presence.register_servlets(hs, self.mock_resource)
+
+ self.u_apple = UserID.from_string("@apple:test")
+ self.u_banana = UserID.from_string("@banana:test")
+
+ @defer.inlineCallbacks
+ def test_get_my_list(self):
+ self.datastore.get_presence_list.return_value = defer.succeed(
+ [{"observed_user_id": "@banana:test", "accepted": True}],
+ )
+
+ (code, response) = yield self.mock_resource.trigger("GET",
+ "/presence/list/%s" % (myid), None)
+
+ self.assertEquals(200, code)
+ self.assertEquals([
+ {"user_id": "@banana:test", "presence": OFFLINE, "accepted": True},
+ ], response)
+
+ self.datastore.get_presence_list.assert_called_with(
+ "apple", accepted=True
+ )
+
+ @defer.inlineCallbacks
+ def test_invite(self):
+ self.datastore.add_presence_list_pending.return_value = (
+ defer.succeed(())
+ )
+ self.datastore.is_presence_visible.return_value = defer.succeed(
+ True
+ )
+
+ (code, response) = yield self.mock_resource.trigger("POST",
+ "/presence/list/%s" % (myid),
+ """{"invite": ["@banana:test"]}"""
+ )
+
+ self.assertEquals(200, code)
+
+ self.datastore.add_presence_list_pending.assert_called_with(
+ "apple", "@banana:test"
+ )
+ self.datastore.set_presence_list_accepted.assert_called_with(
+ "apple", "@banana:test"
+ )
+
+ @defer.inlineCallbacks
+ def test_drop(self):
+ self.datastore.del_presence_list.return_value = (
+ defer.succeed(())
+ )
+
+ (code, response) = yield self.mock_resource.trigger("POST",
+ "/presence/list/%s" % (myid),
+ """{"drop": ["@banana:test"]}"""
+ )
+
+ self.assertEquals(200, code)
+
+ self.datastore.del_presence_list.assert_called_with(
+ "apple", "@banana:test"
+ )
+
+
+class PresenceEventStreamTestCase(unittest.TestCase):
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
+
+ # HIDEOUS HACKERY
+ # TODO(paul): This should be injected in via the HomeServer DI system
+ from synapse.streams.events import (
+ PresenceEventSource, EventSources
+ )
+
+ old_SOURCE_TYPES = EventSources.SOURCE_TYPES
+ def tearDown():
+ EventSources.SOURCE_TYPES = old_SOURCE_TYPES
+ self.tearDown = tearDown
+
+ EventSources.SOURCE_TYPES = {
+ k: NullSource for k in old_SOURCE_TYPES.keys()
+ }
+ EventSources.SOURCE_TYPES["presence"] = PresenceEventSource
+
+ hs = yield setup_test_homeserver(
+ http_client=None,
+ resource_for_client=self.mock_resource,
+ resource_for_federation=self.mock_resource,
+ datastore=Mock(spec=[
+ "set_presence_state",
+ "get_presence_list",
+ "get_rooms_for_user",
+ ]),
+ clock=Mock(spec=[
+ "call_later",
+ "cancel_call_later",
+ "time_msec",
+ "looping_call",
+ ]),
+ )
+
+ hs.get_clock().time_msec.return_value = 1000000
+
+ def _get_user_by_req(req=None, allow_guest=False):
+ return (UserID.from_string(myid), "", False)
+
+ hs.get_v1auth().get_user_by_req = _get_user_by_req
+
+ presence.register_servlets(hs, self.mock_resource)
+ events.register_servlets(hs, self.mock_resource)
+
+ hs.handlers.room_member_handler = Mock(spec=[])
+
+ self.room_members = []
+
+ def get_rooms_for_user(user):
+ if user in self.room_members:
+ return ["a-room"]
+ else:
+ return []
+ hs.handlers.room_member_handler.get_joined_rooms_for_user = get_rooms_for_user
+ hs.handlers.room_member_handler.get_room_members = (
+ lambda r: self.room_members if r == "a-room" else []
+ )
+ hs.handlers.room_member_handler._filter_events_for_client = (
+ lambda user_id, events, **kwargs: events
+ )
+
+ self.mock_datastore = hs.get_datastore()
+ self.mock_datastore.get_app_service_by_token = Mock(return_value=None)
+ self.mock_datastore.get_app_service_by_user_id = Mock(
+ return_value=defer.succeed(None)
+ )
+ self.mock_datastore.get_rooms_for_user = (
+ lambda u: [
+ namedtuple("Room", "room_id")(r)
+ for r in get_rooms_for_user(UserID.from_string(u))
+ ]
+ )
+
+ def get_profile_displayname(user_id):
+ return defer.succeed("Frank")
+ self.mock_datastore.get_profile_displayname = get_profile_displayname
+
+ def get_profile_avatar_url(user_id):
+ return defer.succeed(None)
+ self.mock_datastore.get_profile_avatar_url = get_profile_avatar_url
+
+ def user_rooms_intersect(user_list):
+ room_member_ids = map(lambda u: u.to_string(), self.room_members)
+
+ shared = all(map(lambda i: i in room_member_ids, user_list))
+ return defer.succeed(shared)
+ self.mock_datastore.user_rooms_intersect = user_rooms_intersect
+
+ def get_joined_hosts_for_room(room_id):
+ return []
+ self.mock_datastore.get_joined_hosts_for_room = get_joined_hosts_for_room
+
+ self.presence = hs.get_handlers().presence_handler
+
+ self.u_apple = UserID.from_string("@apple:test")
+ self.u_banana = UserID.from_string("@banana:test")
+
+ @defer.inlineCallbacks
+ def test_shortpoll(self):
+ self.room_members = [self.u_apple, self.u_banana]
+
+ self.mock_datastore.set_presence_state.return_value = defer.succeed(
+ {"state": ONLINE}
+ )
+ self.mock_datastore.get_presence_list.return_value = defer.succeed(
+ []
+ )
+
+ (code, response) = yield self.mock_resource.trigger("GET",
+ "/events?timeout=0", None)
+
+ self.assertEquals(200, code)
+
+ # We've forced there to be only one data stream so the tokens will
+ # all be ours
+
+ # I'll already get my own presence state change
+ self.assertEquals({"start": "0_1_0_0_0", "end": "0_1_0_0_0", "chunk": []},
+ response
+ )
+
+ self.mock_datastore.set_presence_state.return_value = defer.succeed(
+ {"state": ONLINE}
+ )
+ self.mock_datastore.get_presence_list.return_value = defer.succeed([])
+
+ yield self.presence.set_state(self.u_banana, self.u_banana,
+ state={"presence": ONLINE}
+ )
+
+ yield run_on_reactor()
+
+ (code, response) = yield self.mock_resource.trigger("GET",
+ "/events?from=s0_1_0&timeout=0", None)
+
+ self.assertEquals(200, code)
+ self.assertEquals({"start": "s0_1_0_0_0", "end": "s0_2_0_0_0", "chunk": [
+ {"type": "m.presence",
+ "content": {
+ "user_id": "@banana:test",
+ "presence": ONLINE,
+ "displayname": "Frank",
+ "last_active_ago": 0,
+ }},
+ ]}, response)
diff --git a/tests/rest/client/v1/test_profile.py b/tests/rest/client/v1/test_profile.py
new file mode 100644
index 00000000..adcc1d19
--- /dev/null
+++ b/tests/rest/client/v1/test_profile.py
@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests REST events for /profile paths."""
+
+from tests import unittest
+from twisted.internet import defer
+
+from mock import Mock, NonCallableMock
+
+from ....utils import MockHttpResource, setup_test_homeserver
+
+from synapse.api.errors import SynapseError, AuthError
+from synapse.types import UserID
+
+from synapse.rest.client.v1 import profile
+
+myid = "@1234ABCD:test"
+PATH_PREFIX = "/_matrix/client/api/v1"
+
+
+class ProfileTestCase(unittest.TestCase):
+ """ Tests profile management. """
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
+ self.mock_handler = Mock(spec=[
+ "get_displayname",
+ "set_displayname",
+ "get_avatar_url",
+ "set_avatar_url",
+ ])
+
+ hs = yield setup_test_homeserver(
+ "test",
+ http_client=None,
+ resource_for_client=self.mock_resource,
+ federation=Mock(),
+ replication_layer=Mock(),
+ )
+
+ def _get_user_by_req(request=None, allow_guest=False):
+ return (UserID.from_string(myid), "", False)
+
+ hs.get_v1auth().get_user_by_req = _get_user_by_req
+
+ hs.get_handlers().profile_handler = self.mock_handler
+
+ profile.register_servlets(hs, self.mock_resource)
+
+ @defer.inlineCallbacks
+ def test_get_my_name(self):
+ mocked_get = self.mock_handler.get_displayname
+ mocked_get.return_value = defer.succeed("Frank")
+
+ (code, response) = yield self.mock_resource.trigger("GET",
+ "/profile/%s/displayname" % (myid), None)
+
+ self.assertEquals(200, code)
+ self.assertEquals({"displayname": "Frank"}, response)
+ self.assertEquals(mocked_get.call_args[0][0].localpart, "1234ABCD")
+
+ @defer.inlineCallbacks
+ def test_set_my_name(self):
+ mocked_set = self.mock_handler.set_displayname
+ mocked_set.return_value = defer.succeed(())
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ "/profile/%s/displayname" % (myid),
+ '{"displayname": "Frank Jr."}')
+
+ self.assertEquals(200, code)
+ self.assertEquals(mocked_set.call_args[0][0].localpart, "1234ABCD")
+ self.assertEquals(mocked_set.call_args[0][1].localpart, "1234ABCD")
+ self.assertEquals(mocked_set.call_args[0][2], "Frank Jr.")
+
+ @defer.inlineCallbacks
+ def test_set_my_name_noauth(self):
+ mocked_set = self.mock_handler.set_displayname
+ mocked_set.side_effect = AuthError(400, "message")
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ "/profile/%s/displayname" % ("@4567:test"), '"Frank Jr."')
+
+ self.assertTrue(400 <= code < 499,
+ msg="code %d is in the 4xx range" % (code))
+
+ @defer.inlineCallbacks
+ def test_get_other_name(self):
+ mocked_get = self.mock_handler.get_displayname
+ mocked_get.return_value = defer.succeed("Bob")
+
+ (code, response) = yield self.mock_resource.trigger("GET",
+ "/profile/%s/displayname" % ("@opaque:elsewhere"), None)
+
+ self.assertEquals(200, code)
+ self.assertEquals({"displayname": "Bob"}, response)
+
+ @defer.inlineCallbacks
+ def test_set_other_name(self):
+ mocked_set = self.mock_handler.set_displayname
+ mocked_set.side_effect = SynapseError(400, "message")
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ "/profile/%s/displayname" % ("@opaque:elsewhere"), None)
+
+ self.assertTrue(400 <= code <= 499,
+ msg="code %d is in the 4xx range" % (code))
+
+ @defer.inlineCallbacks
+ def test_get_my_avatar(self):
+ mocked_get = self.mock_handler.get_avatar_url
+ mocked_get.return_value = defer.succeed("http://my.server/me.png")
+
+ (code, response) = yield self.mock_resource.trigger("GET",
+ "/profile/%s/avatar_url" % (myid), None)
+
+ self.assertEquals(200, code)
+ self.assertEquals({"avatar_url": "http://my.server/me.png"}, response)
+ self.assertEquals(mocked_get.call_args[0][0].localpart, "1234ABCD")
+
+ @defer.inlineCallbacks
+ def test_set_my_avatar(self):
+ mocked_set = self.mock_handler.set_avatar_url
+ mocked_set.return_value = defer.succeed(())
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ "/profile/%s/avatar_url" % (myid),
+ '{"avatar_url": "http://my.server/pic.gif"}')
+
+ self.assertEquals(200, code)
+ self.assertEquals(mocked_set.call_args[0][0].localpart, "1234ABCD")
+ self.assertEquals(mocked_set.call_args[0][1].localpart, "1234ABCD")
+ self.assertEquals(mocked_set.call_args[0][2],
+ "http://my.server/pic.gif")
diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py
new file mode 100644
index 00000000..77493780
--- /dev/null
+++ b/tests/rest/client/v1/test_rooms.py
@@ -0,0 +1,1052 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests REST events for /rooms paths."""
+
+# twisted imports
+from twisted.internet import defer
+
+import synapse.rest.client.v1.room
+from synapse.api.constants import Membership
+
+from synapse.types import UserID
+
+import json
+import urllib
+
+from ....utils import MockHttpResource, setup_test_homeserver
+from .utils import RestTestCase
+
+from mock import Mock, NonCallableMock
+
+PATH_PREFIX = "/_matrix/client/api/v1"
+
+
+class RoomPermissionsTestCase(RestTestCase):
+ """ Tests room permissions. """
+ user_id = "@sid1:red"
+ rmcreator_id = "@notme:red"
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
+
+ hs = yield setup_test_homeserver(
+ "red",
+ http_client=None,
+ replication_layer=Mock(),
+ ratelimiter=NonCallableMock(spec_set=["send_message"]),
+ )
+ self.ratelimiter = hs.get_ratelimiter()
+ self.ratelimiter.send_message.return_value = (True, 0)
+
+ hs.get_handlers().federation_handler = Mock()
+
+ def _get_user_by_access_token(token=None, allow_guest=False):
+ return {
+ "user": UserID.from_string(self.auth_user_id),
+ "token_id": 1,
+ "is_guest": False,
+ }
+ hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
+
+ def _insert_client_ip(*args, **kwargs):
+ return defer.succeed(None)
+ hs.get_datastore().insert_client_ip = _insert_client_ip
+
+ self.auth_user_id = self.rmcreator_id
+
+ synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
+
+ self.auth = hs.get_v1auth()
+
+ # create some rooms under the name rmcreator_id
+ self.uncreated_rmid = "!aa:test"
+
+ self.created_rmid = yield self.create_room_as(self.rmcreator_id,
+ is_public=False)
+
+ self.created_public_rmid = yield self.create_room_as(self.rmcreator_id,
+ is_public=True)
+
+ # send a message in one of the rooms
+ self.created_rmid_msg_path = ("/rooms/%s/send/m.room.message/a1" %
+ (self.created_rmid))
+ (code, response) = yield self.mock_resource.trigger(
+ "PUT",
+ self.created_rmid_msg_path,
+ '{"msgtype":"m.text","body":"test msg"}')
+ self.assertEquals(200, code, msg=str(response))
+
+ # set topic for public room
+ (code, response) = yield self.mock_resource.trigger(
+ "PUT",
+ "/rooms/%s/state/m.room.topic" % self.created_public_rmid,
+ '{"topic":"Public Room Topic"}')
+ self.assertEquals(200, code, msg=str(response))
+
+ # auth as user_id now
+ self.auth_user_id = self.user_id
+
+ def tearDown(self):
+ pass
+
+# @defer.inlineCallbacks
+# def test_get_message(self):
+# # get message in uncreated room, expect 403
+# (code, response) = yield self.mock_resource.trigger_get(
+# "/rooms/noroom/messages/someid/m1")
+# self.assertEquals(403, code, msg=str(response))
+#
+# # get message in created room not joined (no state), expect 403
+# (code, response) = yield self.mock_resource.trigger_get(
+# self.created_rmid_msg_path)
+# self.assertEquals(403, code, msg=str(response))
+#
+# # get message in created room and invited, expect 403
+# yield self.invite(room=self.created_rmid, src=self.rmcreator_id,
+# targ=self.user_id)
+# (code, response) = yield self.mock_resource.trigger_get(
+# self.created_rmid_msg_path)
+# self.assertEquals(403, code, msg=str(response))
+#
+# # get message in created room and joined, expect 200
+# yield self.join(room=self.created_rmid, user=self.user_id)
+# (code, response) = yield self.mock_resource.trigger_get(
+# self.created_rmid_msg_path)
+# self.assertEquals(200, code, msg=str(response))
+#
+# # get message in created room and left, expect 403
+# yield self.leave(room=self.created_rmid, user=self.user_id)
+# (code, response) = yield self.mock_resource.trigger_get(
+# self.created_rmid_msg_path)
+# self.assertEquals(403, code, msg=str(response))
+
+ @defer.inlineCallbacks
+ def test_send_message(self):
+ msg_content = '{"msgtype":"m.text","body":"hello"}'
+ send_msg_path = (
+ "/rooms/%s/send/m.room.message/mid1" % (self.created_rmid,)
+ )
+
+ # send message in uncreated room, expect 403
+ (code, response) = yield self.mock_resource.trigger(
+ "PUT",
+ "/rooms/%s/send/m.room.message/mid2" % (self.uncreated_rmid,),
+ msg_content
+ )
+ self.assertEquals(403, code, msg=str(response))
+
+ # send message in created room not joined (no state), expect 403
+ (code, response) = yield self.mock_resource.trigger(
+ "PUT",
+ send_msg_path,
+ msg_content
+ )
+ self.assertEquals(403, code, msg=str(response))
+
+ # send message in created room and invited, expect 403
+ yield self.invite(
+ room=self.created_rmid,
+ src=self.rmcreator_id,
+ targ=self.user_id
+ )
+ (code, response) = yield self.mock_resource.trigger(
+ "PUT",
+ send_msg_path,
+ msg_content
+ )
+ self.assertEquals(403, code, msg=str(response))
+
+ # send message in created room and joined, expect 200
+ yield self.join(room=self.created_rmid, user=self.user_id)
+ (code, response) = yield self.mock_resource.trigger(
+ "PUT",
+ send_msg_path,
+ msg_content
+ )
+ self.assertEquals(200, code, msg=str(response))
+
+ # send message in created room and left, expect 403
+ yield self.leave(room=self.created_rmid, user=self.user_id)
+ (code, response) = yield self.mock_resource.trigger(
+ "PUT",
+ send_msg_path,
+ msg_content
+ )
+ self.assertEquals(403, code, msg=str(response))
+
+ @defer.inlineCallbacks
+ def test_topic_perms(self):
+ topic_content = '{"topic":"My Topic Name"}'
+ topic_path = "/rooms/%s/state/m.room.topic" % self.created_rmid
+
+ # set/get topic in uncreated room, expect 403
+ (code, response) = yield self.mock_resource.trigger(
+ "PUT", "/rooms/%s/state/m.room.topic" % self.uncreated_rmid,
+ topic_content)
+ self.assertEquals(403, code, msg=str(response))
+ (code, response) = yield self.mock_resource.trigger_get(
+ "/rooms/%s/state/m.room.topic" % self.uncreated_rmid)
+ self.assertEquals(403, code, msg=str(response))
+
+ # set/get topic in created PRIVATE room not joined, expect 403
+ (code, response) = yield self.mock_resource.trigger(
+ "PUT", topic_path, topic_content)
+ self.assertEquals(403, code, msg=str(response))
+ (code, response) = yield self.mock_resource.trigger_get(topic_path)
+ self.assertEquals(403, code, msg=str(response))
+
+ # set topic in created PRIVATE room and invited, expect 403
+ yield self.invite(room=self.created_rmid, src=self.rmcreator_id,
+ targ=self.user_id)
+ (code, response) = yield self.mock_resource.trigger(
+ "PUT", topic_path, topic_content)
+ self.assertEquals(403, code, msg=str(response))
+
+ # get topic in created PRIVATE room and invited, expect 403
+ (code, response) = yield self.mock_resource.trigger_get(topic_path)
+ self.assertEquals(403, code, msg=str(response))
+
+ # set/get topic in created PRIVATE room and joined, expect 200
+ yield self.join(room=self.created_rmid, user=self.user_id)
+
+ # Only room ops can set topic by default
+ self.auth_user_id = self.rmcreator_id
+ (code, response) = yield self.mock_resource.trigger(
+ "PUT", topic_path, topic_content)
+ self.assertEquals(200, code, msg=str(response))
+ self.auth_user_id = self.user_id
+
+ (code, response) = yield self.mock_resource.trigger_get(topic_path)
+ self.assertEquals(200, code, msg=str(response))
+ self.assert_dict(json.loads(topic_content), response)
+
+ # set/get topic in created PRIVATE room and left, expect 403
+ yield self.leave(room=self.created_rmid, user=self.user_id)
+ (code, response) = yield self.mock_resource.trigger(
+ "PUT", topic_path, topic_content)
+ self.assertEquals(403, code, msg=str(response))
+ (code, response) = yield self.mock_resource.trigger_get(topic_path)
+ self.assertEquals(200, code, msg=str(response))
+
+ # get topic in PUBLIC room, not joined, expect 403
+ (code, response) = yield self.mock_resource.trigger_get(
+ "/rooms/%s/state/m.room.topic" % self.created_public_rmid)
+ self.assertEquals(403, code, msg=str(response))
+
+ # set topic in PUBLIC room, not joined, expect 403
+ (code, response) = yield self.mock_resource.trigger(
+ "PUT",
+ "/rooms/%s/state/m.room.topic" % self.created_public_rmid,
+ topic_content)
+ self.assertEquals(403, code, msg=str(response))
+
+ @defer.inlineCallbacks
+ def _test_get_membership(self, room=None, members=[], expect_code=None):
+ path = "/rooms/%s/state/m.room.member/%s"
+ for member in members:
+ (code, response) = yield self.mock_resource.trigger_get(
+ path %
+ (room, member))
+ self.assertEquals(expect_code, code)
+
+ @defer.inlineCallbacks
+ def test_membership_basic_room_perms(self):
+ # === room does not exist ===
+ room = self.uncreated_rmid
+ # get membership of self, get membership of other, uncreated room
+ # expect all 403s
+ yield self._test_get_membership(
+ members=[self.user_id, self.rmcreator_id],
+ room=room, expect_code=403)
+
+ # trying to invite people to this room should 403
+ yield self.invite(room=room, src=self.user_id, targ=self.rmcreator_id,
+ expect_code=403)
+
+ # set [invite/join/left] of self, set [invite/join/left] of other,
+ # expect all 404s because room doesn't exist on any server
+ for usr in [self.user_id, self.rmcreator_id]:
+ yield self.join(room=room, user=usr, expect_code=404)
+ yield self.leave(room=room, user=usr, expect_code=404)
+
+ @defer.inlineCallbacks
+ def test_membership_private_room_perms(self):
+ room = self.created_rmid
+ # get membership of self, get membership of other, private room + invite
+ # expect all 403s
+ yield self.invite(room=room, src=self.rmcreator_id,
+ targ=self.user_id)
+ yield self._test_get_membership(
+ members=[self.user_id, self.rmcreator_id],
+ room=room, expect_code=403)
+
+ # get membership of self, get membership of other, private room + joined
+ # expect all 200s
+ yield self.join(room=room, user=self.user_id)
+ yield self._test_get_membership(
+ members=[self.user_id, self.rmcreator_id],
+ room=room, expect_code=200)
+
+ # get membership of self, get membership of other, private room + left
+ # expect all 200s
+ yield self.leave(room=room, user=self.user_id)
+ yield self._test_get_membership(
+ members=[self.user_id, self.rmcreator_id],
+ room=room, expect_code=200)
+
+ @defer.inlineCallbacks
+ def test_membership_public_room_perms(self):
+ room = self.created_public_rmid
+ # get membership of self, get membership of other, public room + invite
+ # expect 403
+ yield self.invite(room=room, src=self.rmcreator_id,
+ targ=self.user_id)
+ yield self._test_get_membership(
+ members=[self.user_id, self.rmcreator_id],
+ room=room, expect_code=403)
+
+ # get membership of self, get membership of other, public room + joined
+ # expect all 200s
+ yield self.join(room=room, user=self.user_id)
+ yield self._test_get_membership(
+ members=[self.user_id, self.rmcreator_id],
+ room=room, expect_code=200)
+
+ # get membership of self, get membership of other, public room + left
+ # expect 200.
+ yield self.leave(room=room, user=self.user_id)
+ yield self._test_get_membership(
+ members=[self.user_id, self.rmcreator_id],
+ room=room, expect_code=200)
+
+ @defer.inlineCallbacks
+ def test_invited_permissions(self):
+ room = self.created_rmid
+ yield self.invite(room=room, src=self.rmcreator_id, targ=self.user_id)
+
+ # set [invite/join/left] of other user, expect 403s
+ yield self.invite(room=room, src=self.user_id, targ=self.rmcreator_id,
+ expect_code=403)
+ yield self.change_membership(room=room, src=self.user_id,
+ targ=self.rmcreator_id,
+ membership=Membership.JOIN,
+ expect_code=403)
+ yield self.change_membership(room=room, src=self.user_id,
+ targ=self.rmcreator_id,
+ membership=Membership.LEAVE,
+ expect_code=403)
+
+ @defer.inlineCallbacks
+ def test_joined_permissions(self):
+ room = self.created_rmid
+ yield self.invite(room=room, src=self.rmcreator_id, targ=self.user_id)
+ yield self.join(room=room, user=self.user_id)
+
+ # set invited of self, expect 403
+ yield self.invite(room=room, src=self.user_id, targ=self.user_id,
+ expect_code=403)
+
+ # set joined of self, expect 200 (NOOP)
+ yield self.join(room=room, user=self.user_id)
+
+ other = "@burgundy:red"
+ # set invited of other, expect 200
+ yield self.invite(room=room, src=self.user_id, targ=other,
+ expect_code=200)
+
+ # set joined of other, expect 403
+ yield self.change_membership(room=room, src=self.user_id,
+ targ=other,
+ membership=Membership.JOIN,
+ expect_code=403)
+
+ # set left of other, expect 403
+ yield self.change_membership(room=room, src=self.user_id,
+ targ=other,
+ membership=Membership.LEAVE,
+ expect_code=403)
+
+ # set left of self, expect 200
+ yield self.leave(room=room, user=self.user_id)
+
+ @defer.inlineCallbacks
+ def test_leave_permissions(self):
+ room = self.created_rmid
+ yield self.invite(room=room, src=self.rmcreator_id, targ=self.user_id)
+ yield self.join(room=room, user=self.user_id)
+ yield self.leave(room=room, user=self.user_id)
+
+ # set [invite/join/left] of self, set [invite/join/left] of other,
+ # expect all 403s
+ for usr in [self.user_id, self.rmcreator_id]:
+ yield self.change_membership(
+ room=room,
+ src=self.user_id,
+ targ=usr,
+ membership=Membership.INVITE,
+ expect_code=403
+ )
+
+ yield self.change_membership(
+ room=room,
+ src=self.user_id,
+ targ=usr,
+ membership=Membership.JOIN,
+ expect_code=403
+ )
+
+ # It is always valid to LEAVE if you've already left (currently.)
+ yield self.change_membership(
+ room=room,
+ src=self.user_id,
+ targ=self.rmcreator_id,
+ membership=Membership.LEAVE,
+ expect_code=403
+ )
+
+
+class RoomsMemberListTestCase(RestTestCase):
+ """ Tests /rooms/$room_id/members/list REST events."""
+ user_id = "@sid1:red"
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
+
+ hs = yield setup_test_homeserver(
+ "red",
+ http_client=None,
+ replication_layer=Mock(),
+ ratelimiter=NonCallableMock(spec_set=["send_message"]),
+ )
+ self.ratelimiter = hs.get_ratelimiter()
+ self.ratelimiter.send_message.return_value = (True, 0)
+
+ hs.get_handlers().federation_handler = Mock()
+
+ self.auth_user_id = self.user_id
+
+ def _get_user_by_access_token(token=None, allow_guest=False):
+ return {
+ "user": UserID.from_string(self.auth_user_id),
+ "token_id": 1,
+ "is_guest": False,
+ }
+ hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
+
+ def _insert_client_ip(*args, **kwargs):
+ return defer.succeed(None)
+ hs.get_datastore().insert_client_ip = _insert_client_ip
+
+ synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
+
+ def tearDown(self):
+ pass
+
+ @defer.inlineCallbacks
+ def test_get_member_list(self):
+ room_id = yield self.create_room_as(self.user_id)
+ (code, response) = yield self.mock_resource.trigger_get(
+ "/rooms/%s/members" % room_id)
+ self.assertEquals(200, code, msg=str(response))
+
+ @defer.inlineCallbacks
+ def test_get_member_list_no_room(self):
+ (code, response) = yield self.mock_resource.trigger_get(
+ "/rooms/roomdoesnotexist/members")
+ self.assertEquals(403, code, msg=str(response))
+
+ @defer.inlineCallbacks
+ def test_get_member_list_no_permission(self):
+ room_id = yield self.create_room_as("@some_other_guy:red")
+ (code, response) = yield self.mock_resource.trigger_get(
+ "/rooms/%s/members" % room_id)
+ self.assertEquals(403, code, msg=str(response))
+
+ @defer.inlineCallbacks
+ def test_get_member_list_mixed_memberships(self):
+ room_creator = "@some_other_guy:red"
+ room_id = yield self.create_room_as(room_creator)
+ room_path = "/rooms/%s/members" % room_id
+ yield self.invite(room=room_id, src=room_creator,
+ targ=self.user_id)
+ # can't see list if you're just invited.
+ (code, response) = yield self.mock_resource.trigger_get(room_path)
+ self.assertEquals(403, code, msg=str(response))
+
+ yield self.join(room=room_id, user=self.user_id)
+ # can see list now joined
+ (code, response) = yield self.mock_resource.trigger_get(room_path)
+ self.assertEquals(200, code, msg=str(response))
+
+ yield self.leave(room=room_id, user=self.user_id)
+ # can see old list once left
+ (code, response) = yield self.mock_resource.trigger_get(room_path)
+ self.assertEquals(200, code, msg=str(response))
+
+
+class RoomsCreateTestCase(RestTestCase):
+ """ Tests /rooms and /rooms/$room_id REST events. """
+ user_id = "@sid1:red"
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
+ self.auth_user_id = self.user_id
+
+ hs = yield setup_test_homeserver(
+ "red",
+ http_client=None,
+ replication_layer=Mock(),
+ ratelimiter=NonCallableMock(spec_set=["send_message"]),
+ )
+ self.ratelimiter = hs.get_ratelimiter()
+ self.ratelimiter.send_message.return_value = (True, 0)
+
+ hs.get_handlers().federation_handler = Mock()
+
+ def _get_user_by_access_token(token=None, allow_guest=False):
+ return {
+ "user": UserID.from_string(self.auth_user_id),
+ "token_id": 1,
+ "is_guest": False,
+ }
+ hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
+
+ def _insert_client_ip(*args, **kwargs):
+ return defer.succeed(None)
+ hs.get_datastore().insert_client_ip = _insert_client_ip
+
+ synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
+
+ def tearDown(self):
+ pass
+
+ @defer.inlineCallbacks
+ def test_post_room_no_keys(self):
+ # POST with no config keys, expect new room id
+ (code, response) = yield self.mock_resource.trigger("POST",
+ "/createRoom",
+ "{}")
+ self.assertEquals(200, code, response)
+ self.assertTrue("room_id" in response)
+
+ @defer.inlineCallbacks
+ def test_post_room_visibility_key(self):
+ # POST with visibility config key, expect new room id
+ (code, response) = yield self.mock_resource.trigger(
+ "POST",
+ "/createRoom",
+ '{"visibility":"private"}')
+ self.assertEquals(200, code)
+ self.assertTrue("room_id" in response)
+
+ @defer.inlineCallbacks
+ def test_post_room_custom_key(self):
+ # POST with custom config keys, expect new room id
+ (code, response) = yield self.mock_resource.trigger(
+ "POST",
+ "/createRoom",
+ '{"custom":"stuff"}')
+ self.assertEquals(200, code)
+ self.assertTrue("room_id" in response)
+
+ @defer.inlineCallbacks
+ def test_post_room_known_and_unknown_keys(self):
+ # POST with custom + known config keys, expect new room id
+ (code, response) = yield self.mock_resource.trigger(
+ "POST",
+ "/createRoom",
+ '{"visibility":"private","custom":"things"}')
+ self.assertEquals(200, code)
+ self.assertTrue("room_id" in response)
+
+ @defer.inlineCallbacks
+ def test_post_room_invalid_content(self):
+ # POST with invalid content / paths, expect 400
+ (code, response) = yield self.mock_resource.trigger(
+ "POST",
+ "/createRoom",
+ '{"visibili')
+ self.assertEquals(400, code)
+
+ (code, response) = yield self.mock_resource.trigger(
+ "POST",
+ "/createRoom",
+ '["hello"]')
+ self.assertEquals(400, code)
+
+
+class RoomTopicTestCase(RestTestCase):
+ """ Tests /rooms/$room_id/topic REST events. """
+ user_id = "@sid1:red"
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
+ self.auth_user_id = self.user_id
+
+ hs = yield setup_test_homeserver(
+ "red",
+ http_client=None,
+ replication_layer=Mock(),
+ ratelimiter=NonCallableMock(spec_set=["send_message"]),
+ )
+ self.ratelimiter = hs.get_ratelimiter()
+ self.ratelimiter.send_message.return_value = (True, 0)
+
+ hs.get_handlers().federation_handler = Mock()
+
+ def _get_user_by_access_token(token=None, allow_guest=False):
+ return {
+ "user": UserID.from_string(self.auth_user_id),
+ "token_id": 1,
+ "is_guest": False,
+ }
+
+ hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
+
+ def _insert_client_ip(*args, **kwargs):
+ return defer.succeed(None)
+ hs.get_datastore().insert_client_ip = _insert_client_ip
+
+ synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
+
+ # create the room
+ self.room_id = yield self.create_room_as(self.user_id)
+ self.path = "/rooms/%s/state/m.room.topic" % (self.room_id,)
+
+ def tearDown(self):
+ pass
+
+ @defer.inlineCallbacks
+ def test_invalid_puts(self):
+ # missing keys or invalid json
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ self.path, '{}')
+ self.assertEquals(400, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ self.path, '{"_name":"bob"}')
+ self.assertEquals(400, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ self.path, '{"nao')
+ self.assertEquals(400, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ self.path, '[{"_name":"bob"},{"_name":"jill"}]')
+ self.assertEquals(400, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ self.path, 'text only')
+ self.assertEquals(400, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ self.path, '')
+ self.assertEquals(400, code, msg=str(response))
+
+ # valid key, wrong type
+ content = '{"topic":["Topic name"]}'
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ self.path, content)
+ self.assertEquals(400, code, msg=str(response))
+
+ @defer.inlineCallbacks
+ def test_rooms_topic(self):
+ # nothing should be there
+ (code, response) = yield self.mock_resource.trigger_get(self.path)
+ self.assertEquals(404, code, msg=str(response))
+
+ # valid put
+ content = '{"topic":"Topic name"}'
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ self.path, content)
+ self.assertEquals(200, code, msg=str(response))
+
+ # valid get
+ (code, response) = yield self.mock_resource.trigger_get(self.path)
+ self.assertEquals(200, code, msg=str(response))
+ self.assert_dict(json.loads(content), response)
+
+ @defer.inlineCallbacks
+ def test_rooms_topic_with_extra_keys(self):
+ # valid put with extra keys
+ content = '{"topic":"Seasons","subtopic":"Summer"}'
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ self.path, content)
+ self.assertEquals(200, code, msg=str(response))
+
+ # valid get
+ (code, response) = yield self.mock_resource.trigger_get(self.path)
+ self.assertEquals(200, code, msg=str(response))
+ self.assert_dict(json.loads(content), response)
+
+
+class RoomMemberStateTestCase(RestTestCase):
+ """ Tests /rooms/$room_id/members/$user_id/state REST events. """
+ user_id = "@sid1:red"
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
+ self.auth_user_id = self.user_id
+
+ hs = yield setup_test_homeserver(
+ "red",
+ http_client=None,
+ replication_layer=Mock(),
+ ratelimiter=NonCallableMock(spec_set=["send_message"]),
+ )
+ self.ratelimiter = hs.get_ratelimiter()
+ self.ratelimiter.send_message.return_value = (True, 0)
+
+ hs.get_handlers().federation_handler = Mock()
+
+ def _get_user_by_access_token(token=None, allow_guest=False):
+ return {
+ "user": UserID.from_string(self.auth_user_id),
+ "token_id": 1,
+ "is_guest": False,
+ }
+ hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
+
+ def _insert_client_ip(*args, **kwargs):
+ return defer.succeed(None)
+ hs.get_datastore().insert_client_ip = _insert_client_ip
+
+ synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
+
+ self.room_id = yield self.create_room_as(self.user_id)
+
+ def tearDown(self):
+ pass
+
+ @defer.inlineCallbacks
+ def test_invalid_puts(self):
+ path = "/rooms/%s/state/m.room.member/%s" % (self.room_id, self.user_id)
+ # missing keys or invalid json
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ path, '{}')
+ self.assertEquals(400, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ path, '{"_name":"bob"}')
+ self.assertEquals(400, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ path, '{"nao')
+ self.assertEquals(400, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ path, '[{"_name":"bob"},{"_name":"jill"}]')
+ self.assertEquals(400, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ path, 'text only')
+ self.assertEquals(400, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ path, '')
+ self.assertEquals(400, code, msg=str(response))
+
+ # valid keys, wrong types
+ content = ('{"membership":["%s","%s","%s"]}' %
+ (Membership.INVITE, Membership.JOIN, Membership.LEAVE))
+ (code, response) = yield self.mock_resource.trigger("PUT", path, content)
+ self.assertEquals(400, code, msg=str(response))
+
+ @defer.inlineCallbacks
+ def test_rooms_members_self(self):
+ path = "/rooms/%s/state/m.room.member/%s" % (
+ urllib.quote(self.room_id), self.user_id
+ )
+
+ # valid join message (NOOP since we made the room)
+ content = '{"membership":"%s"}' % Membership.JOIN
+ (code, response) = yield self.mock_resource.trigger("PUT", path, content)
+ self.assertEquals(200, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("GET", path, None)
+ self.assertEquals(200, code, msg=str(response))
+
+ expected_response = {
+ "membership": Membership.JOIN,
+ }
+ self.assertEquals(expected_response, response)
+
+ @defer.inlineCallbacks
+ def test_rooms_members_other(self):
+ self.other_id = "@zzsid1:red"
+ path = "/rooms/%s/state/m.room.member/%s" % (
+ urllib.quote(self.room_id), self.other_id
+ )
+
+ # valid invite message
+ content = '{"membership":"%s"}' % Membership.INVITE
+ (code, response) = yield self.mock_resource.trigger("PUT", path, content)
+ self.assertEquals(200, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("GET", path, None)
+ self.assertEquals(200, code, msg=str(response))
+ self.assertEquals(json.loads(content), response)
+
+ @defer.inlineCallbacks
+ def test_rooms_members_other_custom_keys(self):
+ self.other_id = "@zzsid1:red"
+ path = "/rooms/%s/state/m.room.member/%s" % (
+ urllib.quote(self.room_id), self.other_id
+ )
+
+ # valid invite message with custom key
+ content = ('{"membership":"%s","invite_text":"%s"}' %
+ (Membership.INVITE, "Join us!"))
+ (code, response) = yield self.mock_resource.trigger("PUT", path, content)
+ self.assertEquals(200, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("GET", path, None)
+ self.assertEquals(200, code, msg=str(response))
+ self.assertEquals(json.loads(content), response)
+
+
+class RoomMessagesTestCase(RestTestCase):
+ """ Tests /rooms/$room_id/messages/$user_id/$msg_id REST events. """
+ user_id = "@sid1:red"
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
+ self.auth_user_id = self.user_id
+
+ hs = yield setup_test_homeserver(
+ "red",
+ http_client=None,
+ replication_layer=Mock(),
+ ratelimiter=NonCallableMock(spec_set=["send_message"]),
+ )
+ self.ratelimiter = hs.get_ratelimiter()
+ self.ratelimiter.send_message.return_value = (True, 0)
+
+ hs.get_handlers().federation_handler = Mock()
+
+ def _get_user_by_access_token(token=None, allow_guest=False):
+ return {
+ "user": UserID.from_string(self.auth_user_id),
+ "token_id": 1,
+ "is_guest": False,
+ }
+ hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
+
+ def _insert_client_ip(*args, **kwargs):
+ return defer.succeed(None)
+ hs.get_datastore().insert_client_ip = _insert_client_ip
+
+ synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
+
+ self.room_id = yield self.create_room_as(self.user_id)
+
+ def tearDown(self):
+ pass
+
+ @defer.inlineCallbacks
+ def test_invalid_puts(self):
+ path = "/rooms/%s/send/m.room.message/mid1" % (
+ urllib.quote(self.room_id))
+ # missing keys or invalid json
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ path, '{}')
+ self.assertEquals(400, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ path, '{"_name":"bob"}')
+ self.assertEquals(400, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ path, '{"nao')
+ self.assertEquals(400, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ path, '[{"_name":"bob"},{"_name":"jill"}]')
+ self.assertEquals(400, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ path, 'text only')
+ self.assertEquals(400, code, msg=str(response))
+
+ (code, response) = yield self.mock_resource.trigger("PUT",
+ path, '')
+ self.assertEquals(400, code, msg=str(response))
+
+ @defer.inlineCallbacks
+ def test_rooms_messages_sent(self):
+ path = "/rooms/%s/send/m.room.message/mid1" % (
+ urllib.quote(self.room_id))
+
+ content = '{"body":"test","msgtype":{"type":"a"}}'
+ (code, response) = yield self.mock_resource.trigger("PUT", path, content)
+ self.assertEquals(400, code, msg=str(response))
+
+ # custom message types
+ content = '{"body":"test","msgtype":"test.custom.text"}'
+ (code, response) = yield self.mock_resource.trigger("PUT", path, content)
+ self.assertEquals(200, code, msg=str(response))
+
+# (code, response) = yield self.mock_resource.trigger("GET", path, None)
+# self.assertEquals(200, code, msg=str(response))
+# self.assert_dict(json.loads(content), response)
+
+ # m.text message type
+ path = "/rooms/%s/send/m.room.message/mid2" % (
+ urllib.quote(self.room_id))
+ content = '{"body":"test2","msgtype":"m.text"}'
+ (code, response) = yield self.mock_resource.trigger("PUT", path, content)
+ self.assertEquals(200, code, msg=str(response))
+
+
+class RoomInitialSyncTestCase(RestTestCase):
+ """ Tests /rooms/$room_id/initialSync. """
+ user_id = "@sid1:red"
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
+ self.auth_user_id = self.user_id
+
+ hs = yield setup_test_homeserver(
+ "red",
+ http_client=None,
+ replication_layer=Mock(),
+ ratelimiter=NonCallableMock(spec_set=[
+ "send_message",
+ ]),
+ )
+ self.ratelimiter = hs.get_ratelimiter()
+ self.ratelimiter.send_message.return_value = (True, 0)
+
+ hs.get_handlers().federation_handler = Mock()
+
+ def _get_user_by_access_token(token=None, allow_guest=False):
+ return {
+ "user": UserID.from_string(self.auth_user_id),
+ "token_id": 1,
+ "is_guest": False,
+ }
+ hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
+
+ def _insert_client_ip(*args, **kwargs):
+ return defer.succeed(None)
+ hs.get_datastore().insert_client_ip = _insert_client_ip
+
+ synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
+
+ # Since I'm getting my own presence I need to exist as far as presence
+ # is concerned.
+ hs.get_handlers().presence_handler.registered_user(
+ UserID.from_string(self.user_id)
+ )
+
+ # create the room
+ self.room_id = yield self.create_room_as(self.user_id)
+
+ @defer.inlineCallbacks
+ def test_initial_sync(self):
+ (code, response) = yield self.mock_resource.trigger_get(
+ "/rooms/%s/initialSync" % self.room_id)
+ self.assertEquals(200, code)
+
+ self.assertEquals(self.room_id, response["room_id"])
+ self.assertEquals("join", response["membership"])
+
+ # Room state is easier to assert on if we unpack it into a dict
+ state = {}
+ for event in response["state"]:
+ if "state_key" not in event:
+ continue
+ t = event["type"]
+ if t not in state:
+ state[t] = []
+ state[t].append(event)
+
+ self.assertTrue("m.room.create" in state)
+
+ self.assertTrue("messages" in response)
+ self.assertTrue("chunk" in response["messages"])
+ self.assertTrue("end" in response["messages"])
+
+ self.assertTrue("presence" in response)
+
+ presence_by_user = {e["content"]["user_id"]: e
+ for e in response["presence"]
+ }
+ self.assertTrue(self.user_id in presence_by_user)
+ self.assertEquals("m.presence", presence_by_user[self.user_id]["type"])
+
+
+class RoomMessageListTestCase(RestTestCase):
+ """ Tests /rooms/$room_id/messages REST events. """
+ user_id = "@sid1:red"
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
+ self.auth_user_id = self.user_id
+
+ hs = yield setup_test_homeserver(
+ "red",
+ http_client=None,
+ replication_layer=Mock(),
+ ratelimiter=NonCallableMock(spec_set=["send_message"]),
+ )
+ self.ratelimiter = hs.get_ratelimiter()
+ self.ratelimiter.send_message.return_value = (True, 0)
+
+ hs.get_handlers().federation_handler = Mock()
+
+ def _get_user_by_access_token(token=None, allow_guest=False):
+ return {
+ "user": UserID.from_string(self.auth_user_id),
+ "token_id": 1,
+ "is_guest": False,
+ }
+ hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
+
+ def _insert_client_ip(*args, **kwargs):
+ return defer.succeed(None)
+ hs.get_datastore().insert_client_ip = _insert_client_ip
+
+ synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
+
+ self.room_id = yield self.create_room_as(self.user_id)
+
+ @defer.inlineCallbacks
+ def test_topo_token_is_accepted(self):
+ token = "t1-0_0_0_0_0"
+ (code, response) = yield self.mock_resource.trigger_get(
+ "/rooms/%s/messages?access_token=x&from=%s" %
+ (self.room_id, token))
+ self.assertEquals(200, code)
+ self.assertTrue("start" in response)
+ self.assertEquals(token, response['start'])
+ self.assertTrue("chunk" in response)
+ self.assertTrue("end" in response)
+
+ @defer.inlineCallbacks
+ def test_stream_token_is_rejected(self):
+ (code, response) = yield self.mock_resource.trigger_get(
+ "/rooms/%s/messages?access_token=x&from=s0_0_0_0" %
+ self.room_id)
+ self.assertEquals(400, code)
diff --git a/tests/rest/client/v1/test_typing.py b/tests/rest/client/v1/test_typing.py
new file mode 100644
index 00000000..61b9cc74
--- /dev/null
+++ b/tests/rest/client/v1/test_typing.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests REST events for /rooms paths."""
+
+# twisted imports
+from twisted.internet import defer
+
+import synapse.rest.client.v1.room
+from synapse.types import UserID
+
+from ....utils import MockHttpResource, MockClock, setup_test_homeserver
+from .utils import RestTestCase
+
+from mock import Mock, NonCallableMock
+
+
+PATH_PREFIX = "/_matrix/client/api/v1"
+
+
+class RoomTypingTestCase(RestTestCase):
+ """ Tests /rooms/$room_id/typing/$user_id REST API. """
+ user_id = "@sid:red"
+
+ user = UserID.from_string(user_id)
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.clock = MockClock()
+
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
+ self.auth_user_id = self.user_id
+
+ hs = yield setup_test_homeserver(
+ "red",
+ clock=self.clock,
+ http_client=None,
+ replication_layer=Mock(),
+ ratelimiter=NonCallableMock(spec_set=[
+ "send_message",
+ ]),
+ )
+ self.hs = hs
+
+ self.event_source = hs.get_event_sources().sources["typing"]
+
+ self.ratelimiter = hs.get_ratelimiter()
+ self.ratelimiter.send_message.return_value = (True, 0)
+
+ hs.get_handlers().federation_handler = Mock()
+
+ def _get_user_by_access_token(token=None, allow_guest=False):
+ return {
+ "user": UserID.from_string(self.auth_user_id),
+ "token_id": 1,
+ "is_guest": False,
+ }
+
+ hs.get_v1auth()._get_user_by_access_token = _get_user_by_access_token
+
+ def _insert_client_ip(*args, **kwargs):
+ return defer.succeed(None)
+ hs.get_datastore().insert_client_ip = _insert_client_ip
+
+ def get_room_members(room_id):
+ if room_id == self.room_id:
+ return defer.succeed([self.user])
+ else:
+ return defer.succeed([])
+
+ @defer.inlineCallbacks
+ def fetch_room_distributions_into(room_id, localusers=None,
+ remotedomains=None, ignore_user=None):
+
+ members = yield get_room_members(room_id)
+ for member in members:
+ if ignore_user is not None and member == ignore_user:
+ continue
+
+ if hs.is_mine(member):
+ if localusers is not None:
+ localusers.add(member)
+ else:
+ if remotedomains is not None:
+ remotedomains.add(member.domain)
+ hs.get_handlers().room_member_handler.fetch_room_distributions_into = (
+ fetch_room_distributions_into)
+
+ synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource)
+
+ self.room_id = yield self.create_room_as(self.user_id)
+ # Need another user to make notifications actually work
+ yield self.join(self.room_id, user="@jim:red")
+
+ def tearDown(self):
+ self.hs.get_handlers().typing_notification_handler.tearDown()
+
+ @defer.inlineCallbacks
+ def test_set_typing(self):
+ (code, _) = yield self.mock_resource.trigger("PUT",
+ "/rooms/%s/typing/%s" % (self.room_id, self.user_id),
+ '{"typing": true, "timeout": 30000}'
+ )
+ self.assertEquals(200, code)
+
+ self.assertEquals(self.event_source.get_current_key(), 1)
+ events = yield self.event_source.get_new_events(
+ from_key=0,
+ room_ids=[self.room_id],
+ )
+ self.assertEquals(
+ events[0],
+ [
+ {"type": "m.typing",
+ "room_id": self.room_id,
+ "content": {
+ "user_ids": [self.user_id],
+ }},
+ ]
+ )
+
+ @defer.inlineCallbacks
+ def test_set_not_typing(self):
+ (code, _) = yield self.mock_resource.trigger("PUT",
+ "/rooms/%s/typing/%s" % (self.room_id, self.user_id),
+ '{"typing": false}'
+ )
+ self.assertEquals(200, code)
+
+ @defer.inlineCallbacks
+ def test_typing_timeout(self):
+ (code, _) = yield self.mock_resource.trigger("PUT",
+ "/rooms/%s/typing/%s" % (self.room_id, self.user_id),
+ '{"typing": true, "timeout": 30000}'
+ )
+ self.assertEquals(200, code)
+
+ self.assertEquals(self.event_source.get_current_key(), 1)
+
+ self.clock.advance_time(31);
+
+ self.assertEquals(self.event_source.get_current_key(), 2)
+
+ (code, _) = yield self.mock_resource.trigger("PUT",
+ "/rooms/%s/typing/%s" % (self.room_id, self.user_id),
+ '{"typing": true, "timeout": 30000}'
+ )
+ self.assertEquals(200, code)
+
+ self.assertEquals(self.event_source.get_current_key(), 3)
diff --git a/tests/rest/client/v1/utils.py b/tests/rest/client/v1/utils.py
new file mode 100644
index 00000000..85096a03
--- /dev/null
+++ b/tests/rest/client/v1/utils.py
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# twisted imports
+from twisted.internet import defer
+
+# trial imports
+from tests import unittest
+
+from synapse.api.constants import Membership
+
+import json
+import time
+
+
+class RestTestCase(unittest.TestCase):
+ """Contains extra helper functions to quickly and clearly perform a given
+ REST action, which isn't the focus of the test.
+
+ This subclass assumes there are mock_resource and auth_user_id attributes.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(RestTestCase, self).__init__(*args, **kwargs)
+ self.mock_resource = None
+ self.auth_user_id = None
+
+ @defer.inlineCallbacks
+ def create_room_as(self, room_creator, is_public=True, tok=None):
+ temp_id = self.auth_user_id
+ self.auth_user_id = room_creator
+ path = "/createRoom"
+ content = "{}"
+ if not is_public:
+ content = '{"visibility":"private"}'
+ if tok:
+ path = path + "?access_token=%s" % tok
+ (code, response) = yield self.mock_resource.trigger("POST", path, content)
+ self.assertEquals(200, code, msg=str(response))
+ self.auth_user_id = temp_id
+ defer.returnValue(response["room_id"])
+
+ @defer.inlineCallbacks
+ def invite(self, room=None, src=None, targ=None, expect_code=200, tok=None):
+ yield self.change_membership(room=room, src=src, targ=targ, tok=tok,
+ membership=Membership.INVITE,
+ expect_code=expect_code)
+
+ @defer.inlineCallbacks
+ def join(self, room=None, user=None, expect_code=200, tok=None):
+ yield self.change_membership(room=room, src=user, targ=user, tok=tok,
+ membership=Membership.JOIN,
+ expect_code=expect_code)
+
+ @defer.inlineCallbacks
+ def leave(self, room=None, user=None, expect_code=200, tok=None):
+ yield self.change_membership(room=room, src=user, targ=user, tok=tok,
+ membership=Membership.LEAVE,
+ expect_code=expect_code)
+
+ @defer.inlineCallbacks
+ def change_membership(self, room, src, targ, membership, tok=None,
+ expect_code=200):
+ temp_id = self.auth_user_id
+ self.auth_user_id = src
+
+ path = "/rooms/%s/state/m.room.member/%s" % (room, targ)
+ if tok:
+ path = path + "?access_token=%s" % tok
+
+ data = {
+ "membership": membership
+ }
+
+ (code, response) = yield self.mock_resource.trigger("PUT", path,
+ json.dumps(data))
+ self.assertEquals(expect_code, code, msg=str(response))
+
+ self.auth_user_id = temp_id
+
+ @defer.inlineCallbacks
+ def register(self, user_id):
+ (code, response) = yield self.mock_resource.trigger(
+ "POST",
+ "/register",
+ json.dumps({
+ "user": user_id,
+ "password": "test",
+ "type": "m.login.password"
+ }))
+ self.assertEquals(200, code)
+ defer.returnValue(response)
+
+ @defer.inlineCallbacks
+ def send(self, room_id, body=None, txn_id=None, tok=None,
+ expect_code=200):
+ if txn_id is None:
+ txn_id = "m%s" % (str(time.time()))
+ if body is None:
+ body = "body_text_here"
+
+ path = "/rooms/%s/send/m.room.message/%s" % (room_id, txn_id)
+ content = '{"msgtype":"m.text","body":"%s"}' % body
+ if tok:
+ path = path + "?access_token=%s" % tok
+
+ (code, response) = yield self.mock_resource.trigger("PUT", path, content)
+ self.assertEquals(expect_code, code, msg=str(response))
+
+ def assert_dict(self, required, actual):
+ """Does a partial assert of a dict.
+
+ Args:
+ required (dict): The keys and value which MUST be in 'actual'.
+ actual (dict): The test result. Extra keys will not be checked.
+ """
+ for key in required:
+ self.assertEquals(required[key], actual[key],
+ msg="%s mismatch. %s" % (key, actual))
diff --git a/tests/rest/client/v2_alpha/__init__.py b/tests/rest/client/v2_alpha/__init__.py
new file mode 100644
index 00000000..fa9e17ec
--- /dev/null
+++ b/tests/rest/client/v2_alpha/__init__.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from tests import unittest
+
+from mock import Mock
+
+from ....utils import MockHttpResource, setup_test_homeserver
+
+from synapse.types import UserID
+
+from twisted.internet import defer
+
+
+PATH_PREFIX = "/_matrix/client/v2_alpha"
+
+
+class V2AlphaRestTestCase(unittest.TestCase):
+ # Consumer must define
+ # USER_ID = <some string>
+ # TO_REGISTER = [<list of REST servlets to register>]
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
+
+ hs = yield setup_test_homeserver(
+ datastore=self.make_datastore_mock(),
+ http_client=None,
+ resource_for_client=self.mock_resource,
+ resource_for_federation=self.mock_resource,
+ )
+
+ def _get_user_by_access_token(token=None, allow_guest=False):
+ return {
+ "user": UserID.from_string(self.USER_ID),
+ "token_id": 1,
+ "is_guest": False,
+ }
+ hs.get_auth()._get_user_by_access_token = _get_user_by_access_token
+
+ for r in self.TO_REGISTER:
+ r.register_servlets(hs, self.mock_resource)
+
+ def make_datastore_mock(self):
+ store = Mock(spec=[
+ "insert_client_ip",
+ ])
+ store.get_app_service_by_token = Mock(return_value=None)
+ return store
diff --git a/tests/rest/client/v2_alpha/test_filter.py b/tests/rest/client/v2_alpha/test_filter.py
new file mode 100644
index 00000000..80ddabf8
--- /dev/null
+++ b/tests/rest/client/v2_alpha/test_filter.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from twisted.internet import defer
+
+from mock import Mock
+
+from . import V2AlphaRestTestCase
+
+from synapse.rest.client.v2_alpha import filter
+
+from synapse.api.errors import StoreError
+
+
+class FilterTestCase(V2AlphaRestTestCase):
+ USER_ID = "@apple:test"
+ TO_REGISTER = [filter]
+
+ def make_datastore_mock(self):
+ datastore = super(FilterTestCase, self).make_datastore_mock()
+
+ self._user_filters = {}
+
+ def add_user_filter(user_localpart, definition):
+ filters = self._user_filters.setdefault(user_localpart, [])
+ filter_id = len(filters)
+ filters.append(definition)
+ return defer.succeed(filter_id)
+ datastore.add_user_filter = add_user_filter
+
+ def get_user_filter(user_localpart, filter_id):
+ if user_localpart not in self._user_filters:
+ raise StoreError(404, "No user")
+ filters = self._user_filters[user_localpart]
+ if filter_id >= len(filters):
+ raise StoreError(404, "No filter")
+ return defer.succeed(filters[filter_id])
+ datastore.get_user_filter = get_user_filter
+
+ return datastore
+
+ @defer.inlineCallbacks
+ def test_add_filter(self):
+ (code, response) = yield self.mock_resource.trigger("POST",
+ "/user/%s/filter" % (self.USER_ID),
+ '{"type": ["m.*"]}'
+ )
+ self.assertEquals(200, code)
+ self.assertEquals({"filter_id": "0"}, response)
+
+ self.assertIn("apple", self._user_filters)
+ self.assertEquals(len(self._user_filters["apple"]), 1)
+ self.assertEquals({"type": ["m.*"]}, self._user_filters["apple"][0])
+
+ @defer.inlineCallbacks
+ def test_get_filter(self):
+ self._user_filters["apple"] = [
+ {"type": ["m.*"]}
+ ]
+
+ (code, response) = yield self.mock_resource.trigger("GET",
+ "/user/%s/filter/0" % (self.USER_ID), None
+ )
+ self.assertEquals(200, code)
+ self.assertEquals({"type": ["m.*"]}, response)
+
+ @defer.inlineCallbacks
+ def test_get_filter_no_id(self):
+ self._user_filters["apple"] = [
+ {"type": ["m.*"]}
+ ]
+
+ (code, response) = yield self.mock_resource.trigger("GET",
+ "/user/%s/filter/2" % (self.USER_ID), None
+ )
+ self.assertEquals(404, code)
+
+ @defer.inlineCallbacks
+ def test_get_filter_no_user(self):
+ (code, response) = yield self.mock_resource.trigger("GET",
+ "/user/%s/filter/0" % (self.USER_ID), None
+ )
+ self.assertEquals(404, code)
diff --git a/tests/rest/client/v2_alpha/test_register.py b/tests/rest/client/v2_alpha/test_register.py
new file mode 100644
index 00000000..f9a2b224
--- /dev/null
+++ b/tests/rest/client/v2_alpha/test_register.py
@@ -0,0 +1,135 @@
+from synapse.rest.client.v2_alpha.register import RegisterRestServlet
+from synapse.api.errors import SynapseError
+from twisted.internet import defer
+from mock import Mock, MagicMock
+from tests import unittest
+import json
+
+
+class RegisterRestServletTestCase(unittest.TestCase):
+
+ def setUp(self):
+ # do the dance to hook up request data to self.request_data
+ self.request_data = ""
+ self.request = Mock(
+ content=Mock(read=Mock(side_effect=lambda: self.request_data)),
+ path='/_matrix/api/v2_alpha/register'
+ )
+ self.request.args = {}
+
+ self.appservice = None
+ self.auth = Mock(get_appservice_by_req=Mock(
+ side_effect=lambda x: defer.succeed(self.appservice))
+ )
+
+ self.auth_result = (False, None, None)
+ self.auth_handler = Mock(
+ check_auth=Mock(side_effect=lambda x,y,z: self.auth_result)
+ )
+ self.registration_handler = Mock()
+ self.identity_handler = Mock()
+ self.login_handler = Mock()
+
+ # do the dance to hook it up to the hs global
+ self.handlers = Mock(
+ auth_handler=self.auth_handler,
+ registration_handler=self.registration_handler,
+ identity_handler=self.identity_handler,
+ login_handler=self.login_handler
+ )
+ self.hs = Mock()
+ self.hs.hostname = "superbig~testing~thing.com"
+ self.hs.get_auth = Mock(return_value=self.auth)
+ self.hs.get_handlers = Mock(return_value=self.handlers)
+ self.hs.config.disable_registration = False
+
+ # init the thing we're testing
+ self.servlet = RegisterRestServlet(self.hs)
+
+ @defer.inlineCallbacks
+ def test_POST_appservice_registration_valid(self):
+ user_id = "@kermit:muppet"
+ token = "kermits_access_token"
+ self.request.args = {
+ "access_token": "i_am_an_app_service"
+ }
+ self.request_data = json.dumps({
+ "username": "kermit"
+ })
+ self.appservice = {
+ "id": "1234"
+ }
+ self.registration_handler.appservice_register = Mock(
+ return_value=(user_id, token)
+ )
+ result = yield self.servlet.on_POST(self.request)
+ self.assertEquals(result, (200, {
+ "user_id": user_id,
+ "access_token": token,
+ "home_server": self.hs.hostname
+ }))
+
+ @defer.inlineCallbacks
+ def test_POST_appservice_registration_invalid(self):
+ self.request.args = {
+ "access_token": "i_am_an_app_service"
+ }
+ self.request_data = json.dumps({
+ "username": "kermit"
+ })
+ self.appservice = None # no application service exists
+ result = yield self.servlet.on_POST(self.request)
+ self.assertEquals(result, (401, None))
+
+ def test_POST_bad_password(self):
+ self.request_data = json.dumps({
+ "username": "kermit",
+ "password": 666
+ })
+ d = self.servlet.on_POST(self.request)
+ return self.assertFailure(d, SynapseError)
+
+ def test_POST_bad_username(self):
+ self.request_data = json.dumps({
+ "username": 777,
+ "password": "monkey"
+ })
+ d = self.servlet.on_POST(self.request)
+ return self.assertFailure(d, SynapseError)
+
+ @defer.inlineCallbacks
+ def test_POST_user_valid(self):
+ user_id = "@kermit:muppet"
+ token = "kermits_access_token"
+ self.request_data = json.dumps({
+ "username": "kermit",
+ "password": "monkey"
+ })
+ self.registration_handler.check_username = Mock(return_value=True)
+ self.auth_result = (True, None, {
+ "username": "kermit",
+ "password": "monkey"
+ })
+ self.registration_handler.register = Mock(return_value=(user_id, token))
+
+ result = yield self.servlet.on_POST(self.request)
+ self.assertEquals(result, (200, {
+ "user_id": user_id,
+ "access_token": token,
+ "home_server": self.hs.hostname
+ }))
+
+ def test_POST_disabled_registration(self):
+ self.hs.config.disable_registration = True
+ self.request_data = json.dumps({
+ "username": "kermit",
+ "password": "monkey"
+ })
+ self.registration_handler.check_username = Mock(return_value=True)
+ self.auth_result = (True, None, {
+ "username": "kermit",
+ "password": "monkey"
+ })
+ self.registration_handler.register = Mock(return_value=("@user:id", "t"))
+ d = self.servlet.on_POST(self.request)
+ return self.assertFailure(d, SynapseError)
diff --git a/tests/storage/__init__.py b/tests/storage/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/storage/__init__.py
diff --git a/tests/storage/event_injector.py b/tests/storage/event_injector.py
new file mode 100644
index 00000000..42bd8928
--- /dev/null
+++ b/tests/storage/event_injector.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.api.constants import EventTypes, Membership
+from synapse.types import UserID, RoomID
+
+from tests.utils import setup_test_homeserver
+
+from mock import Mock
+
+
+class EventInjector:
+ def __init__(self, hs):
+ self.hs = hs
+ self.store = hs.get_datastore()
+ self.message_handler = hs.get_handlers().message_handler
+ self.event_builder_factory = hs.get_event_builder_factory()
+
+ @defer.inlineCallbacks
+ def create_room(self, room):
+ builder = self.event_builder_factory.new({
+ "type": EventTypes.Create,
+ "room_id": room.to_string(),
+ "content": {},
+ })
+
+ event, context = yield self.message_handler._create_new_client_event(
+ builder
+ )
+
+ yield self.store.persist_event(event, context)
+
+ @defer.inlineCallbacks
+ def inject_room_member(self, room, user, membership):
+ builder = self.event_builder_factory.new({
+ "type": EventTypes.Member,
+ "sender": user.to_string(),
+ "state_key": user.to_string(),
+ "room_id": room.to_string(),
+ "content": {"membership": membership},
+ })
+
+ event, context = yield self.message_handler._create_new_client_event(
+ builder
+ )
+
+ yield self.store.persist_event(event, context)
+
+ defer.returnValue(event)
+
+ @defer.inlineCallbacks
+ def inject_message(self, room, user, body):
+ builder = self.event_builder_factory.new({
+ "type": EventTypes.Message,
+ "sender": user.to_string(),
+ "state_key": user.to_string(),
+ "room_id": room.to_string(),
+ "content": {"body": body, "msgtype": u"message"},
+ })
+
+ event, context = yield self.message_handler._create_new_client_event(
+ builder
+ )
+
+ yield self.store.persist_event(event, context)
diff --git a/tests/storage/test__base.py b/tests/storage/test__base.py
new file mode 100644
index 00000000..e72cace8
--- /dev/null
+++ b/tests/storage/test__base.py
@@ -0,0 +1,199 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.util.async import ObservableDeferred
+
+from synapse.util.caches.descriptors import Cache, cached
+
+
+class CacheTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.cache = Cache("test")
+
+ def test_empty(self):
+ failed = False
+ try:
+ self.cache.get("foo")
+ except KeyError:
+ failed = True
+
+ self.assertTrue(failed)
+
+ def test_hit(self):
+ self.cache.prefill("foo", 123)
+
+ self.assertEquals(self.cache.get("foo"), 123)
+
+ def test_invalidate(self):
+ self.cache.prefill(("foo",), 123)
+ self.cache.invalidate(("foo",))
+
+ failed = False
+ try:
+ self.cache.get(("foo",))
+ except KeyError:
+ failed = True
+
+ self.assertTrue(failed)
+
+ def test_eviction(self):
+ cache = Cache("test", max_entries=2)
+
+ cache.prefill(1, "one")
+ cache.prefill(2, "two")
+ cache.prefill(3, "three") # 1 will be evicted
+
+ failed = False
+ try:
+ cache.get(1)
+ except KeyError:
+ failed = True
+
+ self.assertTrue(failed)
+
+ cache.get(2)
+ cache.get(3)
+
+ def test_eviction_lru(self):
+ cache = Cache("test", max_entries=2, lru=True)
+
+ cache.prefill(1, "one")
+ cache.prefill(2, "two")
+
+ # Now access 1 again, thus causing 2 to be least-recently used
+ cache.get(1)
+
+ cache.prefill(3, "three")
+
+ failed = False
+ try:
+ cache.get(2)
+ except KeyError:
+ failed = True
+
+ self.assertTrue(failed)
+
+ cache.get(1)
+ cache.get(3)
+
+
+class CacheDecoratorTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def test_passthrough(self):
+ class A(object):
+ @cached()
+ def func(self, key):
+ return key
+
+ a = A()
+
+ self.assertEquals((yield a.func("foo")), "foo")
+ self.assertEquals((yield a.func("bar")), "bar")
+
+ @defer.inlineCallbacks
+ def test_hit(self):
+ callcount = [0]
+
+ class A(object):
+ @cached()
+ def func(self, key):
+ callcount[0] += 1
+ return key
+
+ a = A()
+ yield a.func("foo")
+
+ self.assertEquals(callcount[0], 1)
+
+ self.assertEquals((yield a.func("foo")), "foo")
+ self.assertEquals(callcount[0], 1)
+
+ @defer.inlineCallbacks
+ def test_invalidate(self):
+ callcount = [0]
+
+ class A(object):
+ @cached()
+ def func(self, key):
+ callcount[0] += 1
+ return key
+
+ a = A()
+ yield a.func("foo")
+
+ self.assertEquals(callcount[0], 1)
+
+ a.func.invalidate(("foo",))
+
+ yield a.func("foo")
+
+ self.assertEquals(callcount[0], 2)
+
+ def test_invalidate_missing(self):
+ class A(object):
+ @cached()
+ def func(self, key):
+ return key
+
+ A().func.invalidate(("what",))
+
+ @defer.inlineCallbacks
+ def test_max_entries(self):
+ callcount = [0]
+
+ class A(object):
+ @cached(max_entries=10)
+ def func(self, key):
+ callcount[0] += 1
+ return key
+
+ a = A()
+
+ for k in range(0, 12):
+ yield a.func(k)
+
+ self.assertEquals(callcount[0], 12)
+
+ # There must have been at least 2 evictions, meaning if we calculate
+ # all 12 values again, we must get called at least 2 more times
+ for k in range(0,12):
+ yield a.func(k)
+
+ self.assertTrue(callcount[0] >= 14,
+ msg="Expected callcount >= 14, got %d" % (callcount[0]))
+
+ def test_prefill(self):
+ callcount = [0]
+
+ d = defer.succeed(123)
+
+ class A(object):
+ @cached()
+ def func(self, key):
+ callcount[0] += 1
+ return d
+
+ a = A()
+
+ a.func.prefill(("foo",), ObservableDeferred(d))
+
+ self.assertEquals(a.func("foo").result, d.result)
+ self.assertEquals(callcount[0], 0)
diff --git a/tests/storage/test_appservice.py b/tests/storage/test_appservice.py
new file mode 100644
index 00000000..77376b34
--- /dev/null
+++ b/tests/storage/test_appservice.py
@@ -0,0 +1,407 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from tests import unittest
+from twisted.internet import defer
+
+from tests.utils import setup_test_homeserver
+from synapse.appservice import ApplicationService, ApplicationServiceState
+from synapse.server import HomeServer
+from synapse.storage.appservice import (
+ ApplicationServiceStore, ApplicationServiceTransactionStore
+)
+
+import json
+import os
+import yaml
+from mock import Mock
+from tests.utils import SQLiteMemoryDbPool, MockClock
+
+
+class ApplicationServiceStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.as_yaml_files = []
+ config = Mock(
+ app_service_config_files=self.as_yaml_files
+ )
+ hs = yield setup_test_homeserver(config=config)
+
+ self.as_token = "token1"
+ self.as_url = "some_url"
+ self._add_appservice(self.as_token, self.as_url, "some_hs_token", "bob")
+ self._add_appservice("token2", "some_url", "some_hs_token", "bob")
+ self._add_appservice("token3", "some_url", "some_hs_token", "bob")
+ # must be done after inserts
+ self.store = ApplicationServiceStore(hs)
+
+ def tearDown(self):
+ # TODO: suboptimal that we need to create files for tests!
+ for f in self.as_yaml_files:
+ try:
+ os.remove(f)
+ except:
+ pass
+
+ def _add_appservice(self, as_token, url, hs_token, sender):
+ as_yaml = dict(url=url, as_token=as_token, hs_token=hs_token,
+ sender_localpart=sender, namespaces={})
+ # use the token as the filename
+ with open(as_token, 'w') as outfile:
+ outfile.write(yaml.dump(as_yaml))
+ self.as_yaml_files.append(as_token)
+
+ @defer.inlineCallbacks
+ def test_retrieve_unknown_service_token(self):
+ service = yield self.store.get_app_service_by_token("invalid_token")
+ self.assertEquals(service, None)
+
+ @defer.inlineCallbacks
+ def test_retrieval_of_service(self):
+ stored_service = yield self.store.get_app_service_by_token(
+ self.as_token
+ )
+ self.assertEquals(stored_service.token, self.as_token)
+ self.assertEquals(stored_service.url, self.as_url)
+ self.assertEquals(
+ stored_service.namespaces[ApplicationService.NS_ALIASES],
+ []
+ )
+ self.assertEquals(
+ stored_service.namespaces[ApplicationService.NS_ROOMS],
+ []
+ )
+ self.assertEquals(
+ stored_service.namespaces[ApplicationService.NS_USERS],
+ []
+ )
+
+ @defer.inlineCallbacks
+ def test_retrieval_of_all_services(self):
+ services = yield self.store.get_app_services()
+ self.assertEquals(len(services), 3)
+
+
+class ApplicationServiceTransactionStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.as_yaml_files = []
+
+ config = Mock(
+ app_service_config_files=self.as_yaml_files
+ )
+ hs = yield setup_test_homeserver(config=config)
+ self.db_pool = hs.get_db_pool()
+
+ self.as_list = [
+ {
+ "token": "token1",
+ "url": "https://matrix-as.org",
+ "id": "token1"
+ },
+ {
+ "token": "alpha_tok",
+ "url": "https://alpha.com",
+ "id": "alpha_tok"
+ },
+ {
+ "token": "beta_tok",
+ "url": "https://beta.com",
+ "id": "beta_tok"
+ },
+ {
+ "token": "delta_tok",
+ "url": "https://delta.com",
+ "id": "delta_tok"
+ },
+ ]
+ for s in self.as_list:
+ yield self._add_service(s["url"], s["token"])
+
+ self.as_yaml_files = []
+
+ self.store = TestTransactionStore(hs)
+
+ def _add_service(self, url, as_token):
+ as_yaml = dict(url=url, as_token=as_token, hs_token="something",
+ sender_localpart="a_sender", namespaces={})
+ # use the token as the filename
+ with open(as_token, 'w') as outfile:
+ outfile.write(yaml.dump(as_yaml))
+ self.as_yaml_files.append(as_token)
+
+ def _set_state(self, id, state, txn=None):
+ return self.db_pool.runQuery(
+ "INSERT INTO application_services_state(as_id, state, last_txn) "
+ "VALUES(?,?,?)",
+ (id, state, txn)
+ )
+
+ def _insert_txn(self, as_id, txn_id, events):
+ return self.db_pool.runQuery(
+ "INSERT INTO application_services_txns(as_id, txn_id, event_ids) "
+ "VALUES(?,?,?)",
+ (as_id, txn_id, json.dumps([e.event_id for e in events]))
+ )
+
+ def _set_last_txn(self, as_id, txn_id):
+ return self.db_pool.runQuery(
+ "INSERT INTO application_services_state(as_id, last_txn, state) "
+ "VALUES(?,?,?)",
+ (as_id, txn_id, ApplicationServiceState.UP)
+ )
+
+ @defer.inlineCallbacks
+ def test_get_appservice_state_none(self):
+ service = Mock(id=999)
+ state = yield self.store.get_appservice_state(service)
+ self.assertEquals(None, state)
+
+ @defer.inlineCallbacks
+ def test_get_appservice_state_up(self):
+ yield self._set_state(
+ self.as_list[0]["id"], ApplicationServiceState.UP
+ )
+ service = Mock(id=self.as_list[0]["id"])
+ state = yield self.store.get_appservice_state(service)
+ self.assertEquals(ApplicationServiceState.UP, state)
+
+ @defer.inlineCallbacks
+ def test_get_appservice_state_down(self):
+ yield self._set_state(
+ self.as_list[0]["id"], ApplicationServiceState.UP
+ )
+ yield self._set_state(
+ self.as_list[1]["id"], ApplicationServiceState.DOWN
+ )
+ yield self._set_state(
+ self.as_list[2]["id"], ApplicationServiceState.DOWN
+ )
+ service = Mock(id=self.as_list[1]["id"])
+ state = yield self.store.get_appservice_state(service)
+ self.assertEquals(ApplicationServiceState.DOWN, state)
+
+ @defer.inlineCallbacks
+ def test_get_appservices_by_state_none(self):
+ services = yield self.store.get_appservices_by_state(
+ ApplicationServiceState.DOWN
+ )
+ self.assertEquals(0, len(services))
+
+ @defer.inlineCallbacks
+ def test_set_appservices_state_down(self):
+ service = Mock(id=self.as_list[1]["id"])
+ yield self.store.set_appservice_state(
+ service,
+ ApplicationServiceState.DOWN
+ )
+ rows = yield self.db_pool.runQuery(
+ "SELECT as_id FROM application_services_state WHERE state=?",
+ (ApplicationServiceState.DOWN,)
+ )
+ self.assertEquals(service.id, rows[0][0])
+
+ @defer.inlineCallbacks
+ def test_set_appservices_state_multiple_up(self):
+ service = Mock(id=self.as_list[1]["id"])
+ yield self.store.set_appservice_state(
+ service,
+ ApplicationServiceState.UP
+ )
+ yield self.store.set_appservice_state(
+ service,
+ ApplicationServiceState.DOWN
+ )
+ yield self.store.set_appservice_state(
+ service,
+ ApplicationServiceState.UP
+ )
+ rows = yield self.db_pool.runQuery(
+ "SELECT as_id FROM application_services_state WHERE state=?",
+ (ApplicationServiceState.UP,)
+ )
+ self.assertEquals(service.id, rows[0][0])
+
+ @defer.inlineCallbacks
+ def test_create_appservice_txn_first(self):
+ service = Mock(id=self.as_list[0]["id"])
+ events = [Mock(event_id="e1"), Mock(event_id="e2")]
+ txn = yield self.store.create_appservice_txn(service, events)
+ self.assertEquals(txn.id, 1)
+ self.assertEquals(txn.events, events)
+ self.assertEquals(txn.service, service)
+
+ @defer.inlineCallbacks
+ def test_create_appservice_txn_older_last_txn(self):
+ service = Mock(id=self.as_list[0]["id"])
+ events = [Mock(event_id="e1"), Mock(event_id="e2")]
+ yield self._set_last_txn(service.id, 9643) # AS is falling behind
+ yield self._insert_txn(service.id, 9644, events)
+ yield self._insert_txn(service.id, 9645, events)
+ txn = yield self.store.create_appservice_txn(service, events)
+ self.assertEquals(txn.id, 9646)
+ self.assertEquals(txn.events, events)
+ self.assertEquals(txn.service, service)
+
+ @defer.inlineCallbacks
+ def test_create_appservice_txn_up_to_date_last_txn(self):
+ service = Mock(id=self.as_list[0]["id"])
+ events = [Mock(event_id="e1"), Mock(event_id="e2")]
+ yield self._set_last_txn(service.id, 9643)
+ txn = yield self.store.create_appservice_txn(service, events)
+ self.assertEquals(txn.id, 9644)
+ self.assertEquals(txn.events, events)
+ self.assertEquals(txn.service, service)
+
+ @defer.inlineCallbacks
+ def test_create_appservice_txn_up_fuzzing(self):
+ service = Mock(id=self.as_list[0]["id"])
+ events = [Mock(event_id="e1"), Mock(event_id="e2")]
+ yield self._set_last_txn(service.id, 9643)
+
+ # dump in rows with higher IDs to make sure the queries aren't wrong.
+ yield self._set_last_txn(self.as_list[1]["id"], 119643)
+ yield self._set_last_txn(self.as_list[2]["id"], 9)
+ yield self._set_last_txn(self.as_list[3]["id"], 9643)
+ yield self._insert_txn(self.as_list[1]["id"], 119644, events)
+ yield self._insert_txn(self.as_list[1]["id"], 119645, events)
+ yield self._insert_txn(self.as_list[1]["id"], 119646, events)
+ yield self._insert_txn(self.as_list[2]["id"], 10, events)
+ yield self._insert_txn(self.as_list[3]["id"], 9643, events)
+
+ txn = yield self.store.create_appservice_txn(service, events)
+ self.assertEquals(txn.id, 9644)
+ self.assertEquals(txn.events, events)
+ self.assertEquals(txn.service, service)
+
+ @defer.inlineCallbacks
+ def test_complete_appservice_txn_first_txn(self):
+ service = Mock(id=self.as_list[0]["id"])
+ events = [Mock(event_id="e1"), Mock(event_id="e2")]
+ txn_id = 1
+
+ yield self._insert_txn(service.id, txn_id, events)
+ yield self.store.complete_appservice_txn(txn_id=txn_id, service=service)
+
+ res = yield self.db_pool.runQuery(
+ "SELECT last_txn FROM application_services_state WHERE as_id=?",
+ (service.id,)
+ )
+ self.assertEquals(1, len(res))
+ self.assertEquals(txn_id, res[0][0])
+
+ res = yield self.db_pool.runQuery(
+ "SELECT * FROM application_services_txns WHERE txn_id=?",
+ (txn_id,)
+ )
+ self.assertEquals(0, len(res))
+
+ @defer.inlineCallbacks
+ def test_complete_appservice_txn_existing_in_state_table(self):
+ service = Mock(id=self.as_list[0]["id"])
+ events = [Mock(event_id="e1"), Mock(event_id="e2")]
+ txn_id = 5
+ yield self._set_last_txn(service.id, 4)
+ yield self._insert_txn(service.id, txn_id, events)
+ yield self.store.complete_appservice_txn(txn_id=txn_id, service=service)
+
+ res = yield self.db_pool.runQuery(
+ "SELECT last_txn, state FROM application_services_state WHERE "
+ "as_id=?",
+ (service.id,)
+ )
+ self.assertEquals(1, len(res))
+ self.assertEquals(txn_id, res[0][0])
+ self.assertEquals(ApplicationServiceState.UP, res[0][1])
+
+ res = yield self.db_pool.runQuery(
+ "SELECT * FROM application_services_txns WHERE txn_id=?",
+ (txn_id,)
+ )
+ self.assertEquals(0, len(res))
+
+ @defer.inlineCallbacks
+ def test_get_oldest_unsent_txn_none(self):
+ service = Mock(id=self.as_list[0]["id"])
+
+ txn = yield self.store.get_oldest_unsent_txn(service)
+ self.assertEquals(None, txn)
+
+ @defer.inlineCallbacks
+ def test_get_oldest_unsent_txn(self):
+ service = Mock(id=self.as_list[0]["id"])
+ events = [Mock(event_id="e1"), Mock(event_id="e2")]
+ other_events = [Mock(event_id="e5"), Mock(event_id="e6")]
+
+ # we aren't testing store._base stuff here, so mock this out
+ self.store._get_events_txn = Mock(return_value=events)
+
+ yield self._insert_txn(self.as_list[1]["id"], 9, other_events)
+ yield self._insert_txn(service.id, 10, events)
+ yield self._insert_txn(service.id, 11, other_events)
+ yield self._insert_txn(service.id, 12, other_events)
+
+ txn = yield self.store.get_oldest_unsent_txn(service)
+ self.assertEquals(service, txn.service)
+ self.assertEquals(10, txn.id)
+ self.assertEquals(events, txn.events)
+
+ @defer.inlineCallbacks
+ def test_get_appservices_by_state_single(self):
+ yield self._set_state(
+ self.as_list[0]["id"], ApplicationServiceState.DOWN
+ )
+ yield self._set_state(
+ self.as_list[1]["id"], ApplicationServiceState.UP
+ )
+
+ services = yield self.store.get_appservices_by_state(
+ ApplicationServiceState.DOWN
+ )
+ self.assertEquals(1, len(services))
+ self.assertEquals(self.as_list[0]["id"], services[0].id)
+
+ @defer.inlineCallbacks
+ def test_get_appservices_by_state_multiple(self):
+ yield self._set_state(
+ self.as_list[0]["id"], ApplicationServiceState.DOWN
+ )
+ yield self._set_state(
+ self.as_list[1]["id"], ApplicationServiceState.UP
+ )
+ yield self._set_state(
+ self.as_list[2]["id"], ApplicationServiceState.DOWN
+ )
+ yield self._set_state(
+ self.as_list[3]["id"], ApplicationServiceState.UP
+ )
+
+ services = yield self.store.get_appservices_by_state(
+ ApplicationServiceState.DOWN
+ )
+ self.assertEquals(2, len(services))
+ self.assertEquals(
+ set([self.as_list[2]["id"], self.as_list[0]["id"]]),
+ set([services[0].id, services[1].id])
+ )
+
+
+# required for ApplicationServiceTransactionStoreTestCase tests
+class TestTransactionStore(ApplicationServiceTransactionStore,
+ ApplicationServiceStore):
+
+ def __init__(self, hs):
+ super(TestTransactionStore, self).__init__(hs)
diff --git a/tests/storage/test_background_update.py b/tests/storage/test_background_update.py
new file mode 100644
index 00000000..29289fa9
--- /dev/null
+++ b/tests/storage/test_background_update.py
@@ -0,0 +1,76 @@
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.api.constants import EventTypes
+from synapse.types import UserID, RoomID, RoomAlias
+
+from tests.utils import setup_test_homeserver
+
+from mock import Mock
+
+class BackgroundUpdateTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ hs = yield setup_test_homeserver()
+ self.store = hs.get_datastore()
+ self.clock = hs.get_clock()
+
+ self.update_handler = Mock()
+
+ yield self.store.register_background_update_handler(
+ "test_update", self.update_handler
+ )
+
+ @defer.inlineCallbacks
+ def test_do_background_update(self):
+ desired_count = 1000;
+ duration_ms = 42;
+
+ @defer.inlineCallbacks
+ def update(progress, count):
+ self.clock.advance_time_msec(count * duration_ms)
+ progress = {"my_key": progress["my_key"] + 1}
+ yield self.store.runInteraction(
+ "update_progress",
+ self.store._background_update_progress_txn,
+ "test_update",
+ progress,
+ )
+ defer.returnValue(count)
+
+ self.update_handler.side_effect = update
+
+ yield self.store.start_background_update("test_update", {"my_key": 1})
+
+ self.update_handler.reset_mock()
+ result = yield self.store.do_background_update(
+ duration_ms * desired_count
+ )
+ self.assertIsNotNone(result)
+ self.update_handler.assert_called_once_with(
+ {"my_key": 1}, self.store.DEFAULT_BACKGROUND_BATCH_SIZE
+ )
+
+ @defer.inlineCallbacks
+ def update(progress, count):
+ yield self.store._end_background_update("test_update")
+ defer.returnValue(count)
+
+ self.update_handler.side_effect = update
+
+ self.update_handler.reset_mock()
+ result = yield self.store.do_background_update(
+ duration_ms * desired_count
+ )
+ self.assertIsNotNone(result)
+ self.update_handler.assert_called_once_with(
+ {"my_key": 2}, desired_count
+ )
+
+ self.update_handler.reset_mock()
+ result = yield self.store.do_background_update(
+ duration_ms * desired_count
+ )
+ self.assertIsNone(result)
+ self.assertFalse(self.update_handler.called)
diff --git a/tests/storage/test_base.py b/tests/storage/test_base.py
new file mode 100644
index 00000000..1ddca1da
--- /dev/null
+++ b/tests/storage/test_base.py
@@ -0,0 +1,200 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from mock import Mock, call
+
+from collections import OrderedDict
+
+from synapse.server import HomeServer
+
+from synapse.storage._base import SQLBaseStore
+from synapse.storage.engines import create_engine
+
+
+class SQLBaseStoreTestCase(unittest.TestCase):
+ """ Test the "simple" SQL generating methods in SQLBaseStore. """
+
+ def setUp(self):
+ self.db_pool = Mock(spec=["runInteraction"])
+ self.mock_txn = Mock()
+ self.mock_conn = Mock(spec_set=["cursor", "rollback", "commit"])
+ self.mock_conn.cursor.return_value = self.mock_txn
+ self.mock_conn.rollback.return_value = None
+ # Our fake runInteraction just runs synchronously inline
+
+ def runInteraction(func, *args, **kwargs):
+ return defer.succeed(func(self.mock_txn, *args, **kwargs))
+ self.db_pool.runInteraction = runInteraction
+
+ def runWithConnection(func, *args, **kwargs):
+ return defer.succeed(func(self.mock_conn, *args, **kwargs))
+ self.db_pool.runWithConnection = runWithConnection
+
+ config = Mock()
+ config.event_cache_size = 1
+ hs = HomeServer(
+ "test",
+ db_pool=self.db_pool,
+ config=config,
+ database_engine=create_engine("sqlite3"),
+ )
+
+ self.datastore = SQLBaseStore(hs)
+
+ @defer.inlineCallbacks
+ def test_insert_1col(self):
+ self.mock_txn.rowcount = 1
+
+ yield self.datastore._simple_insert(
+ table="tablename",
+ values={"columname": "Value"}
+ )
+
+ self.mock_txn.execute.assert_called_with(
+ "INSERT INTO tablename (columname) VALUES(?)",
+ ("Value",)
+ )
+
+ @defer.inlineCallbacks
+ def test_insert_3cols(self):
+ self.mock_txn.rowcount = 1
+
+ yield self.datastore._simple_insert(
+ table="tablename",
+ # Use OrderedDict() so we can assert on the SQL generated
+ values=OrderedDict([("colA", 1), ("colB", 2), ("colC", 3)])
+ )
+
+ self.mock_txn.execute.assert_called_with(
+ "INSERT INTO tablename (colA, colB, colC) VALUES(?, ?, ?)",
+ (1, 2, 3,)
+ )
+
+ @defer.inlineCallbacks
+ def test_select_one_1col(self):
+ self.mock_txn.rowcount = 1
+ self.mock_txn.fetchall.return_value = [("Value",)]
+
+ value = yield self.datastore._simple_select_one_onecol(
+ table="tablename",
+ keyvalues={"keycol": "TheKey"},
+ retcol="retcol"
+ )
+
+ self.assertEquals("Value", value)
+ self.mock_txn.execute.assert_called_with(
+ "SELECT retcol FROM tablename WHERE keycol = ?",
+ ["TheKey"]
+ )
+
+ @defer.inlineCallbacks
+ def test_select_one_3col(self):
+ self.mock_txn.rowcount = 1
+ self.mock_txn.fetchone.return_value = (1, 2, 3)
+
+ ret = yield self.datastore._simple_select_one(
+ table="tablename",
+ keyvalues={"keycol": "TheKey"},
+ retcols=["colA", "colB", "colC"]
+ )
+
+ self.assertEquals({"colA": 1, "colB": 2, "colC": 3}, ret)
+ self.mock_txn.execute.assert_called_with(
+ "SELECT colA, colB, colC FROM tablename WHERE keycol = ?",
+ ["TheKey"]
+ )
+
+ @defer.inlineCallbacks
+ def test_select_one_missing(self):
+ self.mock_txn.rowcount = 0
+ self.mock_txn.fetchone.return_value = None
+
+ ret = yield self.datastore._simple_select_one(
+ table="tablename",
+ keyvalues={"keycol": "Not here"},
+ retcols=["colA"],
+ allow_none=True
+ )
+
+ self.assertFalse(ret)
+
+ @defer.inlineCallbacks
+ def test_select_list(self):
+ self.mock_txn.rowcount = 3;
+ self.mock_txn.fetchall.return_value = ((1,), (2,), (3,))
+ self.mock_txn.description = (
+ ("colA", None, None, None, None, None, None),
+ )
+
+ ret = yield self.datastore._simple_select_list(
+ table="tablename",
+ keyvalues={"keycol": "A set"},
+ retcols=["colA"],
+ )
+
+ self.assertEquals([{"colA": 1}, {"colA": 2}, {"colA": 3}], ret)
+ self.mock_txn.execute.assert_called_with(
+ "SELECT colA FROM tablename WHERE keycol = ?",
+ ["A set"]
+ )
+
+ @defer.inlineCallbacks
+ def test_update_one_1col(self):
+ self.mock_txn.rowcount = 1
+
+ yield self.datastore._simple_update_one(
+ table="tablename",
+ keyvalues={"keycol": "TheKey"},
+ updatevalues={"columnname": "New Value"}
+ )
+
+ self.mock_txn.execute.assert_called_with(
+ "UPDATE tablename SET columnname = ? WHERE keycol = ?",
+ ["New Value", "TheKey"]
+ )
+
+ @defer.inlineCallbacks
+ def test_update_one_4cols(self):
+ self.mock_txn.rowcount = 1
+
+ yield self.datastore._simple_update_one(
+ table="tablename",
+ keyvalues=OrderedDict([("colA", 1), ("colB", 2)]),
+ updatevalues=OrderedDict([("colC", 3), ("colD", 4)])
+ )
+
+ self.mock_txn.execute.assert_called_with(
+ "UPDATE tablename SET colC = ?, colD = ? WHERE " +
+ "colA = ? AND colB = ?",
+ [3, 4, 1, 2]
+ )
+
+ @defer.inlineCallbacks
+ def test_delete_one(self):
+ self.mock_txn.rowcount = 1
+
+ yield self.datastore._simple_delete_one(
+ table="tablename",
+ keyvalues={"keycol": "Go away"},
+ )
+
+ self.mock_txn.execute.assert_called_with(
+ "DELETE FROM tablename WHERE keycol = ?",
+ ["Go away"]
+ )
diff --git a/tests/storage/test_directory.py b/tests/storage/test_directory.py
new file mode 100644
index 00000000..b9bfbc00
--- /dev/null
+++ b/tests/storage/test_directory.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.storage.directory import DirectoryStore
+from synapse.types import RoomID, RoomAlias
+
+from tests.utils import setup_test_homeserver
+
+
+class DirectoryStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ hs = yield setup_test_homeserver()
+
+ self.store = DirectoryStore(hs)
+
+ self.room = RoomID.from_string("!abcde:test")
+ self.alias = RoomAlias.from_string("#my-room:test")
+
+ @defer.inlineCallbacks
+ def test_room_to_alias(self):
+ yield self.store.create_room_alias_association(
+ room_alias=self.alias,
+ room_id=self.room.to_string(),
+ servers=["test"],
+ )
+
+ self.assertEquals(
+ ["#my-room:test"],
+ (yield self.store.get_aliases_for_room(self.room.to_string()))
+ )
+
+ @defer.inlineCallbacks
+ def test_alias_to_room(self):
+ yield self.store.create_room_alias_association(
+ room_alias=self.alias,
+ room_id=self.room.to_string(),
+ servers=["test"],
+ )
+
+ self.assertObjectHasAttributes(
+ {
+ "room_id": self.room.to_string(),
+ "servers": ["test"],
+ },
+ (yield self.store.get_association_from_room_alias(self.alias))
+ )
+
+ @defer.inlineCallbacks
+ def test_delete_alias(self):
+ yield self.store.create_room_alias_association(
+ room_alias=self.alias,
+ room_id=self.room.to_string(),
+ servers=["test"],
+ )
+
+ room_id = yield self.store.delete_room_alias(self.alias)
+ self.assertEqual(self.room.to_string(), room_id)
+
+ self.assertIsNone(
+ (yield self.store.get_association_from_room_alias(self.alias))
+ )
diff --git a/tests/storage/test_events.py b/tests/storage/test_events.py
new file mode 100644
index 00000000..31301300
--- /dev/null
+++ b/tests/storage/test_events.py
@@ -0,0 +1,116 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import uuid
+from mock.mock import Mock
+from synapse.types import RoomID, UserID
+
+from tests import unittest
+from twisted.internet import defer
+from tests.storage.event_injector import EventInjector
+
+from tests.utils import setup_test_homeserver
+
+
+class EventsStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ self.hs = yield setup_test_homeserver(
+ resource_for_federation=Mock(),
+ http_client=None,
+ )
+ self.store = self.hs.get_datastore()
+ self.db_pool = self.hs.get_db_pool()
+ self.message_handler = self.hs.get_handlers().message_handler
+ self.event_injector = EventInjector(self.hs)
+
+ @defer.inlineCallbacks
+ def test_count_daily_messages(self):
+ self.db_pool.runQuery("DELETE FROM stats_reporting")
+
+ self.hs.clock.now = 100
+
+ # Never reported before, and nothing which could be reported
+ count = yield self.store.count_daily_messages()
+ self.assertIsNone(count)
+ count = yield self.db_pool.runQuery("SELECT COUNT(*) FROM stats_reporting")
+ self.assertEqual([(0,)], count)
+
+ # Create something to report
+ room = RoomID.from_string("!abc123:test")
+ user = UserID.from_string("@raccoonlover:test")
+ yield self.event_injector.create_room(room)
+
+ self.base_event = yield self._get_last_stream_token()
+
+ yield self.event_injector.inject_message(room, user, "Raccoons are really cute")
+
+ # Never reported before, something could be reported, but isn't because
+ # it isn't old enough.
+ count = yield self.store.count_daily_messages()
+ self.assertIsNone(count)
+ self._assert_stats_reporting(1, self.hs.clock.now)
+
+ # Already reported yesterday, two new events from today.
+ yield self.event_injector.inject_message(room, user, "Yeah they are!")
+ yield self.event_injector.inject_message(room, user, "Incredibly!")
+ self.hs.clock.now += 60 * 60 * 24
+ count = yield self.store.count_daily_messages()
+ self.assertEqual(2, count) # 2 since yesterday
+ self._assert_stats_reporting(3, self.hs.clock.now) # 3 ever
+
+ # Last reported too recently.
+ yield self.event_injector.inject_message(room, user, "Who could disagree?")
+ self.hs.clock.now += 60 * 60 * 22
+ count = yield self.store.count_daily_messages()
+ self.assertIsNone(count)
+ self._assert_stats_reporting(4, self.hs.clock.now)
+
+ # Last reported too long ago
+ yield self.event_injector.inject_message(room, user, "No one.")
+ self.hs.clock.now += 60 * 60 * 26
+ count = yield self.store.count_daily_messages()
+ self.assertIsNone(count)
+ self._assert_stats_reporting(5, self.hs.clock.now)
+
+ # And now let's actually report something
+ yield self.event_injector.inject_message(room, user, "Indeed.")
+ yield self.event_injector.inject_message(room, user, "Indeed.")
+ yield self.event_injector.inject_message(room, user, "Indeed.")
+ # A little over 24 hours is fine :)
+ self.hs.clock.now += (60 * 60 * 24) + 50
+ count = yield self.store.count_daily_messages()
+ self.assertEqual(3, count)
+ self._assert_stats_reporting(8, self.hs.clock.now)
+
+ @defer.inlineCallbacks
+ def _get_last_stream_token(self):
+ rows = yield self.db_pool.runQuery(
+ "SELECT stream_ordering"
+ " FROM events"
+ " ORDER BY stream_ordering DESC"
+ " LIMIT 1"
+ )
+ if not rows:
+ defer.returnValue(0)
+ else:
+ defer.returnValue(rows[0][0])
+
+ @defer.inlineCallbacks
+ def _assert_stats_reporting(self, messages, time):
+ rows = yield self.db_pool.runQuery(
+ "SELECT reported_stream_token, reported_time FROM stats_reporting"
+ )
+ self.assertEqual([(self.base_event + messages, time,)], rows)
diff --git a/tests/storage/test_presence.py b/tests/storage/test_presence.py
new file mode 100644
index 00000000..065eebdb
--- /dev/null
+++ b/tests/storage/test_presence.py
@@ -0,0 +1,161 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.storage.presence import PresenceStore
+from synapse.types import UserID
+
+from tests.utils import setup_test_homeserver, MockClock
+
+
+class PresenceStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ hs = yield setup_test_homeserver(clock=MockClock())
+
+ self.store = PresenceStore(hs)
+
+ self.u_apple = UserID.from_string("@apple:test")
+ self.u_banana = UserID.from_string("@banana:test")
+
+ @defer.inlineCallbacks
+ def test_state(self):
+ yield self.store.create_presence(
+ self.u_apple.localpart
+ )
+
+ state = yield self.store.get_presence_state(
+ self.u_apple.localpart
+ )
+
+ self.assertEquals(
+ {"state": None, "status_msg": None, "mtime": None}, state
+ )
+
+ yield self.store.set_presence_state(
+ self.u_apple.localpart, {"state": "online", "status_msg": "Here"}
+ )
+
+ state = yield self.store.get_presence_state(
+ self.u_apple.localpart
+ )
+
+ self.assertEquals(
+ {"state": "online", "status_msg": "Here", "mtime": 1000000}, state
+ )
+
+ @defer.inlineCallbacks
+ def test_visibility(self):
+ self.assertFalse((yield self.store.is_presence_visible(
+ observed_localpart=self.u_apple.localpart,
+ observer_userid=self.u_banana.to_string(),
+ )))
+
+ yield self.store.allow_presence_visible(
+ observed_localpart=self.u_apple.localpart,
+ observer_userid=self.u_banana.to_string(),
+ )
+
+ self.assertTrue((yield self.store.is_presence_visible(
+ observed_localpart=self.u_apple.localpart,
+ observer_userid=self.u_banana.to_string(),
+ )))
+
+ yield self.store.disallow_presence_visible(
+ observed_localpart=self.u_apple.localpart,
+ observer_userid=self.u_banana.to_string(),
+ )
+
+ self.assertFalse((yield self.store.is_presence_visible(
+ observed_localpart=self.u_apple.localpart,
+ observer_userid=self.u_banana.to_string(),
+ )))
+
+ @defer.inlineCallbacks
+ def test_presence_list(self):
+ self.assertEquals(
+ [],
+ (yield self.store.get_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ ))
+ )
+ self.assertEquals(
+ [],
+ (yield self.store.get_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ accepted=True,
+ ))
+ )
+
+ yield self.store.add_presence_list_pending(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_banana.to_string(),
+ )
+
+ self.assertEquals(
+ [{"observed_user_id": "@banana:test", "accepted": 0}],
+ (yield self.store.get_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ ))
+ )
+ self.assertEquals(
+ [],
+ (yield self.store.get_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ accepted=True,
+ ))
+ )
+
+ yield self.store.set_presence_list_accepted(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_banana.to_string(),
+ )
+
+ self.assertEquals(
+ [{"observed_user_id": "@banana:test", "accepted": 1}],
+ (yield self.store.get_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ ))
+ )
+ self.assertEquals(
+ [{"observed_user_id": "@banana:test", "accepted": 1}],
+ (yield self.store.get_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ accepted=True,
+ ))
+ )
+
+ yield self.store.del_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_banana.to_string(),
+ )
+
+ self.assertEquals(
+ [],
+ (yield self.store.get_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ ))
+ )
+ self.assertEquals(
+ [],
+ (yield self.store.get_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ accepted=True,
+ ))
+ )
diff --git a/tests/storage/test_profile.py b/tests/storage/test_profile.py
new file mode 100644
index 00000000..1fa783f3
--- /dev/null
+++ b/tests/storage/test_profile.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.storage.profile import ProfileStore
+from synapse.types import UserID
+
+from tests.utils import setup_test_homeserver
+
+
+class ProfileStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ hs = yield setup_test_homeserver()
+
+ self.store = ProfileStore(hs)
+
+ self.u_frank = UserID.from_string("@frank:test")
+
+ @defer.inlineCallbacks
+ def test_displayname(self):
+ yield self.store.create_profile(
+ self.u_frank.localpart
+ )
+
+ yield self.store.set_profile_displayname(
+ self.u_frank.localpart, "Frank"
+ )
+
+ self.assertEquals(
+ "Frank",
+ (yield self.store.get_profile_displayname(self.u_frank.localpart))
+ )
+
+ @defer.inlineCallbacks
+ def test_avatar_url(self):
+ yield self.store.create_profile(
+ self.u_frank.localpart
+ )
+
+ yield self.store.set_profile_avatar_url(
+ self.u_frank.localpart, "http://my.site/here"
+ )
+
+ self.assertEquals(
+ "http://my.site/here",
+ (yield self.store.get_profile_avatar_url(self.u_frank.localpart))
+ )
diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py
new file mode 100644
index 00000000..dbf9700e
--- /dev/null
+++ b/tests/storage/test_redaction.py
@@ -0,0 +1,254 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.api.constants import EventTypes, Membership
+from synapse.types import UserID, RoomID
+
+from tests.utils import setup_test_homeserver
+
+from mock import Mock
+
+
+class RedactionTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ hs = yield setup_test_homeserver(
+ resource_for_federation=Mock(),
+ http_client=None,
+ )
+
+ self.store = hs.get_datastore()
+ self.event_builder_factory = hs.get_event_builder_factory()
+ self.handlers = hs.get_handlers()
+ self.message_handler = self.handlers.message_handler
+
+ self.u_alice = UserID.from_string("@alice:test")
+ self.u_bob = UserID.from_string("@bob:test")
+
+ self.room1 = RoomID.from_string("!abc123:test")
+
+ self.depth = 1
+
+ @defer.inlineCallbacks
+ def inject_room_member(self, room, user, membership, replaces_state=None,
+ extra_content={}):
+ content = {"membership": membership}
+ content.update(extra_content)
+ builder = self.event_builder_factory.new({
+ "type": EventTypes.Member,
+ "sender": user.to_string(),
+ "state_key": user.to_string(),
+ "room_id": room.to_string(),
+ "content": content,
+ })
+
+ event, context = yield self.message_handler._create_new_client_event(
+ builder
+ )
+
+ yield self.store.persist_event(event, context)
+
+ defer.returnValue(event)
+
+ @defer.inlineCallbacks
+ def inject_message(self, room, user, body):
+ self.depth += 1
+
+ builder = self.event_builder_factory.new({
+ "type": EventTypes.Message,
+ "sender": user.to_string(),
+ "state_key": user.to_string(),
+ "room_id": room.to_string(),
+ "content": {"body": body, "msgtype": u"message"},
+ })
+
+ event, context = yield self.message_handler._create_new_client_event(
+ builder
+ )
+
+ yield self.store.persist_event(event, context)
+
+ defer.returnValue(event)
+
+ @defer.inlineCallbacks
+ def inject_redaction(self, room, event_id, user, reason):
+ builder = self.event_builder_factory.new({
+ "type": EventTypes.Redaction,
+ "sender": user.to_string(),
+ "state_key": user.to_string(),
+ "room_id": room.to_string(),
+ "content": {"reason": reason},
+ "redacts": event_id,
+ })
+
+ event, context = yield self.message_handler._create_new_client_event(
+ builder
+ )
+
+ yield self.store.persist_event(event, context)
+
+ @defer.inlineCallbacks
+ def test_redact(self):
+ yield self.inject_room_member(
+ self.room1, self.u_alice, Membership.JOIN
+ )
+
+ start = yield self.store.get_room_events_max_id()
+
+ msg_event = yield self.inject_message(self.room1, self.u_alice, u"t")
+
+ end = yield self.store.get_room_events_max_id()
+
+ results, _ = yield self.store.get_room_events_stream(
+ self.u_alice.to_string(),
+ start,
+ end,
+ )
+
+ self.assertEqual(1, len(results))
+
+ # Check event has not been redacted:
+ event = results[0]
+
+ self.assertObjectHasAttributes(
+ {
+ "type": EventTypes.Message,
+ "user_id": self.u_alice.to_string(),
+ "content": {"body": "t", "msgtype": "message"},
+ },
+ event,
+ )
+
+ self.assertFalse("redacted_because" in event.unsigned)
+
+ # Redact event
+ reason = "Because I said so"
+ yield self.inject_redaction(
+ self.room1, msg_event.event_id, self.u_alice, reason
+ )
+
+ results, _ = yield self.store.get_room_events_stream(
+ self.u_alice.to_string(),
+ start,
+ end,
+ )
+
+ self.assertEqual(1, len(results))
+
+ # Check redaction
+
+ event = results[0]
+
+ self.assertEqual(msg_event.event_id, event.event_id)
+
+ self.assertTrue("redacted_because" in event.unsigned)
+
+ self.assertObjectHasAttributes(
+ {
+ "type": EventTypes.Message,
+ "user_id": self.u_alice.to_string(),
+ "content": {},
+ },
+ event,
+ )
+
+ self.assertObjectHasAttributes(
+ {
+ "type": EventTypes.Redaction,
+ "user_id": self.u_alice.to_string(),
+ "content": {"reason": reason},
+ },
+ event.unsigned["redacted_because"],
+ )
+
+ @defer.inlineCallbacks
+ def test_redact_join(self):
+ yield self.inject_room_member(
+ self.room1, self.u_alice, Membership.JOIN
+ )
+
+ start = yield self.store.get_room_events_max_id()
+
+ msg_event = yield self.inject_room_member(
+ self.room1, self.u_bob, Membership.JOIN,
+ extra_content={"blue": "red"},
+ )
+
+ end = yield self.store.get_room_events_max_id()
+
+ results, _ = yield self.store.get_room_events_stream(
+ self.u_alice.to_string(),
+ start,
+ end,
+ )
+
+ self.assertEqual(1, len(results))
+
+ # Check event has not been redacted:
+ event = results[0]
+
+ self.assertObjectHasAttributes(
+ {
+ "type": EventTypes.Member,
+ "user_id": self.u_bob.to_string(),
+ "content": {"membership": Membership.JOIN, "blue": "red"},
+ },
+ event,
+ )
+
+ self.assertFalse(hasattr(event, "redacted_because"))
+
+ # Redact event
+ reason = "Because I said so"
+ yield self.inject_redaction(
+ self.room1, msg_event.event_id, self.u_alice, reason
+ )
+
+ results, _ = yield self.store.get_room_events_stream(
+ self.u_alice.to_string(),
+ start,
+ end,
+ )
+
+ self.assertEqual(1, len(results))
+
+ # Check redaction
+
+ event = results[0]
+
+ self.assertTrue("redacted_because" in event.unsigned)
+
+ self.assertObjectHasAttributes(
+ {
+ "type": EventTypes.Member,
+ "user_id": self.u_bob.to_string(),
+ "content": {"membership": Membership.JOIN},
+ },
+ event,
+ )
+
+ self.assertObjectHasAttributes(
+ {
+ "type": EventTypes.Redaction,
+ "user_id": self.u_alice.to_string(),
+ "content": {"reason": reason},
+ },
+ event.unsigned["redacted_because"],
+ )
diff --git a/tests/storage/test_registration.py b/tests/storage/test_registration.py
new file mode 100644
index 00000000..0cce6c37
--- /dev/null
+++ b/tests/storage/test_registration.py
@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.api.errors import StoreError
+from synapse.storage.registration import RegistrationStore
+from synapse.util import stringutils
+
+from tests.utils import setup_test_homeserver
+
+
+class RegistrationStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ hs = yield setup_test_homeserver()
+ self.db_pool = hs.get_db_pool()
+
+ self.store = RegistrationStore(hs)
+
+ self.user_id = "@my-user:test"
+ self.tokens = ["AbCdEfGhIjKlMnOpQrStUvWxYz",
+ "BcDeFgHiJkLmNoPqRsTuVwXyZa"]
+ self.pwhash = "{xx1}123456789"
+
+ @defer.inlineCallbacks
+ def test_register(self):
+ yield self.store.register(self.user_id, self.tokens[0], self.pwhash)
+
+ self.assertEquals(
+ # TODO(paul): Surely this field should be 'user_id', not 'name'
+ # Additionally surely it shouldn't come in a 1-element list
+ {"name": self.user_id, "password_hash": self.pwhash},
+ (yield self.store.get_user_by_id(self.user_id))
+ )
+
+ result = yield self.store.get_user_by_access_token(self.tokens[0])
+
+ self.assertDictContainsSubset(
+ {
+ "name": self.user_id,
+ },
+ result
+ )
+
+ self.assertTrue("token_id" in result)
+
+ @defer.inlineCallbacks
+ def test_add_tokens(self):
+ yield self.store.register(self.user_id, self.tokens[0], self.pwhash)
+ yield self.store.add_access_token_to_user(self.user_id, self.tokens[1])
+
+ result = yield self.store.get_user_by_access_token(self.tokens[1])
+
+ self.assertDictContainsSubset(
+ {
+ "name": self.user_id,
+ },
+ result
+ )
+
+ self.assertTrue("token_id" in result)
+
+ @defer.inlineCallbacks
+ def test_exchange_refresh_token_valid(self):
+ uid = stringutils.random_string(32)
+ generator = TokenGenerator()
+ last_token = generator.generate(uid)
+
+ self.db_pool.runQuery(
+ "INSERT INTO refresh_tokens(user_id, token) VALUES(?,?)",
+ (uid, last_token,))
+
+ (found_user_id, refresh_token) = yield self.store.exchange_refresh_token(
+ last_token, generator.generate)
+ self.assertEqual(uid, found_user_id)
+
+ rows = yield self.db_pool.runQuery(
+ "SELECT token FROM refresh_tokens WHERE user_id = ?", (uid, ))
+ self.assertEqual([(refresh_token,)], rows)
+ # We issued token 1, then exchanged it for token 2
+ expected_refresh_token = u"%s-%d" % (uid, 2,)
+ self.assertEqual(expected_refresh_token, refresh_token)
+
+ @defer.inlineCallbacks
+ def test_exchange_refresh_token_none(self):
+ uid = stringutils.random_string(32)
+ generator = TokenGenerator()
+ last_token = generator.generate(uid)
+
+ with self.assertRaises(StoreError):
+ yield self.store.exchange_refresh_token(last_token, generator.generate)
+
+ @defer.inlineCallbacks
+ def test_exchange_refresh_token_invalid(self):
+ uid = stringutils.random_string(32)
+ generator = TokenGenerator()
+ last_token = generator.generate(uid)
+ wrong_token = "%s-wrong" % (last_token,)
+
+ self.db_pool.runQuery(
+ "INSERT INTO refresh_tokens(user_id, token) VALUES(?,?)",
+ (uid, wrong_token,))
+
+ with self.assertRaises(StoreError):
+ yield self.store.exchange_refresh_token(last_token, generator.generate)
+
+
+class TokenGenerator:
+ def __init__(self):
+ self._last_issued_token = 0
+
+ def generate(self, user_id):
+ self._last_issued_token += 1
+ return u"%s-%d" % (user_id, self._last_issued_token,)
diff --git a/tests/storage/test_room.py b/tests/storage/test_room.py
new file mode 100644
index 00000000..91c96754
--- /dev/null
+++ b/tests/storage/test_room.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.api.constants import EventTypes
+from synapse.types import UserID, RoomID, RoomAlias
+
+from tests.utils import setup_test_homeserver
+
+
+class RoomStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ hs = yield setup_test_homeserver()
+
+ # We can't test RoomStore on its own without the DirectoryStore, for
+ # management of the 'room_aliases' table
+ self.store = hs.get_datastore()
+
+ self.room = RoomID.from_string("!abcde:test")
+ self.alias = RoomAlias.from_string("#a-room-name:test")
+ self.u_creator = UserID.from_string("@creator:test")
+
+ yield self.store.store_room(self.room.to_string(),
+ room_creator_user_id=self.u_creator.to_string(),
+ is_public=True
+ )
+
+ @defer.inlineCallbacks
+ def test_get_room(self):
+ self.assertDictContainsSubset(
+ {"room_id": self.room.to_string(),
+ "creator": self.u_creator.to_string(),
+ "is_public": True},
+ (yield self.store.get_room(self.room.to_string()))
+ )
+
+ @defer.inlineCallbacks
+ def test_get_rooms(self):
+ # get_rooms does an INNER JOIN on the room_aliases table :(
+
+ rooms = yield self.store.get_rooms(is_public=True)
+ # Should be empty before we add the alias
+ self.assertEquals([], rooms)
+
+ yield self.store.create_room_alias_association(
+ room_alias=self.alias,
+ room_id=self.room.to_string(),
+ servers=["test"]
+ )
+
+ rooms = yield self.store.get_rooms(is_public=True)
+
+ self.assertEquals(1, len(rooms))
+ self.assertEquals({
+ "name": None,
+ "room_id": self.room.to_string(),
+ "topic": None,
+ "aliases": [self.alias.to_string()],
+ "world_readable": False,
+ "guest_can_join": False,
+ }, rooms[0])
+
+
+class RoomEventsStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ hs = setup_test_homeserver()
+
+ # Room events need the full datastore, for persist_event() and
+ # get_room_state()
+ self.store = hs.get_datastore()
+ self.event_factory = hs.get_event_factory()
+
+ self.room = RoomID.from_string("!abcde:test")
+
+ yield self.store.store_room(self.room.to_string(),
+ room_creator_user_id="@creator:text",
+ is_public=True
+ )
+
+ @defer.inlineCallbacks
+ def inject_room_event(self, **kwargs):
+ yield self.store.persist_event(
+ self.event_factory.create_event(
+ room_id=self.room.to_string(),
+ **kwargs
+ )
+ )
+
+ @defer.inlineCallbacks
+ def STALE_test_room_name(self):
+ name = u"A-Room-Name"
+
+ yield self.inject_room_event(
+ etype=EventTypes.Name,
+ name=name,
+ content={"name": name},
+ depth=1,
+ )
+
+ state = yield self.store.get_current_state(
+ room_id=self.room.to_string()
+ )
+
+ self.assertEquals(1, len(state))
+ self.assertObjectHasAttributes(
+ {"type": "m.room.name",
+ "room_id": self.room.to_string(),
+ "name": name},
+ state[0]
+ )
+
+ @defer.inlineCallbacks
+ def STALE_test_room_topic(self):
+ topic = u"A place for things"
+
+ yield self.inject_room_event(
+ etype=EventTypes.Topic,
+ topic=topic,
+ content={"topic": topic},
+ depth=1,
+ )
+
+ state = yield self.store.get_current_state(
+ room_id=self.room.to_string()
+ )
+
+ self.assertEquals(1, len(state))
+ self.assertObjectHasAttributes(
+ {"type": "m.room.topic",
+ "room_id": self.room.to_string(),
+ "topic": topic},
+ state[0]
+ )
+
+ # Not testing the various 'level' methods for now because there's lots
+ # of them and need coalescing; see JIRA SPEC-11
diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py
new file mode 100644
index 00000000..785953cc
--- /dev/null
+++ b/tests/storage/test_roommember.py
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.api.constants import EventTypes, Membership
+from synapse.types import UserID, RoomID
+
+from tests.utils import setup_test_homeserver
+
+from mock import Mock
+
+
+class RoomMemberStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ hs = yield setup_test_homeserver(
+ resource_for_federation=Mock(),
+ http_client=None,
+ )
+ # We can't test the RoomMemberStore on its own without the other event
+ # storage logic
+ self.store = hs.get_datastore()
+ self.event_builder_factory = hs.get_event_builder_factory()
+ self.handlers = hs.get_handlers()
+ self.message_handler = self.handlers.message_handler
+
+ self.u_alice = UserID.from_string("@alice:test")
+ self.u_bob = UserID.from_string("@bob:test")
+
+ # User elsewhere on another host
+ self.u_charlie = UserID.from_string("@charlie:elsewhere")
+
+ self.room = RoomID.from_string("!abc123:test")
+
+ @defer.inlineCallbacks
+ def inject_room_member(self, room, user, membership, replaces_state=None):
+ builder = self.event_builder_factory.new({
+ "type": EventTypes.Member,
+ "sender": user.to_string(),
+ "state_key": user.to_string(),
+ "room_id": room.to_string(),
+ "content": {"membership": membership},
+ })
+
+ event, context = yield self.message_handler._create_new_client_event(
+ builder
+ )
+
+ yield self.store.persist_event(event, context)
+
+ defer.returnValue(event)
+
+ @defer.inlineCallbacks
+ def test_one_member(self):
+ yield self.inject_room_member(self.room, self.u_alice, Membership.JOIN)
+
+ self.assertEquals(
+ Membership.JOIN,
+ (yield self.store.get_room_member(
+ user_id=self.u_alice.to_string(),
+ room_id=self.room.to_string(),
+ )).membership
+ )
+ self.assertEquals(
+ [self.u_alice.to_string()],
+ [m.user_id for m in (
+ yield self.store.get_room_members(self.room.to_string())
+ )]
+ )
+ self.assertEquals(
+ [self.room.to_string()],
+ [m.room_id for m in (
+ yield self.store.get_rooms_for_user_where_membership_is(
+ self.u_alice.to_string(), [Membership.JOIN]
+ ))
+ ]
+ )
+ self.assertFalse(
+ (yield self.store.user_rooms_intersect(
+ [self.u_alice.to_string(), self.u_bob.to_string()]
+ ))
+ )
+
+ @defer.inlineCallbacks
+ def test_two_members(self):
+ yield self.inject_room_member(self.room, self.u_alice, Membership.JOIN)
+ yield self.inject_room_member(self.room, self.u_bob, Membership.JOIN)
+
+ self.assertEquals(
+ {self.u_alice.to_string(), self.u_bob.to_string()},
+ {m.user_id for m in (
+ yield self.store.get_room_members(self.room.to_string())
+ )}
+ )
+ self.assertTrue(
+ (yield self.store.user_rooms_intersect(
+ [self.u_alice.to_string(), self.u_bob.to_string()]
+ ))
+ )
+
+ @defer.inlineCallbacks
+ def test_room_hosts(self):
+ yield self.inject_room_member(self.room, self.u_alice, Membership.JOIN)
+
+ self.assertEquals(
+ {"test"},
+ (yield self.store.get_joined_hosts_for_room(self.room.to_string()))
+ )
+
+ # Should still have just one host after second join from it
+ yield self.inject_room_member(self.room, self.u_bob, Membership.JOIN)
+
+ self.assertEquals(
+ {"test"},
+ (yield self.store.get_joined_hosts_for_room(self.room.to_string()))
+ )
+
+ # Should now have two hosts after join from other host
+ yield self.inject_room_member(self.room, self.u_charlie, Membership.JOIN)
+
+ self.assertEquals(
+ {"test", "elsewhere"},
+ (yield
+ self.store.get_joined_hosts_for_room(self.room.to_string())
+ )
+ )
+
+ # Should still have both hosts
+ yield self.inject_room_member(self.room, self.u_alice, Membership.LEAVE)
+
+ self.assertEquals(
+ {"test", "elsewhere"},
+ (yield
+ self.store.get_joined_hosts_for_room(self.room.to_string())
+ )
+ )
+
+ # Should have only one host after other leaves
+ yield self.inject_room_member(self.room, self.u_charlie, Membership.LEAVE)
+
+ self.assertEquals(
+ {"test"},
+ (yield self.store.get_joined_hosts_for_room(self.room.to_string()))
+ )
diff --git a/tests/storage/test_stream.py b/tests/storage/test_stream.py
new file mode 100644
index 00000000..e5c2c5cc
--- /dev/null
+++ b/tests/storage/test_stream.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.api.constants import EventTypes, Membership
+from synapse.types import UserID, RoomID
+from tests.storage.event_injector import EventInjector
+
+from tests.utils import setup_test_homeserver
+
+from mock import Mock
+
+
+class StreamStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ hs = yield setup_test_homeserver(
+ resource_for_federation=Mock(),
+ http_client=None,
+ )
+
+ self.store = hs.get_datastore()
+ self.event_builder_factory = hs.get_event_builder_factory()
+ self.event_injector = EventInjector(hs)
+ self.handlers = hs.get_handlers()
+ self.message_handler = self.handlers.message_handler
+
+ self.u_alice = UserID.from_string("@alice:test")
+ self.u_bob = UserID.from_string("@bob:test")
+
+ self.room1 = RoomID.from_string("!abc123:test")
+ self.room2 = RoomID.from_string("!xyx987:test")
+
+ @defer.inlineCallbacks
+ def test_event_stream_get_other(self):
+ # Both bob and alice joins the room
+ yield self.event_injector.inject_room_member(
+ self.room1, self.u_alice, Membership.JOIN
+ )
+ yield self.event_injector.inject_room_member(
+ self.room1, self.u_bob, Membership.JOIN
+ )
+
+ # Initial stream key:
+ start = yield self.store.get_room_events_max_id()
+
+ yield self.event_injector.inject_message(self.room1, self.u_alice, u"test")
+
+ end = yield self.store.get_room_events_max_id()
+
+ results, _ = yield self.store.get_room_events_stream(
+ self.u_bob.to_string(),
+ start,
+ end,
+ )
+
+ self.assertEqual(1, len(results))
+
+ event = results[0]
+
+ self.assertObjectHasAttributes(
+ {
+ "type": EventTypes.Message,
+ "user_id": self.u_alice.to_string(),
+ "content": {"body": "test", "msgtype": "message"},
+ },
+ event,
+ )
+
+ @defer.inlineCallbacks
+ def test_event_stream_get_own(self):
+ # Both bob and alice joins the room
+ yield self.event_injector.inject_room_member(
+ self.room1, self.u_alice, Membership.JOIN
+ )
+ yield self.event_injector.inject_room_member(
+ self.room1, self.u_bob, Membership.JOIN
+ )
+
+ # Initial stream key:
+ start = yield self.store.get_room_events_max_id()
+
+ yield self.event_injector.inject_message(self.room1, self.u_alice, u"test")
+
+ end = yield self.store.get_room_events_max_id()
+
+ results, _ = yield self.store.get_room_events_stream(
+ self.u_alice.to_string(),
+ start,
+ end,
+ )
+
+ self.assertEqual(1, len(results))
+
+ event = results[0]
+
+ self.assertObjectHasAttributes(
+ {
+ "type": EventTypes.Message,
+ "user_id": self.u_alice.to_string(),
+ "content": {"body": "test", "msgtype": "message"},
+ },
+ event,
+ )
+
+ @defer.inlineCallbacks
+ def test_event_stream_join_leave(self):
+ # Both bob and alice joins the room
+ yield self.event_injector.inject_room_member(
+ self.room1, self.u_alice, Membership.JOIN
+ )
+ yield self.event_injector.inject_room_member(
+ self.room1, self.u_bob, Membership.JOIN
+ )
+
+ # Then bob leaves again.
+ yield self.event_injector.inject_room_member(
+ self.room1, self.u_bob, Membership.LEAVE
+ )
+
+ # Initial stream key:
+ start = yield self.store.get_room_events_max_id()
+
+ yield self.event_injector.inject_message(self.room1, self.u_alice, u"test")
+
+ end = yield self.store.get_room_events_max_id()
+
+ results, _ = yield self.store.get_room_events_stream(
+ self.u_bob.to_string(),
+ start,
+ end,
+ )
+
+ # We should not get the message, as it happened *after* bob left.
+ self.assertEqual(0, len(results))
+
+ @defer.inlineCallbacks
+ def test_event_stream_prev_content(self):
+ yield self.event_injector.inject_room_member(
+ self.room1, self.u_bob, Membership.JOIN
+ )
+
+ event1 = yield self.event_injector.inject_room_member(
+ self.room1, self.u_alice, Membership.JOIN
+ )
+
+ start = yield self.store.get_room_events_max_id()
+
+ event2 = yield self.event_injector.inject_room_member(
+ self.room1, self.u_alice, Membership.JOIN,
+ )
+
+ end = yield self.store.get_room_events_max_id()
+
+ results, _ = yield self.store.get_room_events_stream(
+ self.u_bob.to_string(),
+ start,
+ end,
+ )
+
+ # We should not get the message, as it happened *after* bob left.
+ self.assertEqual(1, len(results))
+
+ event = results[0]
+
+ self.assertTrue(
+ "prev_content" in event.unsigned,
+ msg="No prev_content key"
+ )
diff --git a/tests/test_distributor.py b/tests/test_distributor.py
new file mode 100644
index 00000000..8ed48cfb
--- /dev/null
+++ b/tests/test_distributor.py
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from . import unittest
+from twisted.internet import defer
+
+from mock import Mock, patch
+
+from synapse.util.distributor import Distributor
+from synapse.util.async import run_on_reactor
+
+
+class DistributorTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.dist = Distributor()
+
+ @defer.inlineCallbacks
+ def test_signal_dispatch(self):
+ self.dist.declare("alert")
+
+ observer = Mock()
+ self.dist.observe("alert", observer)
+
+ d = self.dist.fire("alert", 1, 2, 3)
+ yield d
+ self.assertTrue(d.called)
+ observer.assert_called_with(1, 2, 3)
+
+ @defer.inlineCallbacks
+ def test_signal_dispatch_deferred(self):
+ self.dist.declare("whine")
+
+ d_inner = defer.Deferred()
+ def observer():
+ return d_inner
+ self.dist.observe("whine", observer)
+
+ d_outer = self.dist.fire("whine")
+
+ self.assertFalse(d_outer.called)
+
+ d_inner.callback(None)
+ yield d_outer
+ self.assertTrue(d_outer.called)
+
+ @defer.inlineCallbacks
+ def test_signal_catch(self):
+ self.dist.declare("alarm")
+
+ observers = [Mock() for i in 1, 2]
+ for o in observers:
+ self.dist.observe("alarm", o)
+
+ observers[0].side_effect = Exception("Awoogah!")
+
+ with patch("synapse.util.distributor.logger",
+ spec=["warning"]
+ ) as mock_logger:
+ d = self.dist.fire("alarm", "Go")
+ yield d
+ self.assertTrue(d.called)
+
+ observers[0].assert_called_once_with("Go")
+ observers[1].assert_called_once_with("Go")
+
+ self.assertEquals(mock_logger.warning.call_count, 1)
+ self.assertIsInstance(mock_logger.warning.call_args[0][0],
+ str)
+
+ @defer.inlineCallbacks
+ def test_signal_catch_no_suppress(self):
+ # Gut-wrenching
+ self.dist.suppress_failures = False
+
+ self.dist.declare("whail")
+
+ class MyException(Exception):
+ pass
+
+ @defer.inlineCallbacks
+ def observer():
+ yield run_on_reactor()
+ raise MyException("Oopsie")
+
+ self.dist.observe("whail", observer)
+
+ d = self.dist.fire("whail")
+
+ yield self.assertFailure(d, MyException)
+ self.dist.suppress_failures = True
+
+ @defer.inlineCallbacks
+ def test_signal_prereg(self):
+ observer = Mock()
+ self.dist.observe("flare", observer)
+
+ self.dist.declare("flare")
+ yield self.dist.fire("flare", 4, 5)
+
+ observer.assert_called_with(4, 5)
+
+ def test_signal_undeclared(self):
+ def code():
+ self.dist.fire("notification")
+ self.assertRaises(KeyError, code)
diff --git a/tests/test_state.py b/tests/test_state.py
new file mode 100644
index 00000000..e4e995b7
--- /dev/null
+++ b/tests/test_state.py
@@ -0,0 +1,641 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.events import FrozenEvent
+from synapse.api.auth import Auth
+from synapse.api.constants import EventTypes, Membership
+from synapse.state import StateHandler
+
+from .utils import MockClock
+
+from mock import Mock
+
+
+_next_event_id = 1000
+
+
+def create_event(name=None, type=None, state_key=None, depth=2, event_id=None,
+ prev_events=[], **kwargs):
+ global _next_event_id
+
+ if not event_id:
+ _next_event_id += 1
+ event_id = "$%s:test" % (_next_event_id,)
+
+ if not name:
+ if state_key is not None:
+ name = "<%s-%s, %s>" % (type, state_key, event_id,)
+ else:
+ name = "<%s, %s>" % (type, event_id,)
+
+ d = {
+ "event_id": event_id,
+ "type": type,
+ "sender": "@user_id:example.com",
+ "room_id": "!room_id:example.com",
+ "depth": depth,
+ "prev_events": prev_events,
+ }
+
+ if state_key is not None:
+ d["state_key"] = state_key
+
+ d.update(kwargs)
+
+ event = FrozenEvent(d)
+
+ return event
+
+
+class StateGroupStore(object):
+ def __init__(self):
+ self._event_to_state_group = {}
+ self._group_to_state = {}
+
+ self._next_group = 1
+
+ def get_state_groups(self, room_id, event_ids):
+ groups = {}
+ for event_id in event_ids:
+ group = self._event_to_state_group.get(event_id)
+ if group:
+ groups[group] = self._group_to_state[group]
+
+ return defer.succeed(groups)
+
+ def store_state_groups(self, event, context):
+ if context.current_state is None:
+ return
+
+ state_events = context.current_state
+
+ if event.is_state():
+ state_events[(event.type, event.state_key)] = event
+
+ state_group = context.state_group
+ if not state_group:
+ state_group = self._next_group
+ self._next_group += 1
+
+ self._group_to_state[state_group] = state_events.values()
+
+ self._event_to_state_group[event.event_id] = state_group
+
+
+class DictObj(dict):
+ def __init__(self, **kwargs):
+ super(DictObj, self).__init__(kwargs)
+ self.__dict__ = self
+
+
+class Graph(object):
+ def __init__(self, nodes, edges):
+ events = {}
+ clobbered = set(events.keys())
+
+ for event_id, fields in nodes.items():
+ refs = edges.get(event_id)
+ if refs:
+ clobbered.difference_update(refs)
+ prev_events = [(r, {}) for r in refs]
+ else:
+ prev_events = []
+
+ events[event_id] = create_event(
+ event_id=event_id,
+ prev_events=prev_events,
+ **fields
+ )
+
+ self._leaves = clobbered
+ self._events = sorted(events.values(), key=lambda e: e.depth)
+
+ def walk(self):
+ return iter(self._events)
+
+ def get_leaves(self):
+ return (self._events[i] for i in self._leaves)
+
+
+class StateTestCase(unittest.TestCase):
+ def setUp(self):
+ self.store = Mock(
+ spec_set=[
+ "get_state_groups",
+ "add_event_hashes",
+ ]
+ )
+ hs = Mock(spec=[
+ "get_datastore", "get_auth", "get_state_handler", "get_clock",
+ ])
+ hs.get_datastore.return_value = self.store
+ hs.get_state_handler.return_value = None
+ hs.get_auth.return_value = Auth(hs)
+ hs.get_clock.return_value = MockClock()
+
+ self.state = StateHandler(hs)
+ self.event_id = 0
+
+ @defer.inlineCallbacks
+ def test_branch_no_conflict(self):
+ graph = Graph(
+ nodes={
+ "START": DictObj(
+ type=EventTypes.Create,
+ state_key="",
+ depth=1,
+ ),
+ "A": DictObj(
+ type=EventTypes.Message,
+ depth=2,
+ ),
+ "B": DictObj(
+ type=EventTypes.Message,
+ depth=3,
+ ),
+ "C": DictObj(
+ type=EventTypes.Name,
+ state_key="",
+ depth=3,
+ ),
+ "D": DictObj(
+ type=EventTypes.Message,
+ depth=4,
+ ),
+ },
+ edges={
+ "A": ["START"],
+ "B": ["A"],
+ "C": ["A"],
+ "D": ["B", "C"]
+ }
+ )
+
+ store = StateGroupStore()
+ self.store.get_state_groups.side_effect = store.get_state_groups
+
+ context_store = {}
+
+ for event in graph.walk():
+ context = yield self.state.compute_event_context(event)
+ store.store_state_groups(event, context)
+ context_store[event.event_id] = context
+
+ self.assertEqual(2, len(context_store["D"].current_state))
+
+ @defer.inlineCallbacks
+ def test_branch_basic_conflict(self):
+ graph = Graph(
+ nodes={
+ "START": DictObj(
+ type=EventTypes.Create,
+ state_key="",
+ content={"creator": "@user_id:example.com"},
+ depth=1,
+ ),
+ "A": DictObj(
+ type=EventTypes.Member,
+ state_key="@user_id:example.com",
+ content={"membership": Membership.JOIN},
+ membership=Membership.JOIN,
+ depth=2,
+ ),
+ "B": DictObj(
+ type=EventTypes.Name,
+ state_key="",
+ depth=3,
+ ),
+ "C": DictObj(
+ type=EventTypes.Name,
+ state_key="",
+ depth=4,
+ ),
+ "D": DictObj(
+ type=EventTypes.Message,
+ depth=5,
+ ),
+ },
+ edges={
+ "A": ["START"],
+ "B": ["A"],
+ "C": ["A"],
+ "D": ["B", "C"]
+ }
+ )
+
+ store = StateGroupStore()
+ self.store.get_state_groups.side_effect = store.get_state_groups
+
+ context_store = {}
+
+ for event in graph.walk():
+ context = yield self.state.compute_event_context(event)
+ store.store_state_groups(event, context)
+ context_store[event.event_id] = context
+
+ self.assertSetEqual(
+ {"START", "A", "C"},
+ {e.event_id for e in context_store["D"].current_state.values()}
+ )
+
+ @defer.inlineCallbacks
+ def test_branch_have_banned_conflict(self):
+ graph = Graph(
+ nodes={
+ "START": DictObj(
+ type=EventTypes.Create,
+ state_key="",
+ content={"creator": "@user_id:example.com"},
+ depth=1,
+ ),
+ "A": DictObj(
+ type=EventTypes.Member,
+ state_key="@user_id:example.com",
+ content={"membership": Membership.JOIN},
+ membership=Membership.JOIN,
+ depth=2,
+ ),
+ "B": DictObj(
+ type=EventTypes.Name,
+ state_key="",
+ depth=3,
+ ),
+ "C": DictObj(
+ type=EventTypes.Member,
+ state_key="@user_id_2:example.com",
+ content={"membership": Membership.BAN},
+ membership=Membership.BAN,
+ depth=4,
+ ),
+ "D": DictObj(
+ type=EventTypes.Name,
+ state_key="",
+ depth=4,
+ sender="@user_id_2:example.com",
+ ),
+ "E": DictObj(
+ type=EventTypes.Message,
+ depth=5,
+ ),
+ },
+ edges={
+ "A": ["START"],
+ "B": ["A"],
+ "C": ["B"],
+ "D": ["B"],
+ "E": ["C", "D"]
+ }
+ )
+
+ store = StateGroupStore()
+ self.store.get_state_groups.side_effect = store.get_state_groups
+
+ context_store = {}
+
+ for event in graph.walk():
+ context = yield self.state.compute_event_context(event)
+ store.store_state_groups(event, context)
+ context_store[event.event_id] = context
+
+ self.assertSetEqual(
+ {"START", "A", "B", "C"},
+ {e.event_id for e in context_store["E"].current_state.values()}
+ )
+
+ @defer.inlineCallbacks
+ def test_branch_have_perms_conflict(self):
+ userid1 = "@user_id:example.com"
+ userid2 = "@user_id2:example.com"
+
+ nodes = {
+ "A1": DictObj(
+ type=EventTypes.Create,
+ state_key="",
+ content={"creator": userid1},
+ depth=1,
+ ),
+ "A2": DictObj(
+ type=EventTypes.Member,
+ state_key=userid1,
+ content={"membership": Membership.JOIN},
+ membership=Membership.JOIN,
+ ),
+ "A3": DictObj(
+ type=EventTypes.Member,
+ state_key=userid2,
+ content={"membership": Membership.JOIN},
+ membership=Membership.JOIN,
+ ),
+ "A4": DictObj(
+ type=EventTypes.PowerLevels,
+ state_key="",
+ content={
+ "events": {"m.room.name": 50},
+ "users": {userid1: 100,
+ userid2: 60},
+ },
+ ),
+ "A5": DictObj(
+ type=EventTypes.Name,
+ state_key="",
+ ),
+ "B": DictObj(
+ type=EventTypes.PowerLevels,
+ state_key="",
+ content={
+ "events": {"m.room.name": 50},
+ "users": {userid2: 30},
+ },
+ ),
+ "C": DictObj(
+ type=EventTypes.Name,
+ state_key="",
+ sender=userid2,
+ ),
+ "D": DictObj(
+ type=EventTypes.Message,
+ ),
+ }
+ edges = {
+ "A2": ["A1"],
+ "A3": ["A2"],
+ "A4": ["A3"],
+ "A5": ["A4"],
+ "B": ["A5"],
+ "C": ["A5"],
+ "D": ["B", "C"]
+ }
+ self._add_depths(nodes, edges)
+ graph = Graph(nodes, edges)
+
+ store = StateGroupStore()
+ self.store.get_state_groups.side_effect = store.get_state_groups
+
+ context_store = {}
+
+ for event in graph.walk():
+ context = yield self.state.compute_event_context(event)
+ store.store_state_groups(event, context)
+ context_store[event.event_id] = context
+
+ self.assertSetEqual(
+ {"A1", "A2", "A3", "A5", "B"},
+ {e.event_id for e in context_store["D"].current_state.values()}
+ )
+
+ def _add_depths(self, nodes, edges):
+ def _get_depth(ev):
+ node = nodes[ev]
+ if 'depth' not in node:
+ prevs = edges[ev]
+ depth = max(_get_depth(prev) for prev in prevs) + 1
+ node['depth'] = depth
+ return node['depth']
+
+ for n in nodes:
+ _get_depth(n)
+
+ @defer.inlineCallbacks
+ def test_annotate_with_old_message(self):
+ event = create_event(type="test_message", name="event")
+
+ old_state = [
+ create_event(type="test1", state_key="1"),
+ create_event(type="test1", state_key="2"),
+ create_event(type="test2", state_key=""),
+ ]
+
+ context = yield self.state.compute_event_context(
+ event, old_state=old_state
+ )
+
+ for k, v in context.current_state.items():
+ type, state_key = k
+ self.assertEqual(type, v.type)
+ self.assertEqual(state_key, v.state_key)
+
+ self.assertEqual(
+ set(old_state), set(context.current_state.values())
+ )
+
+ self.assertIsNone(context.state_group)
+
+ @defer.inlineCallbacks
+ def test_annotate_with_old_state(self):
+ event = create_event(type="state", state_key="", name="event")
+
+ old_state = [
+ create_event(type="test1", state_key="1"),
+ create_event(type="test1", state_key="2"),
+ create_event(type="test2", state_key=""),
+ ]
+
+ context = yield self.state.compute_event_context(
+ event, old_state=old_state
+ )
+
+ for k, v in context.current_state.items():
+ type, state_key = k
+ self.assertEqual(type, v.type)
+ self.assertEqual(state_key, v.state_key)
+
+ self.assertEqual(
+ set(old_state),
+ set(context.current_state.values())
+ )
+
+ self.assertIsNone(context.state_group)
+
+ @defer.inlineCallbacks
+ def test_trivial_annotate_message(self):
+ event = create_event(type="test_message", name="event")
+
+ old_state = [
+ create_event(type="test1", state_key="1"),
+ create_event(type="test1", state_key="2"),
+ create_event(type="test2", state_key=""),
+ ]
+
+ group_name = "group_name_1"
+
+ self.store.get_state_groups.return_value = {
+ group_name: old_state,
+ }
+
+ context = yield self.state.compute_event_context(event)
+
+ for k, v in context.current_state.items():
+ type, state_key = k
+ self.assertEqual(type, v.type)
+ self.assertEqual(state_key, v.state_key)
+
+ self.assertEqual(
+ set([e.event_id for e in old_state]),
+ set([e.event_id for e in context.current_state.values()])
+ )
+
+ self.assertEqual(group_name, context.state_group)
+
+ @defer.inlineCallbacks
+ def test_trivial_annotate_state(self):
+ event = create_event(type="state", state_key="", name="event")
+
+ old_state = [
+ create_event(type="test1", state_key="1"),
+ create_event(type="test1", state_key="2"),
+ create_event(type="test2", state_key=""),
+ ]
+
+ group_name = "group_name_1"
+
+ self.store.get_state_groups.return_value = {
+ group_name: old_state,
+ }
+
+ context = yield self.state.compute_event_context(event)
+
+ for k, v in context.current_state.items():
+ type, state_key = k
+ self.assertEqual(type, v.type)
+ self.assertEqual(state_key, v.state_key)
+
+ self.assertEqual(
+ set([e.event_id for e in old_state]),
+ set([e.event_id for e in context.current_state.values()])
+ )
+
+ self.assertIsNone(context.state_group)
+
+ @defer.inlineCallbacks
+ def test_resolve_message_conflict(self):
+ event = create_event(type="test_message", name="event")
+
+ creation = create_event(
+ type=EventTypes.Create, state_key=""
+ )
+
+ old_state_1 = [
+ creation,
+ create_event(type="test1", state_key="1"),
+ create_event(type="test1", state_key="2"),
+ create_event(type="test2", state_key=""),
+ ]
+
+ old_state_2 = [
+ creation,
+ create_event(type="test1", state_key="1"),
+ create_event(type="test3", state_key="2"),
+ create_event(type="test4", state_key=""),
+ ]
+
+ context = yield self._get_context(event, old_state_1, old_state_2)
+
+ self.assertEqual(len(context.current_state), 6)
+
+ self.assertIsNone(context.state_group)
+
+ @defer.inlineCallbacks
+ def test_resolve_state_conflict(self):
+ event = create_event(type="test4", state_key="", name="event")
+
+ creation = create_event(
+ type=EventTypes.Create, state_key=""
+ )
+
+ old_state_1 = [
+ creation,
+ create_event(type="test1", state_key="1"),
+ create_event(type="test1", state_key="2"),
+ create_event(type="test2", state_key=""),
+ ]
+
+ old_state_2 = [
+ creation,
+ create_event(type="test1", state_key="1"),
+ create_event(type="test3", state_key="2"),
+ create_event(type="test4", state_key=""),
+ ]
+
+ context = yield self._get_context(event, old_state_1, old_state_2)
+
+ self.assertEqual(len(context.current_state), 6)
+
+ self.assertIsNone(context.state_group)
+
+ @defer.inlineCallbacks
+ def test_standard_depth_conflict(self):
+ event = create_event(type="test4", name="event")
+
+ member_event = create_event(
+ type=EventTypes.Member,
+ state_key="@user_id:example.com",
+ content={
+ "membership": Membership.JOIN,
+ }
+ )
+
+ creation = create_event(
+ type=EventTypes.Create, state_key="",
+ content={"creator": "@foo:bar"}
+ )
+
+ old_state_1 = [
+ creation,
+ member_event,
+ create_event(type="test1", state_key="1", depth=1),
+ ]
+
+ old_state_2 = [
+ creation,
+ member_event,
+ create_event(type="test1", state_key="1", depth=2),
+ ]
+
+ context = yield self._get_context(event, old_state_1, old_state_2)
+
+ self.assertEqual(old_state_2[2], context.current_state[("test1", "1")])
+
+ # Reverse the depth to make sure we are actually using the depths
+ # during state resolution.
+
+ old_state_1 = [
+ creation,
+ member_event,
+ create_event(type="test1", state_key="1", depth=2),
+ ]
+
+ old_state_2 = [
+ creation,
+ member_event,
+ create_event(type="test1", state_key="1", depth=1),
+ ]
+
+ context = yield self._get_context(event, old_state_1, old_state_2)
+
+ self.assertEqual(old_state_1[2], context.current_state[("test1", "1")])
+
+ def _get_context(self, event, old_state_1, old_state_2):
+ group_name_1 = "group_name_1"
+ group_name_2 = "group_name_2"
+
+ self.store.get_state_groups.return_value = {
+ group_name_1: old_state_1,
+ group_name_2: old_state_2,
+ }
+
+ return self.state.compute_event_context(event)
diff --git a/tests/test_test_utils.py b/tests/test_test_utils.py
new file mode 100644
index 00000000..b42787dd
--- /dev/null
+++ b/tests/test_test_utils.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from tests import unittest
+
+from tests.utils import MockClock
+
+class MockClockTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.clock = MockClock()
+
+ def test_advance_time(self):
+ start_time = self.clock.time()
+
+ self.clock.advance_time(20)
+
+ self.assertEquals(20, self.clock.time() - start_time)
+
+ def test_later(self):
+ invoked = [0, 0]
+
+ def _cb0():
+ invoked[0] = 1
+ self.clock.call_later(10, _cb0)
+
+ def _cb1():
+ invoked[1] = 1
+ self.clock.call_later(20, _cb1)
+
+ self.assertFalse(invoked[0])
+
+ self.clock.advance_time(15)
+
+ self.assertTrue(invoked[0])
+ self.assertFalse(invoked[1])
+
+ self.clock.advance_time(5)
+
+ self.assertTrue(invoked[1])
+
+ def test_cancel_later(self):
+ invoked = [0, 0]
+
+ def _cb0():
+ invoked[0] = 1
+ t0 = self.clock.call_later(10, _cb0)
+
+ def _cb1():
+ invoked[1] = 1
+ t1 = self.clock.call_later(20, _cb1)
+
+ self.clock.cancel_call_later(t0)
+
+ self.clock.advance_time(30)
+
+ self.assertFalse(invoked[0])
+ self.assertTrue(invoked[1])
diff --git a/tests/test_types.py b/tests/test_types.py
new file mode 100644
index 00000000..495cd20f
--- /dev/null
+++ b/tests/test_types.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from tests import unittest
+
+from synapse.api.errors import SynapseError
+from synapse.server import BaseHomeServer
+from synapse.types import UserID, RoomAlias
+
+mock_homeserver = BaseHomeServer(hostname="my.domain")
+
+
+class UserIDTestCase(unittest.TestCase):
+ def test_parse(self):
+ user = UserID.from_string("@1234abcd:my.domain")
+
+ self.assertEquals("1234abcd", user.localpart)
+ self.assertEquals("my.domain", user.domain)
+ self.assertEquals(True, mock_homeserver.is_mine(user))
+
+ def test_pase_empty(self):
+ with self.assertRaises(SynapseError):
+ UserID.from_string("")
+
+
+ def test_build(self):
+ user = UserID("5678efgh", "my.domain")
+
+ self.assertEquals(user.to_string(), "@5678efgh:my.domain")
+
+ def test_compare(self):
+ userA = UserID.from_string("@userA:my.domain")
+ userAagain = UserID.from_string("@userA:my.domain")
+ userB = UserID.from_string("@userB:my.domain")
+
+ self.assertTrue(userA == userAagain)
+ self.assertTrue(userA != userB)
+
+
+class RoomAliasTestCase(unittest.TestCase):
+ def test_parse(self):
+ room = RoomAlias.from_string("#channel:my.domain")
+
+ self.assertEquals("channel", room.localpart)
+ self.assertEquals("my.domain", room.domain)
+ self.assertEquals(True, mock_homeserver.is_mine(room))
+
+ def test_build(self):
+ room = RoomAlias("channel", "my.domain")
+
+ self.assertEquals(room.to_string(), "#channel:my.domain")
diff --git a/tests/unittest.py b/tests/unittest.py
new file mode 100644
index 00000000..fe26b757
--- /dev/null
+++ b/tests/unittest.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from twisted.trial import unittest
+
+import logging
+
+
+# logging doesn't have a "don't log anything at all EVARRRR setting,
+# but since the highest value is 50, 1000000 should do ;)
+NEVER = 1000000
+
+logging.getLogger().addHandler(logging.StreamHandler())
+logging.getLogger().setLevel(NEVER)
+
+
+def around(target):
+ """A CLOS-style 'around' modifier, which wraps the original method of the
+ given instance with another piece of code.
+
+ @around(self)
+ def method_name(orig, *args, **kwargs):
+ return orig(*args, **kwargs)
+ """
+ def _around(code):
+ name = code.__name__
+ orig = getattr(target, name)
+ def new(*args, **kwargs):
+ return code(orig, *args, **kwargs)
+ setattr(target, name, new)
+ return _around
+
+
+class TestCase(unittest.TestCase):
+ """A subclass of twisted.trial's TestCase which looks for 'loglevel'
+ attributes on both itself and its individual test methods, to override the
+ root logger's logging level while that test (case|method) runs."""
+
+ def __init__(self, methodName, *args, **kwargs):
+ super(TestCase, self).__init__(methodName, *args, **kwargs)
+
+ method = getattr(self, methodName)
+
+ level = getattr(method, "loglevel",
+ getattr(self, "loglevel",
+ NEVER))
+
+ @around(self)
+ def setUp(orig):
+ old_level = logging.getLogger().level
+
+ if old_level != level:
+ @around(self)
+ def tearDown(orig):
+ ret = orig()
+ logging.getLogger().setLevel(old_level)
+ return ret
+
+ logging.getLogger().setLevel(level)
+ # Don't set SQL logging
+ logging.getLogger("synapse.storage").setLevel(old_level)
+ return orig()
+
+ def assertObjectHasAttributes(self, attrs, obj):
+ """Asserts that the given object has each of the attributes given, and
+ that the value of each matches according to assertEquals."""
+ for (key, value) in attrs.items():
+ if not hasattr(obj, key):
+ raise AssertionError("Expected obj to have a '.%s'" % key)
+ try:
+ self.assertEquals(attrs[key], getattr(obj, key))
+ except AssertionError as e:
+ raise (type(e))(e.message + " for '.%s'" % key)
+
+
+def DEBUG(target):
+ """A decorator to set the .loglevel attribute to logging.DEBUG.
+ Can apply to either a TestCase or an individual test method."""
+ target.loglevel = logging.DEBUG
+ return target
diff --git a/tests/util/__init__.py b/tests/util/__init__.py
new file mode 100644
index 00000000..9bff9ec1
--- /dev/null
+++ b/tests/util/__init__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
diff --git a/tests/util/test_dict_cache.py b/tests/util/test_dict_cache.py
new file mode 100644
index 00000000..54ff26cd
--- /dev/null
+++ b/tests/util/test_dict_cache.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from twisted.internet import defer
+from tests import unittest
+
+from synapse.util.caches.dictionary_cache import DictionaryCache
+
+
+class DictCacheTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.cache = DictionaryCache("foobar")
+
+ def test_simple_cache_hit_full(self):
+ key = "test_simple_cache_hit_full"
+
+ v = self.cache.get(key)
+ self.assertEqual((False, {}), v)
+
+ seq = self.cache.sequence
+ test_value = {"test": "test_simple_cache_hit_full"}
+ self.cache.update(seq, key, test_value, full=True)
+
+ c = self.cache.get(key)
+ self.assertEqual(test_value, c.value)
+
+ def test_simple_cache_hit_partial(self):
+ key = "test_simple_cache_hit_partial"
+
+ seq = self.cache.sequence
+ test_value = {
+ "test": "test_simple_cache_hit_partial"
+ }
+ self.cache.update(seq, key, test_value, full=True)
+
+ c = self.cache.get(key, ["test"])
+ self.assertEqual(test_value, c.value)
+
+ def test_simple_cache_miss_partial(self):
+ key = "test_simple_cache_miss_partial"
+
+ seq = self.cache.sequence
+ test_value = {
+ "test": "test_simple_cache_miss_partial"
+ }
+ self.cache.update(seq, key, test_value, full=True)
+
+ c = self.cache.get(key, ["test2"])
+ self.assertEqual({}, c.value)
+
+ def test_simple_cache_hit_miss_partial(self):
+ key = "test_simple_cache_hit_miss_partial"
+
+ seq = self.cache.sequence
+ test_value = {
+ "test": "test_simple_cache_hit_miss_partial",
+ "test2": "test_simple_cache_hit_miss_partial2",
+ "test3": "test_simple_cache_hit_miss_partial3",
+ }
+ self.cache.update(seq, key, test_value, full=True)
+
+ c = self.cache.get(key, ["test2"])
+ self.assertEqual({"test2": "test_simple_cache_hit_miss_partial2"}, c.value)
+
+ def test_multi_insert(self):
+ key = "test_simple_cache_hit_miss_partial"
+
+ seq = self.cache.sequence
+ test_value_1 = {
+ "test": "test_simple_cache_hit_miss_partial",
+ }
+ self.cache.update(seq, key, test_value_1, full=False)
+
+ seq = self.cache.sequence
+ test_value_2 = {
+ "test2": "test_simple_cache_hit_miss_partial2",
+ }
+ self.cache.update(seq, key, test_value_2, full=False)
+
+ c = self.cache.get(key)
+ self.assertEqual(
+ {
+ "test": "test_simple_cache_hit_miss_partial",
+ "test2": "test_simple_cache_hit_miss_partial2",
+ },
+ c.value
+ )
diff --git a/tests/util/test_log_context.py b/tests/util/test_log_context.py
new file mode 100644
index 00000000..efa0f28b
--- /dev/null
+++ b/tests/util/test_log_context.py
@@ -0,0 +1,43 @@
+from twisted.internet import defer
+from twisted.internet import reactor
+from .. import unittest
+
+from synapse.util.async import sleep
+from synapse.util.logcontext import LoggingContext
+
+class LoggingContextTestCase(unittest.TestCase):
+
+ def _check_test_key(self, value):
+ self.assertEquals(
+ LoggingContext.current_context().test_key, value
+ )
+
+ def test_with_context(self):
+ with LoggingContext() as context_one:
+ context_one.test_key = "test"
+ self._check_test_key("test")
+
+ def test_chaining(self):
+ with LoggingContext() as context_one:
+ context_one.test_key = "one"
+ with LoggingContext() as context_two:
+ self._check_test_key("one")
+ context_two.test_key = "two"
+ self._check_test_key("two")
+ self._check_test_key("one")
+
+ @defer.inlineCallbacks
+ def test_sleep(self):
+ @defer.inlineCallbacks
+ def competing_callback():
+ with LoggingContext() as competing_context:
+ competing_context.test_key = "competing"
+ yield sleep(0)
+ self._check_test_key("competing")
+
+ reactor.callLater(0, competing_callback)
+
+ with LoggingContext() as context_one:
+ context_one.test_key = "one"
+ yield sleep(0)
+ self._check_test_key("one")
diff --git a/tests/util/test_lrucache.py b/tests/util/test_lrucache.py
new file mode 100644
index 00000000..fc5a9043
--- /dev/null
+++ b/tests/util/test_lrucache.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from .. import unittest
+
+from synapse.util.caches.lrucache import LruCache
+
+class LruCacheTestCase(unittest.TestCase):
+
+ def test_get_set(self):
+ cache = LruCache(1)
+ cache["key"] = "value"
+ self.assertEquals(cache.get("key"), "value")
+ self.assertEquals(cache["key"], "value")
+
+ def test_eviction(self):
+ cache = LruCache(2)
+ cache[1] = 1
+ cache[2] = 2
+
+ self.assertEquals(cache.get(1), 1)
+ self.assertEquals(cache.get(2), 2)
+
+ cache[3] = 3
+
+ self.assertEquals(cache.get(1), None)
+ self.assertEquals(cache.get(2), 2)
+ self.assertEquals(cache.get(3), 3)
+
+ def test_setdefault(self):
+ cache = LruCache(1)
+ self.assertEquals(cache.setdefault("key", 1), 1)
+ self.assertEquals(cache.get("key"), 1)
+ self.assertEquals(cache.setdefault("key", 2), 1)
+ self.assertEquals(cache.get("key"), 1)
+
+ def test_pop(self):
+ cache = LruCache(1)
+ cache["key"] = 1
+ self.assertEquals(cache.pop("key"), 1)
+ self.assertEquals(cache.pop("key"), None)
diff --git a/tests/utils.py b/tests/utils.py
new file mode 100644
index 00000000..91040c2e
--- /dev/null
+++ b/tests/utils.py
@@ -0,0 +1,479 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from synapse.http.server import HttpServer
+from synapse.api.errors import cs_error, CodeMessageException, StoreError
+from synapse.api.constants import EventTypes
+from synapse.storage.prepare_database import prepare_database
+from synapse.storage.engines import create_engine
+from synapse.server import HomeServer
+
+from synapse.util.logcontext import LoggingContext
+
+from twisted.internet import defer, reactor
+from twisted.enterprise.adbapi import ConnectionPool
+
+from collections import namedtuple
+from mock import patch, Mock
+import hashlib
+import urllib
+import urlparse
+
+from inspect import getcallargs
+
+
+@defer.inlineCallbacks
+def setup_test_homeserver(name="test", datastore=None, config=None, **kargs):
+ """Setup a homeserver suitable for running tests against. Keyword arguments
+ are passed to the Homeserver constructor. If no datastore is supplied a
+ datastore backed by an in-memory sqlite db will be given to the HS.
+ """
+ if config is None:
+ config = Mock()
+ config.signing_key = [MockKey()]
+ config.event_cache_size = 1
+ config.disable_registration = False
+ config.macaroon_secret_key = "not even a little secret"
+ config.server_name = "server.under.test"
+
+ if "clock" not in kargs:
+ kargs["clock"] = MockClock()
+
+ if datastore is None:
+ db_pool = SQLiteMemoryDbPool()
+ yield db_pool.prepare()
+ hs = HomeServer(
+ name, db_pool=db_pool, config=config,
+ version_string="Synapse/tests",
+ database_engine=create_engine("sqlite3"),
+ **kargs
+ )
+ else:
+ hs = HomeServer(
+ name, db_pool=None, datastore=datastore, config=config,
+ version_string="Synapse/tests",
+ database_engine=create_engine("sqlite3"),
+ **kargs
+ )
+
+ # bcrypt is far too slow to be doing in unit tests
+ def swap_out_hash_for_testing(old_build_handlers):
+ def build_handlers():
+ handlers = old_build_handlers()
+ auth_handler = handlers.auth_handler
+ auth_handler.hash = lambda p: hashlib.md5(p).hexdigest()
+ auth_handler.validate_hash = lambda p, h: hashlib.md5(p).hexdigest() == h
+ return handlers
+ return build_handlers
+
+ hs.build_handlers = swap_out_hash_for_testing(hs.build_handlers)
+
+ defer.returnValue(hs)
+
+
+def get_mock_call_args(pattern_func, mock_func):
+ """ Return the arguments the mock function was called with interpreted
+ by the pattern functions argument list.
+ """
+ invoked_args, invoked_kargs = mock_func.call_args
+ return getcallargs(pattern_func, *invoked_args, **invoked_kargs)
+
+
+# This is a mock /resource/ not an entire server
+class MockHttpResource(HttpServer):
+
+ def __init__(self, prefix=""):
+ self.callbacks = [] # 3-tuple of method/pattern/function
+ self.prefix = prefix
+
+ def trigger_get(self, path):
+ return self.trigger("GET", path, None)
+
+ @patch('twisted.web.http.Request')
+ @defer.inlineCallbacks
+ def trigger(self, http_method, path, content, mock_request):
+ """ Fire an HTTP event.
+
+ Args:
+ http_method : The HTTP method
+ path : The HTTP path
+ content : The HTTP body
+ mock_request : Mocked request to pass to the event so it can get
+ content.
+ Returns:
+ A tuple of (code, response)
+ Raises:
+ KeyError If no event is found which will handle the path.
+ """
+ path = self.prefix + path
+
+ # annoyingly we return a twisted http request which has chained calls
+ # to get at the http content, hence mock it here.
+ mock_content = Mock()
+ config = {'read.return_value': content}
+ mock_content.configure_mock(**config)
+ mock_request.content = mock_content
+
+ mock_request.method = http_method
+ mock_request.uri = path
+
+ mock_request.getClientIP.return_value = "-"
+
+ mock_request.requestHeaders.getRawHeaders.return_value=[
+ "X-Matrix origin=test,key=,sig="
+ ]
+
+ # return the right path if the event requires it
+ mock_request.path = path
+
+ # add in query params to the right place
+ try:
+ mock_request.args = urlparse.parse_qs(path.split('?')[1])
+ mock_request.path = path.split('?')[0]
+ path = mock_request.path
+ except:
+ pass
+
+ for (method, pattern, func) in self.callbacks:
+ if http_method != method:
+ continue
+
+ matcher = pattern.match(path)
+ if matcher:
+ try:
+ args = [
+ urllib.unquote(u).decode("UTF-8")
+ for u in matcher.groups()
+ ]
+
+ (code, response) = yield func(
+ mock_request,
+ *args
+ )
+ defer.returnValue((code, response))
+ except CodeMessageException as e:
+ defer.returnValue((e.code, cs_error(e.msg)))
+
+ raise KeyError("No event can handle %s" % path)
+
+ def register_path(self, method, path_pattern, callback):
+ self.callbacks.append((method, path_pattern, callback))
+
+
+class MockKey(object):
+ alg = "mock_alg"
+ version = "mock_version"
+ signature = b"\x9a\x87$"
+
+ @property
+ def verify_key(self):
+ return self
+
+ def sign(self, message):
+ return self
+
+ def verify(self, message, sig):
+ assert sig == b"\x9a\x87$"
+
+
+class MockClock(object):
+ now = 1000
+
+ def __init__(self):
+ # list of lists of [absolute_time, callback, expired] in no particular
+ # order
+ self.timers = []
+
+ def time(self):
+ return self.now
+
+ def time_msec(self):
+ return self.time() * 1000
+
+ def call_later(self, delay, callback):
+ current_context = LoggingContext.current_context()
+
+ def wrapped_callback():
+ LoggingContext.thread_local.current_context = current_context
+ callback()
+
+ t = [self.now + delay, wrapped_callback, False]
+ self.timers.append(t)
+
+ return t
+
+ def looping_call(self, function, interval):
+ pass
+
+ def cancel_call_later(self, timer):
+ if timer[2]:
+ raise Exception("Cannot cancel an expired timer")
+
+ timer[2] = True
+ self.timers = [t for t in self.timers if t != timer]
+
+ # For unit testing
+ def advance_time(self, secs):
+ self.now += secs
+
+ timers = self.timers
+ self.timers = []
+
+ for t in timers:
+ time, callback, expired = t
+
+ if expired:
+ raise Exception("Timer already expired")
+
+ if self.now >= time:
+ t[2] = True
+ callback()
+ else:
+ self.timers.append(t)
+
+ def advance_time_msec(self, ms):
+ self.advance_time(ms / 1000.)
+
+
+class SQLiteMemoryDbPool(ConnectionPool, object):
+ def __init__(self):
+ super(SQLiteMemoryDbPool, self).__init__(
+ "sqlite3", ":memory:",
+ cp_min=1,
+ cp_max=1,
+ )
+
+ def prepare(self):
+ engine = create_engine("sqlite3")
+ return self.runWithConnection(
+ lambda conn: prepare_database(conn, engine)
+ )
+
+
+class MemoryDataStore(object):
+
+ Room = namedtuple(
+ "Room",
+ ["room_id", "is_public", "creator"]
+ )
+
+ def __init__(self):
+ self.tokens_to_users = {}
+ self.paths_to_content = {}
+
+ self.members = {}
+ self.rooms = {}
+
+ self.current_state = {}
+ self.events = []
+
+ class Snapshot(namedtuple("Snapshot", "room_id user_id membership_state")):
+ def fill_out_prev_events(self, event):
+ pass
+
+ def snapshot_room(self, room_id, user_id, state_type=None, state_key=None):
+ return self.Snapshot(
+ room_id, user_id, self.get_room_member(user_id, room_id)
+ )
+
+ def register(self, user_id, token, password_hash):
+ if user_id in self.tokens_to_users.values():
+ raise StoreError(400, "User in use.")
+ self.tokens_to_users[token] = user_id
+
+ def get_user_by_access_token(self, token):
+ try:
+ return {
+ "name": self.tokens_to_users[token],
+ }
+ except:
+ raise StoreError(400, "User does not exist.")
+
+ def get_room(self, room_id):
+ try:
+ return self.rooms[room_id]
+ except:
+ return None
+
+ def store_room(self, room_id, room_creator_user_id, is_public):
+ if room_id in self.rooms:
+ raise StoreError(409, "Conflicting room!")
+
+ room = MemoryDataStore.Room(
+ room_id=room_id,
+ is_public=is_public,
+ creator=room_creator_user_id
+ )
+ self.rooms[room_id] = room
+
+ def get_room_member(self, user_id, room_id):
+ return self.members.get(room_id, {}).get(user_id)
+
+ def get_room_members(self, room_id, membership=None):
+ if membership:
+ return [
+ v for k, v in self.members.get(room_id, {}).items()
+ if v.membership == membership
+ ]
+ else:
+ return self.members.get(room_id, {}).values()
+
+ def get_rooms_for_user_where_membership_is(self, user_id, membership_list):
+ return [
+ self.members[r].get(user_id) for r in self.members
+ if user_id in self.members[r] and
+ self.members[r][user_id].membership in membership_list
+ ]
+
+ def get_room_events_stream(self, user_id=None, from_key=None, to_key=None,
+ limit=0, with_feedback=False):
+ return ([], from_key) # TODO
+
+ def get_joined_hosts_for_room(self, room_id):
+ return defer.succeed([])
+
+ def persist_event(self, event):
+ if event.type == EventTypes.Member:
+ room_id = event.room_id
+ user = event.state_key
+ membership = event.membership
+ self.members.setdefault(room_id, {})[user] = event
+
+ if hasattr(event, "state_key"):
+ key = (event.room_id, event.type, event.state_key)
+ self.current_state[key] = event
+
+ self.events.append(event)
+
+ def get_current_state(self, room_id, event_type=None, state_key=""):
+ if event_type:
+ key = (room_id, event_type, state_key)
+ if self.current_state.get(key):
+ return [self.current_state.get(key)]
+ return None
+ else:
+ return [
+ e for e in self.current_state
+ if e[0] == room_id
+ ]
+
+ def set_presence_state(self, user_localpart, state):
+ return defer.succeed({"state": 0})
+
+ def get_presence_list(self, user_localpart, accepted):
+ return []
+
+ def get_room_events_max_id(self):
+ return "s0" # TODO (erikj)
+
+ def get_send_event_level(self, room_id):
+ return defer.succeed(0)
+
+ def get_power_level(self, room_id, user_id):
+ return defer.succeed(0)
+
+ def get_add_state_level(self, room_id):
+ return defer.succeed(0)
+
+ def get_room_join_rule(self, room_id):
+ # TODO (erikj): This should be configurable
+ return defer.succeed("invite")
+
+ def get_ops_levels(self, room_id):
+ return defer.succeed((5, 5, 5))
+
+ def insert_client_ip(self, user, access_token, ip, user_agent):
+ return defer.succeed(None)
+
+
+def _format_call(args, kwargs):
+ return ", ".join(
+ ["%r" % (a) for a in args] +
+ ["%s=%r" % (k, v) for k, v in kwargs.items()]
+ )
+
+
+class DeferredMockCallable(object):
+ """A callable instance that stores a set of pending call expectations and
+ return values for them. It allows a unit test to assert that the given set
+ of function calls are eventually made, by awaiting on them to be called.
+ """
+
+ def __init__(self):
+ self.expectations = []
+ self.calls = []
+
+ def __call__(self, *args, **kwargs):
+ self.calls.append((args, kwargs))
+
+ if not self.expectations:
+ raise ValueError("%r has no pending calls to handle call(%s)" % (
+ self, _format_call(args, kwargs))
+ )
+
+ for (call, result, d) in self.expectations:
+ if args == call[1] and kwargs == call[2]:
+ d.callback(None)
+ return result
+
+ failure = AssertionError("Was not expecting call(%s)" %
+ _format_call(args, kwargs)
+ )
+
+ for _, _, d in self.expectations:
+ try:
+ d.errback(failure)
+ except:
+ pass
+
+ raise failure
+
+ def expect_call_and_return(self, call, result):
+ self.expectations.append((call, result, defer.Deferred()))
+
+ @defer.inlineCallbacks
+ def await_calls(self, timeout=1000):
+ deferred = defer.DeferredList(
+ [d for _, _, d in self.expectations],
+ fireOnOneErrback=True
+ )
+
+ timer = reactor.callLater(
+ timeout/1000,
+ deferred.errback,
+ AssertionError(
+ "%d pending calls left: %s"% (
+ len([e for e in self.expectations if not e[2].called]),
+ [e for e in self.expectations if not e[2].called]
+ )
+ )
+ )
+
+ yield deferred
+
+ timer.cancel()
+
+ self.calls = []
+
+ def assert_had_no_calls(self):
+ if self.calls:
+ calls = self.calls
+ self.calls = []
+
+ raise AssertionError("Expected not to received any calls, got:\n" +
+ "\n".join([
+ "call(%s)" % _format_call(c[0], c[1]) for c in calls
+ ])
+ )