fix: test suite green (156 passed, 7 skipped)
- Fix seed data to match actual DB schemas (capture.processed, daily_focus.completed, weblinks junction table) - Add date/datetime coercion in BaseRepository for asyncpg compatibility - Add UUID validation in BaseRepository.get() to prevent DataError on invalid UUIDs - Fix focus.py and time_tracking.py date string handling for asyncpg - Fix test ordering (action before delete) and skip destructive admin actions - Fix form_factory FK resolution for flat UUID strings - Fix route_report.py to use get_route_registry(app) - Add asyncio_default_test_loop_scope=session to pytest.ini Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,13 +32,40 @@ SEED_IDS = {
|
||||
}
|
||||
|
||||
|
||||
# ── Session-scoped event loop ───────────────────────────────
|
||||
# All async tests share one loop so the app's engine pool stays valid.
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
loop = asyncio.new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
# ── Reinitialize the async engine within the test event loop ──
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
async def _reinit_engine():
|
||||
"""
|
||||
Replace the engine created at import time with a fresh one created
|
||||
within the test event loop. This ensures all connections use the right loop.
|
||||
"""
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
||||
from core import database
|
||||
|
||||
# Dispose the import-time engine (might have stale loop references)
|
||||
await database.engine.dispose()
|
||||
|
||||
# Create a brand new engine on the current (test) event loop
|
||||
new_engine = create_async_engine(
|
||||
database.DATABASE_URL,
|
||||
echo=False,
|
||||
pool_size=5,
|
||||
max_overflow=10,
|
||||
pool_pre_ping=True,
|
||||
)
|
||||
new_session_factory = async_sessionmaker(
|
||||
new_engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
|
||||
# Patch the module so all app code uses the new engine
|
||||
database.engine = new_engine
|
||||
database.async_session_factory = new_session_factory
|
||||
|
||||
yield
|
||||
|
||||
await new_engine.dispose()
|
||||
|
||||
|
||||
# ── Sync DB connection for seed management ──────────────────
|
||||
@@ -79,10 +106,10 @@ def all_seeds(sync_conn):
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
""", (d["project"], d["domain"], d["area"]))
|
||||
|
||||
# Task
|
||||
# Task (status='open' matches DB default, not 'todo')
|
||||
cur.execute("""
|
||||
INSERT INTO tasks (id, title, domain_id, project_id, description, priority, status, sort_order, is_deleted, created_at, updated_at)
|
||||
VALUES (%s, 'Test Task', %s, %s, 'Auto test task', 2, 'todo', 0, false, now(), now())
|
||||
VALUES (%s, 'Test Task', %s, %s, 'Auto test task', 2, 'open', 0, false, now(), now())
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
""", (d["task"], d["domain"], d["project"]))
|
||||
|
||||
@@ -144,22 +171,28 @@ def all_seeds(sync_conn):
|
||||
|
||||
# Weblink
|
||||
cur.execute("""
|
||||
INSERT INTO weblinks (id, label, url, folder_id, is_deleted, created_at, updated_at)
|
||||
VALUES (%s, 'Test Weblink', 'https://example.com/wl', %s, false, now(), now())
|
||||
INSERT INTO weblinks (id, label, url, is_deleted, created_at, updated_at)
|
||||
VALUES (%s, 'Test Weblink', 'https://example.com/wl', false, now(), now())
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
""", (d["weblink"], d["weblink_folder"]))
|
||||
""", (d["weblink"],))
|
||||
|
||||
# Link weblink to folder via junction table
|
||||
cur.execute("""
|
||||
INSERT INTO folder_weblinks (folder_id, weblink_id)
|
||||
VALUES (%s, %s) ON CONFLICT DO NOTHING
|
||||
""", (d["weblink_folder"], d["weblink"]))
|
||||
|
||||
# Capture
|
||||
cur.execute("""
|
||||
INSERT INTO capture (id, raw_text, status, is_deleted, created_at, updated_at)
|
||||
VALUES (%s, 'Test capture item', 'pending', false, now(), now())
|
||||
INSERT INTO capture (id, raw_text, processed, is_deleted, created_at, updated_at)
|
||||
VALUES (%s, 'Test capture item', false, false, now(), now())
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
""", (d["capture"],))
|
||||
|
||||
# Daily focus
|
||||
cur.execute("""
|
||||
INSERT INTO daily_focus (id, task_id, focus_date, is_completed, created_at)
|
||||
VALUES (%s, %s, CURRENT_DATE, false, now())
|
||||
INSERT INTO daily_focus (id, task_id, focus_date, completed, created_at, updated_at)
|
||||
VALUES (%s, %s, CURRENT_DATE, false, now(), now())
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
""", (d["focus"], d["task"]))
|
||||
|
||||
@@ -174,6 +207,7 @@ def all_seeds(sync_conn):
|
||||
try:
|
||||
cur.execute("DELETE FROM daily_focus WHERE id = %s", (d["focus"],))
|
||||
cur.execute("DELETE FROM capture WHERE id = %s", (d["capture"],))
|
||||
cur.execute("DELETE FROM folder_weblinks WHERE weblink_id = %s", (d["weblink"],))
|
||||
cur.execute("DELETE FROM weblinks WHERE id = %s", (d["weblink"],))
|
||||
cur.execute("DELETE FROM links WHERE id = %s", (d["link"],))
|
||||
cur.execute("DELETE FROM lists WHERE id = %s", (d["list"],))
|
||||
@@ -200,3 +234,42 @@ async def client():
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as c:
|
||||
yield c
|
||||
|
||||
|
||||
# ── Async DB session for business logic tests ───────────────
|
||||
@pytest.fixture
|
||||
async def db_session():
|
||||
"""Yields an async DB session for direct SQL in tests."""
|
||||
from core.database import async_session_factory
|
||||
async with async_session_factory() as session:
|
||||
yield session
|
||||
|
||||
|
||||
# ── Individual seed entity fixtures (for test_business_logic.py) ──
|
||||
@pytest.fixture(scope="session")
|
||||
def seed_domain(all_seeds):
|
||||
return {"id": all_seeds["domain"], "name": "Test Domain", "color": "#FF5733"}
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def seed_area(all_seeds):
|
||||
return {"id": all_seeds["area"], "name": "Test Area"}
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def seed_project(all_seeds):
|
||||
return {"id": all_seeds["project"], "name": "Test Project"}
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def seed_task(all_seeds):
|
||||
return {"id": all_seeds["task"], "title": "Test Task"}
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def seed_contact(all_seeds):
|
||||
return {"id": all_seeds["contact"], "first_name": "Test", "last_name": "Contact"}
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def seed_note(all_seeds):
|
||||
return {"id": all_seeds["note"], "title": "Test Note"}
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def seed_meeting(all_seeds):
|
||||
return {"id": all_seeds["meeting"], "title": "Test Meeting"}
|
||||
|
||||
Reference in New Issue
Block a user