"""Unit tests guarding against DB-credential leakage to logs (PR #798 round 2). The reviewer of PR #798 flagged that ``self.database_url`` was being logged verbatim in ``RefreshTokenStorage.initialize()``, exposing any password embedded in a Postgres URL to stdout/stderr and any log aggregator. These tests pin the masking down so a future contributor can't silently reintroduce the leak by adding a new ``logger.info("... %s", database_url)``. """ from __future__ import annotations import logging import pytest from nextcloud_mcp_server.auth.storage import RefreshTokenStorage from nextcloud_mcp_server.config import mask_db_password pytestmark = pytest.mark.unit # Synthetic leak-detection sentinel — embedded into test-only URLs so we # can grep ``caplog`` and prove the masking path never emits the literal # password substring. Not a real credential. SENTINEL_PASSWORD_FRAGMENT = "uniqueSecretSentinel123" # NOSONAR def test_mask_db_password_postgres(): """Postgres URL passwords are replaced with the SQLAlchemy ``***`` token.""" url = ( f"postgresql+asyncpg://mcp:{SENTINEL_PASSWORD_FRAGMENT}@db.example.com:5432/mcp" ) masked = mask_db_password(url) assert SENTINEL_PASSWORD_FRAGMENT not in masked assert "mcp" in masked # username preserved assert "db.example.com" in masked # host preserved def test_mask_db_password_sqlite_passthrough(): """SQLite URLs have no credentials; the function must not corrupt them.""" url = "sqlite+aiosqlite:////tmp/test-tokens.db" masked = mask_db_password(url) assert masked == url def test_mask_db_password_handles_unparseable_url(): """Malformed URLs fall back to a regex scrub instead of raising. A logging path that can raise is worse than a logging path that emits a less-pretty masked value — never let credentials leak just because the URL shape was unexpected. """ url = f"weird-scheme://user:{SENTINEL_PASSWORD_FRAGMENT}@host/db?ssl=disable" masked = mask_db_password(url) assert SENTINEL_PASSWORD_FRAGMENT not in masked async def test_storage_init_does_not_log_password(caplog): """Construct + initialize against a Postgres-shaped URL with a password in the URL and confirm the secret is absent from every captured log.""" # Use a sqlite URL with a fake password-shaped path — we don't need a # real Postgres up to verify the masking logic, only that no log line # ever interpolates the raw URL. A sqlite URL doesn't carry a password # so we test masking by directly invoking the masked log path with a # constructed Postgres URL via mask_db_password itself. caplog.set_level(logging.DEBUG, logger="nextcloud_mcp_server.auth.storage") caplog.set_level(logging.DEBUG, logger="nextcloud_mcp_server.migrations") import tempfile from pathlib import Path with tempfile.TemporaryDirectory() as tmp: db_path = Path(tmp) / "tokens.db" storage = RefreshTokenStorage(db_path=str(db_path), encryption_key=None) await storage.initialize() # Sanity: the sqlite path was logged at least once. assert any("token storage" in rec.message.lower() for rec in caplog.records) # The sentinel should never appear (sqlite URL has no password to leak, # but if a future change reformatted DATABASE_URL into the message it # would). Stay paranoid. for rec in caplog.records: assert SENTINEL_PASSWORD_FRAGMENT not in rec.getMessage(), ( f"Credential sentinel leaked into log: {rec.getMessage()!r}" )