182 lines
7.1 KiB
Python
182 lines
7.1 KiB
Python
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def test_backend_dockerfile_copies_package_sources_before_pip_install() -> None:
|
|
dockerfile = ROOT / "backend" / "Dockerfile"
|
|
lines = dockerfile.read_text(encoding="utf-8").splitlines()
|
|
|
|
pip_install_index = lines.index('RUN pip install --no-cache-dir ".[gis]"')
|
|
preceding = "\n".join(lines[:pip_install_index])
|
|
|
|
assert "COPY pyproject.toml README.md /app/" in preceding
|
|
assert "COPY app /app/app" in preceding
|
|
|
|
|
|
def test_backend_dockerfile_installs_approved_gis_runtime_stack() -> None:
|
|
dockerfile = (ROOT / "backend" / "Dockerfile").read_text(encoding="utf-8")
|
|
|
|
assert 'RUN pip install --no-cache-dir ".[gis]"' in dockerfile
|
|
assert "RUN python scripts/gis_import_smoke.py" in dockerfile
|
|
assert "gdal-bin" in dockerfile
|
|
assert "libgdal-dev" in dockerfile
|
|
assert "libgeos-dev" in dockerfile
|
|
assert "libproj-dev" in dockerfile
|
|
assert "proj-bin" in dockerfile
|
|
|
|
|
|
def test_backend_pyproject_exposes_gis_optional_dependency_group() -> None:
|
|
pyproject = (ROOT / "backend" / "pyproject.toml").read_text(encoding="utf-8")
|
|
|
|
assert "gis = [" in pyproject
|
|
assert '"rasterio>=1.4.3"' in pyproject
|
|
assert '"geopandas>=1.0.1"' in pyproject
|
|
assert '"pyogrio>=0.10.0"' in pyproject
|
|
assert '"ultralytics>=8.3,<9"' not in pyproject.split("gis = [", 1)[1].split("]", 1)[0]
|
|
|
|
|
|
def test_compose_does_not_require_missing_root_env_file() -> None:
|
|
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
|
|
|
assert "env_file:" not in compose
|
|
assert "DATABASE_URL: postgresql+psycopg://geointel:geointel@db:5432/geointel" in compose
|
|
|
|
|
|
def test_compose_exposes_frontend_on_host_port_1202_with_cors_origin() -> None:
|
|
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
|
env_example = (ROOT / ".env.example").read_text(encoding="utf-8")
|
|
|
|
assert '"1202:80"' in compose
|
|
assert "CORS_ORIGINS: http://localhost:1202,http://127.0.0.1:1202" in compose
|
|
assert "http://localhost:1202" in env_example
|
|
assert "http://127.0.0.1:1202" in env_example
|
|
|
|
|
|
def test_env_example_uses_runtime_env_names_read_by_backend_and_frontend() -> None:
|
|
env_example = (ROOT / ".env.example").read_text(encoding="utf-8")
|
|
|
|
assert "YOLO_ENABLED=false" in env_example
|
|
assert "YOLO_MODEL_PATH=" in env_example
|
|
assert "YOLO_MAX_TILES=100" in env_example
|
|
assert "ENABLE_YOLO" not in env_example
|
|
assert "ENABLE_SAM" not in env_example
|
|
assert "VITE_API_BASE_URL=" in env_example
|
|
assert "VITE_API_PROXY_TARGET=http://localhost:8000" in env_example
|
|
|
|
|
|
def test_frontend_uses_same_origin_api_proxy_by_default() -> None:
|
|
api_client = (ROOT / "frontend" / "src" / "services" / "api" / "client.ts").read_text(encoding="utf-8")
|
|
nginx_config = (ROOT / "frontend" / "nginx.conf").read_text(encoding="utf-8")
|
|
dockerfile = (ROOT / "frontend" / "Dockerfile").read_text(encoding="utf-8")
|
|
|
|
assert '?? ""' in api_client
|
|
assert "http://localhost:8000" not in api_client
|
|
assert "FROM nginx:" in dockerfile
|
|
assert "COPY --from=build /app/dist /usr/share/nginx/html" in dockerfile
|
|
assert "location /api/" in nginx_config
|
|
assert "proxy_pass http://backend:8000/api/" in nginx_config
|
|
assert "location = /health" in nginx_config
|
|
assert 'add_header Cache-Control "no-cache"' in nginx_config
|
|
assert "location /assets/" in nginx_config
|
|
assert "try_files $uri $uri/ /index.html" in nginx_config
|
|
|
|
|
|
def test_compose_does_not_publish_postgis_on_default_host_port() -> None:
|
|
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
|
|
|
assert '"5432:5432"' not in compose
|
|
|
|
|
|
def test_compose_waits_for_healthy_database_and_applies_migrations() -> None:
|
|
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
|
|
|
assert "pg_isready -U geointel -d geointel" in compose
|
|
assert "condition: service_healthy" in compose
|
|
assert "sh /app/docker_start.sh" in compose
|
|
|
|
|
|
def test_compose_mounts_demo_fixtures_for_backend_runtime() -> None:
|
|
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
|
|
|
assert "./fixtures:/app/fixtures:ro" in compose
|
|
|
|
|
|
def test_compose_has_backend_and_frontend_healthchecks() -> None:
|
|
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
|
|
|
assert "http://127.0.0.1:8000/health" in compose
|
|
assert "urllib.request.urlopen" in compose
|
|
assert "http://127.0.0.1/health" in compose
|
|
assert "wget -q -O -" in compose
|
|
assert "start_period: 30s" in compose
|
|
assert "start_period: 10s" in compose
|
|
|
|
|
|
def test_frontend_waits_for_healthy_backend_in_compose() -> None:
|
|
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
|
frontend_section = compose.split(" frontend:", 1)[1]
|
|
|
|
assert "backend:" in frontend_section
|
|
assert "condition: service_healthy" in frontend_section
|
|
|
|
|
|
def test_backend_docker_start_script_waits_for_sql_connection_before_migrations() -> None:
|
|
script = (ROOT / "backend" / "docker_start.sh").read_text(encoding="utf-8")
|
|
|
|
assert "Waiting for database connection" in script
|
|
assert "create_engine(settings.database_url" in script
|
|
assert "SELECT 1" in script
|
|
assert "python -m alembic upgrade head" in script
|
|
assert "uvicorn app.main:app --host 0.0.0.0 --port 8000" in script
|
|
|
|
|
|
def test_docker_build_contexts_exclude_vendor_build_and_cache_outputs() -> None:
|
|
required_patterns = {
|
|
"node_modules",
|
|
"dist",
|
|
"__pycache__",
|
|
"*.pyc",
|
|
".pytest_cache",
|
|
}
|
|
|
|
for relative_path in ("backend/.dockerignore", "frontend/.dockerignore"):
|
|
content = (ROOT / relative_path).read_text(encoding="utf-8")
|
|
for pattern in required_patterns:
|
|
assert pattern in content
|
|
|
|
|
|
def test_browser_runtime_verification_script_detects_proxy_contract() -> None:
|
|
script = (ROOT / "scripts" / "verify_browser_runtime.sh").read_text(encoding="utf-8")
|
|
|
|
assert "/api/v1/projects" in script
|
|
assert "<!doctype html" in script
|
|
assert "canonical GeoIntel envelope" in script
|
|
assert '"status":"ok"' in script
|
|
|
|
|
|
def test_gis_runtime_verification_script_detects_required_capabilities() -> None:
|
|
script = (ROOT / "scripts" / "verify_gis_runtime.sh").read_text(encoding="utf-8")
|
|
|
|
assert "/api/v1/system/capabilities" in script
|
|
assert '"postgis":true' in script
|
|
assert '"rasterio":true' in script
|
|
assert '"geopandas":true' in script
|
|
assert "<!doctype html" in script
|
|
|
|
|
|
def test_gis_import_smoke_script_checks_runtime_imports() -> None:
|
|
docker_script = (ROOT / "backend" / "scripts" / "gis_import_smoke.py").read_text(encoding="utf-8")
|
|
root_wrapper = (ROOT / "scripts" / "gis_import_smoke.py").read_text(encoding="utf-8")
|
|
|
|
assert 'REQUIRED_MODULES = ("rasterio", "geopandas", "pyogrio")' in docker_script
|
|
assert "importlib.import_module" in docker_script
|
|
assert '"gis_imports"' in docker_script
|
|
assert 'ROOT / "backend" / "scripts"' in root_wrapper
|
|
assert "from gis_import_smoke import main" in root_wrapper
|
|
|
|
|
|
def test_backend_docker_context_contains_gis_import_smoke_script() -> None:
|
|
assert (ROOT / "backend" / "scripts" / "gis_import_smoke.py").exists()
|