"""Real PostgreSQL tests, isolated in a fresh schema for every test.""" import os from pathlib import Path from uuid import uuid4 import pytest from alembic import command from alembic.config import Config from fastapi.testclient import TestClient from sqlalchemy import create_engine, text from sqlalchemy.engine import make_url from sqlalchemy.orm import sessionmaker from app.db.session import get_db from app.main import create_app from app.models.auth import User from app.services.auth import PASSWORD_HASHER BACKEND = Path(__file__).resolve().parents[1] def migrate(engine, revision="head", *, downgrade=False): config = Config(str(BACKEND / "alembic.ini")) with engine.begin() as connection: config.attributes["connection"] = connection (command.downgrade if downgrade else command.upgrade)(config, revision) @pytest.fixture(scope="session") def postgres_url(tmp_path_factory): url = os.environ.get("TEST_DATABASE_URL") server = None if not url: try: import pgserver except ImportError: pytest.fail("请设置 TEST_DATABASE_URL,或安装可选的本机测试依赖 pgserver") server = pgserver.get_server(tmp_path_factory.mktemp("postgres"), cleanup_mode="stop") url = server.get_uri().replace("postgresql://", "postgresql+psycopg://", 1) if not url.startswith("postgresql+psycopg://"): pytest.fail("TEST_DATABASE_URL 必须使用 postgresql+psycopg://") bootstrap = create_engine(url) with bootstrap.begin() as connection: connection.execute(text("CREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA public")) bootstrap.dispose() yield url if server: server.cleanup() @pytest.fixture def database(postgres_url): schema = "test_" + uuid4().hex admin = create_engine(postgres_url) with admin.begin() as connection: connection.execute(text(f'CREATE SCHEMA "{schema}"')) # 不包含 public,防止误操作已有业务表。 url = make_url(postgres_url).update_query_dict({"options": f"-csearch_path={schema}"}) engine = create_engine(url) yield engine engine.dispose() with admin.begin() as connection: connection.execute(text(f'DROP SCHEMA "{schema}" CASCADE')) admin.dispose() @pytest.fixture def migrated_database(database): migrate(database) return database @pytest.fixture def anonymous_client(migrated_database): app = create_app(migrated_database) factory = sessionmaker(bind=migrated_database) def test_session(): with factory() as session: try: yield session except Exception: session.rollback() raise app.dependency_overrides[get_db] = test_session with TestClient(app, base_url="https://testserver") as client: client.headers["Origin"] = "http://localhost:5173" yield client @pytest.fixture def client(anonymous_client, migrated_database): with sessionmaker(bind=migrated_database)() as db: db.add( User( username="test_admin", password_hash=PASSWORD_HASHER.hash("test-password-123"), role="admin", ) ) db.commit() response = anonymous_client.post( "/api/auth/login", json={"username": "test_admin", "password": "test-password-123"} ) assert response.status_code == 200, response.text anonymous_client.headers["X-CSRF-Token"] = response.json()["csrf_token"] return anonymous_client