mirror of
https://github.com/django/daphne.git
synced 2025-04-21 01:02:06 +03:00
More tests utils for happy users (#162)
* Added Client abstraction * Added apply_routes decorator/contextmanager * Fix apply routes as decorator * Separated Http specific client and 'Simple' client * Remove Clients from ChannelTestCase * Added cookies and headers management * Fix wrong reverting * Fixs for code style * Added space before inline comment
This commit is contained in:
parent
86a6478193
commit
05c41e9ad6
|
@ -1 +1,2 @@
|
|||
from .base import ChannelTestCase # NOQA isort:skip
|
||||
from .base import ChannelTestCase, Client, apply_routes # NOQA isort:skip
|
||||
from .http import HttpClient # NOQA isort:skip
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import copy
|
||||
import random
|
||||
import string
|
||||
from functools import wraps
|
||||
|
||||
from django.test.testcases import TestCase
|
||||
from channels import DEFAULT_CHANNEL_LAYER
|
||||
from channels.routing import Router, include
|
||||
from channels.asgi import channel_layers, ChannelLayerWrapper
|
||||
from channels.message import Message
|
||||
from asgiref.inmemory import ChannelLayer as InMemoryChannelLayer
|
||||
|
@ -59,3 +65,127 @@ class ChannelTestCase(TestCase):
|
|||
else:
|
||||
return None
|
||||
return Message(content, recv_channel, channel_layers[alias])
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""
|
||||
Channel client abstraction that provides easy methods for testing full live cycle of message in channels
|
||||
with determined the reply channel
|
||||
"""
|
||||
|
||||
def __init__(self, alias=DEFAULT_CHANNEL_LAYER):
|
||||
self.reply_channel = alias + ''.join([random.choice(string.ascii_letters) for _ in range(5)])
|
||||
self.alias = alias
|
||||
|
||||
@property
|
||||
def channel_layer(self):
|
||||
"""Channel layer as lazy property"""
|
||||
return channel_layers[self.alias]
|
||||
|
||||
def get_next_message(self, channel):
|
||||
"""
|
||||
Gets the next message that was sent to the channel during the test,
|
||||
or None if no message is available.
|
||||
"""
|
||||
recv_channel, content = channel_layers[self.alias].receive_many([channel])
|
||||
if recv_channel is None:
|
||||
return
|
||||
return Message(content, recv_channel, channel_layers[self.alias])
|
||||
|
||||
def send(self, to, content={}):
|
||||
"""
|
||||
Send a message to a channel.
|
||||
Adds reply_channel name to the message.
|
||||
"""
|
||||
content = copy.deepcopy(content)
|
||||
content.setdefault('reply_channel', self.reply_channel)
|
||||
self.channel_layer.send(to, content)
|
||||
|
||||
def consume(self, channel):
|
||||
"""
|
||||
Get next message for channel name and run appointed consumer
|
||||
"""
|
||||
message = self.get_next_message(channel)
|
||||
if message:
|
||||
consumer, kwargs = self.channel_layer.router.match(message)
|
||||
return consumer(message, **kwargs)
|
||||
|
||||
def send_and_consume(self, channel, content={}):
|
||||
"""
|
||||
Reproduce full live cycle of the message
|
||||
"""
|
||||
self.send(channel, content)
|
||||
return self.consume(channel)
|
||||
|
||||
def receive(self):
|
||||
"""self.get_next_message(self.reply_channel)
|
||||
Get content of next message for reply channel if message exists
|
||||
"""
|
||||
message = self.get_next_message(self.reply_channel)
|
||||
if message:
|
||||
return message.content
|
||||
|
||||
|
||||
class apply_routes(object):
|
||||
"""
|
||||
Decorator/ContextManager for rewrite layers routes in context.
|
||||
Helpful for testing group routes/consumers as isolated application
|
||||
|
||||
The applying routes can be list of instances of Route or list of this lists
|
||||
"""
|
||||
|
||||
def __init__(self, routes, aliases=[DEFAULT_CHANNEL_LAYER]):
|
||||
self._aliases = aliases
|
||||
self.routes = routes
|
||||
self._old_routing = {}
|
||||
|
||||
def enter(self):
|
||||
"""
|
||||
Store old routes and apply new one
|
||||
"""
|
||||
for alias in self._aliases:
|
||||
channel_layer = channel_layers[DEFAULT_CHANNEL_LAYER]
|
||||
self._old_routing[alias] = channel_layer.routing
|
||||
if isinstance(self.routes, (list, tuple)):
|
||||
if isinstance(self.routes[0], (list, tuple)):
|
||||
routes = list(map(include, self.routes))
|
||||
else:
|
||||
routes = self.routes
|
||||
|
||||
channel_layer.routing = routes
|
||||
channel_layer.router = Router(routes)
|
||||
|
||||
def exit(self, exc_type=None, exc_val=None, exc_tb=None):
|
||||
"""
|
||||
Undoes rerouting
|
||||
"""
|
||||
for alias in self._aliases:
|
||||
channel_layer = channel_layers[DEFAULT_CHANNEL_LAYER]
|
||||
channel_layer.routing = self._old_routing[alias]
|
||||
channel_layer.router = Router(self._old_routing[alias])
|
||||
|
||||
__enter__ = enter
|
||||
__exit__ = exit
|
||||
|
||||
def __call__(self, test_func):
|
||||
if isinstance(test_func, type):
|
||||
old_setup = test_func.setUp
|
||||
old_teardown = test_func.tearDown
|
||||
|
||||
def new_setup(this):
|
||||
self.enter()
|
||||
old_setup(this)
|
||||
|
||||
def new_teardown(this):
|
||||
self.exit()
|
||||
old_teardown(this)
|
||||
|
||||
test_func.setUp = new_setup
|
||||
test_func.tearDown = new_teardown
|
||||
return test_func
|
||||
else:
|
||||
@wraps(test_func)
|
||||
def inner(*args, **kwargs):
|
||||
with self:
|
||||
return test_func(*args, **kwargs)
|
||||
return inner
|
||||
|
|
147
channels/tests/http.py
Normal file
147
channels/tests/http.py
Normal file
|
@ -0,0 +1,147 @@
|
|||
|
||||
import copy
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
from ..asgi import channel_layers
|
||||
from ..message import Message
|
||||
from ..sessions import session_for_reply_channel
|
||||
from .base import Client
|
||||
|
||||
|
||||
class HttpClient(Client):
|
||||
"""
|
||||
Channel http/ws client abstraction that provides easy methods for testing full live cycle of message in channels
|
||||
with determined reply channel, auth opportunity, cookies, headers and so on
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(HttpClient, self).__init__(**kwargs)
|
||||
self._session = None
|
||||
self._headers = {}
|
||||
self._cookies = {}
|
||||
|
||||
def set_cookie(self, key, value):
|
||||
"""
|
||||
Set cookie
|
||||
"""
|
||||
self._cookies[key] = value
|
||||
|
||||
def set_header(self, key, value):
|
||||
"""
|
||||
Set header
|
||||
"""
|
||||
if key == 'cookie':
|
||||
raise ValueError('Use set_cookie method for cookie header')
|
||||
self._headers[key] = value
|
||||
|
||||
def get_cookies(self):
|
||||
"""Return cookies"""
|
||||
cookies = copy.copy(self._cookies)
|
||||
if apps.is_installed('django.contrib.sessions'):
|
||||
cookies[settings.SESSION_COOKIE_NAME] = self.session.session_key
|
||||
return cookies
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
headers = copy.deepcopy(self._headers)
|
||||
headers.setdefault('cookie', _encoded_cookies(self.get_cookies()))
|
||||
return headers
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
"""Session as Lazy property: check that django.contrib.sessions is installed"""
|
||||
if not apps.is_installed('django.contrib.sessions'):
|
||||
raise EnvironmentError('Add django.contrib.sessions to the INSTALLED_APPS to use session')
|
||||
if not self._session:
|
||||
self._session = session_for_reply_channel(self.reply_channel)
|
||||
return self._session
|
||||
|
||||
@property
|
||||
def channel_layer(self):
|
||||
"""Channel layer as lazy property"""
|
||||
return channel_layers[self.alias]
|
||||
|
||||
def get_next_message(self, channel):
|
||||
"""
|
||||
Gets the next message that was sent to the channel during the test,
|
||||
or None if no message is available.
|
||||
|
||||
If require is true, will fail the test if no message is received.
|
||||
"""
|
||||
recv_channel, content = channel_layers[self.alias].receive_many([channel])
|
||||
if recv_channel is None:
|
||||
return
|
||||
return Message(content, recv_channel, channel_layers[self.alias])
|
||||
|
||||
def send(self, to, content={}):
|
||||
"""
|
||||
Send a message to a channel.
|
||||
Adds reply_channel name and channel_session to the message.
|
||||
"""
|
||||
content = copy.deepcopy(content)
|
||||
content.setdefault('reply_channel', self.reply_channel)
|
||||
content.setdefault('path', '/')
|
||||
content.setdefault('headers', self.headers)
|
||||
self.channel_layer.send(to, content)
|
||||
|
||||
def consume(self, channel):
|
||||
"""
|
||||
Get next message for channel name and run appointed consumer
|
||||
"""
|
||||
message = self.get_next_message(channel)
|
||||
if message:
|
||||
consumer, kwargs = self.channel_layer.router.match(message)
|
||||
return consumer(message, **kwargs)
|
||||
|
||||
def send_and_consume(self, channel, content={}):
|
||||
"""
|
||||
Reproduce full live cycle of the message
|
||||
"""
|
||||
self.send(channel, content)
|
||||
return self.consume(channel)
|
||||
|
||||
def receive(self):
|
||||
"""
|
||||
Get content of next message for reply channel if message exists
|
||||
"""
|
||||
message = self.get_next_message(self.reply_channel)
|
||||
if message:
|
||||
return message.content
|
||||
|
||||
def login(self, **credentials):
|
||||
"""
|
||||
Returns True if login is possible; False if the provided credentials
|
||||
are incorrect, or the user is inactive, or if the sessions framework is
|
||||
not available.
|
||||
"""
|
||||
from django.contrib.auth import authenticate
|
||||
user = authenticate(**credentials)
|
||||
if user and user.is_active and apps.is_installed('django.contrib.sessions'):
|
||||
self._login(user)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def force_login(self, user, backend=None):
|
||||
if backend is None:
|
||||
backend = settings.AUTHENTICATION_BACKENDS[0]
|
||||
user.backend = backend
|
||||
self._login(user)
|
||||
|
||||
def _login(self, user):
|
||||
from django.contrib.auth import login
|
||||
|
||||
# Fake http request
|
||||
request = type('FakeRequest', (object, ), {'session': self.session, 'META': {}})
|
||||
login(request, user)
|
||||
|
||||
# Save the session values.
|
||||
self.session.save()
|
||||
|
||||
|
||||
def _encoded_cookies(cookies):
|
||||
"""Encode dict of cookies to ascii string"""
|
||||
return ('&'.join('{0}={1}'.format(k, v) for k, v in cookies.items())).encode("ascii")
|
Loading…
Reference in New Issue
Block a user