Django channels 2, accessing db in tests
我最近将项目更新为Django 2和Channel2。现在,我正在尝试重写聊天应用程序的测试。
我遇到了依赖于pytest-django的django db标记的测试问题。我尝试在
如果我在灯具中创建用户并将其正确保存,则会获得一个ID。但是,在我的使用者中,Django在数据库中看不到该用户。并将其视为匿名用户。
我有一个临时令牌,可用于验证websocket.connect上的用户。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | @pytest.fixture def room(): room = generate_room() room.save() return room @pytest.fixture def room_with_user(room, normal_user): room.users.add(normal_user) yield room room.users.remove(normal_user) @pytest.fixture def normal_user(): user = generate_user() user.save() return user @pytest.mark.django_db class TestConnect: @pytest.mark.asyncio async def test_get_connected_client(self, path, room_with_user, temp_token): assert get_path(room_with_user.id) == path communicator = QSWebsocketCommunicator(application, path, query_string=get_query_string(temp_token)) connected, subprotocol = await communicator.connect() assert connected await communicator.disconnect() |
消费者:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class ChatConsumer(JsonWebsocketConsumer): def connect(self): # Called on connection. Either call self.user = self.scope['user'] self.room_id = self.scope['url_route']['kwargs']['room_id'] group = f'room_{self.room_id}' users = list(User.objects.all()) # no users here self.group_name = group if not (self.user is not None and self.user.is_authenticated): return self.close({'Error': 'Not authenticated user'}) try: self.room = Room.objects.get(id=self.room_id, users__id=self.user.id) except ObjectDoesNotExist: return self.close({'Error': 'Room does not exists'}) # Send success response self.accept() # Save user as active self.room.active_users.add(self.user) |
我的身份验证中间件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | class OAuthTokenAuthMiddleware: """ Custom middleware that takes Authorization header and read OAuth token from it. """ def __init__(self, inner): # Store the ASGI application we were passed self.inner = inner def __call__(self, scope): temp_token = self.get_token(scope) scope['user'] = self.validate_token(temp_token) return self.inner(scope) @staticmethod def get_token(scope) -> str: return url_parse.parse_qs(scope['query_string'])[b'token'][0].decode("utf-8") @staticmethod def validate_token(token): try: token = TemporaryToken.objects.select_related('user').get(token=token) if token.is_active(): token.delete() return token.user else: return AnonymousUser() except ObjectDoesNotExist: return AnonymousUser() |
和自定义WebsocketCommunicator,它接受query_string以便包含我的一次性令牌
1 2 3 4 5 6 7 8 9 10 11 12 13 | class QSWebsocketCommunicator(WebsocketCommunicator): def __init__(self, application, path, headers=None, subprotocols=None, query_string: Optional[Union[str, bytes]]=None): if isinstance(query_string, str): query_string = str.encode(query_string) self.scope = { "type":"websocket", "path": path, "headers": headers or [], "subprotocols": subprotocols or [], "query_string": query_string or '' } ApplicationCommunicator.__init__(self, application, self.scope) |
我的问题是如何在测试/灯具中创建用户,房间等对象,以便可以在Django使用者中访问它们。
还是您有另一个想法我该如何克服?
使用您提供的代码来重现您的问题几乎是不可能的。 阅读有关如何创建最小,完整和可验证示例的信息。 但是,我想您应该在测试中使用真实的事务,因为普通的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | # routing.py from django import http from django.conf.urls import url from django.contrib.auth.models import User from channels.routing import ProtocolTypeRouter, URLRouter from channels.generic.websocket import JsonWebsocketConsumer class ChatConsumer(JsonWebsocketConsumer): def connect(self): self.user = self.scope['user'] print('user in scope, set by middleware:', self.user) users = list(User.objects.all()) # no users here print('all users in chat consumer:', users) if not (self.user is not None and self.user.is_authenticated): return self.close({'Error': 'Not authenticated user'}) # Send success response self.accept() class OAuthTokenAuthMiddleware: def __init__(self, inner): # Store the ASGI application we were passed self.inner = inner def __call__(self, scope): token = self.get_token(scope) print('token in middleware:', token) scope['user'] = User.objects.get(username=token) return self.inner(scope) @staticmethod def get_token(scope) -> str: d = http.QueryDict(scope['query_string']) return d['token'] APP = ProtocolTypeRouter({ 'websocket': OAuthTokenAuthMiddleware(URLRouter([url(r'^websocket/$', ChatConsumer)])), }) |
用用户名
1 2 3 4 5 6 7 8 9 | @pytest.fixture(scope='function', autouse=True) def create_user(): with transaction.atomic(): User.objects.all().delete() user = User.objects.create_user( 'spam', '[email protected]', password='eggs', first_name='foo', last_name='bar' ) return user |
现在,我将测试标记为事务性测试,这意味着每个查询实际上都已提交。 现在,测试用户存储在数据库中,并且在中间件/消费者中进行的查询实际上可以返回有意义的东西:
1 2 3 4 5 6 7 8 | @pytest.mark.django_db(transaction=True) @pytest.mark.asyncio async def test_get_connected_client(): app = OAuthTokenAuthMiddleware(URLRouter([url(r'^websocket/$', ChatConsumer)])) communicator = QSWebsocketCommunicator(app, '/websocket/', query_string='token=spam') connected, subprotocol = await communicator.connect() assert connected await communicator.disconnect() |
运行测试测试会产生期望的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | $ pytest -vs ================================== test session starts ================================= platform darwin -- Python 3.6.3, pytest-3.4.0, py-1.5.2, pluggy-0.6.0 -- /Users/hoefling/.virtualenvs/stackoverflow/bin/python cachedir: .pytest_cache Django settings: spam.settings (from environment variable) rootdir: /Users/hoefling/projects/private/stackoverflow/so-49136564/spam, inifile: pytest.ini plugins: celery-4.1.0, forked-0.2, django-3.1.2, cov-2.5.1, asyncio-0.8.0, xdist-1.22.0, mock-1.6.3, hypothesis-3.44.4 collected 1 item tests/test_middleware.py::test_get_connected_client Creating test database for alias 'default'... token in middleware: spam user in scope: spam all users in chat consumer: [<User: spam>] PASSEDDestroying test database for alias 'default'... =============================== 1 passed in 0.38 seconds ================================ |
顺便说一句,由于它现在能够处理查询字符串,因此您不再需要绕过