"""Phase 4 Docker container integration tests. Builds Dockerfile from the project root, runs the container on host port 8767 (avoids collision with Phase 2's 8765 and Phase 3's 8766), and exercises the same HTTP endpoints as Phases 2 and 3 but through Docker's network layer. Run manually with: pytest tests/test_docker_container.py -v Skipped automatically when Docker daemon is unavailable. """ from __future__ import annotations import asyncio import json import random import subprocess import time from pathlib import Path import httpx import httpx_sse import pytest from sre_arena_env.server.sre_arena_env_environment import SreArenaEnvironment from sre_arena_env.models import DefenderAction from sre_arena_env.client import SreArenaEnvClient # ── Constants ───────────────────────────────────────────────────────────────── _IMAGE_TAG = "sre-arena-env:test" _CONTAINER_NAME = "sre-arena-test" _HOST_PORT = 8767 _BASE_HTTP = f"http://127.0.0.1:{_HOST_PORT}" _BASE_WS = f"ws://127.0.0.1:{_HOST_PORT}" _PROJECT_ROOT = Path(__file__).parent.parent # tests/ -> sre_arena_env/ pytestmark = pytest.mark.docker # ── Action helper ───────────────────────────────────────────────────────────── _RULE_POOL = [ "deny 10.0.0.1;", "deny 10.0.0.0/24;", "limit_req_zone $binary_remote_addr zone=flood:10m rate=10r/s;", "limit_req zone=flood burst=5 nodelay;", ] _MW_POOL = [ ("if (req.body.command === 'rm') return res.status(403)", "/api/process"), ("if (req.headers['X-Forwarded-For']) return res.status(403)", "/login"), ] def _action(rng: random.Random) -> DefenderAction: kind = rng.choice(["read_log", "append_nginx_rule", "write_express_middleware"]) if kind == "append_nginx_rule": return DefenderAction(action_type=kind, rule_text=rng.choice(_RULE_POOL)) if kind == "write_express_middleware": js, route = rng.choice(_MW_POOL) return DefenderAction(action_type=kind, route=route, middleware_js=js) return DefenderAction(action_type=kind) # ── Skip guard ──────────────────────────────────────────────────────────────── def _docker_available() -> bool: try: result = subprocess.run( ["docker", "version"], capture_output=True, timeout=5, ) return result.returncode == 0 except (FileNotFoundError, subprocess.TimeoutExpired): return False @pytest.fixture(scope="session", autouse=True) def require_docker() -> None: # type: ignore[return] if not _docker_available(): pytest.skip("Docker not available — skipping all docker tests") # ── Container fixture ───────────────────────────────────────────────────────── @pytest.fixture(scope="session") def docker_container(require_docker: None) -> str: """Build image, start container, wait for health, yield HTTP base URL, teardown.""" # Build image from project root (Dockerfile is at repo root since Phase 5) subprocess.run( ["docker", "build", "-t", _IMAGE_TAG, "."], cwd=_PROJECT_ROOT, check=True, timeout=300, ) # Remove any leftover container from a previous interrupted run subprocess.run( ["docker", "rm", "-f", _CONTAINER_NAME], cwd=_PROJECT_ROOT, capture_output=True, ) # Start container subprocess.run( [ "docker", "run", "-d", "-p", f"{_HOST_PORT}:8000", "--name", _CONTAINER_NAME, _IMAGE_TAG, ], check=True, timeout=30, ) try: # Wait for HTTP health (up to 30 s) deadline = time.monotonic() + 30.0 healthy = False while time.monotonic() < deadline: try: r = httpx.get(f"{_BASE_HTTP}/", timeout=1.0) if r.status_code == 200: healthy = True break except Exception: pass time.sleep(0.5) if not healthy: logs = subprocess.run( ["docker", "logs", _CONTAINER_NAME], capture_output=True, text=True, ) pytest.fail(f"Container not healthy within 30 s.\nLogs:\n{logs.stdout}\n{logs.stderr}") yield _BASE_HTTP finally: # Teardown — runs even if health check or test raises subprocess.run(["docker", "stop", _CONTAINER_NAME], timeout=15, capture_output=True) subprocess.run(["docker", "rm", _CONTAINER_NAME], timeout=10, capture_output=True) # ── Tests ───────────────────────────────────────────────────────────────────── def test_dashboard_html(docker_container: str) -> None: """GET / returns 200 with dashboard HTML containing expected markers.""" r = httpx.get(f"{docker_container}/", timeout=5.0) assert r.status_code == 200 assert "SRE Arena" in r.text assert "