Add comprehensive test suite for backend (133 tests)

Test coverage:
- test_utils.py: formatting helpers (human_size, ticks_to_minutes,
  format_duration, format_bitrate, ffprobe summaries, stream parsing)
- test_path_utils.py: path resolution (prefix, media root mapping,
  edge cases with spaces/special chars)
- test_domain_media.py: Jellyfin item normalization (HDR detection,
  media sources, stream extraction, display formatting)
- test_jobs.py: job template rendering and shell quoting safety
- test_media_index.py: SQLite index CRUD, querying, filtering,
  sorting, pagination
- test_config.py: pydantic-settings env loading
- test_api.py: full FastAPI integration tests with mocked SSH/Jellyfin
  (all endpoints: dashboard, monitoring, media, files, jobs)

All tests run without network/SSH dependencies using mocks.
This commit is contained in:
2026-05-03 12:45:14 +02:00
parent 51b10438a9
commit 47baee854b
10 changed files with 1199 additions and 45 deletions
+40 -37
View File
@@ -1,63 +1,66 @@
"""Backend configuration using pydantic-settings.
Reads from environment variables and .env file automatically.
Uses a flat settings model so that standard env vars like SSH_HOST,
JELLYFIN_URL etc. are picked up directly without nested-model complications.
"""
from __future__ import annotations
from functools import lru_cache
from pathlib import Path
from pydantic_settings import BaseSettings
class JellyfinSettings(BaseSettings):
url: str = ""
api_key: str = ""
user_id: str = ""
model_config = {"env_prefix": "JELLYFIN_"}
class SSHSettings(BaseSettings):
host: str = ""
username: str = ""
port: int = 22
key_filename: str = str(Path.home() / ".ssh" / "id_rsa")
password: str = ""
media_root: str = ""
path_prefix: str = ""
model_config = {"env_prefix": "SSH_"}
class RemoteSettings(BaseSettings):
"""Extra remote settings that don't fit the SSH_ prefix."""
media_root: str = ""
path_prefix: str = ""
model_config = {"env_prefix": "REMOTE_"}
class Settings(BaseSettings):
"""Top-level application settings."""
"""Flat application settings read from env vars / .env file."""
jellyfin: JellyfinSettings = JellyfinSettings()
ssh: SSHSettings = SSHSettings()
remote: RemoteSettings = RemoteSettings()
# Jellyfin
jellyfin_url: str = ""
jellyfin_api_key: str = ""
jellyfin_user_id: str = ""
# Derived convenience properties
# SSH
ssh_host: str = ""
ssh_username: str = ""
ssh_port: int = 22
ssh_key_filename: str = str(Path.home() / ".ssh" / "id_rsa")
ssh_password: str = ""
# Remote paths
remote_media_root: str = ""
remote_path_prefix: str = ""
# Derived convenience
@property
def media_root(self) -> str:
return self.remote.media_root or self.ssh.media_root or ""
return self.remote_media_root or ""
@property
def path_prefix(self) -> str:
return self.remote.path_prefix or self.ssh.path_prefix or ""
return self.remote_path_prefix or ""
model_config = {"env_file": ".env", "env_file_encoding": "utf-8", "extra": "ignore"}
def _find_env_file() -> str | None:
"""Look for .env in CWD, then parent directories up to the repo root."""
current = Path.cwd()
for directory in [current, *current.parents]:
candidate = directory / ".env"
if candidate.is_file():
return str(candidate)
# Stop at repo root (has .git)
if (directory / ".git").exists():
break
return None
@lru_cache
def get_settings() -> Settings:
"""Create a Settings instance (reads env/.env on each call)."""
"""Return a cached Settings instance."""
env_file = _find_env_file()
if env_file:
return Settings(_env_file=env_file)
return Settings()