diff --git a/docs/client-server/howto.rst b/docs/client-server/howto.rst
new file mode 100644
index 0000000000..de096ab60c
--- /dev/null
+++ b/docs/client-server/howto.rst
@@ -0,0 +1,285 @@
+TODO(kegan): Tweak joinalias API keys/path? Event stream historical > live needs
+a token (currently doesn't). im/sync responses include outdated event formats
+(room membership change messages). Room config (specifically: message history,
+public rooms). /register seems super simplistic compared to /login, maybe it
+would be better if /register used the same technique as /login?
+
+
+How to use the client-server API
+================================
+
+This guide focuses on how the client-server APIs *provided by the reference
+home server* can be used. Since this is specific to a home server
+implementation, there may be variations in relation to registering/logging in
+which are not covered in extensive detail in this guide.
+
+If you haven't already, get a home server up and running on
+``http://localhost:8080``.
+
+
+Accounts
+========
+Before you can send and receive messages, you must **register** for an account.
+If you already have an account, you must **login** into it.
+
+Registration
+------------
+The aim of registration is to get a user ID and access token which you will need
+when accessing other APIs::
+
+ curl -XPOST -d '{"user_id":"example", "password":"wordpass"}' "http://localhost:8080/matrix/client/api/v1/register"
+
+ {
+ "access_token": "QGV4YW1wbGU6bG9jYWxob3N0.AqdSzFmFYrLrTmteXc",
+ "home_server": "localhost",
+ "user_id": "@example:localhost"
+ }
+
+NB: If a ``user_id`` is not specified, one will be randomly generated for you.
+If you do not specify a ``password``, you will be unable to login to the account
+if you forget the ``access_token``.
+
+Implementation note: The matrix specification does not enforce how users
+register with a server. It just specifies the URL path and absolute minimum
+keys. The reference home server uses a username/password to authenticate user,
+but other home servers may use different methods.
+
+Login
+-----
+The aim when logging in is to get an access token for your existing user ID::
+
+ curl -XGET "http://localhost:8080/matrix/client/api/v1/login"
+
+ {
+ "type": "m.login.password"
+ }
+
+ curl -XPOST -d '{"type":"m.login.password", "user":"example", "password":"wordpass"}' "http://localhost:8080/matrix/client/api/v1/login"
+
+ {
+ "access_token": "QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd",
+ "home_server": "localhost",
+ "user_id": "@example:localhost"
+ }
+
+Implementation note: Different home servers may implement different methods for
+logging in to an existing account. In order to check that you know how to login
+to this home server, you must perform a ``GET`` first and make sure you
+recognise the login type. If you do not know how to login, you can
+``GET /login/fallback`` which will return a basic webpage which you can use to
+login. The reference home server implementation support username/password login,
+but other home servers may support different login methods (e.g. OAuth2).
+
+
+Communicating
+=============
+
+In order to communicate with another user, you must **create a room** with that
+user and **send a message** to that room.
+
+Creating a room
+---------------
+If you want to send a message to someone, you have to be in a room with them. To
+create a room::
+
+ curl -XPOST -d '{"room_alias_name":"tutorial"}' "http://localhost:8080/matrix/client/api/v1/rooms?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
+
+ {
+ "room_alias": "#tutorial:localhost",
+ "room_id": "!CvcvRuDYDzTOzfKKgh:localhost"
+ }
+
+The "room alias" is a human-readable string which can be shared with other users
+so they can join a room, rather than the room ID which is a randomly generated
+string. You can have multiple room aliases per room.
+
+TODO(kegan): How to add/remove aliases from an existing room.
+
+
+Sending messages
+----------------
+You can now send messages to this room::
+
+ curl -XPUT -d '{"msgtype":"m.text", "body":"hello"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/messages/%40example%3Alocalhost/msgid1?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
+
+NB: There are no limitations to the types of messages which can be exchanged.
+The only requirement is that ``"msgtype"`` is specified.
+
+NB: Depending on the room config, users who join the room may be able to see
+message history from before they joined.
+
+Users and rooms
+===============
+
+Each room can be configured to allow or disallow certain rules. In particular,
+these rules may specify if you require an **invitation** from someone already in
+the room in order to **join the room**. In addition, you may also be able to
+join a room **via a room alias** if one was set up.
+
+Inviting a user to a room
+-------------------------
+You can directly invite a user to a room like so::
+
+ curl -XPUT -d '{"membership":"invite"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
+
+This informs ``@myfriend:localhost`` of the room ID
+``!CvcvRuDYDzTOzfKKgh:localhost`` and allows them to join the room.
+
+Joining a room via an invite
+----------------------------
+If you receive an invite, you can join the room by changing the membership to
+join::
+
+ curl -XPUT -d '{"membership":"join"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
+
+NB: Only the person invited (``@myfriend:localhost``) can change the membership
+state to ``"join"``.
+
+Joining a room via an alias
+---------------------------
+Alternatively, if you know the room alias for this room and the room config
+allows it, you can directly join a room via the alias::
+
+ curl -XPUT -d '{}' "http://localhost:8080/matrix/client/api/v1/join/%23tutorial%3Alocalhost?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
+
+ {
+ "room_id": "!CvcvRuDYDzTOzfKKgh:localhost"
+ }
+
+You will need to use the room ID when sending messages, not the room alias.
+
+NB: If the room is configured to be an invite-only room, you will still require
+an invite in order to join the room even though you know the room alias. As a
+result, it is more common to see a room alias in relation to a public room,
+which do not require invitations.
+
+Getting events
+==============
+An event is some interesting piece of data that a client may be interested in.
+It can be a message in a room, a room invite, etc. There are many different ways
+of getting events, depending on what the client already knows.
+
+Getting all state
+-----------------
+If the client doesn't know any information on the rooms the user is
+invited/joined on, they can get all the user's state for all rooms::
+
+ curl -XGET "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
+
+ [
+ {
+ "membership": "join",
+ "messages": {
+ "chunk": [
+ {
+ "content": {
+ "body": "@example:localhost joined the room.",
+ "hsob_ts": 1408444664249,
+ "membership": "join",
+ "membership_source": "@example:localhost",
+ "membership_target": "@example:localhost",
+ "msgtype": "m.text"
+ },
+ "event_id": "lZjmmlrEvo",
+ "msg_id": "m1408444664249",
+ "room_id": "!CvcvRuDYDzTOzfKKgh:localhost",
+ "type": "m.room.message",
+ "user_id": "_homeserver_"
+ },
+ {
+ "content": {
+ "body": "hello",
+ "hsob_ts": 1408445405672,
+ "msgtype": "m.text"
+ },
+ "event_id": "BiBJqamISg",
+ "msg_id": "msgid1",
+ "room_id": "!CvcvRuDYDzTOzfKKgh:localhost",
+ "type": "m.room.message",
+ "user_id": "@example:localhost"
+ },
+ [...]
+ {
+ "content": {
+ "body": "@myfriend:localhost joined the room.",
+ "hsob_ts": 1408446501661,
+ "membership": "join",
+ "membership_source": "@myfriend:localhost",
+ "membership_target": "@myfriend:localhost",
+ "msgtype": "m.text"
+ },
+ "event_id": "IMmXbOzFAa",
+ "msg_id": "m1408446501661",
+ "room_id": "!CvcvRuDYDzTOzfKKgh:localhost",
+ "type": "m.room.message",
+ "user_id": "_homeserver_"
+ }
+ ],
+ "end": "20",
+ "start": "0"
+ },
+ "room_id": "!CvcvRuDYDzTOzfKKgh:localhost"
+ }
+ ]
+
+This returns all the room IDs of rooms the user is invited/joined on, as well as
+all of the messages and feedback for these rooms. This can be a LOT of data. You
+may just want the most recent message for each room. This can be achieved by
+applying pagination stream parameters to this request::
+
+ curl -XGET "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END&to=START&limit=1"
+
+ [
+ {
+ "membership": "join",
+ "messages": {
+ "chunk": [
+ {
+ "content": {
+ "body": "@myfriend:localhost joined the room.",
+ "hsob_ts": 1408446501661,
+ "membership": "join",
+ "membership_source": "@myfriend:localhost",
+ "membership_target": "@myfriend:localhost",
+ "msgtype": "m.text"
+ },
+ "event_id": "IMmXbOzFAa",
+ "msg_id": "m1408446501661",
+ "room_id": "!CvcvRuDYDzTOzfKKgh:localhost",
+ "type": "m.room.message",
+ "user_id": "_homeserver_"
+ }
+ ],
+ "end": "20",
+ "start": "21"
+ },
+ "room_id": "!CvcvRuDYDzTOzfKKgh:localhost"
+ }
+ ]
+
+Getting live state
+------------------
+Once you know which rooms the client has previously interacted with, you need to
+listen for incoming events. This can be done like so::
+
+ curl -XGET "http://localhost:8080/matrix/client/api/v1/events?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END"
+
+ {
+ "chunk": [],
+ "end": "215",
+ "start": "215"
+ }
+
+This will block waiting for an incoming event, timing out after several seconds.
+Even if there are no new events (as in the example above), there will be some
+pagination stream response keys. The client should make subsequent requests
+using the value of the ``"end"`` key (in this case ``215``) as the ``from``
+query parameter. This value should be stored so when the client reopens your app
+after a period of inactivity, you can resume from where you got up to in the
+event stream. If it has been a long period of inactivity, there may be LOTS of
+events waiting for the user. In this case, you may wish to get all state instead
+and then resume getting live state from a newer end token.
+
+NB: The timeout can be changed by adding a ``timeout`` query parameter, which is
+in milliseconds. A timeout of 0 will not block.
+
diff --git a/synapse/api/urls.py b/synapse/api/urls.py
index 04970adb71..05ca000787 100644
--- a/synapse/api/urls.py
+++ b/synapse/api/urls.py
@@ -17,4 +17,5 @@
CLIENT_PREFIX = "/matrix/client/api/v1"
FEDERATION_PREFIX = "/matrix/federation/v1"
-WEB_CLIENT_PREFIX = "/matrix/client"
\ No newline at end of file
+WEB_CLIENT_PREFIX = "/matrix/client"
+CONTENT_REPO_PREFIX = "/matrix/content"
\ No newline at end of file
diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py
index 3429a29a6b..ca102236cf 100755
--- a/synapse/app/homeserver.py
+++ b/synapse/app/homeserver.py
@@ -24,9 +24,11 @@ from twisted.python.log import PythonLoggingObserver
from twisted.web.resource import Resource
from twisted.web.static import File
from twisted.web.server import Site
-from synapse.http.server import JsonResource, RootRedirect
+from synapse.http.server import JsonResource, RootRedirect, ContentRepoResource
from synapse.http.client import TwistedHttpClient
-from synapse.api.urls import CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX
+from synapse.api.urls import (
+ CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX
+)
from daemonize import Daemonize
@@ -53,6 +55,9 @@ class SynapseHomeServer(HomeServer):
def build_resource_for_web_client(self):
return File("webclient") # TODO configurable?
+ def build_resource_for_content_repo(self):
+ return ContentRepoResource("uploads", self.auth)
+
def build_db_pool(self):
""" Set up all the dbs. Since all the *.sql have IF NOT EXISTS, so we
don't have to worry about overwriting existing content.
@@ -101,7 +106,8 @@ class SynapseHomeServer(HomeServer):
# [ ("/aaa/bbb/cc", Resource1), ("/aaa/dummy", Resource2) ]
desired_tree = [
(CLIENT_PREFIX, self.get_resource_for_client()),
- (FEDERATION_PREFIX, self.get_resource_for_federation())
+ (FEDERATION_PREFIX, self.get_resource_for_federation()),
+ (CONTENT_REPO_PREFIX, self.get_resource_for_content_repo())
]
if web_client:
logger.info("Adding the web client.")
diff --git a/synapse/federation/replication.py b/synapse/federation/replication.py
index 3e5f1a4108..8030d0963f 100644
--- a/synapse/federation/replication.py
+++ b/synapse/federation/replication.py
@@ -158,6 +158,7 @@ class ReplicationLayer(object):
# TODO, add errback, etc.
self._transaction_queue.enqueue_edu(edu)
+ return defer.succeed(None)
@log_function
def make_query(self, destination, query_type, args):
diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py
index f140dc527a..60684f17d7 100644
--- a/synapse/handlers/presence.py
+++ b/synapse/handlers/presence.py
@@ -383,7 +383,7 @@ class PresenceHandler(BaseHandler):
logger.debug("Start polling for presence from %s", user)
if target_user:
- target_users = set(target_user)
+ target_users = set([target_user])
else:
presence = yield self.store.get_presence_list(
user.localpart, accepted=True
@@ -463,9 +463,13 @@ class PresenceHandler(BaseHandler):
deferreds = []
if target_user:
- raise NotImplementedError("TODO: remove one user")
+ if target_user not in self._remote_recvmap:
+ return
+ target_users = set([target_user])
+ else:
+ target_users = self._remote_recvmap.keys()
- remoteusers = [u for u in self._remote_recvmap
+ remoteusers = [u for u in target_users
if user in self._remote_recvmap[u]]
remoteusers_by_domain = partition(remoteusers, lambda u: u.domain)
diff --git a/synapse/http/server.py b/synapse/http/server.py
index bad2738bde..c28d9a33f9 100644
--- a/synapse/http/server.py
+++ b/synapse/http/server.py
@@ -17,16 +17,23 @@
from syutil.jsonutil import (
encode_canonical_json, encode_pretty_printed_json
)
-from synapse.api.errors import cs_exception, CodeMessageException
+from synapse.api.errors import (
+ cs_exception, SynapseError, CodeMessageException, Codes, cs_error
+)
+from synapse.util.stringutils import random_string
from twisted.internet import defer, reactor
+from twisted.protocols.basic import FileSender
from twisted.web import server, resource
from twisted.web.server import NOT_DONE_YET
from twisted.web.util import redirectTo
+import base64
import collections
+import json
import logging
-
+import os
+import re
logger = logging.getLogger(__name__)
@@ -125,7 +132,11 @@ class JsonResource(HttpServer, resource.Resource):
{"error": "Unrecognized request"}
)
except CodeMessageException as e:
- logger.exception(e)
+ if isinstance(e, SynapseError):
+ logger.error("%s SynapseError: %s - %s", request, e.code,
+ e.msg)
+ else:
+ logger.exception(e)
self._send_response(
request,
e.code,
@@ -140,6 +151,14 @@ class JsonResource(HttpServer, resource.Resource):
)
def _send_response(self, request, code, response_json_object):
+ # could alternatively use request.notifyFinish() and flip a flag when
+ # the Deferred fires, but since the flag is RIGHT THERE it seems like
+ # a waste.
+ if request._disconnected:
+ logger.warn(
+ "Not sending response to request %s, already disconnected.",
+ request)
+ return
if not self._request_user_agent_is_curl(request):
json_bytes = encode_canonical_json(response_json_object)
@@ -176,6 +195,141 @@ class RootRedirect(resource.Resource):
return resource.Resource.getChild(self, name, request)
+class ContentRepoResource(resource.Resource):
+ """Provides file uploading and downloading.
+
+ Uploads are POSTed to wherever this Resource is linked to. This resource
+ returns a "content token" which can be used to GET this content again. The
+ token is typically a path, but it may not be. Tokens can expire, be one-time
+ uses, etc.
+
+ In this case, the token is a path to the file and contains 3 interesting
+ sections:
+ - User ID base64d (for namespacing content to each user)
+ - random 24 char string
+ - Content type base64d (so we can return it when clients GET it)
+
+ """
+ isLeaf = True
+
+ def __init__(self, directory, auth):
+ resource.Resource.__init__(self)
+ self.directory = directory
+ self.auth = auth
+
+ if not os.path.isdir(self.directory):
+ os.mkdir(self.directory)
+ logger.info("ContentRepoResource : Created %s directory.",
+ self.directory)
+
+ @defer.inlineCallbacks
+ def map_request_to_name(self, request):
+ # auth the user
+ auth_user = yield self.auth.get_user_by_req(request)
+
+ # namespace all file uploads on the user
+ prefix = base64.urlsafe_b64encode(
+ auth_user.to_string()
+ ).replace('=', '')
+
+ # use a random string for the main portion
+ main_part = random_string(24)
+
+ # suffix with a file extension if we can make one. This is nice to
+ # provide a hint to clients on the file information. We will also reuse
+ # this info to spit back the content type to the client.
+ suffix = ""
+ if request.requestHeaders.hasHeader("Content-Type"):
+ content_type = request.requestHeaders.getRawHeaders(
+ "Content-Type")[0]
+ suffix = "." + base64.urlsafe_b64encode(content_type)
+ if (content_type.split("/")[0].lower() in
+ ["image", "video", "audio"]):
+ file_ext = content_type.split("/")[-1]
+ # be a little paranoid and only allow a-z
+ file_ext = re.sub("[^a-z]", "", file_ext)
+ suffix += "." + file_ext
+
+ file_path = os.path.join(self.directory, prefix + main_part + suffix)
+ logger.info("User %s is uploading a file to path %s",
+ auth_user.to_string(),
+ file_path)
+
+ # keep trying to make a non-clashing file, with a sensible max attempts
+ attempts = 0
+ while os.path.exists(file_path):
+ main_part = random_string(24)
+ file_path = os.path.join(self.directory,
+ prefix + main_part + suffix)
+ attempts += 1
+ if attempts > 25: # really? Really?
+ raise SynapseError(500, "Unable to create file.")
+
+ defer.returnValue(file_path)
+
+ def render_GET(self, request):
+ # no auth here on purpose, to allow anyone to view, even across home
+ # servers.
+
+ # TODO: A little crude here, we could do this better.
+ filename = request.path.split(self.directory + "/")[1]
+ # be paranoid
+ filename = re.sub("[^0-9A-z.-_]", "", filename)
+
+ file_path = self.directory + "/" + filename
+ if os.path.isfile(file_path):
+ # filename has the content type
+ base64_contentype = filename.split(".")[1]
+ content_type = base64.urlsafe_b64decode(base64_contentype)
+ logger.info("Sending file %s", file_path)
+ f = open(file_path, 'rb')
+ request.setHeader('Content-Type', content_type)
+ d = FileSender().beginFileTransfer(f, request)
+
+ # after the file has been sent, clean up and finish the request
+ def cbFinished(ignored):
+ f.close()
+ request.finish()
+ d.addCallback(cbFinished)
+ else:
+ respond_with_json_bytes(
+ request,
+ 404,
+ json.dumps(cs_error("Not found", code=Codes.NOT_FOUND)),
+ send_cors=True)
+
+ return server.NOT_DONE_YET
+
+ def render_POST(self, request):
+ self._async_render(request)
+ return server.NOT_DONE_YET
+
+ @defer.inlineCallbacks
+ def _async_render(self, request):
+ try:
+ fname = yield self.map_request_to_name(request)
+
+ # TODO I have a suspcious feeling this is just going to block
+ with open(fname, "wb") as f:
+ f.write(request.content.read())
+
+ respond_with_json_bytes(request, 200,
+ json.dumps({"content_token": fname}),
+ send_cors=True)
+
+ except CodeMessageException as e:
+ logger.exception(e)
+ respond_with_json_bytes(request, e.code,
+ json.dumps(cs_exception(e)))
+ except Exception as e:
+ logger.error("Failed to store file: %s" % e)
+ respond_with_json_bytes(
+ request,
+ 500,
+ json.dumps({"error": "Internal server error"}),
+ send_cors=True)
+
+
def respond_with_json_bytes(request, code, json_bytes, send_cors=False):
"""Sends encoded JSON in response to the given request.
diff --git a/synapse/rest/room.py b/synapse/rest/room.py
index 89ea9f0d25..1c48e63628 100644
--- a/synapse/rest/room.py
+++ b/synapse/rest/room.py
@@ -170,8 +170,10 @@ class RoomMemberRestServlet(RestServlet):
user = yield self.auth.get_user_by_req(request)
handler = self.handlers.room_member_handler
- member = yield handler.get_room_member(room_id, target_user_id,
- user.to_string())
+ member = yield handler.get_room_member(
+ room_id,
+ urllib.unquote(target_user_id),
+ user.to_string())
if not member:
raise SynapseError(404, "Member not found.",
errcode=Codes.NOT_FOUND)
@@ -183,7 +185,7 @@ class RoomMemberRestServlet(RestServlet):
event = self.event_factory.create_event(
etype=self.get_event_type(),
- target_user_id=target_user_id,
+ target_user_id=urllib.unquote(target_user_id),
room_id=urllib.unquote(roomid),
user_id=user.to_string(),
membership=Membership.LEAVE,
@@ -210,7 +212,7 @@ class RoomMemberRestServlet(RestServlet):
event = self.event_factory.create_event(
etype=self.get_event_type(),
- target_user_id=target_user_id,
+ target_user_id=urllib.unquote(target_user_id),
room_id=urllib.unquote(roomid),
user_id=user.to_string(),
membership=content["membership"],
@@ -218,8 +220,8 @@ class RoomMemberRestServlet(RestServlet):
)
handler = self.handlers.room_member_handler
- result = yield handler.change_membership(event, broadcast_msg=True)
- defer.returnValue((200, result))
+ yield handler.change_membership(event, broadcast_msg=True)
+ defer.returnValue((200, ""))
class MessageRestServlet(RestServlet):
@@ -235,7 +237,7 @@ class MessageRestServlet(RestServlet):
msg_handler = self.handlers.message_handler
msg = yield msg_handler.get_message(room_id=urllib.unquote(room_id),
- sender_id=sender_id,
+ sender_id=urllib.unquote(sender_id),
msg_id=msg_id,
user_id=user.to_string(),
)
@@ -250,7 +252,7 @@ class MessageRestServlet(RestServlet):
def on_PUT(self, request, room_id, sender_id, msg_id):
user = yield self.auth.get_user_by_req(request)
- if user.to_string() != sender_id:
+ if user.to_string() != urllib.unquote(sender_id):
raise SynapseError(403, "Must send messages as yourself.",
errcode=Codes.FORBIDDEN)
diff --git a/synapse/server.py b/synapse/server.py
index 0f7ac352ae..d4c2481483 100644
--- a/synapse/server.py
+++ b/synapse/server.py
@@ -72,6 +72,7 @@ class BaseHomeServer(object):
'resource_for_client',
'resource_for_federation',
'resource_for_web_client',
+ 'resource_for_content_repo',
]
def __init__(self, hostname, **kwargs):
@@ -140,6 +141,7 @@ class HomeServer(BaseHomeServer):
resource_for_client
resource_for_web_client
resource_for_federation
+ resource_for_content_repo
http_client
db_pool
"""
diff --git a/synapse/types.py b/synapse/types.py
index 054b1e713c..b8e191bb3c 100644
--- a/synapse/types.py
+++ b/synapse/types.py
@@ -32,6 +32,12 @@ class DomainSpecificString(
HomeServer as being its own
"""
+ # Deny iteration because it will bite you if you try to create a singleton
+ # set by:
+ # users = set(user)
+ def __iter__(self):
+ raise ValueError("Attempted to iterate a %s" % (type(self).__name__))
+
@classmethod
def from_string(cls, s, hs):
"""Parse the string given by 's' into a structure object."""
diff --git a/tests/federation/test_federation.py b/tests/federation/test_federation.py
index 478ddd879e..58590e4fcd 100644
--- a/tests/federation/test_federation.py
+++ b/tests/federation/test_federation.py
@@ -20,7 +20,7 @@ from twisted.trial import unittest
from mock import Mock
import logging
-from ..utils import MockHttpServer, MockClock
+from ..utils import MockHttpResource, MockClock
from synapse.server import HomeServer
from synapse.federation import initialize_http_replication
@@ -50,7 +50,7 @@ def make_pdu(prev_pdus=[], **kwargs):
class FederationTestCase(unittest.TestCase):
def setUp(self):
- self.mock_http_server = MockHttpServer()
+ self.mock_resource = MockHttpResource()
self.mock_http_client = Mock(spec=[
"get_json",
"put_json",
@@ -70,7 +70,7 @@ class FederationTestCase(unittest.TestCase):
)
self.clock = MockClock()
hs = HomeServer("test",
- resource_for_federation=self.mock_http_server,
+ resource_for_federation=self.mock_resource,
http_client=self.mock_http_client,
db_pool=None,
datastore=self.mock_persistence,
@@ -86,7 +86,7 @@ class FederationTestCase(unittest.TestCase):
)
# Empty context initially
- (code, response) = yield self.mock_http_server.trigger("GET",
+ (code, response) = yield self.mock_resource.trigger("GET",
"/matrix/federation/v1/state/my-context/", None)
self.assertEquals(200, code)
self.assertFalse(response["pdus"])
@@ -111,7 +111,7 @@ class FederationTestCase(unittest.TestCase):
])
)
- (code, response) = yield self.mock_http_server.trigger("GET",
+ (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"]))
@@ -122,7 +122,7 @@ class FederationTestCase(unittest.TestCase):
defer.succeed(None)
)
- (code, response) = yield self.mock_http_server.trigger("GET",
+ (code, response) = yield self.mock_resource.trigger("GET",
"/matrix/federation/v1/pdu/red/abc123def456/", None)
self.assertEquals(404, code)
@@ -141,7 +141,7 @@ class FederationTestCase(unittest.TestCase):
)
)
- (code, response) = yield self.mock_http_server.trigger("GET",
+ (code, response) = yield self.mock_resource.trigger("GET",
"/matrix/federation/v1/pdu/red/abc123def456/", None)
self.assertEquals(200, code)
self.assertEquals(1, len(response["pdus"]))
@@ -225,7 +225,7 @@ class FederationTestCase(unittest.TestCase):
self.federation.register_edu_handler("m.test", recv_observer)
- yield self.mock_http_server.trigger("PUT",
+ yield self.mock_resource.trigger("PUT",
"/matrix/federation/v1/send/1001000/",
"""{
"origin": "remote",
@@ -272,7 +272,7 @@ class FederationTestCase(unittest.TestCase):
self.federation.register_query_handler("a-question", recv_handler)
- code, response = yield self.mock_http_server.trigger("GET",
+ code, response = yield self.mock_resource.trigger("GET",
"/matrix/federation/v1/query/a-question?three=3&four=4", None)
self.assertEquals(200, code)
diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py
index 86bd8bb3f2..8b88c49a0b 100644
--- a/tests/handlers/test_presence.py
+++ b/tests/handlers/test_presence.py
@@ -19,8 +19,9 @@ from twisted.internet import defer
from mock import Mock, call, ANY
import logging
+import json
-from ..utils import MockClock
+from ..utils import MockHttpResource, MockClock, DeferredMockCallable
from synapse.server import HomeServer
from synapse.api.constants import PresenceState
@@ -34,17 +35,27 @@ ONLINE = PresenceState.ONLINE
logging.getLogger().addHandler(logging.NullHandler())
+#logging.getLogger().addHandler(logging.StreamHandler())
+#logging.getLogger().setLevel(logging.DEBUG)
+
+
+def _expect_edu(destination, edu_type, content, origin="test"):
+ return {
+ "origin": origin,
+ "ts": 1000000,
+ "pdus": [],
+ "edus": [
+ {
+ "origin": origin,
+ "destination": destination,
+ "edu_type": edu_type,
+ "content": content,
+ }
+ ],
+ }
-
-class MockReplication(object):
- def __init__(self):
- self.edu_handlers = {}
-
- def register_edu_handler(self, edu_type, handler):
- self.edu_handlers[edu_type] = handler
-
- def received_edu(self, origin, edu_type, content):
- self.edu_handlers[edu_type](origin, content)
+def _make_edu_json(origin, edu_type, content):
+ return json.dumps(_expect_edu("test", edu_type, content, origin=origin))
class JustPresenceHandlers(object):
@@ -209,10 +220,13 @@ class PresenceInvitesTestCase(unittest.TestCase):
""" Tests presence management. """
def setUp(self):
- self.replication = MockReplication()
- self.replication.send_edu = Mock()
+ self.mock_http_client = Mock(spec=[])
+ self.mock_http_client.put_json = DeferredMockCallable()
+
+ self.mock_federation_resource = MockHttpResource()
hs = HomeServer("test",
+ clock=MockClock(),
db_pool=None,
datastore=Mock(spec=[
"has_presence_state",
@@ -221,11 +235,17 @@ class PresenceInvitesTestCase(unittest.TestCase):
"set_presence_list_accepted",
"get_presence_list",
"del_presence_list",
+
+ # Bits that Federation needs
+ "prep_send_transaction",
+ "delivered_txn",
+ "get_received_txn_response",
+ "set_received_txn_response",
]),
handlers=None,
resource_for_client=Mock(),
- http_client=None,
- replication_layer=self.replication
+ resource_for_federation=self.mock_federation_resource,
+ http_client=self.mock_http_client,
)
hs.handlers = JustPresenceHandlers(hs)
@@ -236,6 +256,10 @@ class PresenceInvitesTestCase(unittest.TestCase):
user_localpart in ("apple", "banana"))
self.datastore.has_presence_state = has_presence_state
+ def get_received_txn_response(*args):
+ return defer.succeed(None)
+ self.datastore.get_received_txn_response = get_received_txn_response
+
# Some local users to test with
self.u_apple = hs.parse_userid("@apple:test")
self.u_banana = hs.parse_userid("@banana:test")
@@ -283,7 +307,19 @@ class PresenceInvitesTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_invite_remote(self):
- self.replication.send_edu.return_value = defer.succeed((200, "OK"))
+ put_json = self.mock_http_client.put_json
+ put_json.expect_call_and_return(
+ call("elsewhere",
+ path="/matrix/federation/v1/send/1000000/",
+ data=_expect_edu("elsewhere", "m.presence_invite",
+ content={
+ "observer_user": "@apple:test",
+ "observed_user": "@cabbage:elsewhere",
+ }
+ )
+ ),
+ defer.succeed((200, "OK"))
+ )
yield self.handler.send_invite(
observer_user=self.u_apple, observed_user=self.u_cabbage)
@@ -291,67 +327,79 @@ class PresenceInvitesTestCase(unittest.TestCase):
self.datastore.add_presence_list_pending.assert_called_with(
"apple", "@cabbage:elsewhere")
- self.replication.send_edu.assert_called_with(
- destination="elsewhere",
- edu_type="m.presence_invite",
- content={
- "observer_user": "@apple:test",
- "observed_user": "@cabbage:elsewhere",
- }
- )
+ 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
- self.replication.send_edu.return_value = defer.succeed((200, "OK"))
+ put_json = self.mock_http_client.put_json
+ put_json.expect_call_and_return(
+ call("elsewhere",
+ path="/matrix/federation/v1/send/1000000/",
+ data=_expect_edu("elsewhere", "m.presence_accept",
+ content={
+ "observer_user": "@cabbage:elsewhere",
+ "observed_user": "@apple:test",
+ }
+ )
+ ),
+ defer.succeed((200, "OK"))
+ )
- yield self.replication.received_edu(
- "elsewhere", "m.presence_invite", {
+ yield self.mock_federation_resource.trigger("PUT",
+ "/matrix/federation/v1/send/1000000/",
+ _make_edu_json("elsewhere", "m.presence_invite",
+ content={
"observer_user": "@cabbage:elsewhere",
"observed_user": "@apple:test",
}
+ )
)
self.datastore.allow_presence_visible.assert_called_with(
"apple", "@cabbage:elsewhere")
- self.replication.send_edu.assert_called_with(
- destination="elsewhere",
- edu_type="m.presence_accept",
- content={
- "observer_user": "@cabbage:elsewhere",
- "observed_user": "@apple:test",
- }
- )
+ yield put_json.await_calls()
@defer.inlineCallbacks
def test_invited_remote_nonexistant(self):
- self.replication.send_edu.return_value = defer.succeed((200, "OK"))
-
- yield self.replication.received_edu(
- "elsewhere", "m.presence_invite", {
- "observer_user": "@cabbage:elsewhere",
- "observed_user": "@durian:test",
- }
+ put_json = self.mock_http_client.put_json
+ put_json.expect_call_and_return(
+ call("elsewhere",
+ path="/matrix/federation/v1/send/1000000/",
+ data=_expect_edu("elsewhere", "m.presence_deny",
+ content={
+ "observer_user": "@cabbage:elsewhere",
+ "observed_user": "@durian:test",
+ }
+ )
+ ),
+ defer.succeed((200, "OK"))
)
- self.replication.send_edu.assert_called_with(
- destination="elsewhere",
- edu_type="m.presence_deny",
+ yield self.mock_federation_resource.trigger("PUT",
+ "/matrix/federation/v1/send/1000000/",
+ _make_edu_json("elsewhere", "m.presence_invite",
content={
"observer_user": "@cabbage:elsewhere",
"observed_user": "@durian:test",
}
+ )
)
+ yield put_json.await_calls()
+
@defer.inlineCallbacks
def test_accepted_remote(self):
- yield self.replication.received_edu(
- "elsewhere", "m.presence_accept", {
+ 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.datastore.set_presence_list_accepted.assert_called_with(
@@ -362,11 +410,14 @@ class PresenceInvitesTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_denied_remote(self):
- yield self.replication.received_edu(
- "elsewhere", "m.presence_deny", {
+ 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.datastore.del_presence_list.assert_called_with(
@@ -384,6 +435,14 @@ class PresenceInvitesTestCase(unittest.TestCase):
self.u_apple, target_user=self.u_banana)
@defer.inlineCallbacks
+ def test_drop_remote(self):
+ yield self.handler.drop(
+ observer_user=self.u_apple, observed_user=self.u_cabbage)
+
+ self.datastore.del_presence_list.assert_called_with(
+ "apple", "@cabbage:elsewhere")
+
+ @defer.inlineCallbacks
def test_get_presence_list(self):
self.datastore.get_presence_list.return_value = defer.succeed(
[{"observed_user_id": "@banana:test"}]
@@ -424,22 +483,29 @@ class PresencePushTestCase(unittest.TestCase):
BE WARNED...
"""
def setUp(self):
- self.replication = MockReplication()
- self.replication.send_edu = Mock()
- self.replication.send_edu.return_value = defer.succeed((200, "OK"))
-
self.clock = MockClock()
+ self.mock_http_client = Mock(spec=[])
+ self.mock_http_client.put_json = DeferredMockCallable()
+
+ self.mock_federation_resource = MockHttpResource()
+
hs = HomeServer("test",
clock=self.clock,
db_pool=None,
datastore=Mock(spec=[
"set_presence_state",
+
+ # Bits that Federation needs
+ "prep_send_transaction",
+ "delivered_txn",
+ "get_received_txn_response",
+ "set_received_txn_response",
]),
handlers=None,
resource_for_client=Mock(),
- http_client=None,
- replication_layer=self.replication,
+ resource_for_federation=self.mock_federation_resource,
+ http_client=self.mock_http_client,
)
hs.handlers = JustPresenceHandlers(hs)
@@ -447,6 +513,11 @@ class PresencePushTestCase(unittest.TestCase):
self.mock_update_client.return_value = defer.succeed(None)
self.datastore = hs.get_datastore()
+
+ def get_received_txn_response(*args):
+ return defer.succeed(None)
+ self.datastore.get_received_txn_response = get_received_txn_response
+
self.handler = hs.get_handlers().presence_handler
self.handler.push_update_to_clients = self.mock_update_client
@@ -585,41 +656,54 @@ class PresencePushTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_push_remote(self):
- 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()
- 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,
- {"state": ONLINE})
-
- self.replication.send_edu.assert_has_calls([
- call(
- destination="remote",
- edu_type="m.presence",
+ 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",
"state": "online",
"mtime_age": 0},
],
- }),
- call(
- destination="farm",
- edu_type="m.presence",
+ }
+ )
+ ),
+ defer.succeed((200, "OK"))
+ )
+ 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",
"state": "online",
"mtime_age": 0},
],
- })
- ], any_order=True)
+ }
+ )
+ ),
+ 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()
+ 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,
+ {"state": ONLINE}
+ )
+
+ yield put_json.await_calls()
@defer.inlineCallbacks
def test_recv_remote(self):
@@ -630,14 +714,17 @@ class PresencePushTestCase(unittest.TestCase):
self.room_members = [self.u_banana, self.u_potato]
- yield self.replication.received_edu(
- "remote", "m.presence", {
+ yield self.mock_federation_resource.trigger("PUT",
+ "/matrix/federation/v1/send/1000000/",
+ _make_edu_json("elsewhere", "m.presence",
+ content={
"push": [
{"user_id": "@potato:remote",
"state": "online",
"mtime_age": 1000},
],
}
+ )
)
self.mock_update_client.assert_has_calls([
@@ -683,6 +770,35 @@ class PresencePushTestCase(unittest.TestCase):
@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",
+ "state": "online"},
+ ],
+ }
+ ),
+ ),
+ 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",
+ "state": "offline"},
+ ],
+ }
+ ),
+ ),
+ defer.succeed((200, "OK"))
+ )
# TODO(paul): Gut-wrenching
self.handler._user_cachemap[self.u_apple] = UserPresenceCache()
@@ -694,30 +810,24 @@ class PresencePushTestCase(unittest.TestCase):
"a-room"
)
- self.replication.send_edu.assert_has_calls([
- call(
- destination="remote",
- edu_type="m.presence",
+ 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": "@apple:test",
+ {"user_id": "@clementine:test",
"state": "online"},
],
- }),
- call(
- destination="remote",
- edu_type="m.presence",
- content={
- "push": [
- {"user_id": "@banana:test",
- "state": "offline"},
- ],
- }),
- ], any_order=True)
-
- self.replication.send_edu.reset_mock()
-
- ## Sending newly-joined local user state to remote users
+ }
+ ),
+ ),
+ defer.succeed((200, "OK"))
+ )
self.handler._user_cachemap[self.u_clementine] = UserPresenceCache()
self.handler._user_cachemap[self.u_clementine].update(
@@ -728,17 +838,7 @@ class PresencePushTestCase(unittest.TestCase):
"a-room"
)
- self.replication.send_edu.assert_has_calls(
- call(
- destination="remote",
- edu_type="m.presence",
- content={
- "push": [
- {"user_id": "@clementine:test",
- "state": "online"},
- ],
- }),
- )
+ put_json.await_calls()
class PresencePollingTestCase(unittest.TestCase):
@@ -755,21 +855,34 @@ class PresencePollingTestCase(unittest.TestCase):
def setUp(self):
- self.replication = MockReplication()
- self.replication.send_edu = Mock()
+ self.mock_http_client = Mock(spec=[])
+ self.mock_http_client.put_json = DeferredMockCallable()
+
+ self.mock_federation_resource = MockHttpResource()
hs = HomeServer("test",
+ clock=MockClock(),
db_pool=None,
- datastore=Mock(spec=[]),
+ datastore=Mock(spec=[
+ # Bits that Federation needs
+ "prep_send_transaction",
+ "delivered_txn",
+ "get_received_txn_response",
+ "set_received_txn_response",
+ ]),
handlers=None,
resource_for_client=Mock(),
- http_client=None,
- replication_layer=self.replication,
+ resource_for_federation=self.mock_federation_resource,
+ http_client=self.mock_http_client,
)
hs.handlers = JustPresenceHandlers(hs)
self.datastore = hs.get_datastore()
+ def get_received_txn_response(*args):
+ return defer.succeed(None)
+ self.datastore.get_received_txn_response = get_received_txn_response
+
self.mock_update_client = Mock()
self.mock_update_client.return_value = defer.succeed(None)
@@ -827,8 +940,9 @@ class PresencePollingTestCase(unittest.TestCase):
def test_push_local(self):
# apple goes online
yield self.handler.set_state(
- target_user=self.u_apple, auth_user=self.u_apple,
- state={"state": ONLINE})
+ target_user=self.u_apple, auth_user=self.u_apple,
+ state={"state": ONLINE}
+ )
# apple should see both banana and clementine currently offline
self.mock_update_client.assert_has_calls([
@@ -885,68 +999,92 @@ class PresencePollingTestCase(unittest.TestCase):
@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="/matrix/federation/v1/send/1000000/",
+ data=_expect_edu("remote", "m.presence",
+ content={
+ "poll": [ "@potato:remote" ],
+ },
+ ),
+ ),
+ defer.succeed((200, "OK"))
+ )
+
# clementine goes online
yield self.handler.set_state(
target_user=self.u_clementine, auth_user=self.u_clementine,
state={"state": ONLINE})
- self.replication.send_edu.assert_called_with(
- destination="remote",
- edu_type="m.presence",
- content={
- "poll": [ "@potato:remote" ],
- },
- )
+ yield put_json.await_calls()
# Gut-wrenching tests
self.assertTrue(self.u_potato in self.handler._remote_recvmap)
self.assertTrue(self.u_clementine in
self.handler._remote_recvmap[self.u_potato])
- self.replication.send_edu.reset_mock()
+ put_json.expect_call_and_return(
+ call("remote",
+ path="/matrix/federation/v1/send/1000001/",
+ data=_expect_edu("remote", "m.presence",
+ content={
+ "unpoll": [ "@potato:remote" ],
+ },
+ ),
+ ),
+ defer.succeed((200, "OK"))
+ )
# clementine goes offline
yield self.handler.set_state(
target_user=self.u_clementine, auth_user=self.u_clementine,
state={"state": OFFLINE})
- self.replication.send_edu.assert_called_with(
- destination="remote",
- edu_type="m.presence",
- content={
- "unpoll": [ "@potato:remote" ],
- },
- )
+ put_json.await_calls()
self.assertFalse(self.u_potato in self.handler._remote_recvmap)
@defer.inlineCallbacks
def test_remote_poll_receive(self):
- yield self.replication.received_edu(
- "remote", "m.presence", {
+ 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",
+ "state": "offline",
+ "status_msg": None},
+ ],
+ },
+ ),
+ ),
+ 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)
- self.replication.send_edu.assert_called_with(
- destination="remote",
- edu_type="m.presence",
+ yield self.mock_federation_resource.trigger("PUT",
+ "/matrix/federation/v1/send/1000001/",
+ _make_edu_json("remote", "m.presence",
content={
- "push": [
- {"user_id": "@banana:test",
- "state": "offline",
- "status_msg": None},
- ],
- },
- )
-
- yield self.replication.received_edu(
- "remote", "m.presence", {
"unpoll": [ "@banana:test" ],
}
+ )
)
# Gut-wrenching tests
diff --git a/tests/rest/test_events.py b/tests/rest/test_events.py
index e1b3cb4ddf..1ab92395f2 100644
--- a/tests/rest/test_events.py
+++ b/tests/rest/test_events.py
@@ -29,7 +29,7 @@ from synapse.server import HomeServer
import json
import logging
-from ..utils import MockHttpServer, MemoryDataStore
+from ..utils import MockHttpResource, MemoryDataStore
from .utils import RestTestCase
from mock import Mock
@@ -116,7 +116,7 @@ class EventStreamPermissionsTestCase(RestTestCase):
@defer.inlineCallbacks
def setUp(self):
- self.mock_server = MockHttpServer(prefix=PATH_PREFIX)
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
state_handler = Mock(spec=["handle_new_event"])
state_handler.handle_new_event.return_value = True
@@ -142,9 +142,9 @@ class EventStreamPermissionsTestCase(RestTestCase):
hs.get_clock().time_msec.return_value = 1000000
hs.datastore = MemoryDataStore()
- synapse.rest.register.register_servlets(hs, self.mock_server)
- synapse.rest.events.register_servlets(hs, self.mock_server)
- synapse.rest.room.register_servlets(hs, self.mock_server)
+ synapse.rest.register.register_servlets(hs, self.mock_resource)
+ synapse.rest.events.register_servlets(hs, self.mock_resource)
+ synapse.rest.room.register_servlets(hs, self.mock_resource)
# register an account
self.user_id = "sid1"
@@ -164,12 +164,12 @@ class EventStreamPermissionsTestCase(RestTestCase):
@defer.inlineCallbacks
def test_stream_basic_permissions(self):
# invalid token, expect 403
- (code, response) = yield self.mock_server.trigger_get(
+ (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_server.trigger_get(
+ (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)
@@ -186,7 +186,7 @@ class EventStreamPermissionsTestCase(RestTestCase):
# 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_server.trigger_get(
+ (code, response) = yield self.mock_resource.trigger_get(
"/events?access_token=%s&timeout=0" % (self.token))
self.assertEquals(200, code, msg=str(response))
diff --git a/tests/rest/test_presence.py b/tests/rest/test_presence.py
index 99287823e4..80b5972b4b 100644
--- a/tests/rest/test_presence.py
+++ b/tests/rest/test_presence.py
@@ -21,9 +21,10 @@ from twisted.internet import defer
from mock import Mock
import logging
-from ..utils import MockHttpServer
+from ..utils import MockHttpResource
from synapse.api.constants import PresenceState
+from synapse.handlers.presence import PresenceHandler
from synapse.server import HomeServer
@@ -39,29 +40,49 @@ myid = "@apple:test"
PATH_PREFIX = "/matrix/client/api/v1"
+class JustPresenceHandlers(object):
+ def __init__(self, hs):
+ self.presence_handler = PresenceHandler(hs)
+
+
class PresenceStateTestCase(unittest.TestCase):
def setUp(self):
- self.mock_server = MockHttpServer(prefix=PATH_PREFIX)
- self.mock_handler = Mock(spec=[
- "get_state",
- "set_state",
- ])
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
hs = HomeServer("test",
db_pool=None,
+ datastore=Mock(spec=[
+ "get_presence_state",
+ "set_presence_state",
+ ]),
http_client=None,
datastore=None,
- resource_for_client=self.mock_server,
- resource_for_federation=self.mock_server,
+ resource_for_client=self.mock_resource,
+ resource_for_federation=self.mock_resource,
)
+ hs.handlers = JustPresenceHandlers(hs)
+
+ self.datastore = hs.get_datastore()
+
+ def get_presence_list(*a, **kw):
+ return defer.succeed([])
+ self.datastore.get_presence_list = get_presence_list
def _get_user_by_token(token=None):
return hs.parse_userid(myid)
hs.get_auth().get_user_by_token = _get_user_by_token
- hs.get_handlers().presence_handler = self.mock_handler
+ room_member_handler = hs.handlers.room_member_handler = Mock(
+ spec=[
+ "get_rooms_for_user",
+ ]
+ )
+
+ def get_rooms_for_user(user):
+ return defer.succeed([])
+ room_member_handler.get_rooms_for_user = get_rooms_for_user
hs.register_servlets()
@@ -69,58 +90,75 @@ class PresenceStateTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_get_my_status(self):
- mocked_get = self.mock_handler.get_state
+ mocked_get = self.datastore.get_presence_state
mocked_get.return_value = defer.succeed(
- {"state": ONLINE, "status_msg": "Available"})
+ {"state": ONLINE, "status_msg": "Available"}
+ )
- (code, response) = yield self.mock_server.trigger("GET",
+ (code, response) = yield self.mock_resource.trigger("GET",
"/presence/%s/status" % (myid), None)
self.assertEquals(200, code)
self.assertEquals({"state": ONLINE, "status_msg": "Available"},
response)
- mocked_get.assert_called_with(target_user=self.u_apple,
- auth_user=self.u_apple)
+ mocked_get.assert_called_with("apple")
@defer.inlineCallbacks
def test_set_my_status(self):
- mocked_set = self.mock_handler.set_state
- mocked_set.return_value = defer.succeed(())
+ mocked_set = self.datastore.set_presence_state
+ mocked_set.return_value = defer.succeed({"state": OFFLINE})
- (code, response) = yield self.mock_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
"/presence/%s/status" % (myid),
'{"state": "unavailable", "status_msg": "Away"}')
self.assertEquals(200, code)
- mocked_set.assert_called_with(target_user=self.u_apple,
- auth_user=self.u_apple,
- state={"state": UNAVAILABLE, "status_msg": "Away"})
+ mocked_set.assert_called_with("apple",
+ {"state": UNAVAILABLE, "status_msg": "Away"})
class PresenceListTestCase(unittest.TestCase):
def setUp(self):
- self.mock_server = MockHttpServer(prefix=PATH_PREFIX)
- self.mock_handler = Mock(spec=[
- "get_presence_list",
- "send_invite",
- "drop",
- ])
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
hs = HomeServer("test",
db_pool=None,
+ 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",
+ ]),
http_client=None,
datastore=None,
- resource_for_client=self.mock_server,
- resource_for_federation=self.mock_server
+ resource_for_client=self.mock_resource,
+ resource_for_federation=self.mock_resource
)
+ hs.handlers = JustPresenceHandlers(hs)
+
+ self.datastore = hs.get_datastore()
+
+ 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_token(token=None):
return hs.parse_userid(myid)
- hs.get_auth().get_user_by_token = _get_user_by_token
+ room_member_handler = hs.handlers.room_member_handler = Mock(
+ spec=[
+ "get_rooms_for_user",
+ ]
+ )
- hs.get_handlers().presence_handler = self.mock_handler
+ hs.get_auth().get_user_by_token = _get_user_by_token
hs.register_servlets()
@@ -129,52 +167,66 @@ class PresenceListTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_get_my_list(self):
- self.mock_handler.get_presence_list.return_value = defer.succeed(
- [{"observed_user": self.u_banana}]
+ self.datastore.get_presence_list.return_value = defer.succeed(
+ [{"observed_user_id": "@banana:test"}],
)
- (code, response) = yield self.mock_server.trigger("GET",
+ (code, response) = yield self.mock_resource.trigger("GET",
"/presence_list/%s" % (myid), None)
self.assertEquals(200, code)
- self.assertEquals([{"user_id": "@banana:test"}], response)
+ self.assertEquals(
+ [{"user_id": "@banana:test", "state": OFFLINE}], response
+ )
+
+ self.datastore.get_presence_list.assert_called_with(
+ "apple", accepted=True
+ )
@defer.inlineCallbacks
def test_invite(self):
- self.mock_handler.send_invite.return_value = defer.succeed(())
+ 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_server.trigger("POST",
- "/presence_list/%s" % (myid),
- """{
- "invite": ["@banana:test"]
- }""")
+ (code, response) = yield self.mock_resource.trigger("POST",
+ "/presence_list/%s" % (myid),
+ """{"invite": ["@banana:test"]}"""
+ )
self.assertEquals(200, code)
- self.mock_handler.send_invite.assert_called_with(
- observer_user=self.u_apple, observed_user=self.u_banana)
+ 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.mock_handler.drop.return_value = defer.succeed(())
+ self.datastore.del_presence_list.return_value = (
+ defer.succeed(())
+ )
- (code, response) = yield self.mock_server.trigger("POST",
- "/presence_list/%s" % (myid),
- """{
- "drop": ["@banana:test"]
- }""")
+ (code, response) = yield self.mock_resource.trigger("POST",
+ "/presence_list/%s" % (myid),
+ """{"drop": ["@banana:test"]}"""
+ )
self.assertEquals(200, code)
- self.mock_handler.drop.assert_called_with(
- observer_user=self.u_apple, observed_user=self.u_banana)
+ self.datastore.del_presence_list.assert_called_with(
+ "apple", "@banana:test"
+ )
class PresenceEventStreamTestCase(unittest.TestCase):
def setUp(self):
- self.mock_server = MockHttpServer(prefix=PATH_PREFIX)
-
- # TODO: mocked data store
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
# HIDEOUS HACKERY
# TODO(paul): This should be injected in via the HomeServer DI system
@@ -187,8 +239,8 @@ class PresenceEventStreamTestCase(unittest.TestCase):
hs = HomeServer("test",
db_pool=None,
http_client=None,
- resource_for_client=self.mock_server,
- resource_for_federation=self.mock_server,
+ resource_for_client=self.mock_resource,
+ resource_for_federation=self.mock_resource,
datastore=Mock(spec=[
"set_presence_state",
"get_presence_list",
@@ -228,7 +280,7 @@ class PresenceEventStreamTestCase(unittest.TestCase):
self.mock_datastore.get_presence_list.return_value = defer.succeed(
[])
- (code, response) = yield self.mock_server.trigger("GET",
+ (code, response) = yield self.mock_resource.trigger("GET",
"/events?timeout=0", None)
self.assertEquals(200, code)
@@ -254,7 +306,7 @@ class PresenceEventStreamTestCase(unittest.TestCase):
yield self.presence.set_state(self.u_banana, self.u_banana,
state={"state": ONLINE})
- (code, response) = yield self.mock_server.trigger("GET",
+ (code, response) = yield self.mock_resource.trigger("GET",
"/events?from=1&timeout=0", None)
self.assertEquals(200, code)
diff --git a/tests/rest/test_profile.py b/tests/rest/test_profile.py
index ee47daf4c2..9bd8dc9783 100644
--- a/tests/rest/test_profile.py
+++ b/tests/rest/test_profile.py
@@ -20,7 +20,7 @@ from twisted.internet import defer
from mock import Mock
-from ..utils import MockHttpServer
+from ..utils import MockHttpResource
from synapse.api.errors import SynapseError, AuthError
from synapse.server import HomeServer
@@ -32,7 +32,7 @@ class ProfileTestCase(unittest.TestCase):
""" Tests profile management. """
def setUp(self):
- self.mock_server = MockHttpServer(prefix=PATH_PREFIX)
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
self.mock_handler = Mock(spec=[
"get_displayname",
"set_displayname",
@@ -43,7 +43,7 @@ class ProfileTestCase(unittest.TestCase):
hs = HomeServer("test",
db_pool=None,
http_client=None,
- resource_for_client=self.mock_server,
+ resource_for_client=self.mock_resource,
federation=Mock(),
replication_layer=Mock(),
datastore=None,
@@ -63,7 +63,7 @@ class ProfileTestCase(unittest.TestCase):
mocked_get = self.mock_handler.get_displayname
mocked_get.return_value = defer.succeed("Frank")
- (code, response) = yield self.mock_server.trigger("GET",
+ (code, response) = yield self.mock_resource.trigger("GET",
"/profile/%s/displayname" % (myid), None)
self.assertEquals(200, code)
@@ -75,7 +75,7 @@ class ProfileTestCase(unittest.TestCase):
mocked_set = self.mock_handler.set_displayname
mocked_set.return_value = defer.succeed(())
- (code, response) = yield self.mock_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
"/profile/%s/displayname" % (myid),
'{"displayname": "Frank Jr."}')
@@ -89,7 +89,7 @@ class ProfileTestCase(unittest.TestCase):
mocked_set = self.mock_handler.set_displayname
mocked_set.side_effect = AuthError(400, "message")
- (code, response) = yield self.mock_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
"/profile/%s/displayname" % ("@4567:test"), '"Frank Jr."')
self.assertTrue(400 <= code < 499,
@@ -100,7 +100,7 @@ class ProfileTestCase(unittest.TestCase):
mocked_get = self.mock_handler.get_displayname
mocked_get.return_value = defer.succeed("Bob")
- (code, response) = yield self.mock_server.trigger("GET",
+ (code, response) = yield self.mock_resource.trigger("GET",
"/profile/%s/displayname" % ("@opaque:elsewhere"), None)
self.assertEquals(200, code)
@@ -111,7 +111,7 @@ class ProfileTestCase(unittest.TestCase):
mocked_set = self.mock_handler.set_displayname
mocked_set.side_effect = SynapseError(400, "message")
- (code, response) = yield self.mock_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
"/profile/%s/displayname" % ("@opaque:elsewhere"), None)
self.assertTrue(400 <= code <= 499,
@@ -122,7 +122,7 @@ class ProfileTestCase(unittest.TestCase):
mocked_get = self.mock_handler.get_avatar_url
mocked_get.return_value = defer.succeed("http://my.server/me.png")
- (code, response) = yield self.mock_server.trigger("GET",
+ (code, response) = yield self.mock_resource.trigger("GET",
"/profile/%s/avatar_url" % (myid), None)
self.assertEquals(200, code)
@@ -134,7 +134,7 @@ class ProfileTestCase(unittest.TestCase):
mocked_set = self.mock_handler.set_avatar_url
mocked_set.return_value = defer.succeed(())
- (code, response) = yield self.mock_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
"/profile/%s/avatar_url" % (myid),
'{"avatar_url": "http://my.server/pic.gif"}')
diff --git a/tests/rest/test_rooms.py b/tests/rest/test_rooms.py
index 3ac2bbdd0f..e873181044 100644
--- a/tests/rest/test_rooms.py
+++ b/tests/rest/test_rooms.py
@@ -27,7 +27,7 @@ from synapse.server import HomeServer
import json
import urllib
-from ..utils import MockHttpServer, MemoryDataStore
+from ..utils import MockHttpResource, MemoryDataStore
from .utils import RestTestCase
from mock import Mock
@@ -42,7 +42,7 @@ class RoomPermissionsTestCase(RestTestCase):
@defer.inlineCallbacks
def setUp(self):
- self.mock_server = MockHttpServer(prefix=PATH_PREFIX)
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
state_handler = Mock(spec=["handle_new_event"])
state_handler.handle_new_event.return_value = True
@@ -67,7 +67,7 @@ class RoomPermissionsTestCase(RestTestCase):
self.auth_user_id = self.rmcreator_id
- synapse.rest.room.register_servlets(hs, self.mock_server)
+ synapse.rest.room.register_servlets(hs, self.mock_resource)
self.auth = hs.get_auth()
@@ -85,14 +85,14 @@ class RoomPermissionsTestCase(RestTestCase):
# send a message in one of the rooms
self.created_rmid_msg_path = ("/rooms/%s/messages/%s/midaaa1" %
(self.created_rmid, self.rmcreator_id))
- (code, response) = yield self.mock_server.trigger(
+ (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_server.trigger(
+ (code, response) = yield self.mock_resource.trigger(
"PUT",
"/rooms/%s/topic" % self.created_public_rmid,
'{"topic":"Public Room Topic"}')
@@ -107,31 +107,31 @@ class RoomPermissionsTestCase(RestTestCase):
# @defer.inlineCallbacks
# def test_get_message(self):
# # get message in uncreated room, expect 403
-# (code, response) = yield self.mock_server.trigger_get(
+# (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_server.trigger_get(
+# (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_server.trigger_get(
+# (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_server.trigger_get(
+# (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_server.trigger_get(
+# (code, response) = yield self.mock_resource.trigger_get(
# self.created_rmid_msg_path)
# self.assertEquals(403, code, msg=str(response))
@@ -142,33 +142,33 @@ class RoomPermissionsTestCase(RestTestCase):
(self.created_rmid, self.user_id))
# send message in uncreated room, expect 403
- (code, response) = yield self.mock_server.trigger(
+ (code, response) = yield self.mock_resource.trigger(
"PUT",
"/rooms/%s/messages/%s/mid1" %
(self.uncreated_rmid, self.user_id), 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_server.trigger(
+ (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_server.trigger(
+ (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_server.trigger(
+ (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_server.trigger(
+ (code, response) = yield self.mock_resource.trigger(
"PUT", send_msg_path, msg_content)
self.assertEquals(403, code, msg=str(response))
@@ -178,56 +178,56 @@ class RoomPermissionsTestCase(RestTestCase):
topic_path = "/rooms/%s/topic" % self.created_rmid
# set/get topic in uncreated room, expect 403
- (code, response) = yield self.mock_server.trigger(
+ (code, response) = yield self.mock_resource.trigger(
"PUT", "/rooms/%s/topic" % self.uncreated_rmid,
topic_content)
self.assertEquals(403, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger_get(
+ (code, response) = yield self.mock_resource.trigger_get(
"/rooms/%s/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_server.trigger(
+ (code, response) = yield self.mock_resource.trigger(
"PUT", topic_path, topic_content)
self.assertEquals(403, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger_get(topic_path)
+ (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_server.trigger(
+ (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 200 (or 404)
- (code, response) = yield self.mock_server.trigger_get(topic_path)
+ (code, response) = yield self.mock_resource.trigger_get(topic_path)
self.assertEquals(404, 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)
- (code, response) = yield self.mock_server.trigger(
+ (code, response) = yield self.mock_resource.trigger(
"PUT", topic_path, topic_content)
self.assertEquals(200, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger_get(topic_path)
+ (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_server.trigger(
+ (code, response) = yield self.mock_resource.trigger(
"PUT", topic_path, topic_content)
self.assertEquals(403, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger_get(topic_path)
+ (code, response) = yield self.mock_resource.trigger_get(topic_path)
self.assertEquals(403, code, msg=str(response))
# get topic in PUBLIC room, not joined, expect 200 (or 404)
- (code, response) = yield self.mock_server.trigger_get(
+ (code, response) = yield self.mock_resource.trigger_get(
"/rooms/%s/topic" % self.created_public_rmid)
self.assertEquals(200, code, msg=str(response))
# set topic in PUBLIC room, not joined, expect 403
- (code, response) = yield self.mock_server.trigger(
+ (code, response) = yield self.mock_resource.trigger(
"PUT",
"/rooms/%s/topic" % self.created_public_rmid,
topic_content)
@@ -237,7 +237,7 @@ class RoomPermissionsTestCase(RestTestCase):
def _test_get_membership(self, room=None, members=[], expect_code=None):
path = "/rooms/%s/members/%s/state"
for member in members:
- (code, response) = yield self.mock_server.trigger_get(
+ (code, response) = yield self.mock_resource.trigger_get(
path %
(room, member))
self.assertEquals(expect_code, code)
@@ -391,7 +391,7 @@ class RoomsMemberListTestCase(RestTestCase):
user_id = "@sid1:red"
def setUp(self):
- self.mock_server = MockHttpServer(prefix=PATH_PREFIX)
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
state_handler = Mock(spec=["handle_new_event"])
state_handler.handle_new_event.return_value = True
@@ -416,7 +416,7 @@ class RoomsMemberListTestCase(RestTestCase):
return hs.parse_userid(self.auth_user_id)
hs.get_auth().get_user_by_token = _get_user_by_token
- synapse.rest.room.register_servlets(hs, self.mock_server)
+ synapse.rest.room.register_servlets(hs, self.mock_resource)
def tearDown(self):
pass
@@ -425,13 +425,13 @@ class RoomsMemberListTestCase(RestTestCase):
def test_get_member_list(self):
room_id = "!aa:test"
yield self.create_room_as(room_id, self.user_id)
- (code, response) = yield self.mock_server.trigger_get(
+ (code, response) = yield self.mock_resource.trigger_get(
"/rooms/%s/members/list" % room_id)
self.assertEquals(200, code, msg=str(response))
@defer.inlineCallbacks
def test_get_member_list_no_room(self):
- (code, response) = yield self.mock_server.trigger_get(
+ (code, response) = yield self.mock_resource.trigger_get(
"/rooms/roomdoesnotexist/members/list")
self.assertEquals(403, code, msg=str(response))
@@ -439,7 +439,7 @@ class RoomsMemberListTestCase(RestTestCase):
def test_get_member_list_no_permission(self):
room_id = "!bb:test"
yield self.create_room_as(room_id, "@some_other_guy:red")
- (code, response) = yield self.mock_server.trigger_get(
+ (code, response) = yield self.mock_resource.trigger_get(
"/rooms/%s/members/list" % room_id)
self.assertEquals(403, code, msg=str(response))
@@ -452,17 +452,17 @@ class RoomsMemberListTestCase(RestTestCase):
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_server.trigger_get(room_path)
+ (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_server.trigger_get(room_path)
+ (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 no longer see list, you've left.
- (code, response) = yield self.mock_server.trigger_get(room_path)
+ (code, response) = yield self.mock_resource.trigger_get(room_path)
self.assertEquals(403, code, msg=str(response))
@@ -471,7 +471,7 @@ class RoomsCreateTestCase(RestTestCase):
user_id = "@sid1:red"
def setUp(self):
- self.mock_server = MockHttpServer(prefix=PATH_PREFIX)
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
self.auth_user_id = self.user_id
state_handler = Mock(spec=["handle_new_event"])
@@ -495,7 +495,7 @@ class RoomsCreateTestCase(RestTestCase):
return hs.parse_userid(self.auth_user_id)
hs.get_auth().get_user_by_token = _get_user_by_token
- synapse.rest.room.register_servlets(hs, self.mock_server)
+ synapse.rest.room.register_servlets(hs, self.mock_resource)
def tearDown(self):
pass
@@ -503,7 +503,7 @@ class RoomsCreateTestCase(RestTestCase):
@defer.inlineCallbacks
def test_post_room_no_keys(self):
# POST with no config keys, expect new room id
- (code, response) = yield self.mock_server.trigger("POST", "/rooms",
+ (code, response) = yield self.mock_resource.trigger("POST", "/rooms",
"{}")
self.assertEquals(200, code, response)
self.assertTrue("room_id" in response)
@@ -511,7 +511,7 @@ class RoomsCreateTestCase(RestTestCase):
@defer.inlineCallbacks
def test_post_room_visibility_key(self):
# POST with visibility config key, expect new room id
- (code, response) = yield self.mock_server.trigger("POST", "/rooms",
+ (code, response) = yield self.mock_resource.trigger("POST", "/rooms",
'{"visibility":"private"}')
self.assertEquals(200, code)
self.assertTrue("room_id" in response)
@@ -519,7 +519,7 @@ class RoomsCreateTestCase(RestTestCase):
@defer.inlineCallbacks
def test_post_room_custom_key(self):
# POST with custom config keys, expect new room id
- (code, response) = yield self.mock_server.trigger("POST", "/rooms",
+ (code, response) = yield self.mock_resource.trigger("POST", "/rooms",
'{"custom":"stuff"}')
self.assertEquals(200, code)
self.assertTrue("room_id" in response)
@@ -527,7 +527,7 @@ class RoomsCreateTestCase(RestTestCase):
@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_server.trigger("POST", "/rooms",
+ (code, response) = yield self.mock_resource.trigger("POST", "/rooms",
'{"visibility":"private","custom":"things"}')
self.assertEquals(200, code)
self.assertTrue("room_id" in response)
@@ -535,18 +535,18 @@ class RoomsCreateTestCase(RestTestCase):
@defer.inlineCallbacks
def test_post_room_invalid_content(self):
# POST with invalid content / paths, expect 400
- (code, response) = yield self.mock_server.trigger("POST", "/rooms",
+ (code, response) = yield self.mock_resource.trigger("POST", "/rooms",
'{"visibili')
self.assertEquals(400, code)
- (code, response) = yield self.mock_server.trigger("POST", "/rooms",
+ (code, response) = yield self.mock_resource.trigger("POST", "/rooms",
'["hello"]')
self.assertEquals(400, code)
@defer.inlineCallbacks
def test_put_room_no_keys(self):
# PUT with no config keys, expect new room id
- (code, response) = yield self.mock_server.trigger(
+ (code, response) = yield self.mock_resource.trigger(
"PUT", "/rooms/%21aa%3Atest", "{}"
)
self.assertEquals(200, code)
@@ -555,7 +555,7 @@ class RoomsCreateTestCase(RestTestCase):
@defer.inlineCallbacks
def test_put_room_visibility_key(self):
# PUT with known config keys, expect new room id
- (code, response) = yield self.mock_server.trigger(
+ (code, response) = yield self.mock_resource.trigger(
"PUT", "/rooms/%21bb%3Atest", '{"visibility":"private"}'
)
self.assertEquals(200, code)
@@ -564,7 +564,7 @@ class RoomsCreateTestCase(RestTestCase):
@defer.inlineCallbacks
def test_put_room_custom_key(self):
# PUT with custom config keys, expect new room id
- (code, response) = yield self.mock_server.trigger(
+ (code, response) = yield self.mock_resource.trigger(
"PUT", "/rooms/%21cc%3Atest", '{"custom":"stuff"}'
)
self.assertEquals(200, code)
@@ -573,7 +573,7 @@ class RoomsCreateTestCase(RestTestCase):
@defer.inlineCallbacks
def test_put_room_known_and_unknown_keys(self):
# PUT with custom + known config keys, expect new room id
- (code, response) = yield self.mock_server.trigger(
+ (code, response) = yield self.mock_resource.trigger(
"PUT", "/rooms/%21dd%3Atest",
'{"visibility":"private","custom":"things"}'
)
@@ -584,12 +584,12 @@ class RoomsCreateTestCase(RestTestCase):
def test_put_room_invalid_content(self):
# PUT with invalid content / room names, expect 400
- (code, response) = yield self.mock_server.trigger(
+ (code, response) = yield self.mock_resource.trigger(
"PUT", "/rooms/ee", '{"sdf"'
)
self.assertEquals(400, code)
- (code, response) = yield self.mock_server.trigger(
+ (code, response) = yield self.mock_resource.trigger(
"PUT", "/rooms/ee", '["hello"]'
)
self.assertEquals(400, code)
@@ -599,7 +599,7 @@ class RoomsCreateTestCase(RestTestCase):
yield self.create_room_as("!aa:test", self.user_id)
# PUT with conflicting room ID, expect 409
- (code, response) = yield self.mock_server.trigger(
+ (code, response) = yield self.mock_resource.trigger(
"PUT", "/rooms/%21aa%3Atest", "{}"
)
self.assertEquals(409, code)
@@ -611,7 +611,7 @@ class RoomTopicTestCase(RestTestCase):
@defer.inlineCallbacks
def setUp(self):
- self.mock_server = MockHttpServer(prefix=PATH_PREFIX)
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
self.auth_user_id = self.user_id
self.room_id = "!rid1:test"
self.path = "/rooms/%s/topic" % self.room_id
@@ -637,7 +637,7 @@ class RoomTopicTestCase(RestTestCase):
return hs.parse_userid(self.auth_user_id)
hs.get_auth().get_user_by_token = _get_user_by_token
- synapse.rest.room.register_servlets(hs, self.mock_server)
+ synapse.rest.room.register_servlets(hs, self.mock_resource)
# create the room
yield self.create_room_as(self.room_id, self.user_id)
@@ -648,50 +648,50 @@ class RoomTopicTestCase(RestTestCase):
@defer.inlineCallbacks
def test_invalid_puts(self):
# missing keys or invalid json
- (code, response) = yield self.mock_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
self.path, '{}')
self.assertEquals(400, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
self.path, '{"_name":"bob"}')
self.assertEquals(400, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
self.path, '{"nao')
self.assertEquals(400, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger("PUT",
+ (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_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
self.path, 'text only')
self.assertEquals(400, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger("PUT",
+ (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_server.trigger("PUT",
+ (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_server.trigger_get(self.path)
+ (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_server.trigger("PUT",
+ (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_server.trigger_get(self.path)
+ (code, response) = yield self.mock_resource.trigger_get(self.path)
self.assertEquals(200, code, msg=str(response))
self.assert_dict(json.loads(content), response)
@@ -699,12 +699,12 @@ class RoomTopicTestCase(RestTestCase):
def test_rooms_topic_with_extra_keys(self):
# valid put with extra keys
content = '{"topic":"Seasons","subtopic":"Summer"}'
- (code, response) = yield self.mock_server.trigger("PUT",
+ (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_server.trigger_get(self.path)
+ (code, response) = yield self.mock_resource.trigger_get(self.path)
self.assertEquals(200, code, msg=str(response))
self.assert_dict(json.loads(content), response)
@@ -715,7 +715,7 @@ class RoomMemberStateTestCase(RestTestCase):
@defer.inlineCallbacks
def setUp(self):
- self.mock_server = MockHttpServer(prefix=PATH_PREFIX)
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
self.auth_user_id = self.user_id
self.room_id = "!rid1:test"
@@ -740,7 +740,7 @@ class RoomMemberStateTestCase(RestTestCase):
return hs.parse_userid(self.auth_user_id)
hs.get_auth().get_user_by_token = _get_user_by_token
- synapse.rest.room.register_servlets(hs, self.mock_server)
+ synapse.rest.room.register_servlets(hs, self.mock_resource)
yield self.create_room_as(self.room_id, self.user_id)
@@ -751,34 +751,34 @@ class RoomMemberStateTestCase(RestTestCase):
def test_invalid_puts(self):
path = "/rooms/%s/members/%s/state" % (self.room_id, self.user_id)
# missing keys or invalid json
- (code, response) = yield self.mock_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
path, '{}')
self.assertEquals(400, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
path, '{"_name":"bob"}')
self.assertEquals(400, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
path, '{"nao')
self.assertEquals(400, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger("PUT",
+ (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_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
path, 'text only')
self.assertEquals(400, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger("PUT",
+ (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_server.trigger("PUT", path, content)
+ (code, response) = yield self.mock_resource.trigger("PUT", path, content)
self.assertEquals(400, code, msg=str(response))
@defer.inlineCallbacks
@@ -789,10 +789,10 @@ class RoomMemberStateTestCase(RestTestCase):
# valid join message (NOOP since we made the room)
content = '{"membership":"%s"}' % Membership.JOIN
- (code, response) = yield self.mock_server.trigger("PUT", path, content)
+ (code, response) = yield self.mock_resource.trigger("PUT", path, content)
self.assertEquals(200, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger("GET", path, None)
+ (code, response) = yield self.mock_resource.trigger("GET", path, None)
self.assertEquals(200, code, msg=str(response))
self.assertEquals(json.loads(content), response)
@@ -805,10 +805,10 @@ class RoomMemberStateTestCase(RestTestCase):
# valid invite message
content = '{"membership":"%s"}' % Membership.INVITE
- (code, response) = yield self.mock_server.trigger("PUT", path, content)
+ (code, response) = yield self.mock_resource.trigger("PUT", path, content)
self.assertEquals(200, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger("GET", path, None)
+ (code, response) = yield self.mock_resource.trigger("GET", path, None)
self.assertEquals(200, code, msg=str(response))
self.assertEquals(json.loads(content), response)
@@ -822,10 +822,10 @@ class RoomMemberStateTestCase(RestTestCase):
# valid invite message with custom key
content = ('{"membership":"%s","invite_text":"%s"}' %
(Membership.INVITE, "Join us!"))
- (code, response) = yield self.mock_server.trigger("PUT", path, content)
+ (code, response) = yield self.mock_resource.trigger("PUT", path, content)
self.assertEquals(200, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger("GET", path, None)
+ (code, response) = yield self.mock_resource.trigger("GET", path, None)
self.assertEquals(200, code, msg=str(response))
self.assertEquals(json.loads(content), response)
@@ -836,7 +836,7 @@ class RoomMessagesTestCase(RestTestCase):
@defer.inlineCallbacks
def setUp(self):
- self.mock_server = MockHttpServer(prefix=PATH_PREFIX)
+ self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
self.auth_user_id = self.user_id
self.room_id = "!rid1:test"
@@ -861,7 +861,7 @@ class RoomMessagesTestCase(RestTestCase):
return hs.parse_userid(self.auth_user_id)
hs.get_auth().get_user_by_token = _get_user_by_token
- synapse.rest.room.register_servlets(hs, self.mock_server)
+ synapse.rest.room.register_servlets(hs, self.mock_resource)
yield self.create_room_as(self.room_id, self.user_id)
@@ -874,27 +874,27 @@ class RoomMessagesTestCase(RestTestCase):
urllib.quote(self.room_id), self.user_id
)
# missing keys or invalid json
- (code, response) = yield self.mock_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
path, '{}')
self.assertEquals(400, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
path, '{"_name":"bob"}')
self.assertEquals(400, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
path, '{"nao')
self.assertEquals(400, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger("PUT",
+ (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_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
path, 'text only')
self.assertEquals(400, code, msg=str(response))
- (code, response) = yield self.mock_server.trigger("PUT",
+ (code, response) = yield self.mock_resource.trigger("PUT",
path, '')
self.assertEquals(400, code, msg=str(response))
@@ -905,34 +905,34 @@ class RoomMessagesTestCase(RestTestCase):
)
content = '{"body":"test","msgtype":{"type":"a"}}'
- (code, response) = yield self.mock_server.trigger("PUT", path, content)
+ (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_server.trigger("PUT", path, content)
+ (code, response) = yield self.mock_resource.trigger("PUT", path, content)
self.assertEquals(200, code, msg=str(response))
- # (code, response) = yield self.mock_server.trigger("GET", path, None)
- # self.assertEquals(200, code, msg=str(response))
- # self.assert_dict(json.loads(content), 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/messages/%s/mid2" % (
urllib.quote(self.room_id), self.user_id
)
content = '{"body":"test2","msgtype":"m.text"}'
- (code, response) = yield self.mock_server.trigger("PUT", path, content)
+ (code, response) = yield self.mock_resource.trigger("PUT", path, content)
self.assertEquals(200, code, msg=str(response))
- # (code, response) = yield self.mock_server.trigger("GET", path, None)
- # self.assertEquals(200, code, msg=str(response))
- # self.assert_dict(json.loads(content), 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)
# trying to send message in different user path
path = "/rooms/%s/messages/%s/mid2" % (
urllib.quote(self.room_id), "invalid" + self.user_id
)
content = '{"body":"test2","msgtype":"m.text"}'
- (code, response) = yield self.mock_server.trigger("PUT", path, content)
+ (code, response) = yield self.mock_resource.trigger("PUT", path, content)
self.assertEquals(403, code, msg=str(response))
diff --git a/tests/rest/utils.py b/tests/rest/utils.py
index a543f049c4..8ed228b218 100644
--- a/tests/rest/utils.py
+++ b/tests/rest/utils.py
@@ -27,12 +27,12 @@ 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_server and auth_user_id attributes.
+ 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_server = None
+ self.mock_resource = None
self.auth_user_id = None
def mock_get_user_by_token(self, token=None):
@@ -48,7 +48,7 @@ class RestTestCase(unittest.TestCase):
content = '{"visibility":"private"}'
if tok:
path = path + "?access_token=%s" % tok
- (code, response) = yield self.mock_server.trigger("PUT", path, content)
+ (code, response) = yield self.mock_resource.trigger("PUT", path, content)
self.assertEquals(200, code, msg=str(response))
self.auth_user_id = temp_id
@@ -81,11 +81,11 @@ class RestTestCase(unittest.TestCase):
path = path + "?access_token=%s" % tok
if membership == Membership.LEAVE:
- (code, response) = yield self.mock_server.trigger("DELETE", path,
+ (code, response) = yield self.mock_resource.trigger("DELETE", path,
None)
self.assertEquals(expect_code, code, msg=str(response))
else:
- (code, response) = yield self.mock_server.trigger("PUT", path,
+ (code, response) = yield self.mock_resource.trigger("PUT", path,
'{"membership":"%s"}' % membership)
self.assertEquals(expect_code, code, msg=str(response))
@@ -93,7 +93,7 @@ class RestTestCase(unittest.TestCase):
@defer.inlineCallbacks
def register(self, user_id):
- (code, response) = yield self.mock_server.trigger("POST", "/register",
+ (code, response) = yield self.mock_resource.trigger("POST", "/register",
'{"user_id":"%s"}' % user_id)
self.assertEquals(200, code)
defer.returnValue(response)
@@ -111,7 +111,7 @@ class RestTestCase(unittest.TestCase):
if tok:
path = path + "?access_token=%s" % tok
- (code, response) = yield self.mock_server.trigger("PUT", path, content)
+ (code, response) = yield self.mock_resource.trigger("PUT", path, content)
self.assertEquals(expect_code, code, msg=str(response))
def assert_dict(self, required, actual):
diff --git a/tests/utils.py b/tests/utils.py
index 990380fb1c..c68b17f7b9 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -29,7 +29,8 @@ import json
import urlparse
-class MockHttpServer(HttpServer):
+# This is a mock /resource/ not an entire server
+class MockHttpResource(HttpServer):
def __init__(self, prefix=""):
self.callbacks = [] # 3-tuple of method/pattern/function
@@ -210,3 +211,43 @@ class MemoryDataStore(object):
def get_room_events_max_id(self):
return 0 # TODO (erikj)
+
+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 = []
+
+ def __call__(self, *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
+
+ raise AssertionError("Was not expecting call(%s)" %
+ _format_call(args, kwargs)
+ )
+
+ def expect_call_and_return(self, call, result):
+ self.expectations.append((call, result, defer.Deferred()))
+
+ @defer.inlineCallbacks
+ def await_calls(self):
+ while self.expectations:
+ (_, _, d) = self.expectations.pop(0)
+ yield d
diff --git a/webclient/app.css b/webclient/app.css
index b9e7771ca8..869db69cd6 100644
--- a/webclient/app.css
+++ b/webclient/app.css
@@ -89,6 +89,7 @@ h1 {
height: 100px;
position: relative;
background-color: #000;
+ cursor: pointer;
}
.userAvatar .userAvatarImage {
@@ -251,6 +252,7 @@ h1 {
height: 160px;
display:table-cell;
vertical-align: middle;
+ text-align: center;
}
.profile-avatar img {
@@ -258,6 +260,14 @@ h1 {
max-height: 100%;
}
+/*** User profile page ***/
+#user-ids {
+ padding-left: 1em;
+}
+
+#user-displayname {
+ font-size: 16pt;
+}
/******************************/
#header {
diff --git a/webclient/app.js b/webclient/app.js
index 8d64db92d3..576912be46 100644
--- a/webclient/app.js
+++ b/webclient/app.js
@@ -20,6 +20,7 @@ var matrixWebClient = angular.module('matrixWebClient', [
'LoginController',
'RoomController',
'RoomsController',
+ 'UserController',
'matrixService',
'eventStreamService',
'eventHandlerService',
@@ -33,7 +34,13 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
templateUrl: 'login/login.html',
controller: 'LoginController'
}).
- when('/room/:room_id', {
+ when('/room/:room_id_or_alias', {
+ templateUrl: 'room/room.html',
+ controller: 'RoomController'
+ }).
+ when('/room/', { // room URL with room alias in it (ex: http://127.0.0.1:8000/#/room/#public:localhost:8080) will come here.
+ // The reason is that 2nd hash key breaks routeProvider parameters cutting so that the URL will not match with
+ // the previous '/room/:room_id_or_alias' URL rule
templateUrl: 'room/room.html',
controller: 'RoomController'
}).
@@ -41,6 +48,10 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
templateUrl: 'rooms/rooms.html',
controller: 'RoomsController'
}).
+ when('/user/:user_matrix_id', {
+ templateUrl: 'user/user.html',
+ controller: 'UserController'
+ }).
otherwise({
redirectTo: '/rooms'
});
diff --git a/webclient/components/fileInput/file-input-directive.js b/webclient/components/fileInput/file-input-directive.js
index 9b73f877e9..c5e4ae07a8 100644
--- a/webclient/components/fileInput/file-input-directive.js
+++ b/webclient/components/fileInput/file-input-directive.js
@@ -29,7 +29,7 @@ angular.module('mFileInput', [])
scope: {
selectedFile: '=mFileInput'
},
-
+
link: function(scope, element, attrs, ctrl) {
element.bind("click", function() {
element.find("input")[0].click();
@@ -38,6 +38,9 @@ angular.module('mFileInput', [])
scope.$apply();
});
});
+
+ // Change the mouse icon on mouseover on this element
+ element.css("cursor", "pointer");
}
};
});
\ No newline at end of file
diff --git a/webclient/components/fileUpload/file-upload-service.js b/webclient/components/fileUpload/file-upload-service.js
index 5729d5da48..d620e6a4d0 100644
--- a/webclient/components/fileUpload/file-upload-service.js
+++ b/webclient/components/fileUpload/file-upload-service.js
@@ -16,11 +16,12 @@
'use strict';
+// TODO determine if this is really required as a separate service to matrixService.
/*
* Upload an HTML5 file to a server
*/
angular.module('mFileUpload', [])
-.service('mFileUpload', ['$http', '$q', function ($http, $q) {
+.service('mFileUpload', ['matrixService', '$q', function (matrixService, $q) {
/*
* Upload an HTML5 file to a server and returned a promise
@@ -28,20 +29,19 @@ angular.module('mFileUpload', [])
*/
this.uploadFile = function(file) {
var deferred = $q.defer();
-
- // @TODO: This service runs with the do_POST hacky implementation of /synapse/demos/webserver.py.
- // This is temporary until we have a true file upload service
- console.log("Uploading " + file.name + "...");
- $http.post(file.name, file)
- .success(function(data, status, headers, config) {
- deferred.resolve(location.origin + data.url);
- console.log(" -> Successfully uploaded! Available at " + location.origin + data.url);
- }).
- error(function(data, status, headers, config) {
- console.log(" -> Failed to upload" + file.name);
- deferred.reject();
- });
+ console.log("Uploading " + file.name + "... to /matrix/content");
+ matrixService.uploadContent(file).then(
+ function(response) {
+ var content_url = location.origin + "/matrix/content/" + response.data.content_token;
+ console.log(" -> Successfully uploaded! Available at " + content_url);
+ deferred.resolve(content_url);
+ },
+ function(error) {
+ console.log(" -> Failed to upload " + file.name);
+ deferred.reject(error);
+ }
+ );
return deferred.promise;
};
-}]);
\ No newline at end of file
+}]);
diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js
index 3cd0aa674b..828396c866 100644
--- a/webclient/components/matrix/matrix-service.js
+++ b/webclient/components/matrix/matrix-service.js
@@ -54,13 +54,14 @@ angular.module('matrixService', [])
params.access_token = config.access_token;
+ if (path.indexOf(prefixPath) !== 0) {
+ path = prefixPath + path;
+ }
+
return doBaseRequest(config.homeserver, method, path, params, data, undefined);
};
var doBaseRequest = function(baseUrl, method, path, params, data, headers) {
- if (path.indexOf(prefixPath) !== 0) {
- path = prefixPath + path;
- }
return $http({
method: method,
url: baseUrl + path,
@@ -165,6 +166,16 @@ angular.module('matrixService', [])
return doRequest("DELETE", path, undefined, undefined);
},
+ // Retrieves the room ID corresponding to a room alias
+ resolveRoomAlias:function(room_alias) {
+ var path = "/matrix/client/api/v1/ds/room/$room_alias";
+ room_alias = encodeURIComponent(room_alias);
+
+ path = path.replace("$room_alias", room_alias);
+
+ return doRequest("GET", path, undefined, {});
+ },
+
sendMessage: function(room_id, msg_id, content) {
// The REST path spec
var path = "/rooms/$room_id/messages/$from/$msg_id";
@@ -309,6 +320,17 @@ angular.module('matrixService', [])
return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
},
+ uploadContent: function(file) {
+ var path = "/matrix/content";
+ var headers = {
+ "Content-Type": undefined // undefined means angular will figure it out
+ };
+ var params = {
+ access_token: config.access_token
+ };
+ return doBaseRequest(config.homeserver, "POST", path, params, file, headers);
+ },
+
// start listening on /events
getEventStream: function(from, timeout) {
var path = "/events";
diff --git a/webclient/index.html b/webclient/index.html
index 455eff4a13..51f6ff1f4d 100644
--- a/webclient/index.html
+++ b/webclient/index.html
@@ -16,6 +16,7 @@
<script src="login/login-controller.js"></script>
<script src="room/room-controller.js"></script>
<script src="rooms/rooms-controller.js"></script>
+ <script src="user/user-controller.js"></script>
<script src="components/matrix/matrix-service.js"></script>
<script src="components/matrix/event-stream-service.js"></script>
<script src="components/matrix/event-handler-service.js"></script>
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
index b859f3d7e8..b585e338ed 100644
--- a/webclient/room/room-controller.js
+++ b/webclient/room/room-controller.js
@@ -108,8 +108,11 @@ angular.module('RoomController', ['ngSanitize'])
function($scope, $http, $timeout, $routeParams, $location, matrixService, eventStreamService, eventHandlerService) {
'use strict';
var MESSAGES_PER_PAGINATION = 30;
- $scope.room_id = $routeParams.room_id;
- $scope.room_alias = matrixService.getRoomIdToAliasMapping($scope.room_id);
+
+ // Room ids. Computed and resolved in onInit
+ $scope.room_id = undefined;
+ $scope.room_alias = undefined;
+
$scope.state = {
user_id: matrixService.config().user_id,
events_from: "END", // when to start the event stream from.
@@ -144,7 +147,7 @@ angular.module('RoomController', ['ngSanitize'])
if (document.hidden) {
var notification = new window.Notification(
($scope.members[event.user_id].displayname || event.user_id) +
- " (" + $scope.room_alias + ")",
+ " (" + ($scope.room_alias || $scope.room_id) + ")", // FIXME: don't leak room_ids here
{
"body": event.content.body,
"icon": $scope.members[event.user_id].avatar_url,
@@ -342,7 +345,57 @@ angular.module('RoomController', ['ngSanitize'])
$scope.onInit = function() {
// $timeout(function() { document.getElementById('textInput').focus() }, 0);
console.log("onInit");
+
+ // Does the room ID provided in the URL?
+ var room_id_or_alias;
+ if ($routeParams.room_id_or_alias) {
+ room_id_or_alias = decodeURIComponent($routeParams.room_id_or_alias);
+ }
+
+ if (room_id_or_alias && '!' === room_id_or_alias[0]) {
+ // Yes. We can start right now
+ $scope.room_id = room_id_or_alias;
+ $scope.room_alias = matrixService.getRoomIdToAliasMapping($scope.room_id);
+ onInit2();
+ }
+ else {
+ // No. The URL contains the room alias. Get this alias.
+ if (room_id_or_alias) {
+ // The room alias was passed urlencoded, use it as is
+ $scope.room_alias = room_id_or_alias;
+ }
+ else {
+ // Else get the room alias by hand from the URL
+ // ie: extract #public:localhost:8080 from http://127.0.0.1:8000/#/room/#public:localhost:8080
+ if (3 === location.hash.split("#").length) {
+ $scope.room_alias = "#" + location.hash.split("#")[2];
+ }
+ else {
+ // In case of issue, go to the default page
+ console.log("Error: cannot extract room alias");
+ $location.path("/");
+ return;
+ }
+ }
+
+ // Need a room ID required in Matrix API requests
+ console.log("Resolving alias: " + $scope.room_alias);
+ matrixService.resolveRoomAlias($scope.room_alias).then(function(response) {
+ $scope.room_id = response.data.room_id;
+ console.log(" -> Room ID: " + $scope.room_id);
+
+ // Now, we can start
+ onInit2();
+ },
+ function () {
+ // In case of issue, go to the default page
+ console.log("Error: cannot resolve room alias");
+ $location.path("/");
+ });
+ }
+ };
+ var onInit2 = function() {
// Join the room
matrixService.join($scope.room_id).then(
function() {
@@ -380,6 +433,11 @@ angular.module('RoomController', ['ngSanitize'])
});
};
+ // Open the user profile page
+ $scope.goToUserPage = function(user_id) {
+ $location.url("/user/" + user_id);
+ };
+
$scope.leaveRoom = function() {
matrixService.leave($scope.room_id).then(
diff --git a/webclient/room/room.html b/webclient/room/room.html
index 106a9dfd15..36bd95c1bb 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -10,9 +10,13 @@
<div id="usersTableWrapper">
<table id="usersTable">
<tr ng-repeat="member in members | orderMembersList">
- <td class="userAvatar">
- <img class="userAvatarImage" ng-src="{{member.avatar_url || 'img/default-profile.jpg'}}" width="80" height="80"/>
- <img class="userAvatarGradient" src="img/gradient.png" width="80" height="24"/>
+ <td class="userAvatar" ng-click="goToUserPage(member.id)">
+ <img class="userAvatarImage"
+ ng-src="{{member.avatar_url || 'img/default-profile.jpg'}}"
+ alt="{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}"
+ title="{{ member.id }}"
+ width="80" height="80"/>
+ <img class="userAvatarGradient" src="img/gradient.png" title="{{ member.id }}" width="80" height="24"/>
<div class="userName">{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}<br/>{{ member.displayname ? "" : member.id.substr(member.id.indexOf(':')) }}</div>
</td>
<td class="userPresence" ng-class="member.presenceState === 'online' ? 'online' : (member.presenceState === 'unavailable' ? 'unavailable' : '')">
diff --git a/webclient/rooms/rooms-controller.js b/webclient/rooms/rooms-controller.js
index da79c23d55..a237b59b4e 100644
--- a/webclient/rooms/rooms-controller.js
+++ b/webclient/rooms/rooms-controller.js
@@ -149,12 +149,8 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
$scope.joinAlias = function(room_alias) {
matrixService.joinAlias(room_alias).then(
function(response) {
- if (response.data.hasOwnProperty("room_id")) {
- $location.path("room/" + response.data.room_id);
- return;
- } else {
- // TODO (erikj): Do something here?
- }
+ // Go to this room
+ $location.path("room/" + room_alias);
},
function(error) {
$scope.feedback = "Can't join room: " + error.data;
diff --git a/webclient/rooms/rooms.html b/webclient/rooms/rooms.html
index 007ad29999..2602209bd3 100644
--- a/webclient/rooms/rooms.html
+++ b/webclient/rooms/rooms.html
@@ -65,7 +65,7 @@
<div class="rooms" ng-repeat="(rm_id, room) in rooms">
<div>
- <a href="#/room/{{ rm_id }}" >{{ room.room_alias }}</a> {{room.membership === 'invite' ? ' (invited)' : ''}}
+ <a href="#/room/{{ room.room_alias ? room.room_alias : rm_id }}" >{{ room.room_alias }}</a> {{room.membership === 'invite' ? ' (invited)' : ''}}
</div>
</div>
<br/>
@@ -74,7 +74,7 @@
<div class="public_rooms" ng-repeat="room in public_rooms">
<div>
- <a href="#/room/{{ room.room_id }}" >{{ room.room_alias }}</a>
+ <a href="#/room/{{ room.room_alias ? room.room_alias : room.room_id }}" >{{ room.room_alias }}</a>
</div>
</div>
<br/>
diff --git a/webclient/user/user-controller.js b/webclient/user/user-controller.js
new file mode 100644
index 0000000000..620230561c
--- /dev/null
+++ b/webclient/user/user-controller.js
@@ -0,0 +1,38 @@
+/*
+Copyright 2014 matrix.org
+
+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.
+*/
+
+'use strict';
+
+angular.module('UserController', ['matrixService'])
+.controller('UserController', ['$scope', '$routeParams', 'matrixService',
+ function($scope, $routeParams, matrixService) {
+ $scope.user = {
+ id: $routeParams.user_matrix_id,
+ displayname: "",
+ avatar_url: undefined
+ };
+
+ matrixService.getDisplayName($scope.user.id).then(
+ function(response) {
+ $scope.user.displayname = response.data.displayname;
+ }
+ );
+ matrixService.getProfilePictureUrl($scope.user.id).then(
+ function(response) {
+ $scope.user.avatar_url = response.data.avatar_url;
+ }
+ );
+}]);
\ No newline at end of file
diff --git a/webclient/user/user.html b/webclient/user/user.html
new file mode 100644
index 0000000000..47db09d1ee
--- /dev/null
+++ b/webclient/user/user.html
@@ -0,0 +1,30 @@
+<div ng-controller="UserController" class="user">
+
+ <div id="page">
+ <div id="wrapper">
+
+ <div>
+ <form>
+ <table>
+ <tr>
+ <td>
+ <div class="profile-avatar">
+ <img ng-src="{{ user.avatar_url || 'img/default-profile.jpg' }}"/>
+ </div>
+ </td>
+ <td>
+ <div id="user-ids">
+ <div id="user-displayname">{{ user.displayname }}</div>
+ <div>{{ user.id }}</div>
+ </div>
+ </td>
+ </tr>
+ </table>
+ </form>
+ </div>
+
+ {{ feedback }}
+
+ </div>
+ </div>
+</div>
|