From f44f054f56e3a3bdabb49af1ed7ad61977b7fc0d Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Thu, 3 Sep 2026 22:56:14 +0900 Subject: [PATCH 1/4] Implement equality for SecretBox --- src/utils/secret.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utils/secret.py b/src/utils/secret.py index 1828827..15fdc96 100644 --- a/src/utils/secret.py +++ b/src/utils/secret.py @@ -7,6 +7,12 @@ class SecretBox[T]: def expose_secret(self) -> T: return self._item + def __eq__(self, other): + if not isinstance(other, SecretBox): + return False + + return self._item == other._item + def __str__(self): return '' From 0d270c9cfb3d00dcf8bc20743eadc74717374b55 Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Thu, 3 Sep 2026 22:57:55 +0900 Subject: [PATCH 2/4] Setup exports in the infra __init__.py This helps avoid issues where model classes are not evaluated. If the models are not properly imported then Sqlalchemy can explode during database initialization since the mapped columns have not yet been registered. Later, having the models all imported into __init__.py will help with setting up a test db since it will ensure that the registrations for all models is happening as the classes are evaluated during the imports in the package init file. --- src/infra/__init__.py | 27 +++++++++++++++++++++++++++ src/main.py | 3 +-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/infra/__init__.py b/src/infra/__init__.py index e69de29..8851384 100644 --- a/src/infra/__init__.py +++ b/src/infra/__init__.py @@ -0,0 +1,27 @@ +from .db import Base, CRUD, CRUDRepo, UpdateMissingEntryError, get_database +from .users import UserModel, UserRepoImpl +from .publication import ( + PublicationEntryModel, + PublicationSequenceModel, + PublicationOrderModel, + PublicationModel, + PublicationRepoImpl, +) +from .subscription import SubscriptionModel, SubscriptionRepoImpl + +__all__ = [ + 'Base', + 'CRUD', + 'CRUDRepo', + 'UpdateMissingEntryError', + 'get_database', + 'UserModel', + 'UserRepoImpl', + 'PublicationEntryModel', + 'PublicationSequenceModel', + 'PublicationOrderModel', + 'PublicationModel', + 'PublicationRepoImpl', + 'SubscriptionModel', + 'SubscriptionRepoImpl', +] diff --git a/src/main.py b/src/main.py index aca6e66..f9fb8fd 100644 --- a/src/main.py +++ b/src/main.py @@ -6,8 +6,7 @@ from src.services.email import EmailService, get_email_service from src.services.auth import AuthService from src.services.users.service import UserService from dataclasses import dataclass -from src.infra.db import get_database -from src.infra.users import UserRepoImpl +from src.infra import get_database, UserRepoImpl from waitress import serve from flask import Flask import argparse From 32e3fe04a2f9b1a0d30e20da68d6f6c36cfe5282 Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Thu, 3 Sep 2026 22:58:29 +0900 Subject: [PATCH 3/4] Set up an extremely lightweight set of tests for the user infra implementation --- pytest.ini | 4 ++++ setup.py | 3 +++ shell.nix | 1 + src/infra/db_test.py | 8 ++++++++ src/infra/users_test.py | 31 +++++++++++++++++++++++++++++++ 5 files changed, 47 insertions(+) create mode 100644 pytest.ini create mode 100644 src/infra/db_test.py create mode 100644 src/infra/users_test.py diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..fb33402 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +testpaths = src +python_files = *_test.py +addopts = -ra -q diff --git a/setup.py b/setup.py index d244a90..2b6aaed 100644 --- a/setup.py +++ b/setup.py @@ -6,5 +6,8 @@ setup( packages=find_packages(), include_package_data=True, package_data={'src': ['templates/**/*.html', 'templates/**/*.txt']}, + exclude_package_data={ + '': ['*_test.py'], + }, scripts=['./src/main.py'], ) diff --git a/shell.nix b/shell.nix index 585c993..e5e7b1b 100644 --- a/shell.nix +++ b/shell.nix @@ -20,6 +20,7 @@ mkShell { python313Packages.ruff python313Packages.python-lsp-server python313Packages.jedi-language-server + python313Packages.pytest ty ]; } diff --git a/src/infra/db_test.py b/src/infra/db_test.py new file mode 100644 index 0000000..e362239 --- /dev/null +++ b/src/infra/db_test.py @@ -0,0 +1,8 @@ +from src.infra import Base +from sqlalchemy import Engine, create_engine + + +def mock_db() -> Engine: + engine = create_engine('sqlite:///:memory:', echo=True) + Base.metadata.create_all(engine) + return engine diff --git a/src/infra/users_test.py b/src/infra/users_test.py new file mode 100644 index 0000000..119c135 --- /dev/null +++ b/src/infra/users_test.py @@ -0,0 +1,31 @@ +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 +from src.infra.db_test import mock_db +import pytest + +DB = mock_db() + + +@pytest.fixture +def db(): + return DB + + +@pytest.fixture +def repo(db): + return UserRepoImpl(db) + + +@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), + ] + + +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 From 6c46a1df3a060e3dac1f00245aa299fdc9a12d5e Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Thu, 3 Sep 2026 23:00:07 +0900 Subject: [PATCH 4/4] Add a default False to the user model email_confirmed field to avoid violation errors if the value is not explicitly set --- src/infra/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/infra/users.py b/src/infra/users.py index a01e4bc..cf4170b 100644 --- a/src/infra/users.py +++ b/src/infra/users.py @@ -18,7 +18,7 @@ class UserModel(Base): __tablename__ = 'user' id: Mapped[int] = mapped_column(primary_key=True) email: Mapped[str] = mapped_column(String(254), unique=True) - email_confirmed: Mapped[bool] = mapped_column() + email_confirmed: Mapped[bool] = mapped_column(default=False) password_hash: Mapped[str] = mapped_column(VARCHAR(255)) subscriptions: Mapped[list['SubscriptionModel']] = relationship(back_populates='user')