from __future__ import annotations import os import stat import time from pathlib import Path import pytest from boltclam.clamav import ( ClamAVService, build_freshclam_args, build_scan_args, database_age, database_files, find_clamscan, parse_clamscan_line, parse_extra_options, parse_version_output, scan_targets, ) def _script(path: Path, body: str = "") -> Path: path.write_text("#!/bin/sh\n" + body, encoding="utf-8") path.chmod(path.stat().st_mode | stat.S_IXUSR) return path def test_parse_version_output(): assert parse_version_output("ClamAV 1.4.3/27452/Wed Oct 22 07:17:23 2026") == ( "1.4.3", "27452", "Wed Oct 22 07:17:23 2026") assert parse_version_output("ClamAV 1.4.0") == ("1.4.0", "", "") assert parse_version_output("nonsense") == ("", "", "") def test_parse_extra_options_handles_windows_quotes(): options = parse_extra_options( '--max-filesize=200M --exclude-dir="C:\\Program Files\\Skip" ' '--database="C:\\My DB"') assert options == [ "--max-filesize=200M", "--exclude-dir=C:\\Program Files\\Skip", "--database=C:\\My DB", ] def test_parse_extra_options_refuses_destructive_flags(): for text in ("--remove=yes", "--move=C:\\Quarantine", "--copy=/tmp"): with pytest.raises(ValueError): parse_extra_options(text) def test_parse_extra_options_rejects_malformed_quoting(): with pytest.raises(ValueError): parse_extra_options('--exclude-dir="unterminated') def test_build_scan_args_uses_managed_database_and_excludes(tmp_path): db = tmp_path / "db" db.mkdir() (db / "main.cvd").write_bytes(b"x") quarantine = tmp_path / "quarantine" temp = tmp_path / "temp" args = build_scan_args( Path("/usr/bin/clamscan"), [tmp_path / "target"], database_dir=db, quarantine_dir=quarantine, temp_dir=temp, extra_options=["--max-filesize=200M"]) assert args[0].endswith("clamscan") assert "--recursive=yes" in args assert "--infected" in args assert f"--database={db}" in args assert any(arg.startswith("--exclude-dir=") for arg in args) assert "--max-filesize=200M" in args assert args[-1] == str(tmp_path / "target") def test_build_scan_args_omits_database_when_no_signatures(tmp_path): args = build_scan_args(Path("/usr/bin/clamscan"), [tmp_path]) assert not any(arg.startswith("--database=") for arg in args) def test_parse_clamscan_line_detection_windows_path(): event = parse_clamscan_line( r"C:\Users\aaron\Downloads\invoice.pdf.exe: Win.Trojan.Agent-123 FOUND") assert event == { "kind": "detection", "path": r"C:\Users\aaron\Downloads\invoice.pdf.exe", "signature": "Win.Trojan.Agent-123", } def test_parse_clamscan_line_summary_progress_and_errors(): assert parse_clamscan_line("Scanned files: 18234")["key"] == "scanned_files" assert parse_clamscan_line("Scanned files: 18234")["value"] == "18234" assert parse_clamscan_line("Engine version: 1.4.3")["value"] == "1.4.3" assert parse_clamscan_line("Scanning /home/a/file")["kind"] == "progress" assert parse_clamscan_line("/home/a/file: Access denied")["kind"] == "error" assert parse_clamscan_line("/home/a/file: Size limit exceeded")["kind"] == "skipped" assert parse_clamscan_line("") is None def test_scan_targets_deduplicates_injected_profiles(tmp_path): one = tmp_path / "one" two = tmp_path / "two" one.mkdir() two.mkdir() targets = scan_targets("homes", profiles=[one, two, one, Path("/does/not/exist")]) assert targets == [one.resolve(), two.resolve()] def test_scan_targets_full_uses_injected_drives(tmp_path): drive = tmp_path / "drive" drive.mkdir() assert scan_targets("full", drives=[drive]) == [drive.resolve()] def test_database_detection_and_age(tmp_path): db = tmp_path / "db" db.mkdir() assert database_files(db) == [] main = db / "main.cvd" main.write_bytes(b"signatures") old = time.time() - 3 * 86400 os.utime(main, (old, old)) assert database_files(db) == [main] date, age = database_age(db) assert date assert 2.8 < age < 3.2 def test_clamav_service_reads_a_fake_engine(tmp_path): fake = _script(tmp_path / "clamscan", 'echo "ClamAV 1.4.3/27452/Wed Oct 22 07:17:23 2026"\n') service = ClamAVService(str(fake)) info = service.info(force=True) assert info.available is True assert info.engine_version == "1.4.3" assert info.db_version == "27452" assert info.path == fake assert service.info(force=True).summary.startswith("1.4.3") def test_find_clamscan_accepts_a_file_or_directory(tmp_path): fake = _script(tmp_path / "clamscan") found, error = find_clamscan(str(fake)) assert found == fake and error == "" found, error = find_clamscan(str(tmp_path)) assert found == fake and error == "" missing, error = find_clamscan(str(tmp_path / "missing")) assert missing is None and "does not exist" in error def test_freshclam_arguments_target_managed_database(tmp_path): args = build_freshclam_args(Path("/usr/bin/freshclam"), tmp_path / "db") assert "--foreground" in args assert "--stdout" in args assert f"--datadir={tmp_path / 'db'}" in args