''' Tests for 99-ports-available.sh entrypoint script. This script checks for port conflicts and availability. ''' import os import pathlib import subprocess import time import pytest IMAGE = os.environ.get("NETALERTX_TEST_IMAGE", "netalertx-test") GRACE_SECONDS = float(os.environ.get("NETALERTX_TEST_GRACE", "2")) CONTAINER_TARGETS = { "data": "/data", "app_db": "/data/db", "app_config": "/data/config", "app_log": "/tmp/log", "app_api": os.environ.get("NETALERTX_API", "/tmp/api"), "nginx_conf": "/tmp/nginx/active-config", "services_run": "/tmp/run", } pytestmark = [pytest.mark.docker, pytest.mark.feature_complete] @pytest.fixture(scope="function") def dummy_container(tmp_path): """Fixture that starts a dummy container to occupy ports for testing.""" # Create a simple docker-compose file for the dummy container compose_file = tmp_path / "docker-compose-dummy.yml" with open(compose_file, 'w') as f: f.write("version: '3.8'\n") f.write("services:\n") f.write(" dummy:\n") f.write(" image: alpine:latest\n") f.write(" network_mode: host\n") f.write(" userns_mode: host\n") f.write(" command: sh -c \"while true; do nc -l -p 20211 < /dev/null > /dev/null; done & while true; do nc -l -p 20212 < /dev/null > /dev/null; done & sleep 30\"\n") # Start the dummy container import subprocess result = subprocess.run( ["docker-compose", "-f", str(compose_file), "up", "-d"], capture_output=True, text=True ) if result.returncode != 0: pytest.fail(f"Failed to start dummy container: {result.stderr}") # Wait a bit for the container to start listening time.sleep(3) yield "dummy" # Cleanup subprocess.run(["docker-compose", "-f", str(compose_file), "down"], capture_output=True) def _setup_mount_tree(tmp_path: pathlib.Path, label: str) -> dict[str, pathlib.Path]: """Set up mount tree for testing.""" import shutil base = tmp_path / f"{label}_mount_root" if base.exists(): shutil.rmtree(base) base.mkdir(parents=True) paths: dict[str, pathlib.Path] = {} data_root = base / f"{label}_DATA_INTENTIONAL_NETALERTX_TEST" data_root.mkdir(parents=True, exist_ok=True) data_root.chmod(0o777) paths["data"] = data_root db_dir = data_root / "db" db_dir.mkdir(exist_ok=True) db_dir.chmod(0o777) paths["app_db"] = db_dir config_dir = data_root / "config" config_dir.mkdir(exist_ok=True) config_dir.chmod(0o777) paths["app_config"] = config_dir # Seed config and database from repository defaults when available repo_root = pathlib.Path(__file__).resolve().parents[2] config_src = repo_root / "back" / "app.conf" db_src = repo_root / "db" / "app.db" if config_src.exists(): shutil.copyfile(config_src, config_dir / "app.conf") (config_dir / "app.conf").chmod(0o600) if db_src.exists(): shutil.copyfile(db_src, db_dir / "app.db") (db_dir / "app.db").chmod(0o600) return paths def _build_volume_args(paths: dict[str, pathlib.Path]) -> list[tuple[str, str, bool]]: """Build volume arguments for docker run.""" return [(str(paths["data"]), CONTAINER_TARGETS["data"], False)] def _run_container( label: str, volumes: list[tuple[str, str, bool]] | None = None, *, env: dict[str, str] | None = None, user: str | None = None, network_mode: str | None = "host", extra_args: list[str] | None = None, ) -> subprocess.CompletedProcess[str]: """Run a container and return the result.""" import uuid import re name = f"netalertx-test-{label}-{uuid.uuid4().hex[:8]}".lower() cmd = ["docker", "run", "--rm", "--name", name] if network_mode: cmd.extend(["--network", network_mode]) cmd.extend(["--userns", "host"]) cmd.extend(["--tmpfs", "/tmp:mode=777"]) if user: cmd.extend(["--user", user]) if env: for key, value in env.items(): cmd.extend(["-e", f"{key}={value}"]) if extra_args: cmd.extend(extra_args) for host_path, target, readonly in volumes or []: mount = f"{host_path}:{target}" if readonly: mount += ":ro" cmd.extend(["-v", mount]) # Copy the script content and run it script_path = pathlib.Path("install/production-filesystem/entrypoint.d/99-ports-available.sh") with script_path.open('r', encoding='utf-8') as f: script_cont = f.read() # Use printf to avoid shell interpretation issues script = f"printf '%s\\n' '{script_cont.replace(chr(39), chr(39) + chr(92) + chr(39) + chr(39))}' > /tmp/ports-check.sh && chmod +x /tmp/ports-check.sh && sh /tmp/ports-check.sh" # noqa: E501 - inline script cmd.extend(["--entrypoint", "/bin/sh", IMAGE, "-c", script]) print(f"\n--- DOCKER CMD ---\n{' '.join(cmd)}\n--- END CMD ---\n") result = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=30, check=False, ) # Combine and clean stdout and stderr stdouterr = ( re.sub(r'\x1b\[[0-9;]*m', '', result.stdout or '') + re.sub(r'\x1b\[[0-9;]*m', '', result.stderr or '') ) result.output = stdouterr print(f"\n--- CONTAINER stdout ---\n{result.stdout}") print(f"\n--- CONTAINER stderr ---\n{result.stderr}") print(f"\n--- CONTAINER combined ---\n{result.output}") return result def _assert_contains(result, snippet: str, cmd: list[str] = None) -> None: """Assert that the result output contains the given snippet.""" if snippet not in result.output: cmd_str = " ".join(cmd) if cmd else "" raise AssertionError( f"Expected to find '{snippet}' in container output.\n" f"Got:\n{result.output}\n" f"Container command:\n{cmd_str}" ) def _assert_not_contains(result, snippet: str, cmd: list[str] = None) -> None: """Assert that the result output does not contain the given snippet.""" if snippet in result.output: cmd_str = " ".join(cmd) if cmd else "" raise AssertionError( f"Expected NOT to find '{snippet}' in container output.\n" f"Got:\n{result.output}\n" f"Container command:\n{cmd_str}" ) def test_ports_available_normal_case(tmp_path: pathlib.Path) -> None: """Test ports available script with default ports (should pass without warnings). 99. Ports Available Check: Tests that the script runs without warnings when ports 20211 and 20212 are available and not conflicting. Expected: No warnings about port conflicts or ports in use. Check script: 99-ports-available.sh """ paths = _setup_mount_tree(tmp_path, "ports_normal") volumes = _build_volume_args(paths) result = _run_container("ports-normal", volumes, user="20211:20211", env={"PORT": "99991", "GRAPHQL_PORT": "99992"}) # Should not contain any port warnings _assert_not_contains(result, "Configuration Warning: Both ports are set to") _assert_not_contains(result, "Port Warning: Application port") _assert_not_contains(result, "Port Warning: GraphQL API port") assert result.returncode == 0 def test_ports_conflict_same_number(tmp_path: pathlib.Path) -> None: """Test ports available script when both ports are set to the same number. 99. Ports Available Check: Tests warning when PORT and GRAPHQL_PORT are configured to the same value. Expected: Warning about port conflict. Check script: 99-ports-available.sh """ paths = _setup_mount_tree(tmp_path, "ports_conflict") volumes = _build_volume_args(paths) result = _run_container( "ports-conflict", volumes, user="20211:20211", env={"PORT": "20211", "GRAPHQL_PORT": "20211"} ) _assert_contains(result, "Configuration Warning: Both ports are set to 20211") _assert_contains(result, "The Application port ($PORT) and the GraphQL API port") _assert_contains(result, "are configured to use the") _assert_contains(result, "same port. This will cause a conflict.") assert result.returncode == 0 def test_ports_in_use_warning(dummy_container, tmp_path: pathlib.Path) -> None: """Test ports available script when ports are already in use. 99. Ports Available Check: Tests warning when configured ports are already bound by another process. Expected: Warning about ports being in use. Check script: 99-ports-available.sh """ paths = _setup_mount_tree(tmp_path, "ports_in_use") volumes = _build_volume_args(paths) result = _run_container( "ports-in-use", volumes, user="20211:20211", env={"PORT": "20211", "GRAPHQL_PORT": "20212"} ) _assert_contains(result, "Port Warning: Application port 20211 is already in use") _assert_contains(result, "Port Warning: GraphQL API port 20212 is already in use") assert result.returncode == 0