Newer
Older
navi-1 / tests / integration / conftest.py
"""Integration test fixtures — FastAPI app with mocked dependencies."""

from typing import AsyncGenerator

import pytest
from fastapi.testclient import TestClient

from navi.auth import User
from navi.core.events import StreamEnd, TextDelta
from navi.core.registry import BackendRegistry
from navi.core.session import InMemorySessionStore, Session
from navi.llm.base import Message
from tests.conftest_factory import FakeLLMBackend, make_profile_registry, make_registry_with_tools


class FakeAgent:
    """Deterministic agent for integration tests.

    Yields pre-configured events via run_stream().
    run() returns a fixed string.
    """

    def __init__(self, stream_events=None, run_response="Hello") -> None:
        self._stream_events = stream_events or []
        self._run_response = run_response

    async def run(self, session_id: str, user_message: str, images=None) -> str:
        return self._run_response

    async def run_stream(self, session_id, user_message, images=None, display_message=None):
        for ev in self._stream_events:
            yield ev


@pytest.fixture
def mock_deps(monkeypatch):
    """Patch navi.api.deps internal caches so FastAPI routes see mocked stores.

    We patch the module-level singletons (_session_store, _registries, etc.)
    rather than the getter functions because FastAPI's Depends() captures the
    original function object at import time; replacing the attribute on the
    module does not affect the reference stored in the route decorator.
    """
    import navi.api.deps as deps
    import navi.config as _config

    # Ensure database_url is set so _make_memory_store doesn't raise
    _config.settings.database_url = "postgresql://fake"

    store = InMemorySessionStore()
    profiles = make_profile_registry()
    tools = make_registry_with_tools()
    backends = BackendRegistry()
    backends.register("ollama", FakeLLMBackend())

    # Wire a FakePool onto the in-memory store so admin endpoints that call
    # store._get_pool() can work in integration tests.
    from tests.conftest_factory import FakeConnection, FakePool
    _fake_conn = FakeConnection()
    _fake_pool = FakePool(_fake_conn)

    async def _fake_get_pool():
        return _fake_pool

    store._get_pool = _fake_get_pool
    store._pool = _fake_pool

    # Patch internal singletons so original getter functions return our fakes
    monkeypatch.setattr(deps, "_session_store", store)
    monkeypatch.setattr(deps, "_memory_store", None)
    monkeypatch.setattr(deps, "_registries", (tools, profiles, backends, None))
    monkeypatch.setattr(deps, "_workers", [])

    # Patch auth dependencies so tests don't need real OAuth.
    # Use FastAPI's built-in dependency_overrides mechanism so the replacement
    # is respected even though Depends() captured the original callable at import time.
    fake_user = User(id="test-user", email="test@example.com", display_name="Test", role="admin", permissions=[])

    from navi.main import app
    from navi.api.deps import get_current_user, require_admin, require_permission, require_user
    from navi.auth.deps import get_current_user_ws

    app.dependency_overrides[get_current_user] = lambda: fake_user
    # get_current_user_ws is called directly inside websocket_session (no Depends),
    # so we monkeypatch the source module. Keep dependency_overrides for HTTP
    # endpoints that still use Depends(get_current_user_ws).
    async def _fake_get_user_ws(websocket):
        return fake_user
    monkeypatch.setattr("navi.auth.deps.get_current_user_ws", _fake_get_user_ws)
    monkeypatch.setattr("navi.api.deps.get_current_user_ws", _fake_get_user_ws)
    app.dependency_overrides[get_current_user_ws] = lambda: fake_user
    app.dependency_overrides[require_user] = lambda: fake_user
    app.dependency_overrides[require_admin] = lambda: fake_user
    app.dependency_overrides[require_permission] = lambda p: fake_user

    # Patch get_agent in routes that import it directly (messages.py)
    fake_agent = FakeAgent()
    monkeypatch.setattr("navi.api.routes.messages.get_agent", lambda: fake_agent)
    # websocket imports deps lazily inside the handler — no need to patch directly

    return {
        "session_store": store,
        "profiles": profiles,
        "tools": tools,
        "backends": backends,
        "agent": fake_agent,
        "fake_conn": _fake_conn,
        "fake_pool": _fake_pool,
    }


@pytest.fixture
def client(mock_deps):
    """FastAPI TestClient with mocked dependencies."""
    from navi.main import app

    return TestClient(app)


@pytest.fixture
def make_session(mock_deps):
    """Helper to create a session in the mocked store."""
    store = mock_deps["session_store"]

    async def _make(profile_id="secretary", messages=None):
        session = await store.create(profile_id)
        if messages:
            for m in messages:
                session.messages.append(m)
            await store.save(session)
        return session

    return _make