"""Tests for app/watch/watcher.py — config loading, command building, output parsing.""" from __future__ import annotations import json import tempfile from pathlib import Path from unittest.mock import MagicMock, patch import pytest from app.watch.watcher import ( WatchConfig, WatchSource, Watcher, _strip_docker_ts, load_watch_config, ) # ── Config loading ────────────────────────────────────────────────────────── def test_load_watch_config_missing_file(tmp_path: Path): result = load_watch_config(tmp_path / "nonexistent.yaml") assert result == [] def test_load_watch_config_empty_sources(tmp_path: Path): cfg = tmp_path / "watch.yaml" cfg.write_text("sources: []\n") assert load_watch_config(cfg) == [] def test_load_watch_config_parses_journald(tmp_path: Path): cfg = tmp_path / "watch.yaml" cfg.write_text(""" sources: - type: journald id: system-journal args: ["-u", "sshd"] """) configs = load_watch_config(cfg) assert len(configs) == 1 assert configs[0].source_type == "journald" assert configs[0].source_id == "system-journal" assert configs[0].args == ["-u", "sshd"] def test_load_watch_config_parses_podman(tmp_path: Path): cfg = tmp_path / "watch.yaml" cfg.write_text(""" sources: - type: podman id: podman:turnstone args: ["turnstone"] """) configs = load_watch_config(cfg) assert configs[0].source_type == "podman" assert configs[0].args == ["turnstone"] def test_load_watch_config_no_args_defaults_to_empty(tmp_path: Path): cfg = tmp_path / "watch.yaml" cfg.write_text(""" sources: - type: journald id: system """) configs = load_watch_config(cfg) assert configs[0].args == [] # ── Command building ───────────────────────────────────────────────────────── def _make_source(source_type: str, source_id: str, args: list = None, db=None, pattern=None): if db is None: db = Path("/tmp/fake.db") if pattern is None: pattern = Path("/tmp/fake.yaml") cfg = WatchConfig(source_type=source_type, source_id=source_id, args=args or []) return WatchSource(cfg, db, pattern) def test_build_command_journald(): src = _make_source("journald", "sys") cmd = src._build_command() assert cmd[:3] == ["journalctl", "-f", "--output=json"] def test_build_command_journald_with_unit_filter(): src = _make_source("journald", "sshd", args=["-u", "sshd"]) cmd = src._build_command() assert "-u" in cmd assert "sshd" in cmd def test_build_command_podman_with_container(): src = _make_source("podman", "podman:ts", args=["turnstone"]) cmd = src._build_command() assert cmd[0] == "podman" assert "logs" in cmd assert "-f" in cmd assert "turnstone" in cmd def test_build_command_docker_no_args_returns_none(): src = _make_source("docker", "docker:nginx") cmd = src._build_command() assert cmd is None def test_build_command_unknown_type_returns_none(): src = _make_source("kafka", "topic:logs") cmd = src._build_command() assert cmd is None # ── Docker timestamp stripping ─────────────────────────────────────────────── def test_strip_docker_ts_removes_rfc3339_prefix(): line = "2024-01-15T12:34:56.789012345Z some log line" assert _strip_docker_ts(line) == "some log line" def test_strip_docker_ts_passes_plain_lines(): line = "plain log line without timestamp" assert _strip_docker_ts(line) == line def test_strip_docker_ts_handles_offset_timezone(): # Docker --timestamps always uses Z (UTC), but be safe line = "2024-01-15T12:34:56Z message" assert _strip_docker_ts(line) == "message" # ── Watcher orchestrator ───────────────────────────────────────────────────── def test_watcher_status_empty_when_no_sources(tmp_path: Path): w = Watcher(tmp_path / "fake.db", tmp_path / "fake.yaml") assert w.status == [] assert not w.is_active() def test_watcher_configure_creates_sources(tmp_path: Path): w = Watcher(tmp_path / "fake.db", tmp_path / "fake.yaml") configs = [ WatchConfig("journald", "sys"), WatchConfig("podman", "ts", ["turnstone"]), ] w.configure(configs) assert len(w.status) == 2 assert w.status[0]["source_id"] == "sys" assert w.status[1]["source_id"] == "ts" def test_watcher_status_shows_not_running_before_start(tmp_path: Path): w = Watcher(tmp_path / "fake.db", tmp_path / "fake.yaml") w.configure([WatchConfig("journald", "sys")]) assert not w.is_active() assert w.status[0]["running"] is False