test_web_api_factory_smoke.py 5.84 KB
"""Minimal factory/bootstrap smoke tests for the web API."""

from __future__ import annotations

import pytest

from apps.web_api.bootstrap.runtime import (
    build_http_route_dependencies,
    build_runtime_services,
    build_socket_event_dependencies,
)
from apps.web_api.factory import SECRET_KEY, create_app
from apps.web_api.interfaces import register_http_routes, register_socketio_handlers


@pytest.fixture()
def runtime_app_bundle(isolated_research_task_store):
    """Create a fresh factory app with runtime routes and socket handlers wired."""
    app, socketio = create_app()
    runtime_services = build_runtime_services(
        socketio,
        research_task_store=isolated_research_task_store.store,
    )
    route_dependencies = build_http_route_dependencies(
        runtime_services=runtime_services,
        frontend_dev_server_url=lambda: None,
        socketio=socketio,
        submit_search_request=lambda **_: ({"success": True, "message": "ok"}, 200),
    )

    register_http_routes(app, route_dependencies)
    socket_event_dependencies = build_socket_event_dependencies(
        runtime_services=runtime_services,
    )
    register_socketio_handlers(socketio, socket_event_dependencies)
    app.config.update(TESTING=True)

    return {
        "app": app,
        "socketio": socketio,
        "runtime_services": runtime_services,
    }


def test_factory_app_exposes_blueprints_and_runtime_routes(runtime_app_bundle):
    app = runtime_app_bundle["app"]
    socketio = runtime_app_bundle["socketio"]
    client = app.test_client()

    route_rules = {rule.rule for rule in app.url_map.iter_rules()}
    assert app.config["SECRET_KEY"] == SECRET_KEY
    assert socketio.server is not None
    assert "/api/config" in route_rules
    assert "/api/research/options" in route_rules
    assert "/api/research-tasks" in route_rules
    assert "/api/research-tasks/task-views" in route_rules
    assert "/api/research-tasks/<task_id>" in route_rules
    assert "/api/research-tasks/<task_id>/activate" in route_rules
    assert "/api/research-tasks/<task_id>/analysis-run" in route_rules
    assert "/api/research-tasks/<task_id>/analysis" in route_rules
    assert "/api/research-tasks/<task_id>/analysis-runs" in route_rules
    assert "/api/research-tasks/<task_id>/crawler" in route_rules
    assert "/api/research-tasks/<task_id>/report" in route_rules
    assert "/api/crawler/options" in route_rules
    assert "/api/crawler/jobs" in route_rules
    assert "/api/crawler/jobs/<job_id>" in route_rules
    assert "/" in route_rules
    assert "/api/status" in route_rules
    assert "/api/system/status" in route_rules

    index_response = client.get("/")
    assert index_response.status_code == 200

    status_response = client.get("/api/system/status")
    assert status_response.status_code == 200
    assert status_response.get_json() == {
        "success": True,
        "started": False,
        "starting": False,
    }

    task_views_response = client.get("/api/research-tasks/task-views")
    assert task_views_response.status_code == 200
    assert task_views_response.get_json() == {
        "success": True,
        "active_task_id": None,
        "task_views": [],
    }


def test_factory_app_status_route_uses_injected_forum_registry_state(runtime_app_bundle, monkeypatch):
    app = runtime_app_bundle["app"]
    runtime_services = runtime_app_bundle["runtime_services"]
    client = app.test_client()

    runtime_services.process_registry.set_status("forum", "running")
    monkeypatch.setattr(
        runtime_services.process_manager,
        "check_app_status",
        lambda: None,
    )

    response = client.get("/api/status")

    assert response.status_code == 200
    payload = response.get_json()
    assert payload["forum"] == {
        "status": "running",
        "port": None,
        "output_lines": 0,
    }


def test_factory_app_crawler_jobs_route_smoke(runtime_app_bundle, monkeypatch):
    import backend.crawler.routes as crawler_routes

    class _FakeCrawlerAppService:
        def build_crawler_jobs_payload(self):
            return {
                "current_job": {"id": "crawl-2"},
                "jobs": [{"id": "crawl-2"}, {"id": "crawl-1"}],
            }

    monkeypatch.setattr(
        crawler_routes,
        "CRAWLER_APP_SERVICE",
        _FakeCrawlerAppService(),
        raising=False,
    )

    client = runtime_app_bundle["app"].test_client()
    response = client.get("/api/crawler/jobs")

    assert response.status_code == 200
    assert response.get_json() == {
        "success": True,
        "current_job": {"id": "crawl-2"},
        "jobs": [{"id": "crawl-2"}, {"id": "crawl-1"}],
    }


def test_socket_handlers_attach_cleanly_to_factory_app(runtime_app_bundle, monkeypatch):
    app = runtime_app_bundle["app"]
    socketio = runtime_app_bundle["socketio"]
    runtime_services = runtime_app_bundle["runtime_services"]

    handlers = getattr(socketio.server, "handlers", {}).get("/", {})
    assert "connect" in handlers
    assert "request_status" in handlers

    socket_client = socketio.test_client(app, flask_test_client=app.test_client())
    assert socket_client.is_connected()

    connect_events = socket_client.get_received()
    assert any(event["name"] == "status" for event in connect_events)

    runtime_services.process_registry.set_status("query", "running")
    runtime_services.process_registry.set_status("forum", "starting")
    monkeypatch.setattr(
        runtime_services.process_manager,
        "check_app_status",
        lambda: None,
    )

    socket_client.emit("request_status")
    status_events = socket_client.get_received()
    status_update = next(event for event in status_events if event["name"] == "status_update")
    snapshot = status_update["args"][0]

    assert snapshot["query"] == {"status": "running", "port": 8503}
    assert snapshot["forum"] == {"status": "starting", "port": None}

    socket_client.disconnect()