feat: complete CLI, Docker & deployment docs (Step 10.3)
- Add comprehensive CLI tests for seed and serve commands - Create README.md with development setup, deployment guide, and environment variable reference - Mark Step 10.3 as complete in PLAN.md This completes the final implementation step. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
188
tests/test_cli_serve.py
Normal file
188
tests/test_cli_serve.py
Normal file
@@ -0,0 +1,188 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user