import os import signal import time import pytest from datetime import datetime, timezone from click.testing import CliRunner from autossh_mgr.cli import cli from autossh_mgr.config import save_tunnels, TunnelConfig, ensure_dirs from autossh_mgr.process import read_pid_file, is_process_alive, write_pid_file @pytest.fixture def fake_autossh(tmp_path, monkeypatch): """Replace autossh with a long-running dummy process.""" fake = tmp_path / "autossh" fake.write_text("#!/bin/sh\nexec sleep 60\n") fake.chmod(0o755) monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}") return fake @pytest.fixture def config_dir(tmp_path): ensure_dirs(tmp_path) return tmp_path @pytest.fixture(autouse=True) def set_config_dir(config_dir, monkeypatch): monkeypatch.setenv("AUTOSSH_MGR_CONFIG_DIR", str(config_dir)) @pytest.fixture(autouse=True) def cleanup_processes(config_dir): """Kill any processes recorded in PID files after each test.""" yield pids_dir = config_dir / "pids" if not pids_dir.exists(): return for pid_file in pids_dir.glob("*.pid"): try: lines = pid_file.read_text().splitlines() if lines: os.kill(int(lines[0]), signal.SIGKILL) except (ValueError, OSError, ProcessLookupError): pass @pytest.fixture def tunnel(config_dir): t = TunnelConfig( name="test-tunnel", host="relay.example.com", user="deploy", local_port=19999, remote_port=29999, ) save_tunnels(config_dir, [t]) return t @pytest.fixture def runner(): return CliRunner() def test_start_writes_pid_file(runner, config_dir, tunnel, fake_autossh): result = runner.invoke(cli, ["start", "test-tunnel"]) assert result.exit_code == 0, result.output pid_data = read_pid_file(config_dir, "test-tunnel") assert pid_data is not None pid, _ = pid_data assert is_process_alive(pid) def test_stop_kills_process(runner, config_dir, tunnel, fake_autossh): runner.invoke(cli, ["start", "test-tunnel"]) pid_data = read_pid_file(config_dir, "test-tunnel") assert pid_data is not None pid = pid_data[0] assert is_process_alive(pid) result = runner.invoke(cli, ["stop", "test-tunnel"]) assert result.exit_code == 0 time.sleep(0.2) assert not is_process_alive(pid) assert read_pid_file(config_dir, "test-tunnel") is None def test_status_shows_running(runner, config_dir, tunnel, fake_autossh): runner.invoke(cli, ["start", "test-tunnel"]) result = runner.invoke(cli, ["status", "test-tunnel"]) assert result.exit_code == 0 assert "running" in result.output def test_stale_pid_cleaned_on_start(runner, config_dir, tunnel, fake_autossh): write_pid_file(config_dir, "test-tunnel", 9999999, datetime.now(timezone.utc)) result = runner.invoke(cli, ["start", "test-tunnel"]) assert result.exit_code == 0, result.output assert "stale" in result.output.lower() pid_data = read_pid_file(config_dir, "test-tunnel") assert pid_data is not None assert pid_data[0] != 9999999 def test_restart(runner, config_dir, tunnel, fake_autossh): runner.invoke(cli, ["start", "test-tunnel"]) first_pid = read_pid_file(config_dir, "test-tunnel")[0] result = runner.invoke(cli, ["restart", "test-tunnel"]) assert result.exit_code == 0, result.output second_pid = read_pid_file(config_dir, "test-tunnel")[0] assert second_pid != first_pid assert is_process_alive(second_pid) assert not is_process_alive(first_pid) def test_idempotent_start(runner, config_dir, tunnel, fake_autossh): runner.invoke(cli, ["start", "test-tunnel"]) first_pid = read_pid_file(config_dir, "test-tunnel")[0] result = runner.invoke(cli, ["start", "test-tunnel"]) assert result.exit_code == 0 assert "already running" in result.output assert read_pid_file(config_dir, "test-tunnel")[0] == first_pid def test_idempotent_stop(runner, tunnel): result = runner.invoke(cli, ["stop", "test-tunnel"]) assert result.exit_code == 0 assert "not running" in result.output