diff --git a/pyproject.toml b/pyproject.toml index d255a63..5328c2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ develop = [ "ruff==0.6.8", "pre-commit==3.8.0", "pytest==8.3.3", + "pytest-asyncio==0.24.0", "coverage==7.6.1", ] @@ -25,6 +26,7 @@ strict = true [tool.pytest.ini_options] addopts = "-v --tb=short" +asyncio_default_fixture_loop_scope = "function" [tool.ruff] line-length = 100 diff --git a/src/application/interfaces/__init__.py b/src/application/interfaces/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/application/interfaces/database.py b/src/application/interfaces/database.py new file mode 100644 index 0000000..7b7e04a --- /dev/null +++ b/src/application/interfaces/database.py @@ -0,0 +1,32 @@ +from abc import ABC, abstractmethod +from typing import Generic, Protocol, TypeVar, Optional +from uuid import UUID + + +class HasID(Protocol): + id: UUID # Enforce that any entity passed to the repository must have an `id` attribute + + +T = TypeVar("T", bound=HasID) + + +class Database(ABC, Generic[T]): + @abstractmethod + async def add(self, entity: T) -> None: + pass + + @abstractmethod + async def get(self, id: UUID) -> Optional[T]: + pass + + @abstractmethod + async def update(self, entity: T) -> None: + pass + + @abstractmethod + async def delete(self, id: UUID) -> None: + pass + + @abstractmethod + async def list_all(self) -> list[T]: + pass diff --git a/src/infrastructure/__init__.py b/src/infrastructure/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/infrastructure/databases/__init__.py b/src/infrastructure/databases/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/infrastructure/databases/in_memory.py b/src/infrastructure/databases/in_memory.py new file mode 100644 index 0000000..40d5d05 --- /dev/null +++ b/src/infrastructure/databases/in_memory.py @@ -0,0 +1,26 @@ +from typing import Optional +from uuid import UUID + +from application.interfaces.database import T, Database + + +class InMemoryDatabase(Database[T]): + def __init__(self) -> None: + self._data: dict[UUID, T] = {} + + async def add(self, entity: T) -> None: + self._data[entity.id] = entity + + async def get(self, id: UUID) -> Optional[T]: + return self._data.get(id) + + async def update(self, entity: T) -> None: + if entity.id in self._data: + self._data[entity.id] = entity + + async def delete(self, id: UUID) -> None: + if id in self._data: + del self._data[id] + + async def list_all(self) -> list[T]: + return list(self._data.values()) diff --git a/src/tests/unit/application/test_user.py b/src/tests/unit/application/test_user.py new file mode 100644 index 0000000..919dc94 --- /dev/null +++ b/src/tests/unit/application/test_user.py @@ -0,0 +1,42 @@ +from uuid import UUID + +from application.entities.user import User + + +def test_user_creation() -> None: + user = User( + name="Alice Smith", email="alice@example.com", password="securepassword", confirmed=False + ) + + assert user.name == "Alice Smith" + assert user.email == "alice@example.com" + assert user.password == "securepassword" + assert user.confirmed is False + assert isinstance(user.id, UUID) + + +def test_user_default_id() -> None: + user1 = User( + name="Alice Smith", email="alice@example.com", password="securepassword", confirmed=False + ) + user2 = User( + name="Bob Brown", email="bob@example.com", password="anothersecurepassword", confirmed=True + ) + + assert user1.id != user2.id + + +def test_user_email_format() -> None: + user = User( + name="Alice Smith", email="alice@example.com", password="securepassword", confirmed=False + ) + + assert "@" in user.email + assert "." in user.email.split("@")[-1] + + +def test_user_confirmation() -> None: + user = User( + name="Alice Smith", email="alice@example.com", password="securepassword", confirmed=True + ) + assert user.confirmed is True diff --git a/src/tests/unit/infrastrucure/__init__.py b/src/tests/unit/infrastrucure/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/unit/infrastrucure/databases/__init__.py b/src/tests/unit/infrastrucure/databases/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/unit/infrastrucure/databases/test_in_memory.py b/src/tests/unit/infrastrucure/databases/test_in_memory.py new file mode 100644 index 0000000..5ae9f8c --- /dev/null +++ b/src/tests/unit/infrastrucure/databases/test_in_memory.py @@ -0,0 +1,63 @@ +import pytest + +from application.entities.user import User +from infrastructure.databases.in_memory import InMemoryDatabase + + +@pytest.fixture +def in_memory_db() -> InMemoryDatabase[User]: + return InMemoryDatabase[User]() + + +@pytest.mark.asyncio +async def test_in_memory_database_operations(in_memory_db: InMemoryDatabase[User]) -> None: + # Test data + user1 = User(name="John Doe", email="john@example.com", password="password123", confirmed=True) + user2 = User( + name="Jane Smith", email="jane@example.com", password="password456", confirmed=False + ) + + # 1. Add user1 + await in_memory_db.add(user1) + fetched_user1 = await in_memory_db.get(user1.id) + assert fetched_user1 is not None + assert fetched_user1.name == user1.name + assert fetched_user1.email == user1.email + + # 2. Update user1's name + user1.name = "John Doe Updated" + await in_memory_db.update(user1) + updated_user1 = await in_memory_db.get(user1.id) + assert updated_user1 + assert updated_user1.name == "John Doe Updated" + + # 3. Add user2 + await in_memory_db.add(user2) + fetched_user2 = await in_memory_db.get(user2.id) + assert fetched_user2 is not None + assert fetched_user2.name == user2.name + + # 4. List all users + all_users = await in_memory_db.list_all() + assert len(all_users) == 2 + assert user1 in all_users + assert user2 in all_users + + # 5. Delete user1 + await in_memory_db.delete(user1.id) + deleted_user1 = await in_memory_db.get(user1.id) + assert deleted_user1 is None + + # 6. Check remaining users + remaining_users = await in_memory_db.list_all() + assert len(remaining_users) == 1 + assert user2 in remaining_users + assert user1 not in remaining_users + + # 7. Delete user2 + await in_memory_db.delete(user2.id) + deleted_user2 = await in_memory_db.get(user2.id) + assert deleted_user2 is None + + # 8. Final check for empty database + assert await in_memory_db.list_all() == []