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:
2025-12-31 18:21:14 +00:00
parent 49871f60c5
commit c6a87e35d4
4 changed files with 578 additions and 6 deletions

188
tests/test_cli_serve.py Normal file
View 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