diff --git a/apps/api/src/database.py b/apps/api/src/database.py index ded0e61..f745078 100644 --- a/apps/api/src/database.py +++ b/apps/api/src/database.py @@ -5,10 +5,18 @@ from src.config import Settings, build_database_url settings = Settings() +database_url = settings.database_url + +# SQLite requires aiosqlite and different connect args +connect_args = {} +if database_url.startswith("sqlite"): + connect_args = {"check_same_thread": False} + engine = create_async_engine( - settings.database_url, + database_url, future=True, poolclass=NullPool, + connect_args=connect_args, ) SessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) diff --git a/apps/api/src/models/base.py b/apps/api/src/models/base.py index fbf5edf..c913022 100644 --- a/apps/api/src/models/base.py +++ b/apps/api/src/models/base.py @@ -1,8 +1,7 @@ import uuid from datetime import datetime -from sqlalchemy import DateTime, func -from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy import DateTime, Uuid, func from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column @@ -11,7 +10,7 @@ class Base(DeclarativeBase): class UUIDPrimaryKeyMixin: - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4) class TimestampMixin: diff --git a/apps/api/src/models/git_repository.py b/apps/api/src/models/git_repository.py index d6876ca..5e8aa74 100644 --- a/apps/api/src/models/git_repository.py +++ b/apps/api/src/models/git_repository.py @@ -3,7 +3,7 @@ from datetime import datetime from typing import TYPE_CHECKING from sqlalchemy import Boolean, DateTime, ForeignKey, String -from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy import Uuid as UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from src.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin @@ -18,8 +18,8 @@ class GitRepository(UUIDPrimaryKeyMixin, TimestampMixin, Base): name: Mapped[str] = mapped_column(String(255)) path: Mapped[str] = mapped_column(String(1024)) - project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False) - owner_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + project_id: Mapped[uuid.UUID] = mapped_column(UUID(), ForeignKey("projects.id"), nullable=False) + owner_id: Mapped[uuid.UUID] = mapped_column(UUID(), ForeignKey("users.id"), nullable=False) is_mirror: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) remote_url: Mapped[str | None] = mapped_column(String(1024), nullable=True) last_push: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) diff --git a/apps/api/src/models/project.py b/apps/api/src/models/project.py index 977c5f7..3e970f6 100644 --- a/apps/api/src/models/project.py +++ b/apps/api/src/models/project.py @@ -2,7 +2,7 @@ import uuid from typing import TYPE_CHECKING from sqlalchemy import ForeignKey, String, Text -from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy import Uuid as UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from src.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin @@ -18,9 +18,9 @@ class Project(UUIDPrimaryKeyMixin, TimestampMixin, Base): name: Mapped[str] = mapped_column(String(255)) description: Mapped[str | None] = mapped_column(Text, nullable=True) - owner_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + owner_id: Mapped[uuid.UUID] = mapped_column(UUID(), ForeignKey("users.id"), nullable=False) default_ssh_key_id: Mapped[uuid.UUID | None] = mapped_column( - UUID(as_uuid=True), + UUID(), ForeignKey("ssh_keys.id"), nullable=True, ) diff --git a/apps/api/src/models/ssh_key.py b/apps/api/src/models/ssh_key.py index ce499ee..fb39178 100644 --- a/apps/api/src/models/ssh_key.py +++ b/apps/api/src/models/ssh_key.py @@ -2,7 +2,7 @@ import uuid from typing import TYPE_CHECKING from sqlalchemy import ForeignKey, String, Text -from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy import Uuid as UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from src.models.base import Base, UUIDPrimaryKeyMixin @@ -18,8 +18,8 @@ class SSHKey(UUIDPrimaryKeyMixin, Base): name: Mapped[str] = mapped_column(String(255)) public_key: Mapped[str] = mapped_column(Text) private_key_encrypted: Mapped[str] = mapped_column(Text) - user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) - project_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=True) + user_id: Mapped[uuid.UUID] = mapped_column(UUID(), ForeignKey("users.id"), nullable=False) + project_id: Mapped[uuid.UUID | None] = mapped_column(UUID(), ForeignKey("projects.id"), nullable=True) user: Mapped["User"] = relationship(back_populates="ssh_keys") project: Mapped["Project | None"] = relationship(back_populates="ssh_keys", foreign_keys=[project_id]) diff --git a/apps/api/src/models/user_config.py b/apps/api/src/models/user_config.py index f7c46a5..169de24 100644 --- a/apps/api/src/models/user_config.py +++ b/apps/api/src/models/user_config.py @@ -2,7 +2,7 @@ import uuid from typing import TYPE_CHECKING from sqlalchemy import ForeignKey -from sqlalchemy.dialects.postgresql import JSONB, UUID +from sqlalchemy import JSON, Uuid as UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from src.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin @@ -14,7 +14,7 @@ if TYPE_CHECKING: class UserConfig(UUIDPrimaryKeyMixin, TimestampMixin, Base): __tablename__ = "user_configs" - user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, unique=True) - config: Mapped[dict[str, object]] = mapped_column(JSONB, default=dict, nullable=False) + user_id: Mapped[uuid.UUID] = mapped_column(UUID(), ForeignKey("users.id"), nullable=False, unique=True) + config: Mapped[dict[str, object]] = mapped_column(JSON, default=dict, nullable=False) user: Mapped["User"] = relationship(back_populates="user_config") diff --git a/apps/api/tests/integration/test_auth_api.py b/apps/api/tests/integration/test_auth_api.py index eaa8402..e9c6701 100644 --- a/apps/api/tests/integration/test_auth_api.py +++ b/apps/api/tests/integration/test_auth_api.py @@ -14,18 +14,6 @@ from src.models import Base from src.models.user import User -@pytest.fixture(autouse=True) -def configure_local_database(monkeypatch) -> None: - local_url = build_database_url( - user="headquarter", - password="headquarter", - host="localhost", - port=5432, - database="headquarter", - ) - monkeypatch.setenv("DATABASE_URL", local_url) - - def _prepare_auth_test_db() -> None: async def _run() -> None: engine = create_async_engine( @@ -87,8 +75,7 @@ def _insert_user_for_refresh(user_id: str) -> None: asyncio.run(_run()) -n@pytest.mark.integration - +@pytest.mark.integration def test_login_redirects_to_authentik_authorize_endpoint() -> None: _prepare_auth_test_db() app = _load_app() @@ -100,8 +87,7 @@ def test_login_redirects_to_authentik_authorize_endpoint() -> None: assert "response_type=code" in response.headers["location"] -n@pytest.mark.integration - +@pytest.mark.integration def test_me_returns_401_without_access_cookie() -> None: _prepare_auth_test_db() app = _load_app() @@ -112,8 +98,7 @@ def test_me_returns_401_without_access_cookie() -> None: assert response.status_code == 401 -n@pytest.mark.integration - +@pytest.mark.integration def test_me_returns_user_payload_with_valid_access_cookie() -> None: _prepare_auth_test_db() app = _load_app() @@ -135,8 +120,7 @@ def test_me_returns_user_payload_with_valid_access_cookie() -> None: assert response.json()["email"] == "dev@headquarter.local" -n@pytest.mark.integration - +@pytest.mark.integration def test_logout_clears_auth_cookies() -> None: _prepare_auth_test_db() app = _load_app() @@ -149,8 +133,7 @@ def test_logout_clears_auth_cookies() -> None: assert "access_token=" in response.headers.get("set-cookie", "") -n@pytest.mark.integration - +@pytest.mark.integration def test_callback_rejects_mismatched_state() -> None: _prepare_auth_test_db() app = _load_app() @@ -162,8 +145,7 @@ def test_callback_rejects_mismatched_state() -> None: assert response.status_code == 401 -n@pytest.mark.integration - +@pytest.mark.integration def test_callback_sets_auth_cookies_after_success(monkeypatch) -> None: _prepare_auth_test_db() app = _load_app() @@ -192,8 +174,7 @@ def test_callback_sets_auth_cookies_after_success(monkeypatch) -> None: assert "refresh_token=" in set_cookie_header -n@pytest.mark.integration - +@pytest.mark.integration def test_refresh_rotates_cookie_and_returns_user_payload(monkeypatch) -> None: _prepare_auth_test_db() _insert_user_for_refresh("7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb") @@ -216,8 +197,7 @@ def test_refresh_rotates_cookie_and_returns_user_payload(monkeypatch) -> None: assert "refresh_token=" in response.headers.get("set-cookie", "") -n@pytest.mark.integration - +@pytest.mark.integration def test_refresh_returns_401_for_invalid_refresh_token(monkeypatch) -> None: _prepare_auth_test_db() app = _load_app() diff --git a/apps/api/tests/integration/test_auth_services.py b/apps/api/tests/integration/test_auth_services.py index b783ff8..dfef016 100644 --- a/apps/api/tests/integration/test_auth_services.py +++ b/apps/api/tests/integration/test_auth_services.py @@ -3,21 +3,17 @@ import base64 import httpx import pytest -import pytest_asyncio -from sqlalchemy import text -from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy.ext.asyncio import AsyncSession from src.auth.cookies import build_cookie_options from src.auth.jwt_service import decode_access_token, mint_access_token from src.auth.oidc import build_login_redirect_url, exchange_code_for_tokens, verify_provider_access_token from src.auth.refresh_store import create_refresh_token, hash_refresh_token, revoke_refresh_token, rotate_refresh_token -from src.config import Settings, build_database_url -from src.models import Base +from src.config import Settings from src.models.user import User -n@pytest.mark.integration - +@pytest.mark.integration def test_cookie_options_follow_environment_defaults(monkeypatch) -> None: monkeypatch.setenv("APP_ENV", "development") dev_settings = Settings() @@ -34,8 +30,7 @@ def test_cookie_options_follow_environment_defaults(monkeypatch) -> None: assert prod_options["samesite"] == "strict" -n@pytest.mark.integration - +@pytest.mark.integration def test_login_redirect_url_contains_required_oidc_params() -> None: settings = Settings() @@ -53,8 +48,7 @@ def test_login_redirect_url_contains_required_oidc_params() -> None: assert "nonce=nonce-123" in url -n@pytest.mark.integration - +@pytest.mark.integration def test_mint_and_decode_internal_access_token_round_trip() -> None: settings = Settings() expires_at = datetime.now(UTC) + timedelta(minutes=15) @@ -75,8 +69,7 @@ def test_mint_and_decode_internal_access_token_round_trip() -> None: assert "exp" in claims -n@pytest.mark.integration - +@pytest.mark.integration def test_refresh_token_hash_is_deterministic_and_non_reversible() -> None: raw_token = "refresh-token-abc" @@ -88,8 +81,7 @@ def test_refresh_token_hash_is_deterministic_and_non_reversible() -> None: assert len(first_hash) == 64 -n@pytest.mark.integration - +@pytest.mark.integration def test_decode_access_token_rejects_invalid_signature() -> None: settings = Settings() other_settings = Settings(jwt_secret="different-secret") @@ -108,8 +100,7 @@ def test_decode_access_token_rejects_invalid_signature() -> None: @pytest.mark.asyncio -n@pytest.mark.integration - +@pytest.mark.integration async def test_exchange_code_for_tokens_posts_expected_payload() -> None: settings = Settings() @@ -133,8 +124,7 @@ async def test_exchange_code_for_tokens_posts_expected_payload() -> None: assert token_payload["access_token"] == "provider-token" -n@pytest.mark.integration - +@pytest.mark.integration def test_verify_provider_access_token_with_jwks_oct_key() -> None: settings = Settings(authentik_audience="headquarter-web", authentik_issuer="https://authentik.local/") shared_secret = b"shared-secret-123" @@ -168,35 +158,8 @@ def test_verify_provider_access_token_with_jwks_oct_key() -> None: assert claims["sub"] == "authentik-user" -TEST_DATABASE_URL = build_database_url( - user="headquarter", - password="headquarter", - host="localhost", - port=5432, - database="headquarter", -) - - -@pytest_asyncio.fixture -async def db_session() -> AsyncSession: - engine = create_async_engine(TEST_DATABASE_URL) - session_factory = async_sessionmaker(engine, expire_on_commit=False) - - async with engine.begin() as connection: - await connection.run_sync(Base.metadata.create_all) - - async with session_factory() as session: - await session.execute(text("TRUNCATE TABLE refresh_tokens, users RESTART IDENTITY CASCADE")) - await session.commit() - yield session - await session.rollback() - - await engine.dispose() - - @pytest.mark.asyncio -n@pytest.mark.integration - +@pytest.mark.integration async def test_refresh_store_create_rotate_and_revoke(db_session: AsyncSession) -> None: user = User(email="dev-auth@headquarter.local", name="Dev Auth", authentik_id="auth-dev", avatar_url=None) db_session.add(user) diff --git a/apps/api/tests/integration/test_models.py b/apps/api/tests/integration/test_models.py index 14fd97b..02bd377 100644 --- a/apps/api/tests/integration/test_models.py +++ b/apps/api/tests/integration/test_models.py @@ -1,11 +1,6 @@ -from collections.abc import AsyncIterator - import pytest -import pytest_asyncio -from sqlalchemy import text -from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy.ext.asyncio import AsyncSession -from src.config import build_database_url from src.models import Base from src.models.base import TimestampMixin, UUIDPrimaryKeyMixin from src.models.git_repository import GitRepository @@ -16,55 +11,12 @@ from src.models.user import User from src.models.user_config import UserConfig -TEST_DATABASE_URL = build_database_url( - user="headquarter", - password="headquarter", - host="localhost", - port=5432, - database="headquarter", -) - - -@pytest_asyncio.fixture -async def db_session() -> AsyncIterator[AsyncSession]: - engine = create_async_engine(TEST_DATABASE_URL) - session_factory = async_sessionmaker(engine, expire_on_commit=False) - - async with session_factory() as session: - table_rows = await session.execute( - text( - "SELECT tablename FROM pg_tables " - "WHERE schemaname = 'public' " - "AND tablename = ANY(:table_names)" - ), - { - "table_names": [ - "refresh_tokens", - "user_configs", - "git_repositories", - "projects", - "ssh_keys", - "users", - ] - }, - ) - existing_tables = [row[0] for row in table_rows] - if existing_tables: - await session.execute(text(f"TRUNCATE TABLE {', '.join(existing_tables)} RESTART IDENTITY CASCADE")) - await session.commit() - yield session - await session.rollback() - - await engine.dispose() - - -n@pytest.mark.integration - +@pytest.mark.integration def test_base_metadata_collects_declared_tables() -> None: assert isinstance(Base.metadata.tables, dict) -n@pytest.mark.integration +@pytest.mark.integration def test_shared_mixins_define_expected_columns() -> None: assert "id" in UUIDPrimaryKeyMixin.__dict__ @@ -72,7 +24,7 @@ def test_shared_mixins_define_expected_columns() -> None: assert "updated_at" in TimestampMixin.__dict__ -n@pytest.mark.integration +@pytest.mark.integration def test_expected_tables_are_registered() -> None: assert set(Base.metadata.tables) == { @@ -85,7 +37,7 @@ def test_expected_tables_are_registered() -> None: } -n@pytest.mark.integration +@pytest.mark.integration def test_user_table_has_required_columns() -> None: columns = User.__table__.columns @@ -104,7 +56,7 @@ def test_user_table_has_required_columns() -> None: assert columns["avatar_url"].nullable is True -n@pytest.mark.integration +@pytest.mark.integration def test_project_relationships_point_to_owner_and_default_ssh_key() -> None: owner_fk = next(iter(Project.__table__.c.owner_id.foreign_keys)) @@ -116,7 +68,7 @@ def test_project_relationships_point_to_owner_and_default_ssh_key() -> None: assert Project.default_ssh_key.property.mapper.class_ is SSHKey -n@pytest.mark.integration +@pytest.mark.integration def test_repository_and_user_config_relationships_are_registered() -> None: project_fk = next(iter(GitRepository.__table__.c.project_id.foreign_keys)) @@ -131,7 +83,7 @@ def test_repository_and_user_config_relationships_are_registered() -> None: assert UserConfig.user.property.mapper.class_ is User -n@pytest.mark.integration +@pytest.mark.integration def test_refresh_token_table_has_required_columns_and_relationships() -> None: columns = RefreshToken.__table__.columns @@ -154,7 +106,7 @@ def test_refresh_token_table_has_required_columns_and_relationships() -> None: @pytest.mark.asyncio -n@pytest.mark.integration +@pytest.mark.integration async def test_async_session_can_insert_and_load_user(db_session: AsyncSession) -> None: user = User(email="dev@headquarter.local", name="Dev User", authentik_id="dev-user", avatar_url=None) diff --git a/apps/api/tests/integration/test_projects_api.py b/apps/api/tests/integration/test_projects_api.py index c5ce934..1a884e7 100644 --- a/apps/api/tests/integration/test_projects_api.py +++ b/apps/api/tests/integration/test_projects_api.py @@ -2,7 +2,6 @@ import uuid from datetime import UTC, datetime, timedelta import asyncio -from fastapi.testclient import TestClient import pytest from sqlalchemy import text from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker @@ -14,18 +13,6 @@ from src.models.project import Project from src.models.user import User -@pytest.fixture(autouse=True) -def configure_local_database(monkeypatch) -> None: - local_url = build_database_url( - user="headquarter", - password="headquarter", - host="localhost", - port=5432, - database="headquarter", - ) - monkeypatch.setenv("DATABASE_URL", local_url) - - def _prepare_test_db() -> None: async def _run() -> None: engine = create_async_engine( @@ -132,8 +119,7 @@ def _insert_project(project_id: str, owner_id: str, name: str = "Test Project") asyncio.run(_run()) -n@pytest.mark.integration - +@pytest.mark.integration def test_create_project_requires_authentication() -> None: _prepare_test_db() app = _load_app() @@ -144,8 +130,7 @@ def test_create_project_requires_authentication() -> None: assert response.status_code == 401 -n@pytest.mark.integration - +@pytest.mark.integration def test_create_project_successfully() -> None: _prepare_test_db() user_id = "11111111-1111-1111-1111-111111111111" @@ -165,8 +150,7 @@ def test_create_project_successfully() -> None: assert "id" in data -n@pytest.mark.integration - +@pytest.mark.integration def test_list_projects_returns_only_owned_projects() -> None: _prepare_test_db() user1_id = "11111111-1111-1111-1111-111111111111" @@ -188,8 +172,7 @@ def test_list_projects_returns_only_owned_projects() -> None: assert data[0]["name"] == "User1 Project" -n@pytest.mark.integration - +@pytest.mark.integration def test_update_project_requires_ownership() -> None: _prepare_test_db() owner_id = "11111111-1111-1111-1111-111111111111" @@ -208,8 +191,7 @@ def test_update_project_requires_ownership() -> None: assert response.status_code == 403 -n@pytest.mark.integration - +@pytest.mark.integration def test_update_project_successfully() -> None: _prepare_test_db() owner_id = "11111111-1111-1111-1111-111111111111" @@ -228,8 +210,7 @@ def test_update_project_successfully() -> None: assert data["name"] == "Updated Name" -n@pytest.mark.integration - +@pytest.mark.integration def test_delete_project_requires_ownership() -> None: _prepare_test_db() owner_id = "11111111-1111-1111-1111-111111111111" @@ -248,8 +229,7 @@ def test_delete_project_requires_ownership() -> None: assert response.status_code == 403 -n@pytest.mark.integration - +@pytest.mark.integration def test_delete_project_successfully() -> None: _prepare_test_db() owner_id = "11111111-1111-1111-1111-111111111111" @@ -266,8 +246,7 @@ def test_delete_project_successfully() -> None: assert response.status_code == 204 -n@pytest.mark.integration - +@pytest.mark.integration def test_set_default_ssh_key_requires_ownership() -> None: _prepare_test_db() owner_id = "11111111-1111-1111-1111-111111111111" diff --git a/apps/api/tests/integration/test_seed.py b/apps/api/tests/integration/test_seed.py index a21eedf..46366f7 100644 --- a/apps/api/tests/integration/test_seed.py +++ b/apps/api/tests/integration/test_seed.py @@ -1,41 +1,11 @@ -from collections.abc import AsyncIterator - import pytest -import pytest_asyncio -from sqlalchemy import select, text -from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession -from src.config import build_database_url from src.models.user import User from src.scripts.seed import build_seed_user, seed_database - -TEST_DATABASE_URL = build_database_url( - user="headquarter", - password="headquarter", - host="localhost", - port=5432, - database="headquarter", -) - - -@pytest_asyncio.fixture -async def db_session() -> AsyncIterator[AsyncSession]: - engine = create_async_engine(TEST_DATABASE_URL) - session_factory = async_sessionmaker(engine, expire_on_commit=False) - - async with session_factory() as session: - await session.execute(text("TRUNCATE TABLE git_repositories, projects, users RESTART IDENTITY CASCADE")) - await session.commit() - yield session - await session.execute(text("TRUNCATE TABLE git_repositories, projects, users RESTART IDENTITY CASCADE")) - await session.commit() - - await engine.dispose() - - -n@pytest.mark.integration - +@pytest.mark.integration def test_build_seed_user_returns_deterministic_payload() -> None: payload = build_seed_user() @@ -48,8 +18,7 @@ def test_build_seed_user_returns_deterministic_payload() -> None: @pytest.mark.asyncio -n@pytest.mark.integration - +@pytest.mark.integration async def test_seed_database_creates_development_user(db_session: AsyncSession) -> None: await seed_database(db_session) diff --git a/apps/api/tests/integration/test_ssh_keys_api.py b/apps/api/tests/integration/test_ssh_keys_api.py index 781acf0..b4ce820 100644 --- a/apps/api/tests/integration/test_ssh_keys_api.py +++ b/apps/api/tests/integration/test_ssh_keys_api.py @@ -11,16 +11,14 @@ async def async_client(): @pytest.mark.asyncio -n@pytest.mark.integration - +@pytest.mark.integration async def test_create_ssh_key_requires_authentication(async_client: AsyncClient) -> None: response = await async_client.post("/ssh-keys", json={"name": "test-key"}) assert response.status_code == 401 @pytest.mark.asyncio -n@pytest.mark.integration - +@pytest.mark.integration async def test_list_ssh_keys_requires_authentication(async_client: AsyncClient) -> None: response = await async_client.get("/ssh-keys") assert response.status_code == 401 diff --git a/apps/api/tests/integration/test_users_api.py b/apps/api/tests/integration/test_users_api.py index 7ec2350..2244aa2 100644 --- a/apps/api/tests/integration/test_users_api.py +++ b/apps/api/tests/integration/test_users_api.py @@ -14,18 +14,6 @@ from src.models import Base from src.models.user import User -@pytest.fixture(autouse=True) -def configure_local_database(monkeypatch) -> None: - local_url = build_database_url( - user="headquarter", - password="headquarter", - host="localhost", - port=5432, - database="headquarter", - ) - monkeypatch.setenv("DATABASE_URL", local_url) - - def _prepare_users_test_db() -> None: async def _run() -> None: engine = create_async_engine( @@ -99,8 +87,7 @@ def _create_auth_cookie(user_id: str) -> str: ) -n@pytest.mark.integration - +@pytest.mark.integration def test_get_profile_returns_401_without_cookie() -> None: _prepare_users_test_db() app = _load_app() @@ -111,8 +98,7 @@ def test_get_profile_returns_401_without_cookie() -> None: assert response.status_code == 401 -n@pytest.mark.integration - +@pytest.mark.integration def test_get_profile_returns_user_data() -> None: _prepare_users_test_db() user_id = "7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb" @@ -130,8 +116,7 @@ def test_get_profile_returns_user_data() -> None: assert data["avatar_url"] is None -n@pytest.mark.integration - +@pytest.mark.integration def test_update_profile_changes_name_and_email() -> None: _prepare_users_test_db() user_id = "7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb" @@ -148,8 +133,7 @@ def test_update_profile_changes_name_and_email() -> None: assert data["email"] == "updated@headquarter.local" -n@pytest.mark.integration - +@pytest.mark.integration def test_update_profile_rejects_empty_name() -> None: _prepare_users_test_db() user_id = "7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb" @@ -163,8 +147,7 @@ def test_update_profile_rejects_empty_name() -> None: assert response.status_code == 400 -n@pytest.mark.integration - +@pytest.mark.integration def test_update_profile_rejects_invalid_email() -> None: _prepare_users_test_db() user_id = "7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb" @@ -178,8 +161,7 @@ def test_update_profile_rejects_invalid_email() -> None: assert response.status_code == 400 -n@pytest.mark.integration - +@pytest.mark.integration def test_upload_avatar_updates_avatar_url() -> None: _prepare_users_test_db() user_id = "7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb" @@ -202,8 +184,7 @@ def test_upload_avatar_updates_avatar_url() -> None: assert data["avatar_url"].startswith("/uploads/avatars/") -n@pytest.mark.integration - +@pytest.mark.integration def test_upload_avatar_rejects_invalid_file_type() -> None: _prepare_users_test_db() user_id = "7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb" @@ -221,8 +202,7 @@ def test_upload_avatar_rejects_invalid_file_type() -> None: assert response.status_code == 400 -n@pytest.mark.integration - +@pytest.mark.integration def test_upload_avatar_rejects_oversized_file() -> None: _prepare_users_test_db() user_id = "7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb" diff --git a/apps/api/tests/unit/test_config.py b/apps/api/tests/unit/test_config.py index 90dff32..bd1f30f 100644 --- a/apps/api/tests/unit/test_config.py +++ b/apps/api/tests/unit/test_config.py @@ -4,7 +4,7 @@ from src.config import Settings from src.database import build_database_url -n@pytest.mark.unit +@pytest.mark.unit def test_settings_default_database_url_uses_asyncpg() -> None: settings = Settings() @@ -12,7 +12,7 @@ def test_settings_default_database_url_uses_asyncpg() -> None: assert settings.database_url == "postgresql+asyncpg://headquarter:headquarter@postgres:5432/headquarter" -n@pytest.mark.unit +@pytest.mark.unit def test_build_database_url_uses_explicit_values() -> None: url = build_database_url( @@ -26,7 +26,7 @@ def test_build_database_url_uses_explicit_values() -> None: assert url == "postgresql+asyncpg://user:pass@db:5433/app" -n@pytest.mark.unit +@pytest.mark.unit def test_settings_prefers_explicit_database_url_env(monkeypatch) -> None: monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://local:local@localhost:5432/localdb") @@ -36,7 +36,7 @@ def test_settings_prefers_explicit_database_url_env(monkeypatch) -> None: assert settings.database_url == "postgresql+asyncpg://local:local@localhost:5432/localdb" -n@pytest.mark.unit +@pytest.mark.unit def test_auth_settings_have_secure_defaults() -> None: settings = Settings() @@ -51,7 +51,7 @@ def test_auth_settings_have_secure_defaults() -> None: assert settings.refresh_token_ttl_days == 7 -n@pytest.mark.unit +@pytest.mark.unit def test_cookie_policy_is_strict_in_production(monkeypatch) -> None: monkeypatch.setenv("APP_ENV", "production") @@ -62,7 +62,7 @@ def test_cookie_policy_is_strict_in_production(monkeypatch) -> None: assert settings.cookie_samesite == "strict" -n@pytest.mark.unit +@pytest.mark.unit def test_cookie_policy_is_relaxed_for_local_dev(monkeypatch) -> None: monkeypatch.setenv("APP_ENV", "development") diff --git a/apps/api/tests/unit/test_migration_metadata.py b/apps/api/tests/unit/test_migration_metadata.py index d7d36bd..835e80e 100644 --- a/apps/api/tests/unit/test_migration_metadata.py +++ b/apps/api/tests/unit/test_migration_metadata.py @@ -4,7 +4,7 @@ from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path -n@pytest.mark.unit +@pytest.mark.unit def test_initial_migration_defines_all_core_tables() -> None: migration_path = Path(__file__).resolve().parents[1] / "alembic" / "versions" / "0001_initial_schema.py" @@ -25,7 +25,7 @@ def test_initial_migration_defines_all_core_tables() -> None: ] -n@pytest.mark.unit +@pytest.mark.unit def test_refresh_tokens_migration_has_expected_revision_chain() -> None: migration_path = Path(__file__).resolve().parents[1] / "alembic" / "versions" / "0002_refresh_tokens.py"