deer-flow/backend/tests/test_postgres_asyncpg_url.py
spud 21175ba46f
fix(runtime): persistently fence stale run event producers (#7619)
* fix(runtime): fence revoked run event producers

* fix(runtime): reduce off-mode producer guard locking

* docs: keep inherited guidance within size budget

* chore(runtime): remove development phase labels

* fix(persistence): normalize libpq SSL options for asyncpg connections

* docs(runtime): clarify producer fencing behavior in off mode

* test(persistence): align migration TLS probes with channel-binding rejection

* fix(runtime): authorize fenced Gateway history seeding

* fix(scheduler): preserve takeover identity in recovery publication

* fix(scheduler): report unconfirmed recovery publication

* fix(runtime): preserve history messages before the first human turn

* fix(runtime): clarify run creation failure diagnostic

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-02 15:26:42 +08:00

105 lines
4.3 KiB
Python

"""Async ORM URLs preserve TLS requirements without changing libpq consumers."""
import os
from uuid import uuid4
import pytest
from sqlalchemy import text
from sqlalchemy.engine import make_url
from sqlalchemy.ext.asyncio import create_async_engine
from support.postgres import asyncpg_test_url
from deerflow.config.database_config import DatabaseConfig
from deerflow.persistence import engine as engine_mod
@pytest.mark.parametrize("scheme", ["postgres", "postgresql", "postgresql+asyncpg"])
@pytest.mark.parametrize("mode", [None, "disable", "allow", "prefer", "require", "verify-ca", "verify-full"])
def test_async_orm_url_preserves_tls_credentials_and_query_options(scheme, mode):
uri = f"{scheme}://us%40er%3Aname:p%40ss%2Fword%3A%25@localhost:5432/deerflow_test?target_session_attrs=read-write"
if mode is not None:
uri += f"&sslmode={mode}"
config = DatabaseConfig(backend="postgres", postgres_url=uri)
url = make_url(config.app_sqlalchemy_url)
_, kwargs = url.get_dialect()().create_connect_args(url)
assert url.drivername == "postgresql+asyncpg"
assert kwargs["user"] == "us@er:name"
assert kwargs["password"] == "p@ss/word:%"
assert kwargs["target_session_attrs"] == "read-write"
assert "sslmode" not in kwargs
if mode is None:
assert "ssl" not in kwargs
else:
assert kwargs["ssl"] == mode
assert config.postgres_url == uri
assert make_url(config.app_sync_sqlalchemy_url).query == make_url(uri).query
assert asyncpg_test_url(uri) == config.app_sqlalchemy_url
@pytest.mark.parametrize("query", ["ssl=require", "ssl=require&sslmode=require"])
def test_explicit_matching_ssl_options_are_preserved(query):
config = DatabaseConfig(backend="postgres", postgres_url=f"postgresql://user:password@localhost/test?{query}")
assert make_url(config.app_sqlalchemy_url).query == {"ssl": "require"}
@pytest.mark.parametrize(
"query",
[
"ssl=require&sslmode=disable",
"sslmode=invalid",
"sslmode=",
"ssl=invalid",
"ssl=",
"sslmode=require&sslmode=disable",
"ssl=require&ssl=disable",
"channel_binding=require",
"channel_binding=prefer",
"channel_binding=disable",
"channel_binding=invalid",
"channel_binding=",
],
)
def test_unsupported_or_ambiguous_security_options_are_rejected_without_secrets(query):
config = DatabaseConfig(backend="postgres", postgres_url=f"postgresql://user:private-password@localhost/test?{query}")
with pytest.raises(ValueError) as caught:
_ = config.app_sqlalchemy_url
assert "private-password" not in str(caught.value)
assert make_url(config.app_sync_sqlalchemy_url).query == make_url(config.postgres_url).query
def test_asyncpg_url_normalization_is_idempotent():
from deerflow.persistence.postgres_url import normalize_asyncpg_url
normalized = normalize_asyncpg_url("postgres://user:p%40ss@localhost/test?sslmode=verify-full")
assert normalize_asyncpg_url(normalized) == normalized
@pytest.mark.parametrize("scheme", ["sqlite", "mysql", "postgresql+psycopg"])
def test_asyncpg_url_rejects_other_drivers(scheme):
from deerflow.persistence.postgres_url import normalize_asyncpg_url
with pytest.raises(ValueError, match="PostgreSQL"):
normalize_asyncpg_url(f"{scheme}://user:password@localhost/test")
@pytest.mark.asyncio
async def test_raw_libpq_url_bootstraps_through_database_config():
uri = os.environ.get("TEST_POSTGRES_URI")
if not uri:
pytest.skip("requires TEST_POSTGRES_URI")
schema = "asyncpg_url_" + uuid4().hex
config = DatabaseConfig(backend="postgres", postgres_url=uri, postgres_schema=schema)
cleanup = create_async_engine(asyncpg_test_url(uri))
try:
await engine_mod.init_engine_from_config(config)
async with engine_mod.get_engine().connect() as conn:
assert await conn.scalar(text("SELECT current_schema()")) == schema
assert await conn.scalar(text("SELECT version_num FROM alembic_version"))
assert await conn.scalar(text("SELECT to_regclass('runs')")) == "runs"
assert config.postgres_url == uri
finally:
await engine_mod.close_engine()
async with cleanup.begin() as conn:
await conn.execute(text(f'DROP SCHEMA IF EXISTS "{schema}" CASCADE'))
await cleanup.dispose()