Files
lifeos-dev/tests/conftest.py

203 lines
8.7 KiB
Python

"""
Test fixtures - uses psycopg2 (sync) for seed data to avoid event loop conflicts.
Seeds are session-scoped and committed so the app can see them via its own engine.
"""
import asyncio
import uuid
import pytest
import psycopg2
from httpx import AsyncClient, ASGITransport
from tests.registry import app
TEST_DB_DSN = "host=lifeos-db port=5432 dbname=lifeos_test user=postgres password=UCTOQDZiUhN8U"
# ── Fixed seed UUIDs (stable across test runs) ──────────────
SEED_IDS = {
"domain": "a0000000-0000-0000-0000-000000000001",
"area": "a0000000-0000-0000-0000-000000000002",
"project": "a0000000-0000-0000-0000-000000000003",
"task": "a0000000-0000-0000-0000-000000000004",
"contact": "a0000000-0000-0000-0000-000000000005",
"note": "a0000000-0000-0000-0000-000000000006",
"meeting": "a0000000-0000-0000-0000-000000000007",
"decision": "a0000000-0000-0000-0000-000000000008",
"appointment": "a0000000-0000-0000-0000-000000000009",
"weblink_folder": "a0000000-0000-0000-0000-00000000000a",
"list": "a0000000-0000-0000-0000-00000000000b",
"link": "a0000000-0000-0000-0000-00000000000c",
"weblink": "a0000000-0000-0000-0000-00000000000d",
"capture": "a0000000-0000-0000-0000-00000000000e",
"focus": "a0000000-0000-0000-0000-00000000000f",
}
# ── 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()
# ── Sync DB connection for seed management ──────────────────
@pytest.fixture(scope="session")
def sync_conn():
conn = psycopg2.connect(TEST_DB_DSN)
conn.autocommit = False
yield conn
conn.close()
# ── Seed data (session-scoped, committed) ───────────────────
@pytest.fixture(scope="session")
def all_seeds(sync_conn):
"""Insert all seed data once. Committed so the app's engine can see it."""
cur = sync_conn.cursor()
d = SEED_IDS
try:
# Domain
cur.execute("""
INSERT INTO domains (id, name, color, description, sort_order, is_deleted, created_at, updated_at)
VALUES (%s, 'Test Domain', '#FF5733', 'Auto test domain', 0, false, now(), now())
ON CONFLICT (id) DO NOTHING
""", (d["domain"],))
# Area
cur.execute("""
INSERT INTO areas (id, name, domain_id, description, status, sort_order, is_deleted, created_at, updated_at)
VALUES (%s, 'Test Area', %s, 'Auto test area', 'active', 0, false, now(), now())
ON CONFLICT (id) DO NOTHING
""", (d["area"], d["domain"]))
# Project
cur.execute("""
INSERT INTO projects (id, name, domain_id, area_id, description, status, priority, sort_order, is_deleted, created_at, updated_at)
VALUES (%s, 'Test Project', %s, %s, 'Auto test project', 'active', 2, 0, false, now(), now())
ON CONFLICT (id) DO NOTHING
""", (d["project"], d["domain"], d["area"]))
# Task
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())
ON CONFLICT (id) DO NOTHING
""", (d["task"], d["domain"], d["project"]))
# Contact
cur.execute("""
INSERT INTO contacts (id, first_name, last_name, company, email, is_deleted, created_at, updated_at)
VALUES (%s, 'Test', 'Contact', 'TestCorp', 'test@example.com', false, now(), now())
ON CONFLICT (id) DO NOTHING
""", (d["contact"],))
# Note
cur.execute("""
INSERT INTO notes (id, title, domain_id, body, content_format, is_deleted, created_at, updated_at)
VALUES (%s, 'Test Note', %s, 'Test body content', 'markdown', false, now(), now())
ON CONFLICT (id) DO NOTHING
""", (d["note"], d["domain"]))
# Meeting
cur.execute("""
INSERT INTO meetings (id, title, meeting_date, status, is_deleted, created_at, updated_at)
VALUES (%s, 'Test Meeting', '2025-06-15', 'scheduled', false, now(), now())
ON CONFLICT (id) DO NOTHING
""", (d["meeting"],))
# Decision
cur.execute("""
INSERT INTO decisions (id, title, status, impact, is_deleted, created_at, updated_at)
VALUES (%s, 'Test Decision', 'decided', 'high', false, now(), now())
ON CONFLICT (id) DO NOTHING
""", (d["decision"],))
# Appointment
cur.execute("""
INSERT INTO appointments (id, title, start_at, end_at, all_day, is_deleted, created_at, updated_at)
VALUES (%s, 'Test Appointment', '2025-06-15 10:00:00', '2025-06-15 11:00:00', false, false, now(), now())
ON CONFLICT (id) DO NOTHING
""", (d["appointment"],))
# Weblink folder
cur.execute("""
INSERT INTO weblink_folders (id, name, is_deleted, created_at, updated_at)
VALUES (%s, 'Test Folder', false, now(), now())
ON CONFLICT (id) DO NOTHING
""", (d["weblink_folder"],))
# List
cur.execute("""
INSERT INTO lists (id, name, domain_id, project_id, list_type, is_deleted, created_at, updated_at)
VALUES (%s, 'Test List', %s, %s, 'checklist', false, now(), now())
ON CONFLICT (id) DO NOTHING
""", (d["list"], d["domain"], d["project"]))
# Link
cur.execute("""
INSERT INTO links (id, label, url, domain_id, is_deleted, created_at, updated_at)
VALUES (%s, 'Test Link', 'https://example.com', %s, false, now(), now())
ON CONFLICT (id) DO NOTHING
""", (d["link"], d["domain"]))
# 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())
ON CONFLICT (id) DO NOTHING
""", (d["weblink"], d["weblink_folder"]))
# 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())
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())
ON CONFLICT (id) DO NOTHING
""", (d["focus"], d["task"]))
sync_conn.commit()
except Exception as e:
sync_conn.rollback()
raise RuntimeError(f"Seed data insertion failed: {e}") from e
yield d
# Cleanup: delete all seed data (reverse dependency order)
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 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"],))
cur.execute("DELETE FROM weblink_folders WHERE id = %s", (d["weblink_folder"],))
cur.execute("DELETE FROM appointments WHERE id = %s", (d["appointment"],))
cur.execute("DELETE FROM decisions WHERE id = %s", (d["decision"],))
cur.execute("DELETE FROM meetings WHERE id = %s", (d["meeting"],))
cur.execute("DELETE FROM notes WHERE id = %s", (d["note"],))
cur.execute("DELETE FROM contacts WHERE id = %s", (d["contact"],))
cur.execute("DELETE FROM tasks WHERE id = %s", (d["task"],))
cur.execute("DELETE FROM projects WHERE id = %s", (d["project"],))
cur.execute("DELETE FROM areas WHERE id = %s", (d["area"],))
cur.execute("DELETE FROM domains WHERE id = %s", (d["domain"],))
sync_conn.commit()
except Exception:
sync_conn.rollback()
finally:
cur.close()
# ── HTTP client (function-scoped) ───────────────────────────
@pytest.fixture
async def client():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as c:
yield c