agnes-the-ai-analyst/tests/test_e2e_docker.py
Petr Simecek 78ed5b31fe
fix(tests): refresh nightly docker-e2e asserts after auth + health refactors (#69)
Two assertions in the docker-marker test files had drifted from the live API
and were only caught by the scheduled nightly CI job (run #24947963804,
2026-04-26 04:12 UTC):

- tests/test_docker_full.py::test_app_returns_html_on_root expected 200 on
  GET / — but app/web/router.py:189-193 always returns 302 (to /dashboard
  for authenticated users, /login otherwise) since the auth middleware
  landed. Updated to use follow_redirects=False and assert 302 + location.

- tests/test_e2e_docker.py::TestDockerHealth::test_health_has_duckdb read
  data["checks"]["duckdb"|"database"] — but the health payload shape is
  {"services": {"duckdb_state": ..., "data": ..., "users": ...}} and has
  been since app/api/health.py was last refactored. Updated to read
  services["duckdb_state"]["status"] — same pattern used by the (passing)
  tests/test_api.py::TestHealth suite, so the two test layers now agree.

Both fixes are test-only; no application behavior changes.
2026-04-26 16:12:20 +02:00

115 lines
3.5 KiB
Python

"""E2E Docker tests — spin up containers, test API from outside.
Run with: pytest tests/test_e2e_docker.py -m docker -v
Requires: Docker and docker compose installed.
"""
import os
import subprocess
import time
import pytest
# Skip all tests in this module if docker marker not selected
pytestmark = pytest.mark.docker
COMPOSE_FILE = "docker-compose.test.yml"
BASE_URL = "http://localhost:8000"
def _docker_compose(*args, timeout=60):
"""Run docker compose command."""
cmd = ["docker", "compose", "-f", COMPOSE_FILE] + list(args)
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
def _wait_for_health(url, timeout=30):
"""Poll health endpoint until it responds 200."""
import httpx
deadline = time.time() + timeout
while time.time() < deadline:
try:
resp = httpx.get(f"{url}/api/health", timeout=5)
if resp.status_code == 200:
return True
except Exception:
pass
time.sleep(1)
return False
@pytest.fixture(scope="module")
def docker_env():
"""Start docker compose, yield, then tear down."""
# Check docker is available
result = subprocess.run(["docker", "info"], capture_output=True, timeout=10)
if result.returncode != 0:
pytest.skip("Docker not available")
# Check compose file exists
if not os.path.exists(COMPOSE_FILE):
pytest.skip(f"{COMPOSE_FILE} not found")
# Start services
_docker_compose("up", "-d", "--build")
# Wait for health
if not _wait_for_health(BASE_URL, timeout=60):
# Capture logs for debugging
logs = _docker_compose("logs")
_docker_compose("down", "-v")
pytest.fail(f"Service did not become healthy.\nLogs:\n{logs.stdout}")
yield BASE_URL
# Teardown
_docker_compose("down", "-v")
class TestDockerHealth:
def test_health_endpoint(self, docker_env):
import httpx
resp = httpx.get(f"{docker_env}/api/health")
assert resp.status_code == 200
data = resp.json()
assert data.get("status") in ("ok", "healthy")
def test_health_has_duckdb(self, docker_env):
import httpx
resp = httpx.get(f"{docker_env}/api/health")
data = resp.json()
services = data.get("services", {})
assert "duckdb_state" in services
assert services["duckdb_state"]["status"] == "ok"
class TestDockerFullFlow:
def test_register_and_query_flow(self, docker_env):
import httpx
url = docker_env
# Get auth token
resp = httpx.post(f"{url}/auth/token", json={"email": "admin@test.com"})
if resp.status_code != 200:
# Auto-create user first if needed
pytest.skip("Auth setup required — no admin user in Docker env")
token = resp.json().get("token", "")
headers = {"Authorization": f"Bearer {token}"}
# Register a table
resp = httpx.post(f"{url}/api/admin/register-table", json={
"name": "docker_test", "source_type": "keboola", "query_mode": "local",
}, headers=headers)
assert resp.status_code in (201, 409) # 409 if already exists
# Get registry
resp = httpx.get(f"{url}/api/admin/registry", headers=headers)
assert resp.status_code == 200
assert resp.json()["count"] >= 1
# Get manifest
resp = httpx.get(f"{url}/api/sync/manifest", headers=headers)
assert resp.status_code == 200
assert "tables" in resp.json()
assert "server_time" in resp.json()