belgie-alchemy 0.1.0a4__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- belgie_alchemy/__init__.py +0 -26
- {belgie_alchemy-0.1.0a4.dist-info → belgie_alchemy-0.2.0.dist-info}/METADATA +43 -33
- belgie_alchemy-0.2.0.dist-info/RECORD +7 -0
- {belgie_alchemy-0.1.0a4.dist-info → belgie_alchemy-0.2.0.dist-info}/WHEEL +1 -1
- belgie_alchemy/__tests__/__init__.py +0 -0
- belgie_alchemy/__tests__/adapter/__init__.py +0 -0
- belgie_alchemy/__tests__/adapter/test_adapter.py +0 -493
- belgie_alchemy/__tests__/auth_models/__init__.py +0 -0
- belgie_alchemy/__tests__/auth_models/test_auth_models.py +0 -91
- belgie_alchemy/__tests__/base/__init__.py +0 -0
- belgie_alchemy/__tests__/base/test_base.py +0 -90
- belgie_alchemy/__tests__/conftest.py +0 -39
- belgie_alchemy/__tests__/fixtures/__init__.py +0 -12
- belgie_alchemy/__tests__/fixtures/database.py +0 -38
- belgie_alchemy/__tests__/fixtures/models.py +0 -119
- belgie_alchemy/__tests__/mixins/__init__.py +0 -0
- belgie_alchemy/__tests__/mixins/test_mixins.py +0 -80
- belgie_alchemy/__tests__/settings/__init__.py +0 -0
- belgie_alchemy/__tests__/settings/test_settings.py +0 -342
- belgie_alchemy/__tests__/settings/test_settings_integration.py +0 -416
- belgie_alchemy/__tests__/types/__init__.py +0 -0
- belgie_alchemy/__tests__/types/test_types.py +0 -155
- belgie_alchemy/base.py +0 -25
- belgie_alchemy/mixins.py +0 -83
- belgie_alchemy/types.py +0 -32
- belgie_alchemy-0.1.0a4.dist-info/RECORD +0 -28
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"""Pytest fixtures for alchemy tests."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
6
|
-
|
|
7
|
-
import pytest_asyncio
|
|
8
|
-
|
|
9
|
-
from belgie_alchemy.__tests__.fixtures.database import get_test_engine, get_test_session_factory
|
|
10
|
-
|
|
11
|
-
if TYPE_CHECKING:
|
|
12
|
-
from collections.abc import AsyncGenerator
|
|
13
|
-
|
|
14
|
-
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@pytest_asyncio.fixture
|
|
18
|
-
async def alchemy_engine() -> AsyncGenerator[AsyncEngine, None]:
|
|
19
|
-
"""Create an isolated in-memory SQLite engine for testing.
|
|
20
|
-
|
|
21
|
-
Each test gets its own in-memory database, so Base.metadata.create_all
|
|
22
|
-
is safe even when tests run in parallel - there's no shared state.
|
|
23
|
-
"""
|
|
24
|
-
engine = await get_test_engine()
|
|
25
|
-
yield engine
|
|
26
|
-
await engine.dispose()
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@pytest_asyncio.fixture
|
|
30
|
-
async def alchemy_session_factory(alchemy_engine: AsyncEngine) -> async_sessionmaker[AsyncSession]:
|
|
31
|
-
return await get_test_session_factory(alchemy_engine)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@pytest_asyncio.fixture
|
|
35
|
-
async def alchemy_session(
|
|
36
|
-
alchemy_session_factory: async_sessionmaker[AsyncSession],
|
|
37
|
-
) -> AsyncGenerator[AsyncSession, None]:
|
|
38
|
-
async with alchemy_session_factory() as session:
|
|
39
|
-
yield session
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
from belgie_alchemy.__tests__.fixtures.database import get_test_db, get_test_engine, get_test_session_factory
|
|
2
|
-
from belgie_alchemy.__tests__.fixtures.models import Account, OAuthState, Session, User
|
|
3
|
-
|
|
4
|
-
__all__ = [
|
|
5
|
-
"Account",
|
|
6
|
-
"OAuthState",
|
|
7
|
-
"Session",
|
|
8
|
-
"User",
|
|
9
|
-
"get_test_db",
|
|
10
|
-
"get_test_engine",
|
|
11
|
-
"get_test_session_factory",
|
|
12
|
-
]
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
from collections.abc import AsyncGenerator
|
|
2
|
-
|
|
3
|
-
from sqlalchemy import event
|
|
4
|
-
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
|
|
5
|
-
|
|
6
|
-
from belgie_alchemy import Base
|
|
7
|
-
|
|
8
|
-
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
async def get_test_engine() -> AsyncEngine:
|
|
12
|
-
engine = create_async_engine(
|
|
13
|
-
TEST_DATABASE_URL,
|
|
14
|
-
echo=False,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
@event.listens_for(engine.sync_engine, "connect")
|
|
18
|
-
def _enable_foreign_keys(dbapi_conn, _connection_record) -> None:
|
|
19
|
-
cursor = dbapi_conn.cursor()
|
|
20
|
-
cursor.execute("PRAGMA foreign_keys=ON")
|
|
21
|
-
cursor.close()
|
|
22
|
-
|
|
23
|
-
async with engine.begin() as conn:
|
|
24
|
-
await conn.run_sync(Base.metadata.create_all)
|
|
25
|
-
return engine
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
async def get_test_session_factory(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]:
|
|
29
|
-
return async_sessionmaker(
|
|
30
|
-
engine,
|
|
31
|
-
class_=AsyncSession,
|
|
32
|
-
expire_on_commit=False,
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
async def get_test_db(session_factory: async_sessionmaker[AsyncSession]) -> AsyncGenerator[AsyncSession, None]:
|
|
37
|
-
async with session_factory() as session:
|
|
38
|
-
yield session
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
"""Test models for alchemy tests.
|
|
2
|
-
|
|
3
|
-
Defines concrete auth models for testing. These mirror the examples in
|
|
4
|
-
examples/alchemy/auth_models.py and demonstrate how users would define
|
|
5
|
-
their own models.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
from datetime import datetime # noqa: TC003
|
|
11
|
-
from uuid import UUID # noqa: TC003
|
|
12
|
-
|
|
13
|
-
from sqlalchemy import JSON, ForeignKey, Text, UniqueConstraint
|
|
14
|
-
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
15
|
-
|
|
16
|
-
from belgie_alchemy import Base, DateTimeUTC, PrimaryKeyMixin, TimestampMixin
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class User(Base, PrimaryKeyMixin, TimestampMixin):
|
|
20
|
-
"""Test User model."""
|
|
21
|
-
|
|
22
|
-
__tablename__ = "users"
|
|
23
|
-
|
|
24
|
-
email: Mapped[str] = mapped_column(unique=True, index=True)
|
|
25
|
-
email_verified: Mapped[bool] = mapped_column(default=False)
|
|
26
|
-
name: Mapped[str | None] = mapped_column(default=None)
|
|
27
|
-
image: Mapped[str | None] = mapped_column(default=None)
|
|
28
|
-
scopes: Mapped[list[str] | None] = mapped_column(JSON, default=None)
|
|
29
|
-
custom_field: Mapped[str | None] = mapped_column(default=None)
|
|
30
|
-
|
|
31
|
-
accounts: Mapped[list[Account]] = relationship(
|
|
32
|
-
back_populates="user",
|
|
33
|
-
cascade="all, delete-orphan",
|
|
34
|
-
init=False,
|
|
35
|
-
)
|
|
36
|
-
sessions: Mapped[list[Session]] = relationship(
|
|
37
|
-
back_populates="user",
|
|
38
|
-
cascade="all, delete-orphan",
|
|
39
|
-
init=False,
|
|
40
|
-
)
|
|
41
|
-
oauth_states: Mapped[list[OAuthState]] = relationship(
|
|
42
|
-
back_populates="user",
|
|
43
|
-
cascade="all, delete-orphan",
|
|
44
|
-
init=False,
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class Account(Base, PrimaryKeyMixin, TimestampMixin):
|
|
49
|
-
"""Test Account model."""
|
|
50
|
-
|
|
51
|
-
__tablename__ = "accounts"
|
|
52
|
-
|
|
53
|
-
user_id: Mapped[UUID] = mapped_column(
|
|
54
|
-
ForeignKey("users.id", ondelete="cascade", onupdate="cascade"),
|
|
55
|
-
nullable=False,
|
|
56
|
-
)
|
|
57
|
-
provider: Mapped[str] = mapped_column(Text)
|
|
58
|
-
provider_account_id: Mapped[str] = mapped_column(Text)
|
|
59
|
-
access_token: Mapped[str | None] = mapped_column(default=None)
|
|
60
|
-
refresh_token: Mapped[str | None] = mapped_column(default=None)
|
|
61
|
-
expires_at: Mapped[datetime | None] = mapped_column(DateTimeUTC, default=None)
|
|
62
|
-
token_type: Mapped[str | None] = mapped_column(default=None)
|
|
63
|
-
scope: Mapped[str | None] = mapped_column(default=None)
|
|
64
|
-
id_token: Mapped[str | None] = mapped_column(default=None)
|
|
65
|
-
|
|
66
|
-
user: Mapped[User] = relationship(
|
|
67
|
-
back_populates="accounts",
|
|
68
|
-
lazy="selectin",
|
|
69
|
-
init=False,
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
__table_args__ = (
|
|
73
|
-
UniqueConstraint(
|
|
74
|
-
"provider",
|
|
75
|
-
"provider_account_id",
|
|
76
|
-
name="uq_accounts_provider_provider_account_id",
|
|
77
|
-
),
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
class Session(Base, PrimaryKeyMixin, TimestampMixin):
|
|
82
|
-
"""Test Session model."""
|
|
83
|
-
|
|
84
|
-
__tablename__ = "sessions"
|
|
85
|
-
|
|
86
|
-
user_id: Mapped[UUID] = mapped_column(
|
|
87
|
-
ForeignKey("users.id", ondelete="cascade", onupdate="cascade"),
|
|
88
|
-
nullable=False,
|
|
89
|
-
)
|
|
90
|
-
expires_at: Mapped[datetime] = mapped_column(DateTimeUTC)
|
|
91
|
-
ip_address: Mapped[str | None] = mapped_column(default=None)
|
|
92
|
-
user_agent: Mapped[str | None] = mapped_column(default=None)
|
|
93
|
-
|
|
94
|
-
user: Mapped[User] = relationship(
|
|
95
|
-
back_populates="sessions",
|
|
96
|
-
lazy="selectin",
|
|
97
|
-
init=False,
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
class OAuthState(Base, PrimaryKeyMixin, TimestampMixin):
|
|
102
|
-
"""Test OAuthState model."""
|
|
103
|
-
|
|
104
|
-
__tablename__ = "oauth_states"
|
|
105
|
-
|
|
106
|
-
state: Mapped[str] = mapped_column(unique=True, index=True)
|
|
107
|
-
user_id: Mapped[UUID | None] = mapped_column(
|
|
108
|
-
ForeignKey("users.id", ondelete="set null", onupdate="cascade"),
|
|
109
|
-
nullable=True,
|
|
110
|
-
)
|
|
111
|
-
expires_at: Mapped[datetime] = mapped_column(DateTimeUTC)
|
|
112
|
-
code_verifier: Mapped[str | None] = mapped_column(default=None)
|
|
113
|
-
redirect_url: Mapped[str | None] = mapped_column(default=None)
|
|
114
|
-
|
|
115
|
-
user: Mapped[User] | None = relationship(
|
|
116
|
-
back_populates="oauth_states",
|
|
117
|
-
lazy="selectin",
|
|
118
|
-
init=False,
|
|
119
|
-
)
|
|
File without changes
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
from uuid import UUID
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
from sqlalchemy.exc import IntegrityError
|
|
5
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
6
|
-
|
|
7
|
-
from belgie_alchemy.__tests__.fixtures.models import User
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def test_primary_key_mixin_defaults() -> None:
|
|
11
|
-
id_column = User.__table__.c.id # type: ignore[attr-defined]
|
|
12
|
-
assert id_column.primary_key
|
|
13
|
-
assert str(id_column.server_default.arg) == "gen_random_uuid()"
|
|
14
|
-
assert id_column.index
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def test_primary_key_client_side_generation() -> None:
|
|
18
|
-
"""Test that UUID is generated client-side via default_factory."""
|
|
19
|
-
user1 = User(email="user1@example.com")
|
|
20
|
-
user2 = User(email="user2@example.com")
|
|
21
|
-
|
|
22
|
-
# UUIDs should be generated automatically
|
|
23
|
-
assert isinstance(user1.id, UUID)
|
|
24
|
-
assert isinstance(user2.id, UUID)
|
|
25
|
-
|
|
26
|
-
# UUIDs should be unique
|
|
27
|
-
assert user1.id != user2.id
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@pytest.mark.asyncio
|
|
31
|
-
async def test_primary_key_persists_client_generated_uuid(alchemy_session: AsyncSession) -> None:
|
|
32
|
-
"""Test that client-generated UUIDs are persisted correctly."""
|
|
33
|
-
user = User(email="persist@example.com")
|
|
34
|
-
original_id = user.id
|
|
35
|
-
|
|
36
|
-
alchemy_session.add(user)
|
|
37
|
-
await alchemy_session.commit()
|
|
38
|
-
|
|
39
|
-
# Refresh from database
|
|
40
|
-
await alchemy_session.refresh(user)
|
|
41
|
-
|
|
42
|
-
# UUID should be unchanged
|
|
43
|
-
assert user.id == original_id
|
|
44
|
-
assert isinstance(user.id, UUID)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@pytest.mark.asyncio
|
|
48
|
-
async def test_primary_key_unique_constraint(alchemy_session: AsyncSession) -> None:
|
|
49
|
-
"""Test that duplicate UUIDs are rejected by unique constraint."""
|
|
50
|
-
user1 = User(email="user1@example.com")
|
|
51
|
-
alchemy_session.add(user1)
|
|
52
|
-
await alchemy_session.commit()
|
|
53
|
-
|
|
54
|
-
# Save the ID and expunge user1 to avoid identity conflicts
|
|
55
|
-
user1_id = user1.id
|
|
56
|
-
alchemy_session.expunge(user1)
|
|
57
|
-
|
|
58
|
-
# Try to create another user with the same ID
|
|
59
|
-
user2 = User(email="user2@example.com")
|
|
60
|
-
user2.id = user1_id # Force same ID
|
|
61
|
-
|
|
62
|
-
alchemy_session.add(user2)
|
|
63
|
-
with pytest.raises(IntegrityError):
|
|
64
|
-
await alchemy_session.commit()
|
|
65
|
-
|
|
66
|
-
await alchemy_session.rollback()
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def test_timestamp_mixin_defaults() -> None:
|
|
70
|
-
user = User(email="defaults@example.com")
|
|
71
|
-
assert user.created_at is not None
|
|
72
|
-
assert user.updated_at is not None
|
|
73
|
-
assert user.deleted_at is None
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def test_mark_deleted_sets_timestamp() -> None:
|
|
77
|
-
user = User(email="x@example.com")
|
|
78
|
-
assert user.deleted_at is None
|
|
79
|
-
user.mark_deleted()
|
|
80
|
-
assert user.deleted_at is not None
|
|
File without changes
|
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from importlib.util import find_spec
|
|
3
|
-
from urllib.parse import urlparse
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
from sqlalchemy import text
|
|
7
|
-
from sqlalchemy.exc import IntegrityError
|
|
8
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
9
|
-
|
|
10
|
-
from belgie_alchemy.settings import DatabaseSettings
|
|
11
|
-
|
|
12
|
-
ASYNC_PG_AVAILABLE = find_spec("asyncpg") is not None
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@pytest.mark.asyncio
|
|
16
|
-
async def test_sqlite_engine_fk_enabled() -> None:
|
|
17
|
-
db = DatabaseSettings(dialect={"type": "sqlite", "database": ":memory:", "enable_foreign_keys": True})
|
|
18
|
-
|
|
19
|
-
async with db.engine.connect() as conn:
|
|
20
|
-
result = await conn.execute(text("PRAGMA foreign_keys"))
|
|
21
|
-
value = result.scalar_one()
|
|
22
|
-
|
|
23
|
-
assert value == 1
|
|
24
|
-
await db.engine.dispose()
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@pytest.mark.asyncio
|
|
28
|
-
async def test_session_maker_expire_disabled() -> None:
|
|
29
|
-
db = DatabaseSettings(dialect={"type": "sqlite", "database": ":memory:"})
|
|
30
|
-
|
|
31
|
-
session_factory = db.session_maker
|
|
32
|
-
assert session_factory.kw["expire_on_commit"] is False
|
|
33
|
-
|
|
34
|
-
async with session_factory() as session:
|
|
35
|
-
assert isinstance(session, AsyncSession)
|
|
36
|
-
|
|
37
|
-
await db.engine.dispose()
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def test_postgres_url_creation() -> None:
|
|
41
|
-
if not ASYNC_PG_AVAILABLE:
|
|
42
|
-
pytest.skip("asyncpg not installed; skip postgres engine test")
|
|
43
|
-
|
|
44
|
-
db = DatabaseSettings(
|
|
45
|
-
dialect={
|
|
46
|
-
"type": "postgres",
|
|
47
|
-
"host": "localhost",
|
|
48
|
-
"port": 5432,
|
|
49
|
-
"database": "belgie",
|
|
50
|
-
"username": "user",
|
|
51
|
-
"password": "secret",
|
|
52
|
-
},
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
assert db.engine.url.get_backend_name() == "postgresql"
|
|
56
|
-
assert db.engine.url.get_driver_name() == "asyncpg"
|
|
57
|
-
assert db.engine.url.username == "user"
|
|
58
|
-
assert db.engine.url.host == "localhost"
|
|
59
|
-
assert db.engine.url.database == "belgie"
|
|
60
|
-
assert db.engine.url.port == 5432
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@pytest.mark.asyncio
|
|
64
|
-
async def test_sqlite_fk_violations_raise_error() -> None:
|
|
65
|
-
"""Test that foreign key constraints are enforced when enabled."""
|
|
66
|
-
db = DatabaseSettings(dialect={"type": "sqlite", "database": ":memory:", "enable_foreign_keys": True})
|
|
67
|
-
|
|
68
|
-
# Try to insert a child record with invalid FK - should raise IntegrityError
|
|
69
|
-
async with db.engine.begin() as conn:
|
|
70
|
-
# Create simple test tables
|
|
71
|
-
await conn.execute(text("CREATE TABLE test_parents (id INTEGER PRIMARY KEY, name TEXT)"))
|
|
72
|
-
await conn.execute(
|
|
73
|
-
text(
|
|
74
|
-
"CREATE TABLE test_children "
|
|
75
|
-
"(id INTEGER PRIMARY KEY, parent_id INTEGER NOT NULL REFERENCES test_parents(id), name TEXT)",
|
|
76
|
-
),
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
async with db.session_maker() as session:
|
|
80
|
-
# Try to insert child without parent - should raise IntegrityError
|
|
81
|
-
with pytest.raises(IntegrityError): # noqa: PT012
|
|
82
|
-
await session.execute(text("INSERT INTO test_children (id, parent_id, name) VALUES (1, 999, 'orphan')"))
|
|
83
|
-
await session.commit()
|
|
84
|
-
|
|
85
|
-
await db.engine.dispose()
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
@pytest.mark.asyncio
|
|
89
|
-
async def test_dependency_yields_different_sessions() -> None:
|
|
90
|
-
"""Test that dependency yields different session instances."""
|
|
91
|
-
db = DatabaseSettings(dialect={"type": "sqlite", "database": ":memory:"})
|
|
92
|
-
|
|
93
|
-
sessions = []
|
|
94
|
-
async for session1 in db.dependency():
|
|
95
|
-
sessions.append(session1)
|
|
96
|
-
break
|
|
97
|
-
|
|
98
|
-
async for session2 in db.dependency():
|
|
99
|
-
sessions.append(session2)
|
|
100
|
-
break
|
|
101
|
-
|
|
102
|
-
assert len(sessions) == 2
|
|
103
|
-
assert sessions[0] is not sessions[1]
|
|
104
|
-
await db.engine.dispose()
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
@pytest.mark.asyncio
|
|
108
|
-
async def test_dependency_handles_exceptions() -> None:
|
|
109
|
-
"""Test that dependency properly handles exceptions."""
|
|
110
|
-
db = DatabaseSettings(dialect={"type": "sqlite", "database": ":memory:"})
|
|
111
|
-
|
|
112
|
-
# Simulate exception during request handling
|
|
113
|
-
simulated_error = "Simulated error"
|
|
114
|
-
try:
|
|
115
|
-
async for _session in db.dependency():
|
|
116
|
-
# Force an error
|
|
117
|
-
raise ValueError(simulated_error) # noqa: TRY301
|
|
118
|
-
except ValueError:
|
|
119
|
-
pass # Expected
|
|
120
|
-
|
|
121
|
-
# Verify we can still get new sessions
|
|
122
|
-
async for session in db.dependency():
|
|
123
|
-
assert isinstance(session, AsyncSession)
|
|
124
|
-
break
|
|
125
|
-
|
|
126
|
-
await db.engine.dispose()
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def test_postgres_settings_validation() -> None:
|
|
130
|
-
"""Test that PostgreSQL settings validates all required fields."""
|
|
131
|
-
# Test with valid data
|
|
132
|
-
db = DatabaseSettings(
|
|
133
|
-
dialect={
|
|
134
|
-
"type": "postgres",
|
|
135
|
-
"host": "db.example.com",
|
|
136
|
-
"port": 5433,
|
|
137
|
-
"database": "testdb",
|
|
138
|
-
"username": "testuser",
|
|
139
|
-
"password": "testpass",
|
|
140
|
-
"pool_size": 10,
|
|
141
|
-
"max_overflow": 20,
|
|
142
|
-
},
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
assert db.dialect.type == "postgres"
|
|
146
|
-
assert db.dialect.host == "db.example.com"
|
|
147
|
-
assert db.dialect.port == 5433
|
|
148
|
-
assert db.dialect.database == "testdb"
|
|
149
|
-
assert db.dialect.username == "testuser"
|
|
150
|
-
assert db.dialect.password.get_secret_value() == "testpass"
|
|
151
|
-
assert db.dialect.pool_size == 10
|
|
152
|
-
assert db.dialect.max_overflow == 20
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def test_sqlite_settings_validation() -> None:
|
|
156
|
-
"""Test that SQLite settings validates correctly."""
|
|
157
|
-
db = DatabaseSettings(
|
|
158
|
-
dialect={
|
|
159
|
-
"type": "sqlite",
|
|
160
|
-
"database": "/tmp/test.db", # noqa: S108
|
|
161
|
-
"enable_foreign_keys": False,
|
|
162
|
-
"echo": True,
|
|
163
|
-
},
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
assert db.dialect.type == "sqlite"
|
|
167
|
-
assert db.dialect.database == "/tmp/test.db" # noqa: S108
|
|
168
|
-
assert db.dialect.enable_foreign_keys is False
|
|
169
|
-
assert db.dialect.echo is True
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
# ==================== PostgreSQL Integration Tests ====================
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
@pytest.mark.asyncio
|
|
176
|
-
@pytest.mark.integration
|
|
177
|
-
async def test_postgres_engine_connection() -> None:
|
|
178
|
-
"""Test actual PostgreSQL connection and session creation.
|
|
179
|
-
|
|
180
|
-
This test requires a PostgreSQL instance to be available.
|
|
181
|
-
Set POSTGRES_TEST_URL environment variable or skip.
|
|
182
|
-
"""
|
|
183
|
-
if not ASYNC_PG_AVAILABLE:
|
|
184
|
-
pytest.skip("asyncpg not installed")
|
|
185
|
-
|
|
186
|
-
# Allow configuring test database via environment
|
|
187
|
-
test_url = os.getenv("POSTGRES_TEST_URL")
|
|
188
|
-
if not test_url:
|
|
189
|
-
pytest.skip("POSTGRES_TEST_URL not set - skipping integration test")
|
|
190
|
-
|
|
191
|
-
# Parse URL components (format: postgresql://user:pass@host:port/db)
|
|
192
|
-
try:
|
|
193
|
-
parsed = urlparse(test_url)
|
|
194
|
-
db = DatabaseSettings(
|
|
195
|
-
dialect={
|
|
196
|
-
"type": "postgres",
|
|
197
|
-
"host": parsed.hostname or "localhost",
|
|
198
|
-
"port": parsed.port or 5432,
|
|
199
|
-
"database": parsed.path.lstrip("/") if parsed.path else "postgres",
|
|
200
|
-
"username": parsed.username or "postgres",
|
|
201
|
-
"password": parsed.password or "",
|
|
202
|
-
},
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
# Test basic connection
|
|
206
|
-
async with db.engine.connect() as conn:
|
|
207
|
-
result = await conn.execute(text("SELECT 1 as test"))
|
|
208
|
-
value = result.scalar_one()
|
|
209
|
-
assert value == 1
|
|
210
|
-
|
|
211
|
-
await db.engine.dispose()
|
|
212
|
-
|
|
213
|
-
except (OSError, IntegrityError) as e:
|
|
214
|
-
pytest.skip(f"Could not connect to PostgreSQL: {e}")
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
@pytest.mark.asyncio
|
|
218
|
-
@pytest.mark.integration
|
|
219
|
-
async def test_postgres_session_creation() -> None:
|
|
220
|
-
"""Test that PostgreSQL session factory works correctly."""
|
|
221
|
-
if not ASYNC_PG_AVAILABLE:
|
|
222
|
-
pytest.skip("asyncpg not installed")
|
|
223
|
-
|
|
224
|
-
test_url = os.getenv("POSTGRES_TEST_URL")
|
|
225
|
-
if not test_url:
|
|
226
|
-
pytest.skip("POSTGRES_TEST_URL not set - skipping integration test")
|
|
227
|
-
|
|
228
|
-
try:
|
|
229
|
-
parsed = urlparse(test_url)
|
|
230
|
-
db = DatabaseSettings(
|
|
231
|
-
dialect={
|
|
232
|
-
"type": "postgres",
|
|
233
|
-
"host": parsed.hostname or "localhost",
|
|
234
|
-
"port": parsed.port or 5432,
|
|
235
|
-
"database": parsed.path.lstrip("/") if parsed.path else "postgres",
|
|
236
|
-
"username": parsed.username or "postgres",
|
|
237
|
-
"password": parsed.password or "",
|
|
238
|
-
},
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
# Test session creation
|
|
242
|
-
async with db.session_maker() as session:
|
|
243
|
-
assert isinstance(session, AsyncSession)
|
|
244
|
-
result = await session.execute(text("SELECT version()"))
|
|
245
|
-
version = result.scalar_one()
|
|
246
|
-
assert "PostgreSQL" in version
|
|
247
|
-
|
|
248
|
-
await db.engine.dispose()
|
|
249
|
-
|
|
250
|
-
except (OSError, IntegrityError) as e:
|
|
251
|
-
pytest.skip(f"Could not connect to PostgreSQL: {e}")
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
@pytest.mark.asyncio
|
|
255
|
-
@pytest.mark.integration
|
|
256
|
-
async def test_postgres_dependency_yields_sessions() -> None:
|
|
257
|
-
"""Test that PostgreSQL dependency generator works correctly."""
|
|
258
|
-
if not ASYNC_PG_AVAILABLE:
|
|
259
|
-
pytest.skip("asyncpg not installed")
|
|
260
|
-
|
|
261
|
-
test_url = os.getenv("POSTGRES_TEST_URL")
|
|
262
|
-
if not test_url:
|
|
263
|
-
pytest.skip("POSTGRES_TEST_URL not set - skipping integration test")
|
|
264
|
-
|
|
265
|
-
try:
|
|
266
|
-
parsed = urlparse(test_url)
|
|
267
|
-
db = DatabaseSettings(
|
|
268
|
-
dialect={
|
|
269
|
-
"type": "postgres",
|
|
270
|
-
"host": parsed.hostname or "localhost",
|
|
271
|
-
"port": parsed.port or 5432,
|
|
272
|
-
"database": parsed.path.lstrip("/") if parsed.path else "postgres",
|
|
273
|
-
"username": parsed.username or "postgres",
|
|
274
|
-
"password": parsed.password or "",
|
|
275
|
-
},
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
# Test dependency yields working sessions
|
|
279
|
-
sessions = []
|
|
280
|
-
async for session in db.dependency():
|
|
281
|
-
sessions.append(session)
|
|
282
|
-
# Verify session works
|
|
283
|
-
result = await session.execute(text("SELECT 1"))
|
|
284
|
-
assert result.scalar_one() == 1
|
|
285
|
-
break
|
|
286
|
-
|
|
287
|
-
async for session in db.dependency():
|
|
288
|
-
sessions.append(session)
|
|
289
|
-
break
|
|
290
|
-
|
|
291
|
-
# Verify different session instances
|
|
292
|
-
assert len(sessions) == 2
|
|
293
|
-
assert sessions[0] is not sessions[1]
|
|
294
|
-
|
|
295
|
-
await db.engine.dispose()
|
|
296
|
-
|
|
297
|
-
except (OSError, IntegrityError) as e:
|
|
298
|
-
pytest.skip(f"Could not connect to PostgreSQL: {e}")
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
@pytest.mark.asyncio
|
|
302
|
-
@pytest.mark.integration
|
|
303
|
-
async def test_postgres_connection_pooling() -> None:
|
|
304
|
-
"""Test that PostgreSQL connection pooling is configured correctly."""
|
|
305
|
-
if not ASYNC_PG_AVAILABLE:
|
|
306
|
-
pytest.skip("asyncpg not installed")
|
|
307
|
-
|
|
308
|
-
test_url = os.getenv("POSTGRES_TEST_URL")
|
|
309
|
-
if not test_url:
|
|
310
|
-
pytest.skip("POSTGRES_TEST_URL not set - skipping integration test")
|
|
311
|
-
|
|
312
|
-
try:
|
|
313
|
-
parsed = urlparse(test_url)
|
|
314
|
-
db = DatabaseSettings(
|
|
315
|
-
dialect={
|
|
316
|
-
"type": "postgres",
|
|
317
|
-
"host": parsed.hostname or "localhost",
|
|
318
|
-
"port": parsed.port or 5432,
|
|
319
|
-
"database": parsed.path.lstrip("/") if parsed.path else "postgres",
|
|
320
|
-
"username": parsed.username or "postgres",
|
|
321
|
-
"password": parsed.password or "",
|
|
322
|
-
"pool_size": 5,
|
|
323
|
-
"max_overflow": 10,
|
|
324
|
-
},
|
|
325
|
-
)
|
|
326
|
-
|
|
327
|
-
# Verify pool settings
|
|
328
|
-
assert db.dialect.pool_size == 5
|
|
329
|
-
assert db.dialect.max_overflow == 10
|
|
330
|
-
|
|
331
|
-
# Create multiple sessions to test pooling
|
|
332
|
-
sessions = []
|
|
333
|
-
for _ in range(3):
|
|
334
|
-
async with db.session_maker() as session:
|
|
335
|
-
sessions.append(session)
|
|
336
|
-
result = await session.execute(text("SELECT 1"))
|
|
337
|
-
assert result.scalar_one() == 1
|
|
338
|
-
|
|
339
|
-
await db.engine.dispose()
|
|
340
|
-
|
|
341
|
-
except (OSError, IntegrityError) as e:
|
|
342
|
-
pytest.skip(f"Could not connect to PostgreSQL: {e}")
|