from __future__ import annotations import json import os import stat from pathlib import Path import pytest from boltclam.quarantine import QuarantineError, QuarantineService, delete_file, hash_file from boltclam.storage import Storage def _service(tmp_path) -> QuarantineService: storage = Storage(tmp_path / "history.db") return QuarantineService(storage, tmp_path / "quarantine") def test_quarantine_moves_records_and_writes_metadata(tmp_path): service = _service(tmp_path) source = tmp_path / "malware.exe" source.write_bytes(b"malware bytes") digest = hash_file(source) item = service.quarantine_file(source, "Win.Test", scan_id=7, reason="Unit test") assert not source.exists() destination = Path(item.quarantine_path) assert destination.is_file() assert destination.name.endswith(".quarantine") assert item.sha256 == digest assert service.storage.get_quarantine(item.id).signature == "Win.Test" metadata = json.loads((service.root / f"{item.id}.json").read_text()) assert metadata["original_path"] == str(source) assert metadata["reason"] == "Unit test" def test_restore_to_original_or_chosen_path(tmp_path): service = _service(tmp_path) source = tmp_path / "bad.exe" source.write_bytes(b"x") item = service.quarantine_file(source, "Sig") target = service.restore(item.id) assert target.read_bytes() == b"x" assert service.storage.get_quarantine(item.id).status == "restored" source.write_bytes(b"y") item2 = service.quarantine_file(source, "Sig") chosen = tmp_path / "restored" / "safe.exe" restored = service.restore(item2.id, chosen) assert restored == chosen assert chosen.read_bytes() == b"y" def test_delete_quarantined_item_and_missing_original(tmp_path): service = _service(tmp_path) source = tmp_path / "bad.exe" source.write_bytes(b"x") item = service.quarantine_file(source, "Sig") service.delete(item.id) assert not Path(item.quarantine_path).exists() assert service.storage.get_quarantine(item.id).status == "deleted" with pytest.raises(QuarantineError): service.restore(item.id) def test_refuses_symlink_and_directory(tmp_path): service = _service(tmp_path) directory = tmp_path / "folder" directory.mkdir() with pytest.raises(QuarantineError): service.quarantine_file(directory, "Sig") real = tmp_path / "real.exe" real.write_bytes(b"x") link = tmp_path / "link.exe" try: link.symlink_to(real) except OSError: pytest.skip("symlinks unavailable") with pytest.raises(QuarantineError): service.quarantine_file(link, "Sig") with pytest.raises(QuarantineError): delete_file(directory) def test_delete_file_clears_readonly_bit(tmp_path): target = tmp_path / "readonly.exe" target.write_bytes(b"x") os.chmod(target, stat.S_IREAD) delete_file(target) assert not target.exists() def test_restore_refuses_to_overwrite(tmp_path): service = _service(tmp_path) source = tmp_path / "bad.exe" source.write_bytes(b"x") item = service.quarantine_file(source, "Sig") source.write_bytes(b"replacement") with pytest.raises(QuarantineError): service.restore(item.id)