from __future__ import annotations from pathlib import Path import pytest from control_plane.runtime_plans import DjangoApplicationLaunchConfig from control_plane.runtime_plans import DjangoContainerImageBuildConfig from control_plane.runtime_plans import DjangoContainerRuntimeConfig from control_plane.runtime_plans import PostgresContainerConfig from control_plane.runtime_plans import RedisContainerConfig from control_plane.runtime_plans import build_django_container_image_command from control_plane.runtime_plans import build_django_container_run_command from control_plane.runtime_plans import build_django_migrate_command from control_plane.runtime_plans import build_django_server_command from control_plane.runtime_plans import build_postgres_container_command from control_plane.runtime_plans import build_redis_container_command def test_build_postgres_container_command_uses_hardening_flags() -> None: """PostgreSQL container plan should include core isolation and auth settings.""" command = build_postgres_container_command( PostgresContainerConfig( container_name="postgres-tenant-site", network_name="tenant-net", hostname="postgres.internal", username="site", database_name="site", data_directory=Path("/var/lib/tussilago/postgres/site"), password_file=Path("/run/tussilago/secrets/postgres-password"), ), ) assert "--cap-drop=all" in command assert "--cap-add=CHOWN" in command assert "--cap-add=FOWNER" in command assert "--cap-add=SETUID" in command assert "--cap-add=SETGID" in command assert "--cap-add=DAC_OVERRIDE" in command assert "--security-opt=no-new-privileges" in command assert "POSTGRES_PASSWORD_FILE=/run/secrets/postgres-password" in command assert "password_encryption=scram-sha-256" in command assert command[-1] == "password_encryption=scram-sha-256" def test_build_redis_container_command_requires_password() -> None: """Redis container plan should enforce password auth and readonly base image.""" command = build_redis_container_command( RedisContainerConfig( container_name="redis-tenant-site", network_name="tenant-net", hostname="redis.internal", data_directory=Path("/var/lib/tussilago/redis/site"), password_file=Path("/run/tussilago/secrets/redis-password"), ), ) assert "--read-only" in command assert "/run/secrets/redis-password" in command[-1] assert command[-1].startswith("redis_password=$(cat /run/secrets/redis-password)") def test_build_django_server_command_supports_project_reuse() -> None: """Gunicorn command should optionally reuse the repo-managed uv project.""" command = build_django_server_command( DjangoApplicationLaunchConfig( wsgi_module="tenant_site.wsgi:application", bind_host="127.0.0.1", port=9000, workers=1, uv_project_path=Path("/workspace/Tussilago"), ), ) assert command == ( "uv", "run", "--project", "/workspace/Tussilago", "gunicorn", "--bind", "127.0.0.1:9000", "--workers", "1", "--access-logfile", "-", "--error-logfile", "-", "--capture-output", "--graceful-timeout", "30", "--timeout", "60", "tenant_site.wsgi:application", ) def test_build_django_migrate_command_supports_repo_reuse() -> None: """Migration command should optionally reuse repo-managed uv dependencies.""" assert build_django_migrate_command(Path("/workspace/Tussilago")) == ( "uv", "run", "--project", "/workspace/Tussilago", "python", "manage.py", "migrate", "--noinput", ) def test_build_django_migrate_command_supports_explicit_python_executable() -> None: """Migration command should also support container-local Python execution.""" assert build_django_migrate_command(python_executable=Path("/usr/local/bin/python")) == ( "/usr/local/bin/python", "manage.py", "migrate", "--noinput", ) def test_build_django_container_image_command_targets_checked_in_containerfile() -> None: """Image build plan should reference an explicit Containerfile and context.""" command = build_django_container_image_command( DjangoContainerImageBuildConfig( image_reference="localhost/tussilago-test-django:latest", containerfile_path=Path("/workspace/Tussilago/control_plane/container_assets/test_django/Containerfile"), context_directory=Path("/workspace/Tussilago/control_plane/container_assets/test_django"), ), ) assert command == ( "podman", "build", "--pull=missing", "--tag", "localhost/tussilago-test-django:latest", "--file", "/workspace/Tussilago/control_plane/container_assets/test_django/Containerfile", "/workspace/Tussilago/control_plane/container_assets/test_django", ) def test_build_django_container_run_command_uses_publish_and_secret_mounts() -> None: """Django runtime plan should publish localhost only and mount generated assets read-only.""" command = build_django_container_run_command( DjangoContainerRuntimeConfig( container_name="django-tenant-site", network_name="tenant-net", hostname="django.internal", image_reference="localhost/tussilago-test-django:latest", application_directory=Path("/var/lib/tussilago/test-app"), host_port=19000, environment=( ("DJANGO_SECRET_KEY", "secret"), ("DJANGO_SETTINGS_MODULE", "tenant_site.settings"), ), secret_mounts=((Path("/var/lib/tussilago/postgres-secret"), "/run/postgres-secret"),), labels=(("tussilago.deployment-id", "deployment-1"),), ), command=("/usr/local/bin/python", "-m", "gunicorn"), detach=True, ) assert "--read-only" in command assert "--publish" in command assert "127.0.0.1:19000:8000" in command assert "/var/lib/tussilago/test-app:/srv/test-app:Z,ro" in command assert "/var/lib/tussilago/postgres-secret:/run/postgres-secret:Z,ro" in command assert "DJANGO_SETTINGS_MODULE=tenant_site.settings" in command assert "tussilago.deployment-id=deployment-1" in command def test_build_django_container_run_command_supports_pod_membership() -> None: """Django runtime plan should join a Podman pod instead of publishing ports when requested.""" command = build_django_container_run_command( DjangoContainerRuntimeConfig( container_name="django-tenant-site", network_name="ignored-net", hostname="django.internal", image_reference="localhost/tussilago-test-django:latest", application_directory=Path("/var/lib/tussilago/test-app"), pod_name="pod-tenant-site", host_port=19000, ), command=("/usr/local/bin/python", "-m", "gunicorn"), detach=True, ) assert "--pod" in command assert "pod-tenant-site" in command assert "--publish" not in command def test_build_django_server_command_supports_explicit_python_executable() -> None: """Local smoke flows may launch Gunicorn via an explicit Python executable.""" command = build_django_server_command( DjangoApplicationLaunchConfig( wsgi_module="tenant_site.wsgi:application", bind_host="127.0.0.1", port=9000, workers=1, python_executable=Path("/venv/bin/python"), ), ) assert command[:3] == ("/venv/bin/python", "-m", "gunicorn") def test_build_django_server_command_rejects_conflicting_python_modes() -> None: """Server command builder should reject mixed uv and direct-python execution modes.""" with pytest.raises(ValueError, match="mutually exclusive"): build_django_server_command( DjangoApplicationLaunchConfig( wsgi_module="tenant_site.wsgi:application", python_executable=Path("/venv/bin/python"), uv_project_path=Path("/workspace/Tussilago"), ), )