Create docker/entrypoint.sh to run alembic migrations on API startup. Create backend/worker.py with Celery app config for the compose worker service. Fix README single-container port (8000) and add production compose documentation. Add 27 tests (stack integration + worker) verifying all Docker/compose artifacts are present, consistent, and the /health endpoint responds correctly.
138 lines
4.4 KiB
Python
138 lines
4.4 KiB
Python
"""Stack integration verification tests.
|
|
|
|
These tests verify that all configuration files needed for 'docker compose up'
|
|
are present, consistent, and well-formed. They do NOT start actual containers.
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2] # repo root
|
|
|
|
|
|
class TestDockerComposeConfig:
|
|
"""Verify docker-compose.yml references are satisfied."""
|
|
|
|
def test_docker_compose_exists(self):
|
|
assert (ROOT / "docker-compose.yml").is_file()
|
|
|
|
def test_dockerfile_exists(self):
|
|
assert (ROOT / "docker" / "Dockerfile").is_file()
|
|
|
|
def test_nginx_conf_exists(self):
|
|
assert (ROOT / "docker" / "nginx.conf").is_file()
|
|
|
|
def test_entrypoint_exists(self):
|
|
assert (ROOT / "docker" / "entrypoint.sh").is_file()
|
|
|
|
def test_requirements_txt_exists(self):
|
|
assert (ROOT / "backend" / "requirements.txt").is_file()
|
|
|
|
def test_alembic_ini_exists(self):
|
|
assert (ROOT / "alembic.ini").is_file()
|
|
|
|
def test_alembic_env_exists(self):
|
|
assert (ROOT / "alembic" / "env.py").is_file()
|
|
|
|
def test_alembic_has_migration(self):
|
|
versions = list((ROOT / "alembic" / "versions").glob("*.py"))
|
|
assert len(versions) >= 1, "Expected at least one Alembic migration"
|
|
|
|
|
|
class TestDockerfileConsistency:
|
|
"""Verify Dockerfile references match actual files."""
|
|
|
|
def test_dockerfile_copies_backend(self):
|
|
content = (ROOT / "docker" / "Dockerfile").read_text()
|
|
assert "COPY backend/" in content
|
|
|
|
def test_dockerfile_copies_alembic(self):
|
|
content = (ROOT / "docker" / "Dockerfile").read_text()
|
|
assert "COPY alembic/" in content
|
|
assert "COPY alembic.ini" in content
|
|
|
|
def test_dockerfile_copies_entrypoint(self):
|
|
content = (ROOT / "docker" / "Dockerfile").read_text()
|
|
assert "entrypoint.sh" in content
|
|
|
|
def test_dockerfile_runs_migrations_via_entrypoint(self):
|
|
content = (ROOT / "docker" / "entrypoint.sh").read_text()
|
|
assert "alembic upgrade head" in content
|
|
|
|
|
|
class TestNginxConfig:
|
|
"""Verify nginx proxies correctly."""
|
|
|
|
def test_nginx_proxies_api(self):
|
|
content = (ROOT / "docker" / "nginx.conf").read_text()
|
|
assert "proxy_pass http://promptlooper-api:8000" in content
|
|
|
|
def test_nginx_proxies_websocket(self):
|
|
content = (ROOT / "docker" / "nginx.conf").read_text()
|
|
assert "upgrade" in content.lower()
|
|
|
|
def test_nginx_serves_spa_fallback(self):
|
|
content = (ROOT / "docker" / "nginx.conf").read_text()
|
|
assert "try_files" in content
|
|
assert "/index.html" in content
|
|
|
|
|
|
class TestFrontendBuildability:
|
|
"""Verify frontend has all files needed for a build."""
|
|
|
|
def test_package_json_exists(self):
|
|
assert (ROOT / "frontend" / "package.json").is_file()
|
|
|
|
def test_index_html_exists(self):
|
|
assert (ROOT / "frontend" / "index.html").is_file()
|
|
|
|
def test_main_tsx_exists(self):
|
|
assert (ROOT / "frontend" / "src" / "main.tsx").is_file()
|
|
|
|
def test_app_tsx_exists(self):
|
|
assert (ROOT / "frontend" / "src" / "App.tsx").is_file()
|
|
|
|
def test_all_page_components_exist(self):
|
|
pages = [
|
|
"SetupPage", "LoginPage", "DashboardPage", "ProjectsPage",
|
|
"ExperimentPage", "LivePage", "ComparePage", "AdminPage",
|
|
]
|
|
for page in pages:
|
|
assert (ROOT / "frontend" / "src" / "pages" / f"{page}.tsx").is_file(), f"Missing {page}.tsx"
|
|
|
|
def test_vite_config_exists(self):
|
|
assert (ROOT / "frontend" / "vite.config.ts").is_file()
|
|
|
|
def test_tailwind_config_exists(self):
|
|
assert (ROOT / "frontend" / "tailwind.config.js").is_file()
|
|
|
|
|
|
class TestWorkerConfig:
|
|
"""Verify Celery worker module exists and is importable."""
|
|
|
|
def test_worker_module_exists(self):
|
|
assert (ROOT / "backend" / "worker.py").is_file()
|
|
|
|
|
|
class TestHealthEndpoint:
|
|
"""Verify /health endpoint works in test mode."""
|
|
|
|
def test_health_returns_ok(self):
|
|
from fastapi.testclient import TestClient
|
|
|
|
# Ensure backend is importable
|
|
import sys
|
|
backend_dir = str(ROOT / "backend")
|
|
if backend_dir not in sys.path:
|
|
sys.path.insert(0, backend_dir)
|
|
|
|
from main import app
|
|
client = TestClient(app)
|
|
resp = client.get("/health")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["status"] in ("ok", "degraded")
|
|
assert "database" in data
|
|
assert "redis" in data
|