# ABOUTME: End-to-end tests for serve CLI command. # ABOUTME: Tests server setup and argument handling via mocked uvicorn. import os import subprocess import sys from pathlib import Path from unittest.mock import MagicMock, patch import pytest from animaltrack.cli import main # Get the project root directory (parent of tests/) PROJECT_ROOT = Path(__file__).parent.parent class TestServeCLI: """Tests for serve command.""" def test_serve_runs_migrations_first(self, tmp_path, monkeypatch): """Serve should run migrations before starting server.""" db_path = tmp_path / "test.db" monkeypatch.setenv("DB_PATH", str(db_path)) monkeypatch.setenv("CSRF_SECRET", "test-secret-for-csrf") # Mock uvicorn.run to prevent server from starting with patch("uvicorn.run") as mock_uvicorn: with patch("animaltrack.migrations.run_migrations") as mock_migrations: mock_migrations.return_value = True monkeypatch.setattr("sys.argv", ["animaltrack", "serve"]) main() # Verify migrations were called mock_migrations.assert_called_once() # Verify uvicorn was called after migrations mock_uvicorn.assert_called_once() def test_serve_creates_app(self, tmp_path, monkeypatch): """Serve should create the app before starting.""" db_path = tmp_path / "test.db" monkeypatch.setenv("DB_PATH", str(db_path)) monkeypatch.setenv("CSRF_SECRET", "test-secret-for-csrf") with patch("uvicorn.run") as mock_uvicorn: with patch("animaltrack.migrations.run_migrations") as mock_migrations: mock_migrations.return_value = True with patch("animaltrack.web.app.create_app") as mock_create_app: mock_app = MagicMock() mock_create_app.return_value = (mock_app, None) monkeypatch.setattr("sys.argv", ["animaltrack", "serve"]) main() # Verify create_app was called mock_create_app.assert_called_once() # Verify uvicorn got the app mock_uvicorn.assert_called_once() call_kwargs = mock_uvicorn.call_args assert call_kwargs[0][0] == mock_app def test_serve_uses_default_port(self, tmp_path, monkeypatch): """Serve should use port 3366 by default.""" db_path = tmp_path / "test.db" monkeypatch.setenv("DB_PATH", str(db_path)) monkeypatch.setenv("CSRF_SECRET", "test-secret-for-csrf") with patch("uvicorn.run") as mock_uvicorn: with patch("animaltrack.migrations.run_migrations") as mock_migrations: mock_migrations.return_value = True with patch("animaltrack.web.app.create_app") as mock_create_app: mock_app = MagicMock() mock_create_app.return_value = (mock_app, None) monkeypatch.setattr("sys.argv", ["animaltrack", "serve"]) main() call_kwargs = mock_uvicorn.call_args assert call_kwargs[1]["port"] == 3366 def test_serve_custom_port(self, tmp_path, monkeypatch): """Serve should accept custom port.""" db_path = tmp_path / "test.db" monkeypatch.setenv("DB_PATH", str(db_path)) monkeypatch.setenv("CSRF_SECRET", "test-secret-for-csrf") with patch("uvicorn.run") as mock_uvicorn: with patch("animaltrack.migrations.run_migrations") as mock_migrations: mock_migrations.return_value = True with patch("animaltrack.web.app.create_app") as mock_create_app: mock_app = MagicMock() mock_create_app.return_value = (mock_app, None) monkeypatch.setattr("sys.argv", ["animaltrack", "serve", "--port", "8080"]) main() call_kwargs = mock_uvicorn.call_args assert call_kwargs[1]["port"] == 8080 def test_serve_custom_host(self, tmp_path, monkeypatch): """Serve should accept custom host.""" db_path = tmp_path / "test.db" monkeypatch.setenv("DB_PATH", str(db_path)) monkeypatch.setenv("CSRF_SECRET", "test-secret-for-csrf") with patch("uvicorn.run") as mock_uvicorn: with patch("animaltrack.migrations.run_migrations") as mock_migrations: mock_migrations.return_value = True with patch("animaltrack.web.app.create_app") as mock_create_app: mock_app = MagicMock() mock_create_app.return_value = (mock_app, None) monkeypatch.setattr("sys.argv", ["animaltrack", "serve", "--host", "127.0.0.1"]) main() call_kwargs = mock_uvicorn.call_args assert call_kwargs[1]["host"] == "127.0.0.1" def test_serve_uses_default_host(self, tmp_path, monkeypatch): """Serve should use 0.0.0.0 as default host.""" db_path = tmp_path / "test.db" monkeypatch.setenv("DB_PATH", str(db_path)) monkeypatch.setenv("CSRF_SECRET", "test-secret-for-csrf") with patch("uvicorn.run") as mock_uvicorn: with patch("animaltrack.migrations.run_migrations") as mock_migrations: mock_migrations.return_value = True with patch("animaltrack.web.app.create_app") as mock_create_app: mock_app = MagicMock() mock_create_app.return_value = (mock_app, None) monkeypatch.setattr("sys.argv", ["animaltrack", "serve"]) main() call_kwargs = mock_uvicorn.call_args assert call_kwargs[1]["host"] == "0.0.0.0" def test_serve_exits_on_migration_failure(self, tmp_path, monkeypatch): """Serve should exit 1 if migrations fail.""" db_path = tmp_path / "test.db" monkeypatch.setenv("DB_PATH", str(db_path)) monkeypatch.setenv("CSRF_SECRET", "test-secret-for-csrf") with patch("uvicorn.run") as mock_uvicorn: with patch("animaltrack.migrations.run_migrations") as mock_migrations: mock_migrations.return_value = False monkeypatch.setattr("sys.argv", ["animaltrack", "serve"]) with pytest.raises(SystemExit) as exc_info: main() assert exc_info.value.code == 1 # Uvicorn should not be called if migrations fail mock_uvicorn.assert_not_called() class TestServeHelpAndArgs: """Tests for serve command help and argument parsing.""" def test_serve_help(self): """Should display help for serve command.""" env = os.environ.copy() env["PYTHONPATH"] = str(PROJECT_ROOT / "src") result = subprocess.run( [sys.executable, "-m", "animaltrack.cli", "serve", "--help"], capture_output=True, text=True, env=env, cwd=str(PROJECT_ROOT), ) assert result.returncode == 0 assert "--port" in result.stdout assert "--host" in result.stdout