From 7e2b0590547ebb91de9531d35b9bf6fbfdaa5ae9 Mon Sep 17 00:00:00 2001
From: dmb225 <daomalick225@gmail.com>
Date: Fri, 4 Oct 2024 17:59:23 +0200
Subject: [PATCH] Add database interface and in-memory database infrastructure

---
 pyproject.toml                                |  2 +
 src/application/interfaces/__init__.py        |  0
 src/application/interfaces/database.py        | 32 ++++++++++
 src/infrastructure/__init__.py                |  0
 src/infrastructure/databases/__init__.py      |  0
 src/infrastructure/databases/in_memory.py     | 26 ++++++++
 src/tests/unit/application/test_user.py       | 42 +++++++++++++
 src/tests/unit/infrastrucure/__init__.py      |  0
 .../unit/infrastrucure/databases/__init__.py  |  0
 .../infrastrucure/databases/test_in_memory.py | 63 +++++++++++++++++++
 10 files changed, 165 insertions(+)
 create mode 100644 src/application/interfaces/__init__.py
 create mode 100644 src/application/interfaces/database.py
 create mode 100644 src/infrastructure/__init__.py
 create mode 100644 src/infrastructure/databases/__init__.py
 create mode 100644 src/infrastructure/databases/in_memory.py
 create mode 100644 src/tests/unit/application/test_user.py
 create mode 100644 src/tests/unit/infrastrucure/__init__.py
 create mode 100644 src/tests/unit/infrastrucure/databases/__init__.py
 create mode 100644 src/tests/unit/infrastrucure/databases/test_in_memory.py

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() == []