diff --git a/tests/test_meldingen/__init__.py b/meldingen_core/actions/__init__.py similarity index 100% rename from tests/test_meldingen/__init__.py rename to meldingen_core/actions/__init__.py diff --git a/meldingen_core/actions.py b/meldingen_core/actions/melding.py similarity index 100% rename from meldingen_core/actions.py rename to meldingen_core/actions/melding.py diff --git a/meldingen_core/actions/user.py b/meldingen_core/actions/user.py new file mode 100644 index 0000000..299ab13 --- /dev/null +++ b/meldingen_core/actions/user.py @@ -0,0 +1,52 @@ +from collections.abc import Collection + +from meldingen_core.models import User +from meldingen_core.repositories import BaseUserRepository + + +class UserCreateAction: + """Action that stores a user.""" + + repository: BaseUserRepository + + def __init__(self, repository: BaseUserRepository): + self.repository = repository + + async def __call__(self, user: User) -> None: + await self.repository.add(user) + + +class UserListAction: + """Action that retrieves a list of users.""" + + repository: BaseUserRepository + + def __init__(self, repository: BaseUserRepository): + self.repository = repository + + async def __call__(self, *, limit: int | None = None, offset: int | None = None) -> Collection[User]: + return await self.repository.list(limit=limit, offset=offset) + + +class UserRetrieveAction: + """Action that retrieves a user.""" + + repository: BaseUserRepository + + def __init__(self, repository: BaseUserRepository): + self.repository = repository + + async def __call__(self, pk: int) -> User | None: + return await self.repository.retrieve(pk=pk) + + +class UserDeleteAction: + """Action that deletes a user.""" + + repository: BaseUserRepository + + def __init__(self, repository: BaseUserRepository): + self.repository = repository + + async def __call__(self, pk: int) -> None: + await self.repository.delete(pk=pk) diff --git a/meldingen_core/models.py b/meldingen_core/models.py index 7ce5def..c19abbc 100644 --- a/meldingen_core/models.py +++ b/meldingen_core/models.py @@ -2,3 +2,10 @@ class Melding: """This is the base model for a 'melding'.""" text: str + + +class User: + """This is the base model for a 'user'.""" + + username: str + email: str diff --git a/meldingen_core/repositories.py b/meldingen_core/repositories.py index 87e9564..03cca53 100644 --- a/meldingen_core/repositories.py +++ b/meldingen_core/repositories.py @@ -2,7 +2,7 @@ from collections.abc import Collection from typing import Generic, TypeVar -from meldingen_core.models import Melding +from meldingen_core.models import Melding, User T = TypeVar("T") T_co = TypeVar("T_co", covariant=True) @@ -21,6 +21,13 @@ async def list(self, *, limit: int | None = None, offset: int | None = None) -> async def retrieve(self, pk: int) -> T_co | None: ... + async def delete(self, pk: int) -> None: + raise NotImplemented + class BaseMeldingRepository(BaseRepository[Melding, Melding], metaclass=ABCMeta): """Repository for Melding.""" + + +class BaseUserRepository(BaseRepository[User, User], metaclass=ABCMeta): + """Repository for User.""" diff --git a/tests/test_actions/__init__.py b/tests/test_actions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_meldingen/test_actions.py b/tests/test_actions/test_melding_actions.py similarity index 97% rename from tests/test_meldingen/test_actions.py rename to tests/test_actions/test_melding_actions.py index 8a0ccfd..3a3a484 100644 --- a/tests/test_meldingen/test_actions.py +++ b/tests/test_actions/test_melding_actions.py @@ -1,7 +1,7 @@ import pytest import pytest_asyncio -from meldingen_core.actions import MeldingCreateAction, MeldingListAction, MeldingRetrieveAction +from meldingen_core.actions.melding import MeldingCreateAction, MeldingListAction, MeldingRetrieveAction from meldingen_core.models import Melding from meldingen_core.repositories import BaseMeldingRepository diff --git a/tests/test_actions/test_user_actions.py b/tests/test_actions/test_user_actions.py new file mode 100644 index 0000000..2f9d85f --- /dev/null +++ b/tests/test_actions/test_user_actions.py @@ -0,0 +1,195 @@ +import pytest +import pytest_asyncio + +from meldingen_core.actions.user import UserCreateAction, UserDeleteAction, UserListAction, UserRetrieveAction +from meldingen_core.models import User +from meldingen_core.repositories import BaseUserRepository + +# Fixtures + + +@pytest.fixture +def unpopulated_users_repository() -> BaseUserRepository: + class TestUserRepository(BaseUserRepository): + data: list[User] + + def __init__(self) -> None: + self.data = [] + + async def add(self, user: User) -> None: + self.data.append(user) + + async def list(self, *, limit: int | None = None, offset: int | None = None) -> list[User]: + if limit and offset: + return self.data[offset : offset + limit] + elif limit and not offset: + return self.data[:limit] + elif not limit and offset: + return self.data[offset:] + else: + return self.data + + async def retrieve(self, pk: int) -> User | None: + for _user in self.data: + if _user.username == str(pk): + return _user + return None + + async def find_by_email(self, email: str) -> User | None: + for _user in self.data: + if _user.email == email: + return _user + return None + + async def delete(self, pk: int) -> None: + for _user in self.data[:]: + if _user.username == str(pk): + self.data.remove(_user) + return None + + repository = TestUserRepository() + return repository + + +@pytest_asyncio.fixture +async def populated_users_repository( + unpopulated_users_repository: BaseUserRepository, +) -> BaseUserRepository: + for _pk in range(10): + user = User() + user.username = f"{_pk}" + user.email = f"user-{_pk}@example.com" + await unpopulated_users_repository.add(user) + + return unpopulated_users_repository + + +@pytest.fixture +def users_create_action( + unpopulated_users_repository: BaseUserRepository, +) -> UserCreateAction: + return UserCreateAction(unpopulated_users_repository) + + +@pytest.fixture +def users_list_action( + populated_users_repository: BaseUserRepository, +) -> UserListAction: + return UserListAction(populated_users_repository) + + +@pytest.fixture +def users_retrieve_action( + populated_users_repository: BaseUserRepository, +) -> UserRetrieveAction: + return UserRetrieveAction(populated_users_repository) + + +@pytest.fixture +def users_delete_action( + populated_users_repository: BaseUserRepository, +) -> UserDeleteAction: + return UserDeleteAction(populated_users_repository) + + +# PyTest Classes + + +class TestUserCreateAction: + @pytest.mark.asyncio + async def test_add(self, users_create_action: UserCreateAction) -> None: + assert len(await users_create_action.repository.list()) == 0 + + user = User() + user.username = "1" + + await users_create_action(user) + + assert len(await users_create_action.repository.list()) == 1 + assert await users_create_action.repository.retrieve(pk=1) == user + + +class TestUserListAction: + @pytest.mark.asyncio + async def test_list_all(self, users_list_action: UserListAction) -> None: + users = await users_list_action() + + assert len(users) == 10 + + @pytest.mark.parametrize( + "limit, expected_result", + [(1, 1), (5, 5), (10, 10), (20, 10)], + ) + @pytest.mark.asyncio + async def test_list_limit(self, users_list_action: UserListAction, limit: int, expected_result: int) -> None: + users = await users_list_action(limit=limit) + + assert len(users) == expected_result + + @pytest.mark.parametrize("offset, expected_result", [(1, 9), (5, 5), (10, 0), (20, 0)]) + @pytest.mark.asyncio + async def test_list_offset( + self, + users_list_action: UserListAction, + offset: int, + expected_result: int, + ) -> None: + users = await users_list_action(offset=offset) + + assert len(users) == expected_result + + @pytest.mark.parametrize( + "limit, offset, expected_result", + [(10, 0, 10), (5, 0, 5), (10, 10, 0), (20, 0, 10)], + ) + @pytest.mark.asyncio + async def test_list_limit_offset( + self, + users_list_action: UserListAction, + limit: int, + offset: int, + expected_result: int, + ) -> None: + users = await users_list_action(limit=limit, offset=offset) + + assert len(users) == expected_result + + +class TestUserRetrieveAction: + @pytest.mark.parametrize("pk", [1, 2, 3, 4, 5]) + @pytest.mark.asyncio + async def test_retrieve_existing_users(self, users_retrieve_action: UserRetrieveAction, pk: int) -> None: + user = await users_retrieve_action(pk=pk) + + assert user is not None + assert user.username == str(pk) + + @pytest.mark.parametrize("pk", [101, 102, 103, 104, 105]) + @pytest.mark.asyncio + async def test_retrieve_non_existing_users(self, users_retrieve_action: UserRetrieveAction, pk: int) -> None: + user = await users_retrieve_action(pk=pk) + + assert user is None + + +class TestUserDeleteAction: + @pytest.mark.parametrize("pk", [1, 2, 3, 4, 5]) + @pytest.mark.asyncio + async def test_delete_existing_user(self, users_delete_action: UserDeleteAction, pk: int) -> None: + assert await users_delete_action.repository.retrieve(pk=pk) is not None + assert len(await users_delete_action.repository.list()) == 10 + + await users_delete_action(pk=pk) + + assert await users_delete_action.repository.retrieve(pk=pk) is None + assert len(await users_delete_action.repository.list()) == 9 + + @pytest.mark.parametrize("pk", [101, 102, 103, 104, 105]) + @pytest.mark.asyncio + async def test_delete_non_existing_user(self, users_delete_action: UserDeleteAction, pk: int) -> None: + assert await users_delete_action.repository.retrieve(pk=pk) is None + assert len(await users_delete_action.repository.list()) == 10 + + await users_delete_action(pk=pk) + + assert len(await users_delete_action.repository.list()) == 10