From fecb4fa05e8788aefa971a7fed5220f2d3bf4cbb Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 2 Jul 2026 17:54:31 +0200 Subject: [PATCH] test(automations): add integration harness --- .../tests/integration/automations/__init__.py | 0 .../tests/integration/automations/conftest.py | 88 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 surfsense_backend/tests/integration/automations/__init__.py create mode 100644 surfsense_backend/tests/integration/automations/conftest.py diff --git a/surfsense_backend/tests/integration/automations/__init__.py b/surfsense_backend/tests/integration/automations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/surfsense_backend/tests/integration/automations/conftest.py b/surfsense_backend/tests/integration/automations/conftest.py new file mode 100644 index 000000000..74c9e1808 --- /dev/null +++ b/surfsense_backend/tests/integration/automations/conftest.py @@ -0,0 +1,88 @@ +"""Shared fixtures for automations integration tests. + +Bridges the code-under-test to the transactional ``db_session`` so real +behavior runs against real Postgres and rolls back at test end: + +* ``client`` — httpx over ASGI with ``get_async_session``/``get_auth_context`` + overridden to the test session + owner. +* ``tools_use_test_session`` — the watch tools open their own + ``async_session_maker()``; repoint it at ``db_session`` so they join the + test transaction (and can see the fixture's uncommitted membership/models). +* ``enqueue_spy`` — capture ``automation_run_execute.apply_async`` so run-now + can be asserted without a Redis broker. +""" + +from __future__ import annotations + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager + +import httpx +import pytest +import pytest_asyncio +from httpx import ASGITransport +from sqlalchemy.ext.asyncio import AsyncSession + +from app.app import app +from app.auth.context import AuthContext +from app.db import User, get_async_session +from app.users import get_auth_context + + +@pytest_asyncio.fixture +async def client( + db_session: AsyncSession, db_user: User +) -> AsyncGenerator[httpx.AsyncClient, None]: + async def override_session() -> AsyncGenerator[AsyncSession, None]: + yield db_session + + async def override_auth() -> AuthContext: + return AuthContext.session(db_user) + + previous = app.dependency_overrides.copy() + app.dependency_overrides[get_async_session] = override_session + app.dependency_overrides[get_auth_context] = override_auth + try: + async with httpx.AsyncClient( + transport=ASGITransport(app=app), + base_url="http://test", + timeout=30.0, + follow_redirects=False, + ) as c: + yield c + finally: + app.dependency_overrides.clear() + app.dependency_overrides.update(previous) + + +@pytest.fixture +def tools_use_test_session(monkeypatch, db_session: AsyncSession) -> None: + """Make the watch tools' ``async_session_maker()`` yield the test session.""" + + @asynccontextmanager + async def _session_cm(): + yield db_session # owned by the outer fixture; do not close + + from app.agents.chat.multi_agent_chat.subagents.builtins.intelligence_agent.tools import ( + refresh_watch, + start_watch, + stop_watch, + ) + + for module in (start_watch, stop_watch, refresh_watch): + monkeypatch.setattr(module, "async_session_maker", _session_cm) + + +@pytest.fixture +def enqueue_spy(monkeypatch) -> list[dict]: + """Capture Celery enqueues so run-now needs no broker.""" + import app.automations.dispatch.launch as launch_mod + + calls: list[dict] = [] + + def _spy(*args, **kwargs): + calls.append({"args": args, "kwargs": kwargs}) + return None + + monkeypatch.setattr(launch_mod.automation_run_execute, "apply_async", _spy) + return calls