feat(web): Add Console page with terminal and editor (#17)

This commit is contained in:
Bas Nijholt
2025-12-18 10:29:15 -08:00
committed by GitHub
parent 78bf90afd9
commit a6e491575a
5 changed files with 309 additions and 91 deletions

54
tests/web/test_backup.py Normal file
View File

@@ -0,0 +1,54 @@
"""Tests for file backup functionality."""
from pathlib import Path
from compose_farm.web.routes.api import _backup_file, _save_with_backup
def test_backup_creates_timestamped_file(tmp_path: Path) -> None:
"""Test that backup creates file in .backups with correct content."""
test_file = tmp_path / "test.yaml"
test_file.write_text("original content")
backup_path = _backup_file(test_file)
assert backup_path is not None
assert backup_path.parent.name == ".backups"
assert backup_path.name.startswith("test.yaml.")
assert backup_path.read_text() == "original content"
def test_backup_returns_none_for_nonexistent_file(tmp_path: Path) -> None:
"""Test that backup returns None if file doesn't exist."""
assert _backup_file(tmp_path / "nonexistent.yaml") is None
def test_save_creates_new_file(tmp_path: Path) -> None:
"""Test that save creates new file without backup."""
test_file = tmp_path / "new.yaml"
assert _save_with_backup(test_file, "content") is True
assert test_file.read_text() == "content"
assert not (tmp_path / ".backups").exists()
def test_save_skips_unchanged_content(tmp_path: Path) -> None:
"""Test that save returns False and creates no backup if unchanged."""
test_file = tmp_path / "test.yaml"
test_file.write_text("same")
assert _save_with_backup(test_file, "same") is False
assert not (tmp_path / ".backups").exists()
def test_save_creates_backup_before_overwrite(tmp_path: Path) -> None:
"""Test that save backs up original before overwriting."""
test_file = tmp_path / "test.yaml"
test_file.write_text("original")
assert _save_with_backup(test_file, "new") is True
assert test_file.read_text() == "new"
backups = list((tmp_path / ".backups").glob("test.yaml.*"))
assert len(backups) == 1
assert backups[0].read_text() == "original"

View File

@@ -0,0 +1,111 @@
"""Tests to verify template context variables match what templates expect.
Uses runtime validation by actually rendering templates and catching errors.
"""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
import pytest
from fastapi.testclient import TestClient
if TYPE_CHECKING:
from compose_farm.config import Config
@pytest.fixture
def mock_config(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Config:
"""Create a minimal mock config for template testing."""
compose_dir = tmp_path / "compose"
compose_dir.mkdir()
# Create minimal service directory
svc_dir = compose_dir / "test-service"
svc_dir.mkdir()
(svc_dir / "compose.yaml").write_text("services:\n app:\n image: nginx\n")
config_path = tmp_path / "compose-farm.yaml"
config_path.write_text(f"""
compose_dir: {compose_dir}
hosts:
local-host:
address: localhost
services:
test-service: local-host
""")
state_path = tmp_path / "compose-farm-state.yaml"
state_path.write_text("deployed:\n test-service: local-host\n")
from compose_farm.config import load_config
config = load_config(config_path)
# Patch get_config in all relevant modules
from compose_farm.web import deps
from compose_farm.web.routes import api, pages
monkeypatch.setattr(deps, "get_config", lambda: config)
monkeypatch.setattr(api, "get_config", lambda: config)
monkeypatch.setattr(pages, "get_config", lambda: config)
return config
@pytest.fixture
def client(mock_config: Config) -> TestClient:
"""Create a test client with mocked config."""
from compose_farm.web.app import create_app
return TestClient(create_app())
class TestPageTemplatesRender:
"""Test that page templates render without missing variables."""
def test_index_renders(self, client: TestClient) -> None:
"""Test index page renders without errors."""
response = client.get("/")
assert response.status_code == 200
assert "Compose Farm" in response.text
def test_console_renders(self, client: TestClient) -> None:
"""Test console page renders without errors."""
response = client.get("/console")
assert response.status_code == 200
assert "Console" in response.text
assert "Terminal" in response.text
def test_service_detail_renders(self, client: TestClient) -> None:
"""Test service detail page renders without errors."""
response = client.get("/service/test-service")
assert response.status_code == 200
assert "test-service" in response.text
class TestPartialTemplatesRender:
"""Test that partial templates render without missing variables."""
def test_sidebar_renders(self, client: TestClient) -> None:
"""Test sidebar partial renders without errors."""
response = client.get("/partials/sidebar")
assert response.status_code == 200
assert "Dashboard" in response.text
assert "Console" in response.text
def test_stats_renders(self, client: TestClient) -> None:
"""Test stats partial renders without errors."""
response = client.get("/partials/stats")
assert response.status_code == 200
def test_pending_renders(self, client: TestClient) -> None:
"""Test pending partial renders without errors."""
response = client.get("/partials/pending")
assert response.status_code == 200
def test_services_by_host_renders(self, client: TestClient) -> None:
"""Test services_by_host partial renders without errors."""
response = client.get("/partials/services-by-host")
assert response.status_code == 200