From c8829c1fd740125a154a7fa715da18d7d53d6499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9fano=20Troncaro?= Date: Tue, 7 Oct 2025 15:52:41 -0300 Subject: [PATCH 1/5] feat: user factory --- tests/factories/__init__.py | 1 + tests/factories/user_factory.py | 71 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 tests/factories/__init__.py create mode 100644 tests/factories/user_factory.py diff --git a/tests/factories/__init__.py b/tests/factories/__init__.py new file mode 100644 index 0000000..5801c1e --- /dev/null +++ b/tests/factories/__init__.py @@ -0,0 +1 @@ +from .user_factory import UserFactory diff --git a/tests/factories/user_factory.py b/tests/factories/user_factory.py new file mode 100644 index 0000000..7eada40 --- /dev/null +++ b/tests/factories/user_factory.py @@ -0,0 +1,71 @@ +from typing import Sequence +from uuid import UUID, uuid4 + +from sqlalchemy.orm import Session + +from app.auth.utils import security +from app.users.models.user import User +from app.users.services.users_service import UsersService + + +class UserFactory: + def __init__(self, session: Session): + self.session = session + self.service = UsersService(session) + + def create( + self, + *, + id: UUID | None = None, + email: str = "test@user.com", + password: str = "password", + ) -> User: + if id is None: + id = uuid4() + + user = User( + id=id, + email=email, + hashed_password=security.get_password_hash(password), + ) + + self.session.add(user) + self.session.flush() + return user + + def create_many( + self, + amount: int, + *, + email_base: str = "test@user.com", + ids: Sequence[UUID] | None, + emails: Sequence[str] | None = None, + passwords: Sequence[str] | None = None, + ) -> list[User]: + assert amount > 1, "Amount must be greater than 1" + + if ids is not None: + assert len(ids) == amount, "Wrong number of ids" + else: + ids = [uuid4() for _ in range(amount)] + + if emails is not None: + assert len(emails) == amount, "Wrong number of emails" + else: + assert email_base.count("@") == 1, ( + "`email_base` must contain one `@`" + ) + address, domain = email_base.split("@") + emails = [f"{address}{i}@{domain}" for i in range(amount)] + + if passwords is not None: + assert len(passwords) == amount, "Wrong number of passwords" + else: + passwords = ["password" for _ in range(amount)] + + users = [ + self.create(id=id, email=email, password=password) + for id, email, password in zip(ids, emails, passwords, strict=True) + ] + + return users From b0f9364b6f534f04a1e5eef28cd754265621c401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9fano=20Troncaro?= Date: Fri, 10 Oct 2025 12:31:21 -0300 Subject: [PATCH 2/5] feat: add user factory --- .../models/__init__.py | 2 +- .../models/user_2fa.py | 20 +++- .../use_cases/__init__.py | 2 + app/users/models/__init__.py | 2 +- app/users/models/user.py | 19 +++- app/users/repositories/__init__.py | 1 + tests/auth/api/endpoints/test_auth.py | 31 ++++--- tests/conftest.py | 16 +++- tests/factories/user_factory.py | 19 +++- .../test_create_new_user_2fa_endpoint.py | 25 +++-- .../test_verify_user_2fa_endpoint.py | 92 +++++++++---------- 11 files changed, 132 insertions(+), 97 deletions(-) diff --git a/app/two_factor_authentication/models/__init__.py b/app/two_factor_authentication/models/__init__.py index 83b8f2e..354e05d 100644 --- a/app/two_factor_authentication/models/__init__.py +++ b/app/two_factor_authentication/models/__init__.py @@ -1 +1 @@ -from .user_2fa import Users2FA \ No newline at end of file +from .user_2fa import Users2FA diff --git a/app/two_factor_authentication/models/user_2fa.py b/app/two_factor_authentication/models/user_2fa.py index 78b914c..0dcc59d 100644 --- a/app/two_factor_authentication/models/user_2fa.py +++ b/app/two_factor_authentication/models/user_2fa.py @@ -1,15 +1,25 @@ +from typing import TYPE_CHECKING from uuid import UUID -from app.common.models.base_class import Base from sqlalchemy import ForeignKey -from sqlalchemy.orm import ( - Mapped, - mapped_column, -) +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.common.models.base_class import Base + + +if TYPE_CHECKING: + from app.users.models import User class Users2FA(Base): __tablename__ = "users_2fa" + + # fields secret_key: Mapped[str] user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id")) active: Mapped[bool] + + # relationships + user: Mapped["User"] = relationship( + back_populates="two_factor_authentications", + ) diff --git a/app/two_factor_authentication/use_cases/__init__.py b/app/two_factor_authentication/use_cases/__init__.py index e69de29..28eac66 100644 --- a/app/two_factor_authentication/use_cases/__init__.py +++ b/app/two_factor_authentication/use_cases/__init__.py @@ -0,0 +1,2 @@ +from .create_new_user_2fa_use_case import CreateNewUser2FAUseCase +from .verify_user_2fa_use_case import VerifyUser2FAUseCase diff --git a/app/users/models/__init__.py b/app/users/models/__init__.py index a5057a0..ee4c00b 100644 --- a/app/users/models/__init__.py +++ b/app/users/models/__init__.py @@ -1 +1 @@ -from .user import User # noqa +from .user import User diff --git a/app/users/models/user.py b/app/users/models/user.py index 503186e..f71b329 100644 --- a/app/users/models/user.py +++ b/app/users/models/user.py @@ -1,14 +1,27 @@ -from sqlalchemy.orm import Mapped, mapped_column +from typing import TYPE_CHECKING + +from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.types import String from app.common.models.base_class import Base - from app.users.constants.user_constants import USER_EMAIL_MAX_LENGTH +if TYPE_CHECKING: + from app.two_factor_authentication.models.user_2fa import Users2FA + + class User(Base): __tablename__ = "users" + + # fields email: Mapped[str] = mapped_column( - String(USER_EMAIL_MAX_LENGTH), unique=True + String(USER_EMAIL_MAX_LENGTH), + unique=True, ) hashed_password: Mapped[str] + + # relationships + two_factor_authentications: Mapped[list["Users2FA"]] = relationship( + back_populates="user", + ) diff --git a/app/users/repositories/__init__.py b/app/users/repositories/__init__.py index e69de29..44767f0 100644 --- a/app/users/repositories/__init__.py +++ b/app/users/repositories/__init__.py @@ -0,0 +1 @@ +from .users_repository import UsersRepository, users_repository diff --git a/tests/auth/api/endpoints/test_auth.py b/tests/auth/api/endpoints/test_auth.py index 7428560..f5ccf05 100644 --- a/tests/auth/api/endpoints/test_auth.py +++ b/tests/auth/api/endpoints/test_auth.py @@ -1,35 +1,38 @@ -from sqlalchemy.orm import Session - from fastapi.testclient import TestClient -from tests.utils.create_user import create_user +from tests.factories import UserFactory + login_path = "api/v1/auth/login" class TestLogin: - def test_login(self, client: TestClient, session: Session): - created_user = create_user(session) + def test_login( + self, client: TestClient, user_factory: UserFactory + ) -> None: + user_password = "password" + + user = user_factory.create(password=user_password) response = client.post( login_path, json={ - "email": created_user.email, - "password": "password", + "email": user.email, + "password": user_password, }, ) assert response.status_code == 204 def test_login_incorrect_password( - self, client: TestClient, session: Session - ): - created_user = create_user(session) + self, client: TestClient, user_factory: UserFactory + ) -> None: + user = user_factory.create(password="password") response = client.post( login_path, json={ - "email": created_user.email, + "email": user.email, "password": "incorrect_password", }, ) @@ -37,9 +40,9 @@ def test_login_incorrect_password( assert response.status_code == 401 def test_login_non_existent_email( - self, client: TestClient, session: Session - ): - create_user(session) + self, client: TestClient, user_factory: UserFactory + ) -> None: + user_factory.create(email="test@user.com") response = client.post( login_path, diff --git a/tests/conftest.py b/tests/conftest.py index b94c9d4..ff05bd0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,18 @@ from typing import Generator + +from fastapi.testclient import TestClient import pytest from sqlalchemy import RootTransaction, event -from fastapi.testclient import TestClient +from sqlalchemy.orm import Session from app.common.api.dependencies.get_session import get_session -from app.core.config import get_settings +from app.core.config import settings from app.db.session import engine, SessionLocal from app.main import app -from sqlalchemy.orm import Session -settings = get_settings() +from tests.factories import UserFactory + + TEST_DATABASE_URI = settings.SQLALCHEMY_DATABASE_URI @@ -48,3 +51,8 @@ def override_get_session() -> Generator: with TestClient(app) as client: yield client + + +@pytest.fixture() +def user_factory(session: Session) -> UserFactory: + return UserFactory(session) diff --git a/tests/factories/user_factory.py b/tests/factories/user_factory.py index 7eada40..2b95715 100644 --- a/tests/factories/user_factory.py +++ b/tests/factories/user_factory.py @@ -1,17 +1,17 @@ -from typing import Sequence +from typing import Any, Sequence from uuid import UUID, uuid4 from sqlalchemy.orm import Session from app.auth.utils import security -from app.users.models.user import User -from app.users.services.users_service import UsersService +from app.two_factor_authentication.use_cases import CreateNewUser2FAUseCase +from app.users.models import User class UserFactory: def __init__(self, session: Session): self.session = session - self.service = UsersService(session) + self._create_2fa = CreateNewUser2FAUseCase(session).execute def create( self, @@ -19,6 +19,7 @@ def create( id: UUID | None = None, email: str = "test@user.com", password: str = "password", + with_2fa: bool = False, ) -> User: if id is None: id = uuid4() @@ -31,6 +32,13 @@ def create( self.session.add(user) self.session.flush() + + if with_2fa: + self._create_2fa(user.id) + + self.session.refresh( + user, attribute_names=("two_factor_authentications",) + ) return user def create_many( @@ -41,6 +49,7 @@ def create_many( ids: Sequence[UUID] | None, emails: Sequence[str] | None = None, passwords: Sequence[str] | None = None, + **kw: Any, ) -> list[User]: assert amount > 1, "Amount must be greater than 1" @@ -64,7 +73,7 @@ def create_many( passwords = ["password" for _ in range(amount)] users = [ - self.create(id=id, email=email, password=password) + self.create(id=id, email=email, password=password, **kw) for id, email, password in zip(ids, emails, passwords, strict=True) ] diff --git a/tests/two_factor_authentication/endpoints/test_create_new_user_2fa_endpoint.py b/tests/two_factor_authentication/endpoints/test_create_new_user_2fa_endpoint.py index 677406d..ab72e9e 100644 --- a/tests/two_factor_authentication/endpoints/test_create_new_user_2fa_endpoint.py +++ b/tests/two_factor_authentication/endpoints/test_create_new_user_2fa_endpoint.py @@ -1,32 +1,29 @@ +from urllib.parse import quote + from fastapi.testclient import TestClient from sqlalchemy.orm import Session -from app.core.config import get_settings +from app.core.config import settings from app.two_factor_authentication.services.users_2fa_service import ( Users2FAService, ) -from tests.utils.create_user import create_user -from urllib.parse import quote -settings = get_settings() +from tests.factories import UserFactory class TestCreateNewUser2FAEndpoint: def test_create_new_user_2fa( - self, client: TestClient, session: Session + self, client: TestClient, session: Session, user_factory: UserFactory ) -> None: - created_user = create_user(session) - - response = client.post( - f"api/v1/users/{created_user.id}/totp", - ) - user_2fa = Users2FAService(session).get_by_user_id(created_user.id) + user = user_factory.create() + response = client.post(f"api/v1/users/{user.id}/totp") assert response.status_code == 200 - assert user_2fa + user_2fa = Users2FAService(session).get_by_user_id(user.id) + assert user_2fa, "User2FA not created" assert ( response.json()["provisioning_url"] - == f"otpauth://totp/{settings.PROJECT_NAME}:{quote(created_user.email)}?secret={user_2fa.secret_key}&issuer={settings.PROJECT_NAME}" - ) + == f"otpauth://totp/{settings.PROJECT_NAME}:{quote(user.email)}?secret={user_2fa.secret_key}&issuer={settings.PROJECT_NAME}" + ), "Wrong provisioning url" diff --git a/tests/two_factor_authentication/endpoints/test_verify_user_2fa_endpoint.py b/tests/two_factor_authentication/endpoints/test_verify_user_2fa_endpoint.py index f9b9728..5fdcb20 100644 --- a/tests/two_factor_authentication/endpoints/test_verify_user_2fa_endpoint.py +++ b/tests/two_factor_authentication/endpoints/test_verify_user_2fa_endpoint.py @@ -1,79 +1,71 @@ +from typing import Any, Final +from uuid import UUID + +from fastapi import Response from fastapi.testclient import TestClient import pyotp from sqlalchemy.orm import Session -from app.core.config import get_settings -from app.two_factor_authentication.services.users_2fa_service import ( - Users2FAService, -) -from app.two_factor_authentication.use_cases.create_new_user_2fa_use_case import ( - CreateNewUser2FAUseCase, -) -from tests.utils.create_user import create_user +from app.two_factor_authentication.models import Users2FA -settings = get_settings() +from tests.factories import UserFactory class TestVerifyUser2FAEndpoint: + URL: Final = "api/v1/users/{user_id}/totp/verify" + def test_verify_correct_user_2fa( - self, client: TestClient, session: Session + self, + client: TestClient, + user_factory: UserFactory, ) -> None: - created_user = create_user(session) - CreateNewUser2FAUseCase(session).execute(created_user.id) - - user_2fa = Users2FAService(session).get_by_user_id(created_user.id) - - assert user_2fa + user = user_factory.create(with_2fa=True) + assert (user_2fa := user.two_factor_authentications[0]) totp = pyotp.TOTP(user_2fa.secret_key) valid_code = totp.now() - response = client.post( - f"api/v1/users/{created_user.id}/totp/verify", - json={"user_code": valid_code}, - ) - + response = self._post(client, user.id, valid_code) assert response.status_code == 200 def test_verify_correct_user_2fa_and_mark_active( - self, client: TestClient, session: Session + self, + client: TestClient, + session: Session, + user_factory: UserFactory, ) -> None: - created_user = create_user(session) - CreateNewUser2FAUseCase(session).execute(created_user.id) - - user_2fa = Users2FAService(session).get_by_user_id(created_user.id) - - assert user_2fa + user = user_factory.create(with_2fa=True) + assert (user_2fa := user.two_factor_authentications[0]) totp = pyotp.TOTP(user_2fa.secret_key) valid_code = totp.now() - response = client.post( - f"api/v1/users/{created_user.id}/totp/verify", - json={"user_code": valid_code, "mark_active": True}, - ) - + response = self._post(client, user.id, valid_code, mark_active=True) assert response.status_code == 200 - user_2fa = Users2FAService(session).get_by_user_id(created_user.id) - - assert user_2fa and user_2fa.active + assert (user_2fa := session.get(Users2FA, user_2fa.id)) + assert user_2fa.active def test_verify_incorrect_user_2fa( - self, client: TestClient, session: Session + self, + client: TestClient, + user_factory: UserFactory, ) -> None: - created_user = create_user(session) - CreateNewUser2FAUseCase(session).execute(created_user.id) - - user_2fa = Users2FAService(session).get_by_user_id(created_user.id) - - assert user_2fa - - response = client.post( - f"api/v1/users/{created_user.id}/totp/verify", - json={ - "user_code": "invalid_code", - }, - ) + user = user_factory.create(with_2fa=True) + response = self._post(client, user.id, "invalid_code") assert response.status_code == 401 + + def _post( + self, + client: TestClient, + user_id: UUID, + code: str, + mark_active: bool | None = None, + ) -> Response: + url = self.URL.format(user_id=user_id) + json: dict[str, Any] = {"user_code": code} + if mark_active is not None: + json["mark_active"] = mark_active + + return client.post(url, json=json) From 435412f9a783a7b3171dfed180d3a74afb0d19da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9fano=20Troncaro?= Date: Fri, 10 Oct 2025 14:22:56 -0300 Subject: [PATCH 3/5] tests: refactor reset password test --- tests/auth/api/endpoints/test_reset_password.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/auth/api/endpoints/test_reset_password.py b/tests/auth/api/endpoints/test_reset_password.py index c13b4f8..a74482b 100644 --- a/tests/auth/api/endpoints/test_reset_password.py +++ b/tests/auth/api/endpoints/test_reset_password.py @@ -3,17 +3,17 @@ from app.auth.schemas.token_schema import EmailTokenPayload from app.auth.utils.security import create_access_token, verify_password -from app.users.models.user import User -from tests.utils.create_user import create_user + +from tests.factories import UserFactory class TestResetPasswordEndpoint: def test_reset_password_success( - self, client: TestClient, session: Session + self, client: TestClient, user_factory: UserFactory, session: Session ) -> None: - created_user = create_user(session) + user = user_factory.create() reset_token = create_access_token( - EmailTokenPayload(user_email=created_user.email) + EmailTokenPayload(user_email=user.email) ) new_password = "MyNewSecurePassword123!" @@ -25,8 +25,8 @@ def test_reset_password_success( assert response.status_code == 204 - user = session.query(User).filter(User.id == created_user.id).first() - assert user and verify_password(new_password, user.hashed_password) + session.refresh(user) + assert verify_password(new_password, user.hashed_password) def test_reset_password_invalid_token( self, From 4874c2fdbda3265f758c36970278288af249b919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9fano=20Troncaro?= Date: Tue, 14 Oct 2025 19:06:47 -0300 Subject: [PATCH 4/5] feat: refactor user factory for better performance --- tests/factories/user_factory.py | 133 +++++++++++++++++++++----------- 1 file changed, 88 insertions(+), 45 deletions(-) diff --git a/tests/factories/user_factory.py b/tests/factories/user_factory.py index 2b95715..6c08acd 100644 --- a/tests/factories/user_factory.py +++ b/tests/factories/user_factory.py @@ -1,6 +1,8 @@ -from typing import Any, Sequence -from uuid import UUID, uuid4 +from datetime import datetime +from typing import Any, Iterable, Sequence +from uuid import UUID +from pydantic import BaseModel from sqlalchemy.orm import Session from app.auth.utils import security @@ -9,72 +11,113 @@ class UserFactory: + class _UserConfig(BaseModel): + id: UUID | None = None + created_at: datetime | None = None + email: str = "test@user.com" + password: str = "password" + with_2fa: bool = False + + @property + def config(self) -> type[_UserConfig]: + return self.__class__._UserConfig + def __init__(self, session: Session): self.session = session - self._create_2fa = CreateNewUser2FAUseCase(session).execute + self._create_2fa_use_case = CreateNewUser2FAUseCase(session) def create( self, - *, - id: UUID | None = None, - email: str = "test@user.com", - password: str = "password", - with_2fa: bool = False, + config: _UserConfig | None = None, + /, + **kw: Any, ) -> User: - if id is None: - id = uuid4() - - user = User( - id=id, - email=email, - hashed_password=security.get_password_hash(password), - ) + if config is None: + config = self.config(**kw) + user = self._create_model(config) self.session.add(user) self.session.flush() - if with_2fa: - self._create_2fa(user.id) + if config.with_2fa: + self._create_2fa(user) - self.session.refresh( - user, attribute_names=("two_factor_authentications",) - ) return user def create_many( self, amount: int, + /, *, - email_base: str = "test@user.com", - ids: Sequence[UUID] | None, - emails: Sequence[str] | None = None, - passwords: Sequence[str] | None = None, + email_base: str = "test@mail.com", + configurations: Sequence[_UserConfig] | None = None, **kw: Any, ) -> list[User]: assert amount > 1, "Amount must be greater than 1" + assert email_base.count("@") == 1, ( + "`email_base` must contain exactly one `@`" + ) - if ids is not None: - assert len(ids) == amount, "Wrong number of ids" - else: - ids = [uuid4() for _ in range(amount)] + configurations = self._parse_configurations( + amount, email_base, configurations, kw + ) - if emails is not None: - assert len(emails) == amount, "Wrong number of emails" - else: - assert email_base.count("@") == 1, ( - "`email_base` must contain one `@`" - ) - address, domain = email_base.split("@") - emails = [f"{address}{i}@{domain}" for i in range(amount)] + users = [self._create_model(config) for config in configurations] - if passwords is not None: - assert len(passwords) == amount, "Wrong number of passwords" - else: - passwords = ["password" for _ in range(amount)] + self.session.add_all(users) + self.session.flush() - users = [ - self.create(id=id, email=email, password=password, **kw) - for id, email, password in zip(ids, emails, passwords, strict=True) - ] + self._create_2fa( + user + for user, config in zip(users, configurations, strict=True) + if config.with_2fa + ) return users + + def _create_model(self, config: _UserConfig, /) -> User: + user = User( + email=config.email, + hashed_password=security.get_password_hash(config.password), + ) + + if config.id: + user.id = config.id + + if config.created_at: + user.created_at = config.created_at + + return user + + def _create_2fa(self, users: User | Iterable[User], /) -> None: + if isinstance(users, User): + users = [users] + + for user in users: + self._create_2fa_use_case.execute(user.id) + self.session.refresh( + user, attribute_names=("two_factor_authentications",) + ) + + def _parse_configurations( + self, + amount: int, + email_base: str, + configurations: Sequence[_UserConfig] | None, + kw: dict[str, Any], + ) -> list[_UserConfig]: + address, domain = email_base.split("@") + + parsed_configurations = [] + for i in range(amount): + if configurations is None or len(configurations) <= i: + config = self.config(**kw) + else: + config = configurations[i] + + if "email" not in config.model_fields_set: + config.email = f"{address}{i}@{domain}" + + parsed_configurations.append(config) + + return parsed_configurations From f6a5bf3f2d259c30c2f497c90d8175b8893ed436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9fano=20Troncaro?= Date: Tue, 14 Oct 2025 19:42:15 -0300 Subject: [PATCH 5/5] feat: client factory --- tests/conftest.py | 15 ++++---- tests/factories/__init__.py | 1 + tests/factories/client_factory.py | 34 +++++++++++++++++++ tests/factories/user_factory.py | 3 ++ .../test_verify_user_2fa_endpoint.py | 4 +-- 5 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 tests/factories/client_factory.py diff --git a/tests/conftest.py b/tests/conftest.py index ff05bd0..d27d048 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,12 +5,11 @@ from sqlalchemy import RootTransaction, event from sqlalchemy.orm import Session -from app.common.api.dependencies.get_session import get_session from app.core.config import settings from app.db.session import engine, SessionLocal from app.main import app -from tests.factories import UserFactory +from tests.factories import ClientFactory, UserFactory TEST_DATABASE_URI = settings.SQLALCHEMY_DATABASE_URI @@ -42,15 +41,13 @@ def end_savepoint(session: Session, transaction: RootTransaction) -> None: @pytest.fixture() -def client(session: Session) -> Generator: - # Use the same session as the session fixture - def override_get_session() -> Generator: - yield session +def client_factory(session: Session) -> ClientFactory: + return ClientFactory(app, session) - app.dependency_overrides[get_session] = override_get_session - with TestClient(app) as client: - yield client +@pytest.fixture() +def client(client_factory: ClientFactory) -> TestClient: + return client_factory.create() @pytest.fixture() diff --git a/tests/factories/__init__.py b/tests/factories/__init__.py index 5801c1e..a7d8ede 100644 --- a/tests/factories/__init__.py +++ b/tests/factories/__init__.py @@ -1 +1,2 @@ +from .client_factory import ClientFactory from .user_factory import UserFactory diff --git a/tests/factories/client_factory.py b/tests/factories/client_factory.py new file mode 100644 index 0000000..0057f4f --- /dev/null +++ b/tests/factories/client_factory.py @@ -0,0 +1,34 @@ +from typing import Generator +from fastapi import FastAPI +from fastapi.testclient import TestClient +from sqlalchemy.orm import Session + +from app.auth.schemas.token_schema import TokenPayload +from app.auth.utils import security +from app.common.api.dependencies.get_session import get_session +from app.users.models.user import User + + +class ClientFactory: + def __init__(self, app: FastAPI, session: Session): + self._app = app + self._session = session + + def _get_session() -> Generator: + yield self._session + + self._app.dependency_overrides[get_session] = _get_session + + def create(self, user_to_log_in: User | None = None, /) -> TestClient: + client = TestClient(self._app) + + if not user_to_log_in: + return client + + # NOTE: set access token cookie to get around login endpoint limiter + access_token = security.create_access_token( + TokenPayload(user_id=str(user_to_log_in.id)) + ) + client.cookies = {"access_token": access_token} + + return client diff --git a/tests/factories/user_factory.py b/tests/factories/user_factory.py index 6c08acd..d08a414 100644 --- a/tests/factories/user_factory.py +++ b/tests/factories/user_factory.py @@ -98,6 +98,9 @@ def _create_2fa(self, users: User | Iterable[User], /) -> None: self.session.refresh( user, attribute_names=("two_factor_authentications",) ) + assert len(user.two_factor_authentications) == 1, ( + "UserFactory failed to create user 2fa" + ) def _parse_configurations( self, diff --git a/tests/two_factor_authentication/endpoints/test_verify_user_2fa_endpoint.py b/tests/two_factor_authentication/endpoints/test_verify_user_2fa_endpoint.py index 5fdcb20..7487b05 100644 --- a/tests/two_factor_authentication/endpoints/test_verify_user_2fa_endpoint.py +++ b/tests/two_factor_authentication/endpoints/test_verify_user_2fa_endpoint.py @@ -20,7 +20,7 @@ def test_verify_correct_user_2fa( user_factory: UserFactory, ) -> None: user = user_factory.create(with_2fa=True) - assert (user_2fa := user.two_factor_authentications[0]) + user_2fa = user.two_factor_authentications[0] totp = pyotp.TOTP(user_2fa.secret_key) valid_code = totp.now() @@ -35,7 +35,7 @@ def test_verify_correct_user_2fa_and_mark_active( user_factory: UserFactory, ) -> None: user = user_factory.create(with_2fa=True) - assert (user_2fa := user.two_factor_authentications[0]) + user_2fa = user.two_factor_authentications[0] totp = pyotp.TOTP(user_2fa.secret_key) valid_code = totp.now()