11"""
22E2E test fixtures with real PostgreSQL database.
33
4- Uses a separate 'test_e2e' schema to isolate test data from production.
4+ Uses a separate 'test' database to isolate test data from production.
55Tests are skipped if DATABASE_URL is not set or database is unreachable.
66
77Connection modes:
8- - Local: Direct DATABASE_URL from .env
8+ - Local: Direct DATABASE_URL from .env (auto-derives test database)
99- CI: DATABASE_URL via Cloud SQL Proxy (localhost:5432 -> Cloud SQL)
10+ - Explicit: TEST_DATABASE_URL for custom test database
1011
1112Note: These tests must NOT be run with pytest-xdist parallelization
12- as multiple workers would conflict on the shared test_e2e schema .
13+ as multiple workers would conflict on the shared test database .
1314"""
1415
1516import asyncio
1617import os
1718
1819import pytest
1920import pytest_asyncio
20- from sqlalchemy import text
2121from sqlalchemy .ext .asyncio import AsyncSession , async_sessionmaker , create_async_engine
2222
2323from core .database .models import Base
2424
2525
26- TEST_SCHEMA = "test_e2e"
2726CONNECTION_TIMEOUT = 10 # seconds - skip tests if DB unreachable
2827
2928# Test data constants
3231
3332
3433def _get_database_url ():
35- """Get DATABASE_URL from environment, loading .env if needed ."""
34+ """Get test database URL, defaulting to 'test' database on same instance ."""
3635 from dotenv import load_dotenv
3736
3837 load_dotenv ()
3938
40- database_url = os .environ .get ("DATABASE_URL" )
39+ # Prefer explicit TEST_DATABASE_URL
40+ database_url = os .environ .get ("TEST_DATABASE_URL" )
41+
4142 if not database_url :
42- return None
43+ # Derive from DATABASE_URL by replacing database name with 'test'
44+ prod_url = os .environ .get ("DATABASE_URL" )
45+ if not prod_url :
46+ return None
47+ # Replace database name at end of URL (e.g., /pyplots -> /test)
48+ database_url = prod_url .rsplit ("/" , 1 )[0 ] + "/test"
4349
4450 # Ensure async driver
4551 if database_url .startswith ("postgresql://" ):
@@ -53,53 +59,47 @@ def _get_database_url():
5359@pytest_asyncio .fixture (scope = "function" )
5460async def pg_engine ():
5561 """
56- Create PostgreSQL engine and setup test schema .
62+ Create PostgreSQL engine for test database .
5763
58- Creates a separate 'test_e2e' schema to isolate tests from production data .
59- The schema is dropped and recreated for each test.
64+ Uses a separate 'test' database to isolate tests from production.
65+ Tables are dropped and recreated for each test.
6066 Skips tests if database is unreachable.
6167 """
6268 database_url = _get_database_url ()
6369 if not database_url :
6470 pytest .skip ("DATABASE_URL not set - skipping PostgreSQL E2E tests" )
6571
66- # Create temporary engine for schema setup
67- temp_engine = create_async_engine (database_url , echo = False , connect_args = {"timeout" : CONNECTION_TIMEOUT })
72+ engine = create_async_engine (
73+ database_url ,
74+ echo = False ,
75+ connect_args = {"timeout" : CONNECTION_TIMEOUT },
76+ )
77+
6878 try :
6979 async with asyncio .timeout (CONNECTION_TIMEOUT + 2 ):
70- async with temp_engine .begin () as conn :
71- await conn .execute (text (f"DROP SCHEMA IF EXISTS { TEST_SCHEMA } CASCADE" ))
72- await conn .execute (text (f"CREATE SCHEMA { TEST_SCHEMA } " ))
73- await temp_engine .dispose ()
80+ async with engine .begin () as conn :
81+ # Drop all tables for clean state
82+ await conn .run_sync (Base .metadata .drop_all )
83+ # Create fresh tables
84+ await conn .run_sync (Base .metadata .create_all )
7485 except (TimeoutError , asyncio .TimeoutError , OSError ) as e :
75- await temp_engine .dispose ()
86+ await engine .dispose ()
7687 pytest .skip (f"Database unreachable (timeout) - skipping E2E tests: { e } " )
7788 except Exception as e :
78- await temp_engine .dispose ()
89+ await engine .dispose ()
7990 pytest .skip (f"Database connection failed - skipping E2E tests: { e } " )
8091
81- # Create main engine with search_path set at connection level
82- engine = create_async_engine (
83- database_url ,
84- echo = False ,
85- connect_args = {"server_settings" : {"search_path" : TEST_SCHEMA }, "timeout" : CONNECTION_TIMEOUT },
86- )
87-
88- # Create tables in test schema
89- async with engine .begin () as conn :
90- await conn .run_sync (Base .metadata .create_all )
91-
9292 yield engine
9393
94- # Cleanup: Drop entire test schema
94+ # Cleanup: Drop all tables
9595 async with engine .begin () as conn :
96- await conn .execute ( text ( f"DROP SCHEMA IF EXISTS { TEST_SCHEMA } CASCADE" ) )
96+ await conn .run_sync ( Base . metadata . drop_all )
9797 await engine .dispose ()
9898
9999
100100@pytest_asyncio .fixture
101101async def pg_session (pg_engine ):
102- """Create session with test schema (search_path set at engine level) ."""
102+ """Create session for test database ."""
103103 async_session = async_sessionmaker (pg_engine , class_ = AsyncSession , expire_on_commit = False )
104104 async with async_session () as session :
105105 yield session
@@ -109,10 +109,10 @@ async def pg_session(pg_engine):
109109@pytest_asyncio .fixture
110110async def pg_db_with_data (pg_session ):
111111 """
112- Seed test schema with sample data.
112+ Seed test database with sample data.
113113
114114 Creates the same test data as tests/conftest.py:test_db_with_data
115- but in the PostgreSQL test schema .
115+ but in the PostgreSQL test database .
116116 """
117117 from core .database .models import Impl , Library , Spec
118118
0 commit comments