"""Tests for the qBittorrent log ingestor.""" from __future__ import annotations import pytest from app.ingest.qbittorrent import is_qbit_log, parse SAMPLE_LOG = """\ (2026/05/09 14:10:01) qBittorrent v5.0.3 started (2026/05/09 14:10:02) [Warning] Tracker 'http://tracker.example.com/announce' is not working. Reason: Connection timed out (2026/05/09 14:10:03) [Critical] Couldn't listen on any of the network interfaces. Aborting! (2026/05/09 14:10:04) Download of 'ubuntu-24.04.iso' has finished. (2026/05/09 14:10:05) [Warning] Hash check failed for piece 42 of 'ubuntu-24.04.iso' (2026/05/09 14:10:06) Some long message that continues on the next line and a third line (2026/05/09 14:10:07) Normal message without bracket level """ DASH_FORMAT = "(2026-05-09 14:10:01) qBittorrent v4.6.2 started\n" class TestDetector: def test_detects_slash_format(self): assert is_qbit_log("(2026/05/09 14:10:01) qBittorrent started") def test_detects_dash_format(self): assert is_qbit_log(DASH_FORMAT.strip()) def test_rejects_plex_format(self): assert not is_qbit_log("Jan 01, 2026 12:00:00.000 [12345] DEBUG - message") def test_rejects_journald_json(self): assert not is_qbit_log('{"__REALTIME_TIMESTAMP": "12345", "MESSAGE": "hi"}') def test_rejects_plaintext(self): assert not is_qbit_log("2026-05-09 14:10:01 some syslog line") class TestParser: def _parse(self, text: str) -> list: return list(parse(iter(text.splitlines(keepends=True)), "qbit_test", [])) def test_entry_count(self): entries = self._parse(SAMPLE_LOG) assert len(entries) == 7 def test_startup_entry(self): e = self._parse(SAMPLE_LOG)[0] assert "qBittorrent v5.0.3 started" in e.text # No bracket level + no severity keyword in text → None (consistent with other ingestors) assert e.severity is None assert e.timestamp_iso == "2026-05-09T14:10:01+00:00" def test_warning_severity(self): entries = self._parse(SAMPLE_LOG) tracker_entry = entries[1] assert tracker_entry.severity == "WARN" assert "not working" in tracker_entry.text def test_critical_severity(self): entries = self._parse(SAMPLE_LOG) port_entry = entries[2] assert port_entry.severity == "CRITICAL" def test_multiline_continuation(self): entries = self._parse(SAMPLE_LOG) multiline = entries[5] assert "continues on the next line" in multiline.text assert "third line" in multiline.text def test_no_level_bracket_falls_back_to_detect(self): entries = self._parse(SAMPLE_LOG) last = entries[6] assert last.text == "Normal message without bracket level" def test_source_id_propagated(self): entries = self._parse(SAMPLE_LOG) assert all(e.source_id == "qbit_test" for e in entries) def test_sequence_is_monotonic(self): entries = self._parse(SAMPLE_LOG) sequences = [e.sequence for e in entries] assert sequences == sorted(sequences) assert len(set(sequences)) == len(sequences) def test_dash_format_timestamp(self): entries = list(parse(iter(DASH_FORMAT.splitlines(keepends=True)), "qbit", [])) assert len(entries) == 1 assert entries[0].timestamp_iso == "2026-05-09T14:10:01+00:00"