mirror of
https://git.anonymousland.org/anonymousland/synapse-product.git
synced 2024-12-26 17:59:24 -05:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
d05ff3e098
@ -21,8 +21,13 @@ from synapse.server import HomeServer
|
|||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
from twisted.enterprise import adbapi
|
from twisted.enterprise import adbapi
|
||||||
from twisted.python.log import PythonLoggingObserver
|
from twisted.python.log import PythonLoggingObserver
|
||||||
from synapse.http.server import TwistedHttpServer
|
from twisted.web.resource import Resource
|
||||||
|
from twisted.web.static import File
|
||||||
|
from twisted.web.server import Site
|
||||||
|
from synapse.http.server import JsonResource
|
||||||
from synapse.http.client import TwistedHttpClient
|
from synapse.http.client import TwistedHttpClient
|
||||||
|
from synapse.rest.base import CLIENT_PREFIX
|
||||||
|
from synapse.federation.transport import PREFIX
|
||||||
|
|
||||||
from daemonize import Daemonize
|
from daemonize import Daemonize
|
||||||
|
|
||||||
@ -35,12 +40,19 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class SynapseHomeServer(HomeServer):
|
class SynapseHomeServer(HomeServer):
|
||||||
def build_http_server(self):
|
|
||||||
return TwistedHttpServer()
|
|
||||||
|
|
||||||
def build_http_client(self):
|
def build_http_client(self):
|
||||||
return TwistedHttpClient()
|
return TwistedHttpClient()
|
||||||
|
|
||||||
|
def build_resource_for_client(self):
|
||||||
|
return JsonResource()
|
||||||
|
|
||||||
|
def build_resource_for_federation(self):
|
||||||
|
return JsonResource()
|
||||||
|
|
||||||
|
def build_resource_for_web_client(self):
|
||||||
|
return File("webclient")
|
||||||
|
|
||||||
def build_db_pool(self):
|
def build_db_pool(self):
|
||||||
""" Set up all the dbs. Since all the *.sql have IF NOT EXISTS, so we
|
""" Set up all the dbs. Since all the *.sql have IF NOT EXISTS, so we
|
||||||
don't have to worry about overwriting existing content.
|
don't have to worry about overwriting existing content.
|
||||||
@ -73,6 +85,69 @@ class SynapseHomeServer(HomeServer):
|
|||||||
|
|
||||||
return pool
|
return pool
|
||||||
|
|
||||||
|
def create_resource_tree(self, web_client):
|
||||||
|
"""Create the resource tree for this Home Server.
|
||||||
|
|
||||||
|
This in unduly complicated because Twisted does not support putting
|
||||||
|
child resources more than 1 level deep at a time.
|
||||||
|
"""
|
||||||
|
desired_tree = [ # list containing (path_str, Resource)
|
||||||
|
(CLIENT_PREFIX, self.get_resource_for_client()),
|
||||||
|
(PREFIX, self.get_resource_for_federation())
|
||||||
|
]
|
||||||
|
if web_client:
|
||||||
|
logger.info("Adding the web client.")
|
||||||
|
desired_tree.append(("/matrix/client", # TODO constant please
|
||||||
|
self.get_resource_for_web_client()))
|
||||||
|
|
||||||
|
self.root_resource = Resource()
|
||||||
|
# ideally we'd just use getChild and putChild but getChild doesn't work
|
||||||
|
# unless you give it a Request object IN ADDITION to the name :/ So
|
||||||
|
# instead, we'll store a copy of this mapping so we can actually add
|
||||||
|
# extra resources to existing nodes. See self._resource_id for the key.
|
||||||
|
resource_mappings = {}
|
||||||
|
for (full_path, resource) in desired_tree:
|
||||||
|
logging.info("Attaching %s to path %s", resource, full_path)
|
||||||
|
last_resource = self.root_resource
|
||||||
|
for path_seg in full_path.split('/')[1:-1]:
|
||||||
|
if not path_seg in last_resource.listNames():
|
||||||
|
# resource doesn't exist
|
||||||
|
child_resource = Resource()
|
||||||
|
last_resource.putChild(path_seg, child_resource)
|
||||||
|
res_id = self._resource_id(last_resource, path_seg)
|
||||||
|
resource_mappings[res_id] = child_resource
|
||||||
|
last_resource = child_resource
|
||||||
|
else:
|
||||||
|
# we have an existing Resource, pull it out.
|
||||||
|
res_id = self._resource_id(last_resource, path_seg)
|
||||||
|
last_resource = resource_mappings[res_id]
|
||||||
|
|
||||||
|
# now attach the actual resource
|
||||||
|
last_path_seg = full_path.split('/')[-1]
|
||||||
|
last_resource.putChild(last_path_seg, resource)
|
||||||
|
res_id = self._resource_id(last_resource, last_path_seg)
|
||||||
|
resource_mappings[res_id] = resource
|
||||||
|
|
||||||
|
return self.root_resource
|
||||||
|
|
||||||
|
def _resource_id(self, resource, path_seg):
|
||||||
|
"""Construct an arbitrary resource ID so you can retrieve the mapping
|
||||||
|
later.
|
||||||
|
|
||||||
|
If you want to represent resource A putChild resource B with path C,
|
||||||
|
the mapping should looks like _resource_id(A,C) = B.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource (Resource): The *parent* Resource
|
||||||
|
path_seg (str): The name of the child Resource to be attached.
|
||||||
|
Returns:
|
||||||
|
str: A unique string which can be a key to the child Resource.
|
||||||
|
"""
|
||||||
|
return "%s-%s" % (resource, path_seg)
|
||||||
|
|
||||||
|
def start_listening(self, port):
|
||||||
|
reactor.listenTCP(port, Site(self.root_resource))
|
||||||
|
|
||||||
|
|
||||||
def setup_logging(verbosity=0, filename=None, config_path=None):
|
def setup_logging(verbosity=0, filename=None, config_path=None):
|
||||||
""" Sets up logging with verbosity levels.
|
""" Sets up logging with verbosity levels.
|
||||||
@ -150,7 +225,8 @@ def setup():
|
|||||||
|
|
||||||
hs.register_servlets()
|
hs.register_servlets()
|
||||||
|
|
||||||
hs.get_http_server().start_listening(args.port)
|
hs.create_resource_tree(web_client=args.webclient)
|
||||||
|
hs.start_listening(args.port)
|
||||||
|
|
||||||
hs.build_db_pool()
|
hs.build_db_pool()
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ from .transport import TransportLayer
|
|||||||
def initialize_http_replication(homeserver):
|
def initialize_http_replication(homeserver):
|
||||||
transport = TransportLayer(
|
transport = TransportLayer(
|
||||||
homeserver.hostname,
|
homeserver.hostname,
|
||||||
server=homeserver.get_http_server(),
|
server=homeserver.get_resource_for_federation(),
|
||||||
client=homeserver.get_http_client()
|
client=homeserver.get_http_client()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -52,10 +52,9 @@ class HttpServer(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# The actual HTTP server impl, using twisted http server
|
class JsonResource(HttpServer, resource.Resource):
|
||||||
class TwistedHttpServer(HttpServer, resource.Resource):
|
""" This implements the HttpServer interface and provides JSON support for
|
||||||
""" This wraps the twisted HTTP server, and triggers the correct callbacks
|
Resources.
|
||||||
on the transport_layer.
|
|
||||||
|
|
||||||
Register callbacks via register_path()
|
Register callbacks via register_path()
|
||||||
"""
|
"""
|
||||||
|
@ -15,8 +15,7 @@
|
|||||||
|
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
room, events, register, login, profile, public, presence, im, directory,
|
room, events, register, login, profile, public, presence, im, directory
|
||||||
webclient
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -32,19 +31,15 @@ class RestServletFactory(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
http_server = hs.get_http_server()
|
client_resource = hs.get_resource_for_client()
|
||||||
|
|
||||||
# TODO(erikj): There *must* be a better way of doing this.
|
# TODO(erikj): There *must* be a better way of doing this.
|
||||||
room.register_servlets(hs, http_server)
|
room.register_servlets(hs, client_resource)
|
||||||
events.register_servlets(hs, http_server)
|
events.register_servlets(hs, client_resource)
|
||||||
register.register_servlets(hs, http_server)
|
register.register_servlets(hs, client_resource)
|
||||||
login.register_servlets(hs, http_server)
|
login.register_servlets(hs, client_resource)
|
||||||
profile.register_servlets(hs, http_server)
|
profile.register_servlets(hs, client_resource)
|
||||||
public.register_servlets(hs, http_server)
|
public.register_servlets(hs, client_resource)
|
||||||
presence.register_servlets(hs, http_server)
|
presence.register_servlets(hs, client_resource)
|
||||||
im.register_servlets(hs, http_server)
|
im.register_servlets(hs, client_resource)
|
||||||
directory.register_servlets(hs, http_server)
|
directory.register_servlets(hs, client_resource)
|
||||||
|
|
||||||
def register_web_client(self, hs):
|
|
||||||
http_server = hs.get_http_server()
|
|
||||||
webclient.register_servlets(hs, http_server)
|
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
""" This module contains base REST classes for constructing REST servlets. """
|
""" This module contains base REST classes for constructing REST servlets. """
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
CLIENT_PREFIX = "/matrix/client/api/v1"
|
||||||
|
|
||||||
|
|
||||||
def client_path_pattern(path_regex):
|
def client_path_pattern(path_regex):
|
||||||
"""Creates a regex compiled client path with the correct client path
|
"""Creates a regex compiled client path with the correct client path
|
||||||
@ -27,7 +29,7 @@ def client_path_pattern(path_regex):
|
|||||||
Returns:
|
Returns:
|
||||||
SRE_Pattern
|
SRE_Pattern
|
||||||
"""
|
"""
|
||||||
return re.compile("^/matrix/client/api/v1" + path_regex)
|
return re.compile("^" + CLIENT_PREFIX + path_regex)
|
||||||
|
|
||||||
|
|
||||||
class RestServlet(object):
|
class RestServlet(object):
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
from synapse.rest.base import RestServlet
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class WebClientRestServlet(RestServlet):
|
|
||||||
# No PATTERN; we have custom dispatch rules here
|
|
||||||
|
|
||||||
def register(self, http_server):
|
|
||||||
http_server.register_path("GET",
|
|
||||||
re.compile("^/$"),
|
|
||||||
self.on_GET_redirect)
|
|
||||||
http_server.register_path("GET",
|
|
||||||
re.compile("^/matrix/client$"),
|
|
||||||
self.on_GET)
|
|
||||||
|
|
||||||
def on_GET(self, request):
|
|
||||||
return (200, "not implemented")
|
|
||||||
|
|
||||||
def on_GET_redirect(self, request):
|
|
||||||
request.setHeader("Location", request.uri + "matrix/client")
|
|
||||||
return (302, None)
|
|
||||||
|
|
||||||
|
|
||||||
def register_servlets(hs, http_server):
|
|
||||||
logger.info("Registering web client.")
|
|
||||||
WebClientRestServlet(hs).register(http_server)
|
|
@ -55,7 +55,6 @@ class BaseHomeServer(object):
|
|||||||
|
|
||||||
DEPENDENCIES = [
|
DEPENDENCIES = [
|
||||||
'clock',
|
'clock',
|
||||||
'http_server',
|
|
||||||
'http_client',
|
'http_client',
|
||||||
'db_pool',
|
'db_pool',
|
||||||
'persistence_service',
|
'persistence_service',
|
||||||
@ -70,6 +69,9 @@ class BaseHomeServer(object):
|
|||||||
'room_lock_manager',
|
'room_lock_manager',
|
||||||
'notifier',
|
'notifier',
|
||||||
'distributor',
|
'distributor',
|
||||||
|
'resource_for_client',
|
||||||
|
'resource_for_federation',
|
||||||
|
'resource_for_web_client',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, hostname, **kwargs):
|
def __init__(self, hostname, **kwargs):
|
||||||
@ -135,7 +137,9 @@ class HomeServer(BaseHomeServer):
|
|||||||
required.
|
required.
|
||||||
|
|
||||||
It still requires the following to be specified by the caller:
|
It still requires the following to be specified by the caller:
|
||||||
http_server
|
resource_for_client
|
||||||
|
resource_for_web_client
|
||||||
|
resource_for_federation
|
||||||
http_client
|
http_client
|
||||||
db_pool
|
db_pool
|
||||||
"""
|
"""
|
||||||
@ -178,9 +182,6 @@ class HomeServer(BaseHomeServer):
|
|||||||
|
|
||||||
def register_servlets(self):
|
def register_servlets(self):
|
||||||
""" Register all servlets associated with this HomeServer.
|
""" Register all servlets associated with this HomeServer.
|
||||||
|
|
||||||
Args:
|
|
||||||
host_web_client (bool): True to host the web client as well.
|
|
||||||
"""
|
"""
|
||||||
# Simply building the ServletFactory is sufficient to have it register
|
# Simply building the ServletFactory is sufficient to have it register
|
||||||
factory = self.get_rest_servlet_factory()
|
self.get_rest_servlet_factory()
|
||||||
|
@ -70,7 +70,7 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.clock = MockClock()
|
self.clock = MockClock()
|
||||||
hs = HomeServer("test",
|
hs = HomeServer("test",
|
||||||
http_server=self.mock_http_server,
|
resource_for_federation=self.mock_http_server,
|
||||||
http_client=self.mock_http_client,
|
http_client=self.mock_http_client,
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
datastore=self.mock_persistence,
|
datastore=self.mock_persistence,
|
||||||
|
@ -51,7 +51,7 @@ class DirectoryTestCase(unittest.TestCase):
|
|||||||
"get_association_from_room_alias",
|
"get_association_from_room_alias",
|
||||||
]),
|
]),
|
||||||
http_client=None,
|
http_client=None,
|
||||||
http_server=Mock(),
|
resource_for_federation=Mock(),
|
||||||
replication_layer=self.mock_federation,
|
replication_layer=self.mock_federation,
|
||||||
)
|
)
|
||||||
hs.handlers = DirectoryHandlers(hs)
|
hs.handlers = DirectoryHandlers(hs)
|
||||||
|
@ -42,7 +42,7 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
"persist_event",
|
"persist_event",
|
||||||
"store_room",
|
"store_room",
|
||||||
]),
|
]),
|
||||||
http_server=NonCallableMock(),
|
resource_for_federation=NonCallableMock(),
|
||||||
http_client=NonCallableMock(spec_set=[]),
|
http_client=NonCallableMock(spec_set=[]),
|
||||||
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
|
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
|
||||||
handlers=NonCallableMock(spec_set=[
|
handlers=NonCallableMock(spec_set=[
|
||||||
|
@ -66,7 +66,7 @@ class PresenceStateTestCase(unittest.TestCase):
|
|||||||
"set_presence_list_accepted",
|
"set_presence_list_accepted",
|
||||||
]),
|
]),
|
||||||
handlers=None,
|
handlers=None,
|
||||||
http_server=Mock(),
|
resource_for_federation=Mock(),
|
||||||
http_client=None,
|
http_client=None,
|
||||||
)
|
)
|
||||||
hs.handlers = JustPresenceHandlers(hs)
|
hs.handlers = JustPresenceHandlers(hs)
|
||||||
@ -188,7 +188,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
|
|||||||
"del_presence_list",
|
"del_presence_list",
|
||||||
]),
|
]),
|
||||||
handlers=None,
|
handlers=None,
|
||||||
http_server=Mock(),
|
resource_for_client=Mock(),
|
||||||
http_client=None,
|
http_client=None,
|
||||||
replication_layer=self.replication
|
replication_layer=self.replication
|
||||||
)
|
)
|
||||||
@ -402,7 +402,7 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
"set_presence_state",
|
"set_presence_state",
|
||||||
]),
|
]),
|
||||||
handlers=None,
|
handlers=None,
|
||||||
http_server=Mock(),
|
resource_for_client=Mock(),
|
||||||
http_client=None,
|
http_client=None,
|
||||||
replication_layer=self.replication,
|
replication_layer=self.replication,
|
||||||
)
|
)
|
||||||
@ -727,7 +727,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
db_pool=None,
|
db_pool=None,
|
||||||
datastore=Mock(spec=[]),
|
datastore=Mock(spec=[]),
|
||||||
handlers=None,
|
handlers=None,
|
||||||
http_server=Mock(),
|
resource_for_client=Mock(),
|
||||||
http_client=None,
|
http_client=None,
|
||||||
replication_layer=self.replication,
|
replication_layer=self.replication,
|
||||||
)
|
)
|
||||||
|
@ -71,7 +71,7 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
|||||||
"set_profile_displayname",
|
"set_profile_displayname",
|
||||||
]),
|
]),
|
||||||
handlers=None,
|
handlers=None,
|
||||||
http_server=Mock(),
|
resource_for_federation=Mock(),
|
||||||
http_client=None,
|
http_client=None,
|
||||||
replication_layer=MockReplication(),
|
replication_layer=MockReplication(),
|
||||||
)
|
)
|
||||||
|
@ -56,7 +56,7 @@ class ProfileTestCase(unittest.TestCase):
|
|||||||
"set_profile_avatar_url",
|
"set_profile_avatar_url",
|
||||||
]),
|
]),
|
||||||
handlers=None,
|
handlers=None,
|
||||||
http_server=Mock(),
|
resource_for_federation=Mock(),
|
||||||
replication_layer=self.mock_federation,
|
replication_layer=self.mock_federation,
|
||||||
)
|
)
|
||||||
hs.handlers = ProfileHandlers(hs)
|
hs.handlers = ProfileHandlers(hs)
|
||||||
@ -139,7 +139,7 @@ class ProfileTestCase(unittest.TestCase):
|
|||||||
mocked_set = self.datastore.set_profile_avatar_url
|
mocked_set = self.datastore.set_profile_avatar_url
|
||||||
mocked_set.return_value = defer.succeed(())
|
mocked_set.return_value = defer.succeed(())
|
||||||
|
|
||||||
yield self.handler.set_avatar_url(self.frank, self.frank,
|
yield self.handler.set_avatar_url(self.frank, self.frank,
|
||||||
"http://my.server/pic.gif")
|
"http://my.server/pic.gif")
|
||||||
|
|
||||||
mocked_set.assert_called_with("1234ABCD", "http://my.server/pic.gif")
|
mocked_set.assert_called_with("1234ABCD", "http://my.server/pic.gif")
|
||||||
|
@ -46,7 +46,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
|||||||
"get_room",
|
"get_room",
|
||||||
"store_room",
|
"store_room",
|
||||||
]),
|
]),
|
||||||
http_server=NonCallableMock(),
|
resource_for_federation=NonCallableMock(),
|
||||||
http_client=NonCallableMock(spec_set=[]),
|
http_client=NonCallableMock(spec_set=[]),
|
||||||
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
|
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
|
||||||
handlers=NonCallableMock(spec_set=[
|
handlers=NonCallableMock(spec_set=[
|
||||||
@ -317,7 +317,6 @@ class RoomCreationTest(unittest.TestCase):
|
|||||||
datastore=NonCallableMock(spec_set=[
|
datastore=NonCallableMock(spec_set=[
|
||||||
"store_room",
|
"store_room",
|
||||||
]),
|
]),
|
||||||
http_server=NonCallableMock(),
|
|
||||||
http_client=NonCallableMock(spec_set=[]),
|
http_client=NonCallableMock(spec_set=[]),
|
||||||
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
|
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
|
||||||
handlers=NonCallableMock(spec_set=[
|
handlers=NonCallableMock(spec_set=[
|
||||||
|
@ -51,7 +51,8 @@ class PresenceStateTestCase(unittest.TestCase):
|
|||||||
hs = HomeServer("test",
|
hs = HomeServer("test",
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
http_client=None,
|
http_client=None,
|
||||||
http_server=self.mock_server,
|
resource_for_client=self.mock_server,
|
||||||
|
resource_for_federation=self.mock_server,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_user_by_token(token=None):
|
def _get_user_by_token(token=None):
|
||||||
@ -108,7 +109,8 @@ class PresenceListTestCase(unittest.TestCase):
|
|||||||
hs = HomeServer("test",
|
hs = HomeServer("test",
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
http_client=None,
|
http_client=None,
|
||||||
http_server=self.mock_server,
|
resource_for_client=self.mock_server,
|
||||||
|
resource_for_federation=self.mock_server
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_user_by_token(token=None):
|
def _get_user_by_token(token=None):
|
||||||
@ -183,7 +185,8 @@ class PresenceEventStreamTestCase(unittest.TestCase):
|
|||||||
hs = HomeServer("test",
|
hs = HomeServer("test",
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
http_client=None,
|
http_client=None,
|
||||||
http_server=self.mock_server,
|
resource_for_client=self.mock_server,
|
||||||
|
resource_for_federation=self.mock_server,
|
||||||
datastore=Mock(spec=[
|
datastore=Mock(spec=[
|
||||||
"set_presence_state",
|
"set_presence_state",
|
||||||
"get_presence_list",
|
"get_presence_list",
|
||||||
|
@ -43,7 +43,7 @@ class ProfileTestCase(unittest.TestCase):
|
|||||||
hs = HomeServer("test",
|
hs = HomeServer("test",
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
http_client=None,
|
http_client=None,
|
||||||
http_server=self.mock_server,
|
resource_for_client=self.mock_server,
|
||||||
federation=Mock(),
|
federation=Mock(),
|
||||||
replication_layer=Mock(),
|
replication_layer=Mock(),
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user