Подготовка к переносу классов моделей
This commit is contained in:
@@ -9,3 +9,5 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from .ui_language import UILanguage
|
from .ui_language import UILanguage
|
||||||
|
from .auth_reject_reson import AuthRejectReason
|
||||||
|
|
||||||
|
|||||||
42
net/xeaf/rack/enums/auth_reject_reson.py
Normal file
42
net/xeaf/rack/enums/auth_reject_reson.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса AuthRejectReason
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CoreEnum
|
||||||
|
|
||||||
|
|
||||||
|
class AuthRejectReason(CoreEnum):
|
||||||
|
"""
|
||||||
|
Перечисление причин отказа в авторизации
|
||||||
|
"""
|
||||||
|
|
||||||
|
INVALID_USERNAME_OR_PASSWORD = "invalid_username_or_password"
|
||||||
|
""" Некорректный логин или пароль """
|
||||||
|
|
||||||
|
ACCOUNT_IS_NOT_ACTIVE = "account_is_not_active"
|
||||||
|
""" Аккаунт не активен """
|
||||||
|
|
||||||
|
REGISTRATION_IS_NOT_COMPLETE = "registration_is_not_complete"
|
||||||
|
""" Регистрация не завершена """
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_message(cls, reason: str) -> str:
|
||||||
|
"""
|
||||||
|
Возвращает текст сообщения по причине отказа в авторизации
|
||||||
|
"""
|
||||||
|
|
||||||
|
if reason == cls.INVALID_USERNAME_OR_PASSWORD:
|
||||||
|
return _("rack.errors.invalid_username_or_password")
|
||||||
|
elif reason == cls.ACCOUNT_IS_NOT_ACTIVE:
|
||||||
|
return _("rack.errors.account_is_not_active")
|
||||||
|
elif reason == cls.REGISTRATION_IS_NOT_COMPLETE:
|
||||||
|
return _("rack.errors.registration_is_not_complete")
|
||||||
|
return ""
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
Пакет классов менеджеров
|
Пакет классов менеджеров
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from .account_manager import AccountManager
|
||||||
from .configuration_manager import ConfigurationManager
|
from .configuration_manager import ConfigurationManager
|
||||||
from .crypto_manager import CryptoManager
|
from .crypto_manager import CryptoManager
|
||||||
from .locale_manager import LocaleManager
|
from .locale_manager import LocaleManager
|
||||||
|
from .session_manager import SessionManager
|
||||||
|
|||||||
@@ -8,10 +8,19 @@
|
|||||||
Описание класса AccountManager
|
Описание класса AccountManager
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from net.xeaf.rack.models.account_model import AccountModel
|
||||||
|
|
||||||
|
|
||||||
class AccountManager:
|
class AccountManager:
|
||||||
"""
|
"""
|
||||||
Реализует методы работы с Аккаунтами
|
Реализует методы работы с Аккаунтами
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
@classmethod
|
||||||
|
def restore_account(cls, account: AccountModel) -> None:
|
||||||
|
"""
|
||||||
|
Восстанавливает удаленный Аккаунт
|
||||||
|
"""
|
||||||
|
|
||||||
|
if account.is_deleted():
|
||||||
|
account.undelete(save=True)
|
||||||
|
|||||||
@@ -8,15 +8,20 @@
|
|||||||
Описание класса SessionManager
|
Описание класса SessionManager
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from typing import NoReturn
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.contrib.auth import authenticate
|
||||||
from environ import ImproperlyConfigured
|
from environ import ImproperlyConfigured
|
||||||
|
|
||||||
|
from net.xeaf.rack.enums import AuthRejectReason
|
||||||
from net.xeaf.rack.managers import ConfigurationManager
|
from net.xeaf.rack.managers import ConfigurationManager
|
||||||
from net.xeaf.rack.managers import CryptoManager
|
from net.xeaf.rack.managers import CryptoManager
|
||||||
from net.xeaf.rack.models import AccountModel
|
from net.xeaf.rack.models import AccountModel
|
||||||
from net.xeaf.rack.models import SessionModel
|
from net.xeaf.rack.models import SessionModel
|
||||||
|
from net.xeaf.rack.utils.exceptions import UnauthorizedException
|
||||||
|
from .account_manager import AccountManager
|
||||||
|
|
||||||
|
|
||||||
class SessionManager[T:SessionModel]:
|
class SessionManager[T:SessionModel]:
|
||||||
@@ -27,21 +32,39 @@ class SessionManager[T:SessionModel]:
|
|||||||
DEFAULT_SESSION_TTL = 3600
|
DEFAULT_SESSION_TTL = 3600
|
||||||
""" Время жизни сессии по умолчанию """
|
""" Время жизни сессии по умолчанию """
|
||||||
|
|
||||||
|
__model_class: Type[T] | None
|
||||||
|
""" Хранилище для класса сессии """
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def authenticate(cls, username: str, password: str) -> SessionModel:
|
def authenticate(cls, username: str, password: str, device_id: str | None = None) -> T:
|
||||||
"""
|
"""
|
||||||
Авторизует новую сессию
|
Авторизует новую сессию
|
||||||
|
|
||||||
:param username: Имя пользователя
|
:param username: Имя пользователя
|
||||||
:param password: Пароль
|
:param password: Пароль
|
||||||
|
:param device_id: Идентификатор устройства
|
||||||
|
|
||||||
:return: Объект данных сессии
|
:return: Объект данных сессии
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
# noinspection PyTypeChecker
|
||||||
|
account: AccountModel | None = authenticate(username=username, password=password)
|
||||||
|
|
||||||
|
# Обрабатываем ошибки
|
||||||
|
if account is None:
|
||||||
|
cls._raise_auth_error(AuthRejectReason.INVALID_USERNAME_OR_PASSWORD)
|
||||||
|
elif not account.is_active:
|
||||||
|
cls._raise_auth_error(AuthRejectReason.ACCOUNT_IS_NOT_ACTIVE)
|
||||||
|
elif account.joined_at is None:
|
||||||
|
cls._raise_auth_error(AuthRejectReason.REGISTRATION_IS_NOT_COMPLETE)
|
||||||
|
|
||||||
|
# Создаем и возвращаем объект сессии
|
||||||
|
AccountManager.restore_account(account)
|
||||||
|
session = cls.open_session(account, account, device_id)
|
||||||
|
return session
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_by_token(cls, token: str) -> SessionModel | None:
|
def find_by_token(cls, token: str) -> T | None:
|
||||||
"""
|
"""
|
||||||
Возвращает объект данных сессии по токену
|
Возвращает объект данных сессии по токену
|
||||||
|
|
||||||
@@ -50,7 +73,8 @@ class SessionManager[T:SessionModel]:
|
|||||||
:return: Объект данных сессии или None, если не найдена или просрочена
|
:return: Объект данных сессии или None, если не найдена или просрочена
|
||||||
"""
|
"""
|
||||||
|
|
||||||
session = SessionModel.objects.filter(token__exact=token).first()
|
model = cls._get_session_model_class()
|
||||||
|
session = model.objects.filter(token__exact=token).first()
|
||||||
if session is not None and not session.is_expired():
|
if session is not None and not session.is_expired():
|
||||||
return session
|
return session
|
||||||
|
|
||||||
@@ -74,7 +98,17 @@ class SessionManager[T:SessionModel]:
|
|||||||
return session
|
return session
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def renew_session(cls, session: SessionModel) -> None:
|
def close_session(cls, session: T) -> None:
|
||||||
|
"""
|
||||||
|
Закрывает сессию пользователя
|
||||||
|
|
||||||
|
:param session: Модель данных сессии
|
||||||
|
"""
|
||||||
|
|
||||||
|
session.expire(save=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def renew_session(cls, session: T) -> None:
|
||||||
"""
|
"""
|
||||||
Обновляет время существование сессии
|
Обновляет время существование сессии
|
||||||
|
|
||||||
@@ -84,16 +118,6 @@ class SessionManager[T:SessionModel]:
|
|||||||
ttl = ConfigurationManager.get_int("SESSION_TTL", cls.DEFAULT_SESSION_TTL)
|
ttl = ConfigurationManager.get_int("SESSION_TTL", cls.DEFAULT_SESSION_TTL)
|
||||||
session.renew(ttl, save=True)
|
session.renew(ttl, save=True)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def close_session(cls, session: SessionModel) -> None:
|
|
||||||
"""
|
|
||||||
Закрывает сессию пользователя
|
|
||||||
|
|
||||||
:param session: Модель данных сессии
|
|
||||||
"""
|
|
||||||
|
|
||||||
session.expire(save=True)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create_session_model(cls, account: AccountModel, actual_account: AccountModel) -> T:
|
def _create_session_model(cls, account: AccountModel, actual_account: AccountModel) -> T:
|
||||||
"""
|
"""
|
||||||
@@ -118,16 +142,31 @@ class SessionManager[T:SessionModel]:
|
|||||||
:return: Класс модели данных сессии
|
:return: Класс модели данных сессии
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_path = ConfigurationManager.get_string("AUTH_SESSION_MODEL", "net_xeaf_rack.AccountModel")
|
if cls.__model_class is None:
|
||||||
app_label, model_name = model_path.split(".")
|
model_path = ConfigurationManager.get_string("AUTH_SESSION_MODEL", "net_xeaf_rack.SessionModel")
|
||||||
result = apps.get_model(app_label, model_name)
|
app_label, model_name = model_path.split(".")
|
||||||
|
model = apps.get_model(app_label, model_name)
|
||||||
|
|
||||||
# Нет класса
|
# Нет класса
|
||||||
if not result:
|
if not model:
|
||||||
raise ImproperlyConfigured("AUTH_SESSION_MODEL must be set in settings.py")
|
raise ImproperlyConfigured("AUTH_SESSION_MODEL must be set in settings.py")
|
||||||
|
|
||||||
# Некорректный класс
|
# Некорректный класс
|
||||||
if not issubclass(result, SessionModel):
|
if not issubclass(model, SessionModel):
|
||||||
raise ImproperlyConfigured(f"AUTH_SESSION_MODEL must be a subclass of {SessionModel.__name__}")
|
raise ImproperlyConfigured(f"AUTH_SESSION_MODEL must be a subclass of {SessionModel.__name__}")
|
||||||
|
|
||||||
return result
|
cls.__model_class = model
|
||||||
|
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
return cls.__model_class
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _raise_auth_error(cls, reason: AuthRejectReason) -> NoReturn:
|
||||||
|
"""
|
||||||
|
Выбрасывает исключение с ошибкой аутентификации
|
||||||
|
|
||||||
|
:param reason: Причина ошибки
|
||||||
|
"""
|
||||||
|
|
||||||
|
detail = AuthRejectReason.get_message(reason)
|
||||||
|
raise UnauthorizedException(detail=detail, reason=reason)
|
||||||
|
|||||||
@@ -11,3 +11,4 @@
|
|||||||
from .configuration_manager_tests import ConfigurationManagerTests
|
from .configuration_manager_tests import ConfigurationManagerTests
|
||||||
from .crypto_manager_tests import CryptoManagerTests
|
from .crypto_manager_tests import CryptoManagerTests
|
||||||
from .locale_manager_tests import LocaleManagerTests
|
from .locale_manager_tests import LocaleManagerTests
|
||||||
|
# from .sesson_manager_tests import SessionManagerTests
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from net.xeaf.rack.managers import LocaleManager
|
|||||||
|
|
||||||
class LocaleManagerTests(TestCase):
|
class LocaleManagerTests(TestCase):
|
||||||
"""
|
"""
|
||||||
Тесты для менеджера локализации
|
Тесты для класса LocaleManager
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
216
net/xeaf/rack/tests/managers/sesson_manager_tests.py
Normal file
216
net/xeaf/rack/tests/managers/sesson_manager_tests.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса SessionManagerTest
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.test import override_settings
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CoreTestCase
|
||||||
|
from net.xeaf.rack.enums import AuthRejectReason
|
||||||
|
from net.xeaf.rack.management.userstory import AccountTestData
|
||||||
|
from net.xeaf.rack.management.userstory import SessionTestData
|
||||||
|
from net.xeaf.rack.managers import SessionManager
|
||||||
|
from net.xeaf.rack.models import AccountModel
|
||||||
|
from net.xeaf.rack.models import SessionModel
|
||||||
|
from net.xeaf.rack.utils.exceptions import UnauthorizedException
|
||||||
|
|
||||||
|
|
||||||
|
class SessionManagerTests(CoreTestCase):
|
||||||
|
"""
|
||||||
|
Тесты для класса SessionManager
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_should_successfully_authorize_ivanov_with_real_password(self) -> None:
|
||||||
|
"""
|
||||||
|
Проверка успешного создания сессии для эталонного аккаунта
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = SessionManager.authenticate(
|
||||||
|
username="i.ivanov@xeaf.net",
|
||||||
|
password=AccountTestData.DEFAULT_PASSWORD,
|
||||||
|
device_id="mobile_app"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверяем, что вернулась живая сессия со сгенерированным токеном
|
||||||
|
self.assertIsNotNone(session)
|
||||||
|
self.assertEqual(len(session.token), 40)
|
||||||
|
self.assertEqual(session.account_id, AccountTestData.I_IVANOV_ID)
|
||||||
|
self.assertEqual(session.actual_account_id, AccountTestData.I_IVANOV_ID)
|
||||||
|
self.assertEqual(session.device_id, "mobile_app")
|
||||||
|
|
||||||
|
# Честно проверяем запись физически внутри БД
|
||||||
|
db_session = SessionModel.objects.get(token=session.token)
|
||||||
|
self.assertEqual(db_session.account_id, AccountTestData.I_IVANOV_ID)
|
||||||
|
|
||||||
|
def test_should_raise_error_when_password_is_incorrect(self) -> None:
|
||||||
|
"""
|
||||||
|
Менеджер должен выкинуть исключение ядра при неверном пароле
|
||||||
|
"""
|
||||||
|
|
||||||
|
with self.assertRaises(UnauthorizedException) as context:
|
||||||
|
SessionManager.authenticate(
|
||||||
|
username="i.ivanov@xeaf.net",
|
||||||
|
password="wrong_password_here"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsInstance(context.exception.meta, dict)
|
||||||
|
self.assertEqual(context.exception.meta['reason'], AuthRejectReason.INVALID_USERNAME_OR_PASSWORD)
|
||||||
|
|
||||||
|
def test_should_raise_error_when_username_does_not_exist(self) -> None:
|
||||||
|
"""
|
||||||
|
Менеджер должен выкинуть исключение ядра, если email не найден
|
||||||
|
"""
|
||||||
|
|
||||||
|
with self.assertRaises(UnauthorizedException) as context:
|
||||||
|
SessionManager.authenticate(
|
||||||
|
username="unknown.user@xeaf.net",
|
||||||
|
password=AccountTestData.DEFAULT_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsInstance(context.exception.meta, dict)
|
||||||
|
self.assertEqual(context.exception.meta['reason'], AuthRejectReason.INVALID_USERNAME_OR_PASSWORD)
|
||||||
|
|
||||||
|
def test_should_raise_error_if_account_is_inactive(self) -> None:
|
||||||
|
"""
|
||||||
|
Проверка деструктивного изменения БД: отключение аккаунта блокирует вход
|
||||||
|
"""
|
||||||
|
|
||||||
|
petrov = AccountModel.objects.get(id=AccountTestData.P_PETROV_ID)
|
||||||
|
petrov.is_active = False
|
||||||
|
petrov.save()
|
||||||
|
|
||||||
|
with self.assertRaises(UnauthorizedException) as context:
|
||||||
|
SessionManager.authenticate(
|
||||||
|
username="p.petrov@xeaf.net",
|
||||||
|
password=AccountTestData.DEFAULT_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsInstance(context.exception.meta, dict)
|
||||||
|
self.assertEqual(context.exception.meta['reason'], AuthRejectReason.ACCOUNT_IS_NOT_ACTIVE)
|
||||||
|
|
||||||
|
def test_should_find_real_ivanov_session_by_token(self) -> None:
|
||||||
|
"""
|
||||||
|
Поиск существующей и активной сессии по токену из фикстуры
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = SessionManager.find_by_token(SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||||
|
|
||||||
|
self.assertIsNotNone(session)
|
||||||
|
self.assertIsInstance(session, SessionModel)
|
||||||
|
self.assertEqual(session.token, SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||||
|
self.assertEqual(session.account_id, AccountTestData.I_IVANOV_ID)
|
||||||
|
|
||||||
|
def test_should_return_none_for_non_existent_token(self) -> None:
|
||||||
|
"""
|
||||||
|
Запрос несуществующего токена должен возвращать None
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = SessionManager.find_by_token("non_existent_token_garbage_data")
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
def test_should_return_none_if_session_is_expired_in_db(self) -> None:
|
||||||
|
"""
|
||||||
|
Проверка деструктивного изменения БД: просроченная сессия не считывается
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = SessionModel.objects.get(token=SessionTestData.P_PETROV_SESSION_TOKEN)
|
||||||
|
|
||||||
|
# Искусственно сдвигаем дату экспирации в далекое прошлое прямо в базе данных
|
||||||
|
session.expires_at = timezone.now() - datetime.timedelta(days=10)
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
result = SessionManager.find_by_token(SessionTestData.P_PETROV_SESSION_TOKEN)
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
def test_should_fail_finding_if_session_was_physically_deleted_from_db(self) -> None:
|
||||||
|
"""
|
||||||
|
Полное физическое удаление строки из БД корректно обрабатывается
|
||||||
|
"""
|
||||||
|
|
||||||
|
SessionModel.objects.filter(token=SessionTestData.I_IVANOV_SESSION_TOKEN).delete()
|
||||||
|
|
||||||
|
result = SessionManager.find_by_token(SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
@override_settings(SESSION_TTL=1800)
|
||||||
|
def test_should_open_session_and_apply_override_settings_ttl(self) -> None:
|
||||||
|
"""
|
||||||
|
Открытие сессии должно использовать живой ConfigurationManager без заглушек
|
||||||
|
"""
|
||||||
|
|
||||||
|
ivanov = AccountModel.objects.get(id=AccountTestData.I_IVANOV_ID)
|
||||||
|
petrov = AccountModel.objects.get(id=AccountTestData.P_PETROV_ID)
|
||||||
|
|
||||||
|
# Создаем сессию имперсонации (Иванов под видом Петра)
|
||||||
|
session = SessionManager.open_session(account=ivanov, actual_account=petrov, device_id="tablet_v1")
|
||||||
|
|
||||||
|
# Проверяем, что запись успешно материализовалась в ОЗУ
|
||||||
|
db_session = SessionModel.objects.get(token=session.token)
|
||||||
|
self.assertEqual(db_session.account, ivanov)
|
||||||
|
self.assertEqual(db_session.actual_account, petrov)
|
||||||
|
self.assertEqual(db_session.device_id, "tablet_v1")
|
||||||
|
self.assertGreater(db_session.expires_at, timezone.now())
|
||||||
|
|
||||||
|
def test_should_physically_expire_session_on_close_session(self) -> None:
|
||||||
|
"""
|
||||||
|
Закрытие сессии переводит модель в статус expired прямо в БД
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = SessionModel.objects.get(token=SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||||
|
|
||||||
|
SessionManager.close_session(session)
|
||||||
|
|
||||||
|
# Перечитываем чистую запись из БД
|
||||||
|
db_session = SessionModel.objects.get(token=SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||||
|
self.assertTrue(db_session.is_expired())
|
||||||
|
|
||||||
|
@override_settings(SESSION_TTL=9999)
|
||||||
|
def test_should_extend_expiration_time_on_renew_session(self) -> None:
|
||||||
|
"""
|
||||||
|
Обновление сессии сдвигает expires_at вперед на величину из настроек
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = SessionModel.objects.get(token=SessionTestData.P_PETROV_SESSION_TOKEN)
|
||||||
|
initial_expires_at = session.expires_at
|
||||||
|
|
||||||
|
SessionManager.renew_session(session)
|
||||||
|
|
||||||
|
db_session = SessionModel.objects.get(token=SessionTestData.P_PETROV_SESSION_TOKEN)
|
||||||
|
self.assertGreater(db_session.expires_at, initial_expires_at)
|
||||||
|
|
||||||
|
def test_protected_create_session_model_writes_correct_record(self) -> None:
|
||||||
|
"""
|
||||||
|
Прямой вызов защищенного метода фабрики для хирургического теста записи
|
||||||
|
"""
|
||||||
|
|
||||||
|
ivanov = AccountModel.objects.get(id=AccountTestData.I_IVANOV_ID)
|
||||||
|
|
||||||
|
# Запускаем защищенный метод фабрики в обход цепочек обновления TTL
|
||||||
|
session = SessionManager._create_session_model(account=ivanov, actual_account=ivanov)
|
||||||
|
|
||||||
|
# Доказываем линтеру, что мы получили объект, и проверяем его наличие в БД
|
||||||
|
self.assertIsInstance(session, SessionModel)
|
||||||
|
self.assertTrue(SessionModel.objects.filter(token=session.token).exists())
|
||||||
|
|
||||||
|
@override_settings(AUTH_SESSION_MODEL="invalid_app.MissingModel")
|
||||||
|
def test_protected_get_session_model_class_raises_on_missing_model(self) -> None:
|
||||||
|
"""
|
||||||
|
Защищенный метод разрешения класса должен падать, если модель не найдена
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Сбрасываем внутренний кэш класса в синглтоне менеджера перед тестом
|
||||||
|
SessionManager._SessionManager__model_class = None
|
||||||
|
|
||||||
|
with self.assertRaises(ImproperlyConfigured) as context:
|
||||||
|
SessionManager._get_session_model_class()
|
||||||
|
|
||||||
|
self.assertIn("must be set in settings.py", str(context.exception))
|
||||||
Reference in New Issue
Block a user