"""Tests for BaseExporter, ExportResult, ExportOptions, and BaseFileVerifier.""" import hashlib import tempfile from pathlib import Path import pytest from local_deep_research.exporters.base import ( BaseExporter, ExportOptions, ExportResult, ) from local_deep_research.security.file_integrity.base_verifier import ( BaseFileVerifier, FileType, ) # --- Concrete subclasses for testing abstract bases --- class StubExporter(BaseExporter): @property def format_name(self) -> str: return "stub" @property def file_extension(self) -> str: return ".stub" @property def mimetype(self) -> str: return "application/x-stub" def export(self, markdown_content, options=None): return ExportResult( content=markdown_content.encode(), filename="out.stub", mimetype=self.mimetype, ) class StubVerifier(BaseFileVerifier): def should_verify(self, file_path: Path) -> bool: return file_path.suffix == ".stub" def get_file_type(self) -> FileType: return FileType.EXPORT def allows_modifications(self) -> bool: return False # --- ExportResult tests --- class TestExportResult: def test_fields(self): result = ExportResult( content=b"hello", filename="f.txt", mimetype="text/plain" ) assert result.content == b"hello" assert result.filename == "f.txt" assert result.mimetype == "text/plain" # --- ExportOptions tests --- class TestExportOptions: def test_defaults(self): opts = ExportOptions() assert opts.title is None assert opts.metadata is None assert opts.custom_options == {} def test_with_values(self): opts = ExportOptions( title="My Report", metadata={"author": "Alice"}, custom_options={"css": "body{}"}, ) assert opts.title == "My Report" assert opts.metadata == {"author": "Alice"} assert opts.custom_options == {"css": "body{}"} # --- BaseExporter._validate_content_size tests --- class TestValidateContentSize: def test_under_limit_no_error(self): exporter = StubExporter() exporter._validate_content_size("short content") def test_over_limit_raises(self): exporter = StubExporter() oversized = "x" * (exporter.MAX_CONTENT_SIZE + 1) with pytest.raises(ValueError, match="exceeds maximum size"): exporter._validate_content_size(oversized) def test_exactly_at_limit_no_error(self): exporter = StubExporter() exact = "x" * exporter.MAX_CONTENT_SIZE exporter._validate_content_size(exact) # --- BaseExporter._generate_safe_filename tests --- class TestGenerateSafeFilename: def test_with_title(self): exporter = StubExporter() filename = exporter._generate_safe_filename("My Report") assert filename == "My_Report.stub" def test_without_title(self): exporter = StubExporter() assert exporter._generate_safe_filename(None) == "research_report.stub" assert exporter._generate_safe_filename("") == "research_report.stub" def test_special_chars_stripped(self): exporter = StubExporter() filename = exporter._generate_safe_filename("Hello! @World# (2024)") # Only word chars, spaces, hyphens survive; spaces become underscores assert "!" not in filename assert "@" not in filename assert "#" not in filename assert filename.endswith(".stub") def test_long_title_truncated(self): exporter = StubExporter() long_title = "A" * 100 filename = exporter._generate_safe_filename(long_title) # safe_title is truncated to 50 chars, plus extension assert filename == "A" * 50 + ".stub" # --- BaseExporter._prepend_title_if_needed tests --- class TestPrependTitleIfNeeded: def test_no_title_returns_content(self): exporter = StubExporter() content = "Some body text" assert exporter._prepend_title_if_needed(content, None) == content assert exporter._prepend_title_if_needed(content, "") == content def test_content_starts_with_same_title(self): exporter = StubExporter() content = "# My Title\n\nBody text" result = exporter._prepend_title_if_needed(content, "My Title") assert result == content # unchanged def test_content_starts_with_different_heading(self): exporter = StubExporter() content = "## Section One\n\nBody text" result = exporter._prepend_title_if_needed(content, "My Title") # Has a heading already, so title is NOT prepended assert result == content def test_content_without_heading_gets_title(self): exporter = StubExporter() content = "Just some plain text" result = exporter._prepend_title_if_needed(content, "My Title") assert result == "# My Title\n\nJust some plain text" def test_content_with_leading_whitespace_and_heading(self): exporter = StubExporter() content = " # Existing Heading\n\nBody" result = exporter._prepend_title_if_needed(content, "New Title") # lstrip() finds '#', so title is NOT prepended assert result == content # --- BaseFileVerifier tests --- class TestBaseFileVerifier: def test_calculate_checksum(self): verifier = StubVerifier() with tempfile.NamedTemporaryFile(delete=False, suffix=".stub") as f: f.write(b"test data for checksum") f.flush() tmp_path = Path(f.name) try: result = verifier.calculate_checksum(tmp_path) expected = hashlib.sha256(b"test data for checksum").hexdigest() assert result == expected finally: tmp_path.unlink() def test_get_algorithm(self): verifier = StubVerifier() assert verifier.get_algorithm() == "sha256" def test_checksum_file_not_found(self): verifier = StubVerifier() with pytest.raises(FileNotFoundError): verifier.calculate_checksum(Path("/nonexistent/file.stub"))