fix(tests): a failing database test no longer prints the password (T-12)

pytest prints a failing test's fixture arguments via repr() as part of the
default traceback header — no -v or --showlocals needed — so any test
taking the test_database_url fixture and failing printed the real
TEST_DATABASE_URL straight into the terminal, password included.

test_database_url now returns a MaskedURL: a str subclass identical to a
plain string for every functional use (passed to .replace(), urlsplit(),
a subprocess environment) but whose repr() masks the password while still
showing host and database name. __str__ is untouched, so every caller
still connects with the real value.

tests/test_conftest.py proves it, both for the fixture itself and for
MaskedURL's string behaviour in isolation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 00:28:34 +02:00
co-authored by Claude Sonnet 5
parent a81b9a071b
commit 2f53722078
3 changed files with 65 additions and 1 deletions
+4
View File
@@ -24,6 +24,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`tests/test_tenants_migration.py` now ends on `upgrade head` rather than its own
revision, so a full suite run leaves the test database at head instead of one or two
migrations behind it.
- **A failing database test no longer prints the test database password** (core-api
T-12) — the `test_database_url` fixture now returns a `MaskedURL`: identical to a
plain string for every functional use, but its `repr()` — which pytest prints for a
failing test's fixture arguments, without needing `-v` — masks the password.
## [1.15.0] - 2026-10-19
+18 -1
View File
@@ -49,6 +49,23 @@ def run_alembic(url: str, *args: str) -> subprocess.CompletedProcess:
return result
class MaskedURL(str):
"""A database URL, fully usable as a plain string (passed to `.replace()`,
`urlsplit()`, a subprocess environment, ...) but never through `repr()`
(core-api T-12): pytest prints a failing test's fixture arguments using
`repr()`, with no `-v`/`--showlocals` needed to trigger it — that is how
a failing database test used to print this fixture's password straight
into the terminal. `__str__` is deliberately left alone, so the real
value is still what every caller actually connects with."""
def __repr__(self) -> str:
parts = urlsplit(self)
netloc = parts.netloc
if parts.password:
netloc = netloc.replace(f":{parts.password}@", ":***@")
return repr(parts._replace(netloc=netloc).geturl())
@pytest.fixture(scope="session")
def test_database_url() -> str:
"""The core_api_test database URL, or skip/refuse per the rules above."""
@@ -62,7 +79,7 @@ def test_database_url() -> str:
"TEST_DATABASE_URL resolves to 'core_api' (production) — refusing to use it for tests"
)
return url
return MaskedURL(url)
def _grant_test_office_gemini(url: str) -> None:
+43
View File
@@ -0,0 +1,43 @@
"""Tests for conftest.py's own test-infrastructure fixtures.
core-api T-12: a failing database test used to print the test database
password, because pytest renders a failing test's fixture arguments with
`repr()` — no `-v` or `--showlocals` needed to trigger it, just a test that
takes `test_database_url` and fails. `MaskedURL` (conftest.py) is the fix:
same string, same connection, but a `repr()` that never shows the password.
"""
from urllib.parse import urlsplit
from tests.conftest import MaskedURL
def test_database_url_fixture_renders_without_its_password(test_database_url):
"""Mutation check: revert `MaskedURL.__repr__` to the inherited `str`
one (or hand back a plain `str` from the fixture) and this fails — the
real password would appear in `repr(test_database_url)`, exactly as it
did in a failing test's traceback before T-12."""
password = urlsplit(test_database_url).password
assert password, "fixture must carry a real password for this test to mean anything"
rendered = repr(test_database_url)
assert password not in rendered
# Host and database name are still visible — only the password is masked.
assert "***" in rendered
assert urlsplit(test_database_url).path in rendered
def test_masked_url_is_still_a_fully_usable_string():
"""The wrapper changes only `repr()`; every other string operation the
rest of the suite relies on (`.replace()`, `urlsplit()`, use as a
subprocess environment value) must see the real value unchanged."""
raw = "postgresql://core_api_test:s3cr3t@localhost:5432/core_api_test"
masked = MaskedURL(raw)
assert masked == raw
assert str(masked) == raw
assert masked.replace("postgresql://", "postgresql+asyncpg://", 1) == raw.replace(
"postgresql://", "postgresql+asyncpg://", 1
)
assert urlsplit(masked).password == "s3cr3t"
assert "s3cr3t" not in repr(masked)