430 lines
15 KiB
Python
430 lines
15 KiB
Python
# This is free software for the public good of a permacomputer hosted at
|
|
# permacomputer.com, an always-on computer by the people, for the people.
|
|
# One which is durable, easy to repair, & distributed like tap water
|
|
# for machine learning intelligence.
|
|
#
|
|
# The permacomputer is community-owned infrastructure optimized around
|
|
# four values:
|
|
#
|
|
# TRUTH First principles, math & science, open source code freely distributed
|
|
# FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
|
# HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
|
# LOVE Be yourself without hurting others, cooperation through natural law
|
|
#
|
|
# This software contributes to that vision by archiving the web, preserving digital knowledge before it disappears.
|
|
# Code is seeds to sprout on any abandoned technology.
|
|
|
|
"""
|
|
Tests for archive module.
|
|
|
|
Tests site archiver functionality for creating distributable tar.gz packages.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
|
import tempfile
|
|
import shutil
|
|
import os
|
|
import json
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
from archive import (
|
|
SiteArchiver,
|
|
sanitize_filename,
|
|
url_to_path,
|
|
html_to_markdown,
|
|
HAS_HTML2TEXT,
|
|
)
|
|
|
|
|
|
class TestSanitizeFilename:
|
|
"""Test filename sanitization."""
|
|
|
|
def test_simple_name(self):
|
|
"""Test simple name passes through."""
|
|
result = sanitize_filename("example")
|
|
assert result == "example"
|
|
|
|
def test_removes_special_chars(self):
|
|
"""Test special characters are removed."""
|
|
result = sanitize_filename("test<>:/\\|?*file")
|
|
# Should not contain any of those characters
|
|
for char in '<>:"/\\|?*':
|
|
assert char not in result
|
|
|
|
def test_domain_with_dots(self):
|
|
"""Test domain with dots is sanitized."""
|
|
result = sanitize_filename("www.example.com")
|
|
assert "." not in result
|
|
|
|
def test_collapses_multiple_dashes(self):
|
|
"""Test multiple dashes are collapsed."""
|
|
result = sanitize_filename("test---file")
|
|
assert "---" not in result
|
|
|
|
def test_strips_leading_trailing(self):
|
|
"""Test leading/trailing dashes are stripped."""
|
|
result = sanitize_filename("--test--")
|
|
assert not result.startswith("-")
|
|
assert not result.endswith("-")
|
|
|
|
def test_empty_string(self):
|
|
"""Test empty string returns 'unnamed'."""
|
|
result = sanitize_filename("")
|
|
assert result == "unnamed"
|
|
|
|
def test_only_special_chars(self):
|
|
"""Test string of only special chars returns 'unnamed'."""
|
|
result = sanitize_filename(":::///")
|
|
assert result == "unnamed"
|
|
|
|
def test_max_length(self):
|
|
"""Test max length truncation."""
|
|
long_name = "a" * 300
|
|
result = sanitize_filename(long_name)
|
|
assert len(result) <= 200
|
|
|
|
|
|
class TestUrlToPath:
|
|
"""Test URL to filesystem path conversion."""
|
|
|
|
def test_root_url(self):
|
|
"""Test root URL becomes index.html."""
|
|
result = url_to_path("https://example.com/")
|
|
assert result == "index.html"
|
|
|
|
def test_root_url_with_path_slash(self):
|
|
"""Test root URL with explicit path slash."""
|
|
# Note: url_to_path expects URLs to have at least a "/" path
|
|
result = url_to_path("https://example.com/about/")
|
|
assert "about" in result
|
|
assert result.endswith("index.html")
|
|
|
|
def test_html_file(self):
|
|
"""Test .html file preserves extension."""
|
|
result = url_to_path("https://example.com/page.html")
|
|
assert result == "page.html"
|
|
|
|
def test_htm_file(self):
|
|
"""Test .htm file preserves extension."""
|
|
result = url_to_path("https://example.com/page.htm")
|
|
assert result == "page.htm"
|
|
|
|
def test_directory_path(self):
|
|
"""Test directory path gets index.html."""
|
|
result = url_to_path("https://example.com/about")
|
|
assert result.endswith("index.html")
|
|
assert "about" in result
|
|
|
|
def test_nested_directory(self):
|
|
"""Test nested directory path."""
|
|
result = url_to_path("https://example.com/blog/posts")
|
|
assert "blog" in result
|
|
assert "posts" in result
|
|
assert result.endswith("index.html")
|
|
|
|
def test_file_with_extension(self):
|
|
"""Test file with non-html extension."""
|
|
result = url_to_path("https://example.com/document.pdf")
|
|
assert result == "document.pdf"
|
|
|
|
|
|
class TestHtmlToMarkdown:
|
|
"""Test HTML to markdown conversion."""
|
|
|
|
@pytest.mark.skipif(not HAS_HTML2TEXT, reason="html2text not installed")
|
|
def test_simple_html(self):
|
|
"""Test converting simple HTML."""
|
|
html = "<h1>Title</h1><p>Paragraph text.</p>"
|
|
result = html_to_markdown(html)
|
|
assert "Title" in result
|
|
assert "Paragraph" in result
|
|
|
|
@pytest.mark.skipif(not HAS_HTML2TEXT, reason="html2text not installed")
|
|
def test_preserves_links(self):
|
|
"""Test links are preserved."""
|
|
html = '<a href="https://example.com">Link</a>'
|
|
result = html_to_markdown(html)
|
|
assert "https://example.com" in result or "[Link]" in result
|
|
|
|
@pytest.mark.skipif(not HAS_HTML2TEXT, reason="html2text not installed")
|
|
def test_preserves_images(self):
|
|
"""Test images are preserved."""
|
|
html = '<img src="image.jpg" alt="Test">'
|
|
result = html_to_markdown(html)
|
|
# Should have image reference
|
|
assert "image.jpg" in result or "Test" in result
|
|
|
|
def test_without_html2text(self):
|
|
"""Test fallback when html2text not available."""
|
|
# When html2text is not installed, should return original HTML
|
|
if not HAS_HTML2TEXT:
|
|
html = "<h1>Test</h1>"
|
|
result = html_to_markdown(html)
|
|
assert result == html
|
|
|
|
|
|
class TestSiteArchiverInitialization:
|
|
"""Test SiteArchiver class initialization."""
|
|
|
|
def test_default_initialization(self):
|
|
"""Test default initialization values."""
|
|
archiver = SiteArchiver()
|
|
|
|
assert archiver.output_dir == Path(".")
|
|
assert archiver.include_screenshots is True
|
|
assert archiver.include_markdown == HAS_HTML2TEXT
|
|
assert archiver.fast_mode is False
|
|
assert archiver.trim_wrapper is False
|
|
assert archiver.show_progress is True
|
|
assert archiver.fresh_start is False
|
|
|
|
def test_custom_output_dir(self):
|
|
"""Test custom output directory."""
|
|
archiver = SiteArchiver(output_dir="/tmp/archives")
|
|
assert archiver.output_dir == Path("/tmp/archives")
|
|
|
|
def test_disable_screenshots(self):
|
|
"""Test disabling screenshots."""
|
|
archiver = SiteArchiver(include_screenshots=False)
|
|
assert archiver.include_screenshots is False
|
|
assert archiver.screenshot_config.enabled is False
|
|
|
|
def test_disable_markdown(self):
|
|
"""Test disabling markdown."""
|
|
archiver = SiteArchiver(include_markdown=False)
|
|
assert archiver.include_markdown is False
|
|
|
|
def test_fast_mode(self):
|
|
"""Test enabling fast mode."""
|
|
archiver = SiteArchiver(fast_mode=True)
|
|
assert archiver.fast_mode is True
|
|
|
|
def test_trim_wrapper(self):
|
|
"""Test trim wrapper option."""
|
|
archiver = SiteArchiver(trim_wrapper=True)
|
|
assert archiver.trim_wrapper is True
|
|
|
|
def test_fresh_start(self):
|
|
"""Test fresh start option."""
|
|
archiver = SiteArchiver(fresh_start=True)
|
|
assert archiver.fresh_start is True
|
|
|
|
|
|
class TestSiteArchiverExtractTitle:
|
|
"""Test SiteArchiver._extract_title method."""
|
|
|
|
def setup_method(self):
|
|
self.archiver = SiteArchiver()
|
|
|
|
def test_extract_title_from_title_tag(self):
|
|
"""Test extracting title from <title> tag."""
|
|
html = "<html><head><title>Page Title</title></head><body></body></html>"
|
|
result = self.archiver._extract_title(html)
|
|
assert result == "Page Title"
|
|
|
|
def test_extract_title_from_h1(self):
|
|
"""Test extracting title from <h1> when no title tag."""
|
|
html = "<html><body><h1>Heading Title</h1></body></html>"
|
|
result = self.archiver._extract_title(html)
|
|
assert result == "Heading Title"
|
|
|
|
def test_extract_title_empty(self):
|
|
"""Test extracting title from empty HTML."""
|
|
html = "<html><body></body></html>"
|
|
result = self.archiver._extract_title(html)
|
|
assert result is None
|
|
|
|
def test_extract_title_invalid_html(self):
|
|
"""Test extracting title from invalid HTML."""
|
|
html = "not html at all"
|
|
result = self.archiver._extract_title(html)
|
|
assert result is None
|
|
|
|
|
|
class TestSiteArchiverWriteIndex:
|
|
"""Test SiteArchiver._write_index_html method."""
|
|
|
|
def setup_method(self):
|
|
self.temp_dir = tempfile.mkdtemp()
|
|
self.archiver = SiteArchiver()
|
|
|
|
def teardown_method(self):
|
|
if os.path.exists(self.temp_dir):
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
def test_write_index_creates_file(self):
|
|
"""Test that index.html is created."""
|
|
sitemap = [
|
|
{"path": "html/page1.html", "title": "Page 1"},
|
|
{"path": "html/page2.html", "title": "Page 2"},
|
|
]
|
|
self.archiver._write_index_html(Path(self.temp_dir), sitemap, "example.com")
|
|
|
|
index_path = Path(self.temp_dir) / "index.html"
|
|
assert index_path.exists()
|
|
|
|
def test_write_index_contains_domain(self):
|
|
"""Test that index contains domain name."""
|
|
sitemap = []
|
|
self.archiver._write_index_html(Path(self.temp_dir), sitemap, "example.com")
|
|
|
|
index_path = Path(self.temp_dir) / "index.html"
|
|
content = index_path.read_text()
|
|
assert "example.com" in content
|
|
|
|
def test_write_index_contains_links(self):
|
|
"""Test that index contains page links."""
|
|
sitemap = [
|
|
{"path": "html/page1.html", "title": "Page 1"},
|
|
]
|
|
self.archiver._write_index_html(Path(self.temp_dir), sitemap, "example.com")
|
|
|
|
index_path = Path(self.temp_dir) / "index.html"
|
|
content = index_path.read_text()
|
|
assert "Page 1" in content
|
|
assert "html/page1.html" in content
|
|
|
|
def test_write_index_limits_to_1000(self):
|
|
"""Test that index limits sitemap to 1000 entries."""
|
|
sitemap = [{"path": f"html/page{i}.html", "title": f"Page {i}"} for i in range(1500)]
|
|
self.archiver._write_index_html(Path(self.temp_dir), sitemap, "example.com")
|
|
|
|
index_path = Path(self.temp_dir) / "index.html"
|
|
content = index_path.read_text()
|
|
# Should mention there are more pages
|
|
assert "more pages" in content or "500 more" in content or "and" in content
|
|
|
|
|
|
class TestSiteArchiverArchive:
|
|
"""Test SiteArchiver.archive method."""
|
|
|
|
def setup_method(self):
|
|
self.temp_dir = tempfile.mkdtemp()
|
|
self.archiver = SiteArchiver(output_dir=self.temp_dir)
|
|
|
|
def teardown_method(self):
|
|
if os.path.exists(self.temp_dir):
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_archive_creates_tarball(self):
|
|
"""Test that archive creates a tar.gz file."""
|
|
with patch('archive.NeoPig') as MockNeoPig:
|
|
# Setup mocks
|
|
mock_pig = AsyncMock()
|
|
MockNeoPig.return_value = mock_pig
|
|
mock_pig.init = AsyncMock()
|
|
mock_pig.crawl = AsyncMock(return_value={})
|
|
mock_pig.db = AsyncMock()
|
|
mock_pig.db.get_all_media_uri_mappings = AsyncMock(return_value={})
|
|
mock_pig.db.get_pages_by_domain = AsyncMock(return_value=[])
|
|
mock_pig.db.close = AsyncMock()
|
|
mock_pig.screenshot_config = MagicMock()
|
|
mock_pig.screenshot_config.enabled = True
|
|
mock_pig._clear_state = MagicMock()
|
|
mock_pig.seen_pages = set()
|
|
mock_pig.seen_media = {}
|
|
mock_pig.seen_screenshots = set()
|
|
|
|
# Run archive
|
|
try:
|
|
result = await self.archiver.archive(
|
|
"https://example.com",
|
|
depth=1,
|
|
max_pages=1,
|
|
)
|
|
# Check if tarball was created (might not be if mocking isn't complete)
|
|
# assert result.suffix == ".gz" or str(result).endswith(".tar.gz")
|
|
except Exception:
|
|
pass # Full archive requires extensive mocking
|
|
|
|
|
|
class TestHAS_HTML2TEXT:
|
|
"""Test html2text availability constant."""
|
|
|
|
def test_has_html2text_is_bool(self):
|
|
"""Test HAS_HTML2TEXT is a boolean."""
|
|
assert isinstance(HAS_HTML2TEXT, bool)
|
|
|
|
|
|
class TestArchiveIntegration:
|
|
"""Integration tests for archive functionality."""
|
|
|
|
def setup_method(self):
|
|
self.temp_dir = tempfile.mkdtemp()
|
|
|
|
def teardown_method(self):
|
|
if os.path.exists(self.temp_dir):
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
def test_sanitize_domain_for_filename(self):
|
|
"""Test domain sanitization produces valid filename."""
|
|
domains = [
|
|
"example.com",
|
|
"sub.example.com",
|
|
"example.com:8080",
|
|
"example.co.uk",
|
|
]
|
|
for domain in domains:
|
|
result = sanitize_filename(domain)
|
|
# Should not contain problematic characters
|
|
assert "/" not in result
|
|
assert "\\" not in result
|
|
assert ":" not in result
|
|
# Should be valid filename
|
|
assert len(result) > 0
|
|
|
|
def test_url_to_path_for_common_patterns(self):
|
|
"""Test URL to path for common URL patterns."""
|
|
test_cases = [
|
|
("https://example.com/", "index.html"),
|
|
("https://example.com/about", "about/index.html"),
|
|
("https://example.com/blog/", "blog/index.html"),
|
|
("https://example.com/doc.html", "doc.html"),
|
|
("https://example.com/file.pdf", "file.pdf"),
|
|
]
|
|
for url, expected in test_cases:
|
|
result = url_to_path(url)
|
|
assert expected in result or result == expected
|
|
|
|
|
|
class TestUpgradeNeopigInArchive:
|
|
"""Test upgrade_neopig_in_archive function."""
|
|
|
|
def test_function_exists(self):
|
|
"""Test function is importable."""
|
|
from archive import upgrade_neopig_in_archive
|
|
assert callable(upgrade_neopig_in_archive)
|
|
|
|
|
|
class TestArchiverOptions:
|
|
"""Test various archiver configuration options."""
|
|
|
|
def test_screenshot_config_created(self):
|
|
"""Test screenshot config is created."""
|
|
archiver = SiteArchiver(include_screenshots=True)
|
|
assert archiver.screenshot_config is not None
|
|
assert archiver.screenshot_config.enabled is True
|
|
|
|
def test_screenshot_config_disabled(self):
|
|
"""Test screenshot config when disabled."""
|
|
archiver = SiteArchiver(include_screenshots=False)
|
|
assert archiver.screenshot_config.enabled is False
|
|
|
|
def test_markdown_respects_html2text(self):
|
|
"""Test markdown option respects html2text availability."""
|
|
archiver = SiteArchiver(include_markdown=True)
|
|
# Should be True only if html2text is installed
|
|
assert archiver.include_markdown == HAS_HTML2TEXT
|
|
|
|
def test_output_dir_path_conversion(self):
|
|
"""Test output_dir is converted to Path."""
|
|
archiver = SiteArchiver(output_dir="/custom/path")
|
|
assert isinstance(archiver.output_dir, Path)
|
|
assert str(archiver.output_dir) == "/custom/path"
|
|
|
|
|
|
if __name__ == '__main__':
|
|
pytest.main([__file__, '-v'])
|