Добавлен класс ExpiredTokenAuthentication

This commit is contained in:
2026-07-14 17:29:54 +03:00
parent 5330137041
commit 3af5f9988a
4 changed files with 202 additions and 1 deletions

View File

@@ -0,0 +1,12 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Пакет описания тестов классов авторизации
"""
from .expiring_token_authentication_tests import ExpiringTokenAuthenticationTests

View File

@@ -0,0 +1,173 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса ExpiringTokenAuthenticationTests
"""
import datetime
from django.utils import timezone
from django.utils import translation
from rest_framework.parsers import JSONParser
from rest_framework.request import Request
from rest_framework.test import APIRequestFactory
from net.xeaf.rack.core import CoreTestCase
from net.xeaf.rack.management.userstory import AccountTestData
from net.xeaf.rack.management.userstory import SessionTestData
from net.xeaf.rack.models import AccountModel
from net.xeaf.rack.models import SessionModel
from net.xeaf.rack.utils.auth import ExpiringTokenAuthentication
class ExpiringTokenAuthenticationTests(CoreTestCase):
"""
Тесты для ExpiringTokenAuthentication
"""
def setUp(self) -> None:
"""
Подготовка к тестированию
"""
super().setUp()
self.auth = ExpiringTokenAuthentication()
self.factory = APIRequestFactory()
self.old_language = translation.get_language()
def tearDown(self) -> None:
"""
Восстанавливаем язык глобального контекста после тестов
"""
super().tearDown()
translation.activate(self.old_language)
@classmethod
def _wrap_to_drf_request(cls, wsgi_request) -> Request:
"""
Вспомогательный метод для честного оборачивания Django WSGIRequest в DRF Request
"""
return Request(wsgi_request, parsers=[JSONParser()])
def test_authenticate_adjusts_language_via_query_param(self) -> None:
"""
Должен переключить глобальную локаль Django, используя механизмы LocaleManager
"""
# 1. Создаем нативный запрос Django
wsgi_req = self.factory.get("/", {"x-ui-language": "en"})
# 2. Оборачиваем его в честный DRF Request, у которого ЕСТЬ query_params
drf_request = self._wrap_to_drf_request(wsgi_req)
self.auth.authenticate(drf_request)
# Проверяем, что LocaleManager отработал "вживую" и активировал перевод
self.assertEqual(translation.get_language(), "en")
self.assertEqual(getattr(drf_request, "LANGUAGE_CODE", None), "EN")
def test_authenticate_extracts_token_from_query_params_and_logs_in(self) -> None:
"""
Должен успешно авторизовать Ивана Иванова, если его токен передан в GET параметре
"""
wsgi_req = self.factory.get("/", {ExpiringTokenAuthentication.AUTH_TOKEN_PARAM: SessionTestData.I_IVANOV_SESSION_TOKEN})
drf_request = self._wrap_to_drf_request(wsgi_req)
result = self.auth.authenticate(drf_request)
# Линтер-безопасная проверка контейнера
self.assertIsInstance(result, tuple)
assert result is not None
account, session = result
self.assertEqual(account.id, AccountTestData.I_IVANOV_ID)
self.assertEqual(session.token, SessionTestData.I_IVANOV_SESSION_TOKEN)
def test_authenticate_fallback_to_super_headers_if_no_query_param(self) -> None:
"""
Должен отработать через стандартный заголовок 'Authorization: Token ...' родительского класса
"""
wsgi_req = self.factory.get("/", HTTP_AUTHORIZATION=f"Token {SessionTestData.P_PETROV_SESSION_TOKEN}")
drf_request = self._wrap_to_drf_request(wsgi_req)
result = self.auth.authenticate(drf_request)
self.assertIsInstance(result, tuple)
assert result is not None
account, session = result
self.assertEqual(account.id, AccountTestData.P_PETROV_ID)
def test_authenticate_credentials_returns_none_tuple_if_token_not_found(self) -> None:
"""
Должен мягко вернуть (None, None), если токена вообще нет в базе сессий
"""
result = self.auth.authenticate_credentials("completely_fake_token")
self.assertEqual(result, (None, None))
def test_authenticate_credentials_returns_none_tuple_if_session_is_expired(self) -> None:
"""
Должен вернуть (None, None), если сессия Петра Петрова просрочена
"""
session = SessionModel.objects.get(token=SessionTestData.P_PETROV_SESSION_TOKEN)
session.expires_at = timezone.now() - datetime.timedelta(seconds=1)
session.save()
result = self.auth.authenticate_credentials(SessionTestData.P_PETROV_SESSION_TOKEN)
self.assertEqual(result, (None, None))
def test_authenticate_credentials_returns_none_tuple_if_account_is_not_active(self) -> None:
"""
Должен вернуть (None, None), если AccountManager заблокировал вход (is_active = False)
"""
account = AccountModel.objects.get(id=AccountTestData.I_IVANOV_ID)
account.is_active = False
account.save()
result = self.auth.authenticate_credentials(SessionTestData.I_IVANOV_SESSION_TOKEN)
self.assertEqual(result, (None, None))
def test_authenticate_credentials_returns_none_tuple_if_account_is_deleted(self) -> None:
"""
Должен вернуть (None, None), если пользователь помечен как удаленный
"""
account = AccountModel.objects.get(id=AccountTestData.I_IVANOV_ID)
account.soft_delete(save=True)
result = self.auth.authenticate_credentials(SessionTestData.I_IVANOV_SESSION_TOKEN)
self.assertEqual(result, (None, None))
def test_authenticate_credentials_renews_session_timestamp_on_success(self) -> None:
"""
Должен обновить время жизни сессии в БД и вернуть валидные объекты при успешном входе
"""
session_before = SessionModel.objects.get(token=SessionTestData.I_IVANOV_SESSION_TOKEN)
result = self.auth.authenticate_credentials(SessionTestData.I_IVANOV_SESSION_TOKEN)
self.assertIsInstance(result, tuple)
assert result is not None
account, session = result
self.assertIsInstance(account, AccountModel)
self.assertEqual(account.id, AccountTestData.I_IVANOV_ID)
self.assertIsInstance(session, SessionModel)
session.refresh_from_db()
self.assertGreater(session.expires_at, session_before.expires_at)

View File

@@ -7,3 +7,5 @@
""" """
Классов авторизации Классов авторизации
""" """
from .expiring_token_authentication import ExpiringTokenAuthentication

View File

@@ -13,6 +13,7 @@ from typing import Any
from rest_framework.authentication import TokenAuthentication from rest_framework.authentication import TokenAuthentication
from rest_framework.request import Request from rest_framework.request import Request
from net.xeaf.rack.managers import AccountManager
from net.xeaf.rack.managers import LocaleManager from net.xeaf.rack.managers import LocaleManager
from net.xeaf.rack.managers import SessionManager from net.xeaf.rack.managers import SessionManager
from net.xeaf.rack.models import AccountModel from net.xeaf.rack.models import AccountModel
@@ -54,8 +55,21 @@ class ExpiringTokenAuthentication(TokenAuthentication):
return None, None return None, None
account = session.account account = session.account
if not account.is_active or account.is_deleted(): if not self._check_account(account):
return None, None return None, None
SessionManager.renew_session(session) SessionManager.renew_session(session)
return session.account, session return session.account, session
@classmethod
def _check_account(cls, account: AccountModel) -> bool:
"""
Проверка учетной записи
:param account: Учетная запись
"""
# Можно прошедшим проверку и не удаленным
reject = AccountManager.check_account_auth(account)
result = reject is None and not account.is_deleted()
return result