From 0d884e583670f7349cd6060a86ac4f9cb568966d Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Fri, 4 Sep 2026 00:08:01 +0900 Subject: [PATCH] Add a CRUD Repo test helper to handle basic crud repo testing --- src/infra/db_tests.py | 56 +++++++++++++++++++++++++++++++++++++++++ src/infra/users_test.py | 50 ++++++++++++++++++++---------------- 2 files changed, 85 insertions(+), 21 deletions(-) create mode 100644 src/infra/db_tests.py diff --git a/src/infra/db_tests.py b/src/infra/db_tests.py new file mode 100644 index 0000000..e764924 --- /dev/null +++ b/src/infra/db_tests.py @@ -0,0 +1,56 @@ +from sqlalchemy import Engine +from typing import Callable +from src.infra import CRUDRepo, Base +import pytest + + +class CRUDRepoHelper[A: Base, B, C]: + @pytest.fixture + def repo(self, db: Engine) -> CRUDRepo[A, B, C]: + import pdb + + pdb.set_trace() + raise NotImplementedError() + + @pytest.fixture + def dto(self) -> C: + raise NotImplementedError() + + @pytest.fixture + def item(self, repo: CRUDRepo[A, B, C], dto: C) -> B: + return repo.create(dto, None) + + @pytest.fixture + def transform(self) -> Callable[[B], B]: + raise NotImplementedError + + @pytest.fixture + def expected_transformed_value(self, item: B) -> B: + raise NotImplementedError() + + def test_create(self, item: B): + assert item is not None + + def test_create_returns_none_if_invalid(self, repo: CRUDRepo[A, B, C], dto: C): + def always_invalid(*_args, **_kw): + raise ValueError('test') + + with pytest.raises(ValueError): + repo.create(dto, always_invalid) + + def test_create_returns_a_value_if_valid(self, repo: CRUDRepo[A, B, C], dto: C): + assert repo.create(dto, lambda _: None) is not None + + def test_get_returns_none_when_missing(self, repo: CRUDRepo[A, B, C]): + assert repo.get(42) is None + + def test_get_returns_a_value_if_it_exists(self, repo: CRUDRepo[A, B, C], item: B): + assert repo.get(1) == item + + def test_update(self, repo: CRUDRepo[A, B, C], transform: Callable[[B], B], item: B, expected_transformed_value: B): + assert repo.update(1, transform) == expected_transformed_value + + def test_delete(self, repo: CRUDRepo[A, B, C], item: B): + assert repo.get(1) is not None + repo.delete(1, None) + assert repo.get(1) is None diff --git a/src/infra/users_test.py b/src/infra/users_test.py index 80ce6bc..3184b25 100644 --- a/src/infra/users_test.py +++ b/src/infra/users_test.py @@ -1,36 +1,44 @@ +from typing import Callable +from sqlalchemy import Engine +from src.infra.db_tests import CRUDRepoHelper from src.utils.secret import SecretBox from src.services.users.data import UserDTO, User -from src.services.users.repo import UserRepo from src.infra.users import UserRepoImpl import pytest -@pytest.fixture -def repo(db): - return UserRepoImpl(db) +class TestUserRepoImpl(CRUDRepoHelper): + @pytest.fixture + def repo(self, db: Engine) -> UserRepoImpl: + return UserRepoImpl(db) + @pytest.fixture + def dto(self) -> UserDTO: + return UserDTO('example@example.com', SecretBox('hunter1')) -@pytest.fixture -def users(repo: UserRepo): - return [ - repo.create(UserDTO('example@example.com', SecretBox('hunter1')), None), - repo.create(UserDTO('example2@example.com', SecretBox('hunter2')), None), - ] + @pytest.fixture + def expected_transformed_value(self, item: User) -> User: + return User(id=item.id, email='example2@example.com', email_confirmed=True, password_hash=item.password_hash) + @pytest.fixture + def transform(self) -> Callable[[User], User]: + def do_transform(user: User) -> User: + user.email = 'example2@example.com' + user.email_confirmed = True + return user -def test_getting_a_user_by_email(users: list[User], repo: UserRepo): - assert repo.get_user_by_email('example@example.com') == users[0] - assert repo.get_user_by_email('fred@example.com') is None + return do_transform + def test_getting_a_user_by_email(self, repo: UserRepoImpl, item: User): + assert repo.get_user_by_email('example@example.com') == item + assert repo.get_user_by_email('fred@example.com') is None -def test_authenticating_as_a_user_can_succeed(users: list[User], repo: UserRepo): - assert repo.auth_as_user(UserDTO('example@example.com', SecretBox('hunter1'))) == users[0] + def test_authenticating_as_a_user_can_succeed(self, repo: UserRepoImpl, item: User): + assert repo.auth_as_user(UserDTO('example@example.com', SecretBox('hunter1'))) == item + def test_authenticating_fails_with_bad_password(self, repo: UserRepoImpl, item: User): + assert repo.auth_as_user(UserDTO('example@example.com', SecretBox('something-wrong'))) is None -def test_authenticating_fails_with_bad_password(users: list[User], repo: UserRepo): - assert repo.auth_as_user(UserDTO('example@example.com', SecretBox('something-wrong'))) is None - - -def test_authenticating_fails_for_nonexistent_user(users: list[User], repo: UserRepo): - assert repo.auth_as_user(UserDTO('rando@example.com', SecretBox('test123'))) is None + def test_authenticating_fails_for_nonexistent_user(self, repo: UserRepoImpl): + assert repo.auth_as_user(UserDTO('rando@example.com', SecretBox('test123'))) is None