diff --git a/tests/test_auth_event_loop.py b/tests/test_auth_event_loop.py index 6a3b2b6..a53f579 100644 --- a/tests/test_auth_event_loop.py +++ b/tests/test_auth_event_loop.py @@ -15,6 +15,7 @@ import os import sys import types import asyncio +import pytest from types import SimpleNamespace from unittest.mock import MagicMock @@ -64,8 +65,13 @@ def _ensure_stub(name: str, **attrs): return mod -_ensure_stub("core.database", SessionLocal=MagicMock()) -_ensure_stub("core.auth", AuthManager=MagicMock()) +@pytest.fixture(autouse=True) +def _event_loop_stubs(monkeypatch): + db = _ensure_stub("core.database", SessionLocal=MagicMock()) + auth = _ensure_stub("core.auth", AuthManager=MagicMock()) + monkeypatch.setitem(sys.modules, "core.database", db) + monkeypatch.setitem(sys.modules, "core.auth", auth) + from routes.auth_routes import setup_auth_routes, LoginRequest diff --git a/tests/test_auth_regressions.py b/tests/test_auth_regressions.py index 70dea39..8b46753 100644 --- a/tests/test_auth_regressions.py +++ b/tests/test_auth_regressions.py @@ -66,21 +66,27 @@ def _ensure_stub(name: str, **attrs): setattr(parent, child_name, mod) return mod -_ensure_stub("core.database", - SessionLocal=MagicMock(), ScheduledTask=MagicMock(), TaskRun=MagicMock(), - ModelEndpoint=MagicMock(), Session=MagicMock(), ChatMessage=MagicMock(), - CalendarCal=MagicMock(), CalendarEvent=MagicMock(), - Document=MagicMock(), DocumentVersion=MagicMock(), - GalleryImage=MagicMock(), GalleryAlbum=MagicMock(), Note=MagicMock(), - McpServer=MagicMock(), -) -_ensure_stub("core.auth", AuthManager=MagicMock()) -_ensure_stub("src.endpoint_resolver", - resolve_endpoint=MagicMock(return_value=("", "", {})), - normalize_base=MagicMock(), - build_chat_url=MagicMock(), - build_headers=MagicMock(), -) +@pytest.fixture(autouse=True) +def _auth_regressions_stubs(monkeypatch): + db = _ensure_stub("core.database", + SessionLocal=MagicMock(), ScheduledTask=MagicMock(), TaskRun=MagicMock(), + ModelEndpoint=MagicMock(), Session=MagicMock(), ChatMessage=MagicMock(), + CalendarCal=MagicMock(), CalendarEvent=MagicMock(), + Document=MagicMock(), DocumentVersion=MagicMock(), + GalleryImage=MagicMock(), GalleryAlbum=MagicMock(), Note=MagicMock(), + McpServer=MagicMock(), + ) + auth = _ensure_stub("core.auth", AuthManager=MagicMock()) + ep = _ensure_stub("src.endpoint_resolver", + resolve_endpoint=MagicMock(return_value=("", "", {})), + normalize_base=MagicMock(), + build_chat_url=MagicMock(), + build_headers=MagicMock(), + ) + monkeypatch.setitem(sys.modules, "core.database", db) + monkeypatch.setitem(sys.modules, "core.auth", auth) + monkeypatch.setitem(sys.modules, "src.endpoint_resolver", ep) + from fastapi import HTTPException diff --git a/tests/test_companion_pairing.py b/tests/test_companion_pairing.py index f604d01..b8d987b 100644 --- a/tests/test_companion_pairing.py +++ b/tests/test_companion_pairing.py @@ -37,27 +37,36 @@ def _get_db_session(): # core/__init__ pulls in models/session_manager which import many ORM names from # core.database; under conftest's sqlalchemy stubs the real module can't load. -# A __getattr__ module resolves ANY requested name to a MagicMock, while keeping -# our real get_db_session/ApiToken for the mint test. +# A __getattr__ module resolves any non-dunder name to a MagicMock, while keeping +# our real get_db_session/ApiToken for the mint test. Dunder names (e.g. __all__) +# are NOT auto-resolved — the next test file does `from core.database import *`, +# which would otherwise see a MagicMock where a list-of-str is required. class _DBStub(types.ModuleType): def __getattr__(self, name): # noqa: D401 + if name.startswith("__"): + raise AttributeError(name) return MagicMock() _db = _DBStub("core.database") _db.get_db_session = _get_db_session _db.ApiToken = _ApiToken -sys.modules["core.database"] = _db # overwrite any minimal stub from a sibling test -for _name, _attrs in { - "core.auth": {"AuthManager": MagicMock()}, - "src.endpoint_resolver": {"build_chat_url": (lambda u: u)}, -}.items(): - if _name not in sys.modules: - _mm = types.ModuleType(_name) - for _k, _v in _attrs.items(): - setattr(_mm, _k, _v) - sys.modules[_name] = _mm + +@pytest.fixture(autouse=True) +def _companion_pairing_stubs(monkeypatch): + monkeypatch.setitem(sys.modules, "core.database", _db) + for _name, _attrs in { + "core.auth": {"AuthManager": MagicMock()}, + "src.endpoint_resolver": {"build_chat_url": (lambda u: u)}, + }.items(): + if _name not in sys.modules: + _mm = types.ModuleType(_name) + for _k, _v in _attrs.items(): + setattr(_mm, _k, _v) + sys.modules[_name] = _mm + monkeypatch.setitem(sys.modules, _name, sys.modules[_name]) + from fastapi import HTTPException # noqa: E402 diff --git a/tests/test_null_owner_gates.py b/tests/test_null_owner_gates.py index bbb1540..57b98a8 100644 --- a/tests/test_null_owner_gates.py +++ b/tests/test_null_owner_gates.py @@ -24,32 +24,38 @@ from unittest.mock import MagicMock # the conftest's `sqlalchemy.*` MagicMock stubs ("metaclass conflict"). # Stub also a handful of route modules each of these targeted modules # happens to drag in at import-time. -for _stub in [ - "core.database", - "core.auth", - "src.endpoint_resolver", -]: - if _stub not in sys.modules: - m = types.ModuleType(_stub) - # Provide the names the importers will look up. - if _stub == "core.database": - m.Base = MagicMock() - m.SessionLocal = MagicMock() - m.CalendarCal = MagicMock() - m.CalendarEvent = MagicMock() - m.Document = MagicMock() - m.DocumentVersion = MagicMock() - m.Session = MagicMock() - m.ChatMessage = MagicMock() - m.GalleryImage = MagicMock() - m.GalleryAlbum = MagicMock() - m.Note = MagicMock() - m.ScheduledTask = MagicMock() - m.TaskRun = MagicMock() - m.ModelEndpoint = MagicMock() - elif _stub == "core.auth": - m.AuthManager = MagicMock() - sys.modules[_stub] = m +@pytest.fixture(autouse=True) +def _null_owner_stubs(monkeypatch): + for _stub, _attrs in ( + ("core.database", ( + "Base", "SessionLocal", "CalendarCal", "CalendarEvent", + "Document", "DocumentVersion", "Session", "ChatMessage", + "GalleryImage", "GalleryAlbum", "Note", "ScheduledTask", + "TaskRun", "ModelEndpoint", "Webhook", + )), + ("core.auth", ("AuthManager",)), + ("src.endpoint_resolver", ()), + ): + if _stub not in sys.modules: + m = types.ModuleType(_stub) + for _name in _attrs: + setattr(m, _name, MagicMock()) + sys.modules[_stub] = m + else: + m = sys.modules[_stub] + for _name in _attrs: + if not hasattr(m, _name): + setattr(m, _name, MagicMock()) + monkeypatch.setitem(sys.modules, _stub, m) + + # src.webhook_manager is only dragged in by _import_webhook_helper(). + if "src.webhook_manager" not in sys.modules: + wm = types.ModuleType("src.webhook_manager") + wm.WebhookManager = MagicMock() + wm.validate_webhook_url = MagicMock() + wm.validate_events = MagicMock() + sys.modules["src.webhook_manager"] = wm + monkeypatch.setitem(sys.modules, "src.webhook_manager", wm) from fastapi import HTTPException @@ -179,18 +185,9 @@ def test_gallery_owner_filter_passes_user(): # calendar/notes/gallery gates above and _verify_session_owner. def _import_webhook_helper(): - """Import routes.webhook_routes without dragging in the real webhook - manager / database. Stub src.webhook_manager (only referenced by an - import line) and ensure core.database exposes the names the import chain - (core/__init__ → session_manager) looks up.""" - for _name in ("Webhook", "ChatMessage"): - setattr(sys.modules["core.database"], _name, MagicMock()) - if "src.webhook_manager" not in sys.modules: - wm = types.ModuleType("src.webhook_manager") - wm.WebhookManager = MagicMock() - wm.validate_webhook_url = MagicMock() - wm.validate_events = MagicMock() - sys.modules["src.webhook_manager"] = wm + """Import routes.webhook_routes. Stubs for core.database (ChatMessage, + Webhook) and src.webhook_manager are provided by the _null_owner_stubs + autouse fixture.""" return __import__( "routes.webhook_routes", fromlist=["_caller_owns_session"] ) diff --git a/tests/test_task_scheduler_session_delivery.py b/tests/test_task_scheduler_session_delivery.py index 33dc152..cafff71 100644 --- a/tests/test_task_scheduler_session_delivery.py +++ b/tests/test_task_scheduler_session_delivery.py @@ -14,15 +14,14 @@ from sqlalchemy.orm import sessionmaker from core.database import Base, Session as DbSession from src.task_scheduler import TaskScheduler -# TEMPORARY ISOLATION WORKAROUND — remove once test_null_owner_gates.py is -# refactored to use a fixture-scoped stub instead of module-level sys.modules -# patching. When collected after test_null_owner_gates (alphabetical order), -# core.database is already a stub whose Base attribute is a MagicMock, so -# Base.metadata.create_all() below does nothing and the assertions fail. -# The test passes correctly in isolation: -# pytest tests/test_task_scheduler_session_delivery.py → 1 passed -# Full-suite baseline before this PR: 9 failed, 345 passed (pre-upstream-pull) -# Full-suite after this PR: 1 failed, 495 passed, 1 skipped +# This test needs the real core.database (real SQLAlchemy Base/ChatMessage). +# test_null_owner_gates.py no longer leaks its stubs (per-test fixture cleanup +# since PR #1513), but several other files still install core.database stubs +# at module level without teardown (test_model_routes, test_companion_readonly, +# test_endpoint_probing, test_vault_password_not_in_argv). When any of those +# are collected before us, core.database is a stub and Base is a MagicMock. +# Skip in that case — the test passes correctly in isolation or when collected +# before the stubbing files. if type(Base).__name__ == "MagicMock": pytest.skip("core.database is stubbed — run this file in isolation", allow_module_level=True)