mirror of
https://github.com/basnijholt/compose-farm.git
synced 2026-09-16 16:31:40 +01:00
Some checks failed
Release Drafter / update_release_draft (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
Update README.md / update_readme (push) Has been cancelled
CI / test (macos-latest, 3.11) (push) Has been cancelled
CI / test (macos-latest, 3.12) (push) Has been cancelled
CI / test (macos-latest, 3.13) (push) Has been cancelled
CI / test (ubuntu-latest, 3.11) (push) Has been cancelled
CI / test (ubuntu-latest, 3.12) (push) Has been cancelled
CI / test (ubuntu-latest, 3.13) (push) Has been cancelled
CI / browser-tests (push) Has been cancelled
CI / lint (push) Has been cancelled
Docs / build (push) Has been cancelled
Docs / deploy (push) Has been cancelled
* chore: refresh maintenance tooling * fix: restore cli startup budget * fix: keep rich help in startup test
249 lines
9.0 KiB
Python
249 lines
9.0 KiB
Python
"""Tests for web API helper functions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
from pydantic import ValidationError
|
|
|
|
if TYPE_CHECKING:
|
|
from compose_farm.config import Config
|
|
|
|
|
|
class TestExtractConfigError:
|
|
"""Tests for extract_config_error helper."""
|
|
|
|
def test_validation_error_with_location(self) -> None:
|
|
from compose_farm.config import Config, Host
|
|
from compose_farm.web.deps import extract_config_error
|
|
|
|
# Trigger a validation error with an extra field
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Config(
|
|
hosts={"server": Host(address="192.168.1.1")},
|
|
stacks={"app": "server"},
|
|
unknown_field="bad", # type: ignore[call-arg] # ty: ignore[unknown-argument]
|
|
)
|
|
|
|
msg = extract_config_error(exc_info.value)
|
|
assert "unknown_field" in msg
|
|
assert "Extra inputs are not permitted" in msg
|
|
|
|
def test_validation_error_nested_location(self) -> None:
|
|
from compose_farm.config import Host
|
|
from compose_farm.web.deps import extract_config_error
|
|
|
|
# Trigger a validation error with a nested extra field
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Host(address="192.168.1.1", bad_key="value") # type: ignore[call-arg] # ty: ignore[unknown-argument]
|
|
|
|
msg = extract_config_error(exc_info.value)
|
|
assert "bad_key" in msg
|
|
assert "Extra inputs are not permitted" in msg
|
|
|
|
def test_regular_exception(self) -> None:
|
|
from compose_farm.web.deps import extract_config_error
|
|
|
|
exc = ValueError("Something went wrong")
|
|
msg = extract_config_error(exc)
|
|
assert msg == "Something went wrong"
|
|
|
|
def test_file_not_found_exception(self) -> None:
|
|
from compose_farm.web.deps import extract_config_error
|
|
|
|
exc = FileNotFoundError("Config file not found")
|
|
msg = extract_config_error(exc)
|
|
assert msg == "Config file not found"
|
|
|
|
|
|
class TestValidateYaml:
|
|
"""Tests for _validate_yaml helper."""
|
|
|
|
def test_valid_yaml(self) -> None:
|
|
from compose_farm.web.routes.api import _validate_yaml
|
|
|
|
# Should not raise
|
|
_validate_yaml("key: value")
|
|
_validate_yaml("list:\n - item1\n - item2")
|
|
_validate_yaml("")
|
|
|
|
def test_invalid_yaml(self) -> None:
|
|
from compose_farm.web.routes.api import _validate_yaml
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
_validate_yaml("key: [unclosed")
|
|
|
|
assert exc_info.value.status_code == 400
|
|
assert "Invalid YAML" in exc_info.value.detail
|
|
|
|
|
|
class TestGetStackComposePath:
|
|
"""Tests for _get_stack_compose_path helper."""
|
|
|
|
def test_stack_found(self, mock_config: Config) -> None:
|
|
from compose_farm.web.routes.api import _get_stack_compose_path
|
|
|
|
path = _get_stack_compose_path("plex")
|
|
assert isinstance(path, Path)
|
|
assert path.name == "compose.yaml"
|
|
assert path.parent.name == "plex"
|
|
|
|
def test_stack_not_found(self, mock_config: Config) -> None:
|
|
from compose_farm.web.routes.api import _get_stack_compose_path
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
_get_stack_compose_path("nonexistent")
|
|
|
|
assert exc_info.value.status_code == 404
|
|
assert "not found" in exc_info.value.detail
|
|
|
|
|
|
class TestIsLocalHost:
|
|
"""Tests for is_local_host helper."""
|
|
|
|
def test_returns_true_when_web_stack_host_matches(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""is_local_host returns True when host matches web stack host."""
|
|
from compose_farm.config import Config, Host
|
|
from compose_farm.web.deps import is_local_host
|
|
|
|
monkeypatch.setenv("CF_WEB_STACK", "compose-farm")
|
|
config = Config(
|
|
hosts={"nas": Host(address="10.99.99.1"), "nuc": Host(address="10.99.99.2")},
|
|
stacks={"compose-farm": "nas"},
|
|
)
|
|
host = config.hosts["nas"]
|
|
assert is_local_host("nas", host, config) is True
|
|
|
|
def test_returns_false_when_web_stack_host_differs(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""is_local_host returns False when host does not match web stack host."""
|
|
from compose_farm.config import Config, Host
|
|
from compose_farm.web.deps import is_local_host
|
|
|
|
monkeypatch.setenv("CF_WEB_STACK", "compose-farm")
|
|
config = Config(
|
|
hosts={"nas": Host(address="10.99.99.1"), "nuc": Host(address="10.99.99.2")},
|
|
stacks={"compose-farm": "nas"},
|
|
)
|
|
host = config.hosts["nuc"]
|
|
# nuc is not local, and not matching the web stack host
|
|
assert is_local_host("nuc", host, config) is False
|
|
|
|
|
|
class TestGetLocalHost:
|
|
"""Tests for get_local_host helper."""
|
|
|
|
def test_returns_web_stack_host(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""get_local_host returns the web stack host when in container."""
|
|
from compose_farm.config import Config, Host
|
|
from compose_farm.web.deps import get_local_host
|
|
|
|
monkeypatch.setenv("CF_WEB_STACK", "compose-farm")
|
|
config = Config(
|
|
hosts={"nas": Host(address="10.99.99.1"), "nuc": Host(address="10.99.99.2")},
|
|
stacks={"compose-farm": "nas"},
|
|
)
|
|
assert get_local_host(config) == "nas"
|
|
|
|
def test_ignores_unknown_web_stack(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""get_local_host ignores web stack if it's not in stacks."""
|
|
from compose_farm.config import Config, Host
|
|
from compose_farm.web.deps import get_local_host
|
|
|
|
monkeypatch.setenv("CF_WEB_STACK", "unknown-stack")
|
|
# Use address that won't match local machine to avoid is_local() fallback
|
|
config = Config(
|
|
hosts={"nas": Host(address="10.99.99.1")},
|
|
stacks={"test": "nas"},
|
|
)
|
|
# Should fall back to auto-detection (which won't match anything here)
|
|
assert get_local_host(config) is None
|
|
|
|
def test_returns_none_outside_container(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""get_local_host returns None when CF_WEB_STACK not set."""
|
|
from compose_farm.config import Config, Host
|
|
from compose_farm.web.deps import get_local_host
|
|
|
|
monkeypatch.delenv("CF_WEB_STACK", raising=False)
|
|
config = Config(
|
|
hosts={"nas": Host(address="10.99.99.1")},
|
|
stacks={"compose-farm": "nas"},
|
|
)
|
|
assert get_local_host(config) is None
|
|
|
|
|
|
class TestRenderContainers:
|
|
"""Tests for container template rendering."""
|
|
|
|
def test_render_running_container(self, mock_config: Config) -> None:
|
|
from compose_farm.web.routes.api import _render_containers
|
|
|
|
containers = [{"Name": "plex", "State": "running"}]
|
|
html = _render_containers("plex", "server-1", containers)
|
|
|
|
assert "badge-success" in html
|
|
assert "plex" in html
|
|
assert "initExecTerminal" in html
|
|
|
|
def test_render_unknown_state(self, mock_config: Config) -> None:
|
|
from compose_farm.web.routes.api import _render_containers
|
|
|
|
containers = [{"Name": "plex", "State": "unknown"}]
|
|
html = _render_containers("plex", "server-1", containers)
|
|
|
|
assert "loading-spinner" in html
|
|
|
|
def test_render_exited_success(self, mock_config: Config) -> None:
|
|
from compose_farm.web.routes.api import _render_containers
|
|
|
|
containers = [{"Name": "plex", "State": "exited", "ExitCode": 0}]
|
|
html = _render_containers("plex", "server-1", containers)
|
|
|
|
assert "badge-neutral" in html
|
|
assert "exited (0)" in html
|
|
|
|
def test_render_exited_error(self, mock_config: Config) -> None:
|
|
from compose_farm.web.routes.api import _render_containers
|
|
|
|
containers = [{"Name": "plex", "State": "exited", "ExitCode": 1}]
|
|
html = _render_containers("plex", "server-1", containers)
|
|
|
|
assert "badge-error" in html
|
|
assert "exited (1)" in html
|
|
|
|
def test_render_other_state(self, mock_config: Config) -> None:
|
|
from compose_farm.web.routes.api import _render_containers
|
|
|
|
containers = [{"Name": "plex", "State": "restarting"}]
|
|
html = _render_containers("plex", "server-1", containers)
|
|
|
|
assert "badge-warning" in html
|
|
assert "restarting" in html
|
|
|
|
def test_render_with_header(self, mock_config: Config) -> None:
|
|
from compose_farm.web.routes.api import _render_containers
|
|
|
|
containers = [{"Name": "plex", "State": "running"}]
|
|
html = _render_containers("plex", "server-1", containers, show_header=True)
|
|
|
|
assert "server-1" in html
|
|
assert "font-semibold" in html
|
|
|
|
def test_render_multiple_containers(self, mock_config: Config) -> None:
|
|
from compose_farm.web.routes.api import _render_containers
|
|
|
|
containers = [
|
|
{"Name": "app-web-1", "State": "running"},
|
|
{"Name": "app-db-1", "State": "running"},
|
|
]
|
|
html = _render_containers("app", "server-1", containers)
|
|
|
|
assert "app-web-1" in html
|
|
assert "app-db-1" in html
|