496 lines
18 KiB
Python
496 lines
18 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
Test Media Download Functionality
|
||
|
|
|
||
|
|
This script tests that media files (images and attachments) are properly downloaded
|
||
|
|
to the assets subfolder and referenced correctly in the HTML output.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
import json
|
||
|
|
import logging
|
||
|
|
import sys
|
||
|
|
import tempfile
|
||
|
|
from pathlib import Path
|
||
|
|
import os
|
||
|
|
from unittest.mock import AsyncMock, MagicMock
|
||
|
|
|
||
|
|
# Add the current directory to the path so we can import modules
|
||
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
|
|
|
||
|
|
from snapshot_downloader import SnapshotDownloader
|
||
|
|
|
||
|
|
|
||
|
|
class MediaDownloadTester:
|
||
|
|
"""Test class for media download functionality."""
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
"""Initialize the tester."""
|
||
|
|
self.logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
def test_assets_folder_creation(self):
|
||
|
|
"""Test that assets subfolder is created correctly."""
|
||
|
|
print("=" * 60)
|
||
|
|
print("TEST: Assets Folder Creation")
|
||
|
|
print("=" * 60)
|
||
|
|
|
||
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||
|
|
print("1. Testing assets folder creation...")
|
||
|
|
|
||
|
|
downloader = SnapshotDownloader(output_dir=temp_dir)
|
||
|
|
|
||
|
|
# Check if assets folder was created
|
||
|
|
assets_dir = Path(temp_dir) / "assets"
|
||
|
|
if assets_dir.exists() and assets_dir.is_dir():
|
||
|
|
print(" ✅ Assets folder created successfully")
|
||
|
|
else:
|
||
|
|
print(" ❌ Assets folder not created")
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Check if it's accessible
|
||
|
|
if downloader.assets_dir == assets_dir:
|
||
|
|
print(" ✅ Assets directory property set correctly")
|
||
|
|
else:
|
||
|
|
print(" ❌ Assets directory property incorrect")
|
||
|
|
return False
|
||
|
|
|
||
|
|
print("\n✅ Assets folder creation test passed!")
|
||
|
|
return True
|
||
|
|
|
||
|
|
def test_filename_sanitization(self):
|
||
|
|
"""Test filename sanitization functionality."""
|
||
|
|
print("\n" + "=" * 60)
|
||
|
|
print("TEST: Filename Sanitization")
|
||
|
|
print("=" * 60)
|
||
|
|
|
||
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||
|
|
downloader = SnapshotDownloader(output_dir=temp_dir)
|
||
|
|
|
||
|
|
test_cases = [
|
||
|
|
{
|
||
|
|
"input": "normal_filename.jpg",
|
||
|
|
"expected": "normal_filename.jpg",
|
||
|
|
"description": "Normal filename"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"input": "file<with>invalid:chars.png",
|
||
|
|
"expected": "file_with_invalid_chars.png",
|
||
|
|
"description": "Invalid characters"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"input": " .leading_trailing_spaces. ",
|
||
|
|
"expected": "leading_trailing_spaces",
|
||
|
|
"description": "Leading/trailing spaces and dots"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"input": "",
|
||
|
|
"expected": "media_file",
|
||
|
|
"description": "Empty filename"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"input": "file/with\\path|chars?.txt",
|
||
|
|
"expected": "file_with_path_chars_.txt",
|
||
|
|
"description": "Path characters"
|
||
|
|
}
|
||
|
|
]
|
||
|
|
|
||
|
|
print("1. Testing filename sanitization cases...")
|
||
|
|
for i, test_case in enumerate(test_cases, 1):
|
||
|
|
print(f"\n{i}. {test_case['description']}")
|
||
|
|
print(f" Input: '{test_case['input']}'")
|
||
|
|
|
||
|
|
result = downloader._sanitize_filename(test_case['input'])
|
||
|
|
print(f" Output: '{result}'")
|
||
|
|
|
||
|
|
if result == test_case['expected']:
|
||
|
|
print(" ✅ Correctly sanitized")
|
||
|
|
else:
|
||
|
|
print(f" ❌ Expected: '{test_case['expected']}'")
|
||
|
|
return False
|
||
|
|
|
||
|
|
print("\n✅ Filename sanitization test passed!")
|
||
|
|
return True
|
||
|
|
|
||
|
|
async def test_media_download_mock(self):
|
||
|
|
"""Test media download with mocked HTTP responses."""
|
||
|
|
print("\n" + "=" * 60)
|
||
|
|
print("TEST: Media Download (Mocked)")
|
||
|
|
print("=" * 60)
|
||
|
|
|
||
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||
|
|
downloader = SnapshotDownloader(output_dir=temp_dir)
|
||
|
|
|
||
|
|
print("1. Testing image download...")
|
||
|
|
|
||
|
|
# Mock media object
|
||
|
|
mock_media = {
|
||
|
|
"id": 794684,
|
||
|
|
"fileName": "test_image.jpeg",
|
||
|
|
"type": "image",
|
||
|
|
"mimeType": "image/jpeg",
|
||
|
|
"updated": "2025-07-31T12:46:24.413",
|
||
|
|
"status": "available",
|
||
|
|
"downloadable": True
|
||
|
|
}
|
||
|
|
|
||
|
|
# Create mock session and response
|
||
|
|
mock_response = AsyncMock()
|
||
|
|
mock_response.status = 200
|
||
|
|
mock_response.raise_for_status = MagicMock()
|
||
|
|
|
||
|
|
# Mock file content
|
||
|
|
fake_image_content = b"fake_image_data_for_testing"
|
||
|
|
|
||
|
|
async def mock_iter_chunked(chunk_size):
|
||
|
|
yield fake_image_content
|
||
|
|
|
||
|
|
mock_response.content.iter_chunked = mock_iter_chunked
|
||
|
|
|
||
|
|
mock_session = AsyncMock()
|
||
|
|
mock_session.get.return_value.__aenter__.return_value = mock_response
|
||
|
|
|
||
|
|
# Test the download
|
||
|
|
result = await downloader.download_media_file(mock_session, mock_media)
|
||
|
|
|
||
|
|
# Check result
|
||
|
|
if result == "assets/test_image.jpeg":
|
||
|
|
print(" ✅ Download returned correct relative path")
|
||
|
|
else:
|
||
|
|
print(f" ❌ Expected 'assets/test_image.jpeg', got '{result}'")
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Check file was created
|
||
|
|
expected_file = Path(temp_dir) / "assets" / "test_image.jpeg"
|
||
|
|
if expected_file.exists():
|
||
|
|
print(" ✅ File created in assets folder")
|
||
|
|
|
||
|
|
# Check file content
|
||
|
|
with open(expected_file, 'rb') as f:
|
||
|
|
content = f.read()
|
||
|
|
if content == fake_image_content:
|
||
|
|
print(" ✅ File content matches")
|
||
|
|
else:
|
||
|
|
print(" ❌ File content doesn't match")
|
||
|
|
return False
|
||
|
|
else:
|
||
|
|
print(" ❌ File not created")
|
||
|
|
return False
|
||
|
|
|
||
|
|
print("\n2. Testing existing file handling...")
|
||
|
|
|
||
|
|
# Test downloading the same file again (should return existing)
|
||
|
|
result2 = await downloader.download_media_file(mock_session, mock_media)
|
||
|
|
if result2 == "assets/test_image.jpeg":
|
||
|
|
print(" ✅ Existing file handling works")
|
||
|
|
else:
|
||
|
|
print(" ❌ Existing file handling failed")
|
||
|
|
return False
|
||
|
|
|
||
|
|
print("\n3. Testing download failure...")
|
||
|
|
|
||
|
|
# Test with invalid media (no ID)
|
||
|
|
invalid_media = {"fileName": "no_id_file.jpg"}
|
||
|
|
result3 = await downloader.download_media_file(mock_session, invalid_media)
|
||
|
|
if result3 is None:
|
||
|
|
print(" ✅ Properly handles invalid media")
|
||
|
|
else:
|
||
|
|
print(" ❌ Should return None for invalid media")
|
||
|
|
return False
|
||
|
|
|
||
|
|
print("\n✅ Media download mock test passed!")
|
||
|
|
return True
|
||
|
|
|
||
|
|
async def test_media_formatting_integration(self):
|
||
|
|
"""Test media formatting with downloaded files."""
|
||
|
|
print("\n" + "=" * 60)
|
||
|
|
print("TEST: Media Formatting Integration")
|
||
|
|
print("=" * 60)
|
||
|
|
|
||
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||
|
|
downloader = SnapshotDownloader(output_dir=temp_dir)
|
||
|
|
|
||
|
|
print("1. Testing snapshot with media formatting...")
|
||
|
|
|
||
|
|
# Create a test image file in assets
|
||
|
|
test_image_path = Path(temp_dir) / "assets" / "test_snapshot_image.jpeg"
|
||
|
|
test_image_path.parent.mkdir(exist_ok=True)
|
||
|
|
test_image_path.write_bytes(b"fake_image_content")
|
||
|
|
|
||
|
|
# Mock snapshot with media
|
||
|
|
mock_snapshot = {
|
||
|
|
"id": 123456,
|
||
|
|
"type": "Snapshot",
|
||
|
|
"child": {"forename": "Test", "surname": "Child"},
|
||
|
|
"author": {"forename": "Test", "surname": "Teacher"},
|
||
|
|
"startTime": "2024-01-15T10:30:00",
|
||
|
|
"notes": "<p>Test snapshot with media</p>",
|
||
|
|
"media": [
|
||
|
|
{
|
||
|
|
"id": 123456,
|
||
|
|
"fileName": "test_snapshot_image.jpeg",
|
||
|
|
"type": "image",
|
||
|
|
"mimeType": "image/jpeg",
|
||
|
|
"updated": "2024-01-15T10:30:00",
|
||
|
|
"status": "available",
|
||
|
|
"downloadable": True
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
# Mock session to simulate successful download
|
||
|
|
mock_session = AsyncMock()
|
||
|
|
|
||
|
|
# Override the download_media_file method to return our test path
|
||
|
|
original_download = downloader.download_media_file
|
||
|
|
async def mock_download(session, media):
|
||
|
|
if media.get('fileName') == 'test_snapshot_image.jpeg':
|
||
|
|
return "assets/test_snapshot_image.jpeg"
|
||
|
|
return await original_download(session, media)
|
||
|
|
|
||
|
|
downloader.download_media_file = mock_download
|
||
|
|
|
||
|
|
# Test formatting
|
||
|
|
html_content = await downloader.format_snapshot_html(mock_snapshot, mock_session)
|
||
|
|
|
||
|
|
print("2. Checking HTML content for media references...")
|
||
|
|
|
||
|
|
# Check for local image reference
|
||
|
|
if 'src="assets/test_snapshot_image.jpeg"' in html_content:
|
||
|
|
print(" ✅ Local image path found in HTML")
|
||
|
|
else:
|
||
|
|
print(" ❌ Local image path not found")
|
||
|
|
print(" Debug: Looking for image references...")
|
||
|
|
if 'assets/' in html_content:
|
||
|
|
print(" Found assets/ references in HTML")
|
||
|
|
if 'test_snapshot_image.jpeg' in html_content:
|
||
|
|
print(" Found filename in HTML")
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Check for image grid structure
|
||
|
|
if 'class="image-grid"' in html_content:
|
||
|
|
print(" ✅ Image grid structure present")
|
||
|
|
else:
|
||
|
|
print(" ❌ Image grid structure missing")
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Check for image metadata
|
||
|
|
if 'class="image-caption"' in html_content and 'class="image-meta"' in html_content:
|
||
|
|
print(" ✅ Image caption and metadata present")
|
||
|
|
else:
|
||
|
|
print(" ❌ Image caption or metadata missing")
|
||
|
|
return False
|
||
|
|
|
||
|
|
print("\n✅ Media formatting integration test passed!")
|
||
|
|
return True
|
||
|
|
|
||
|
|
async def test_complete_html_generation_with_media(self):
|
||
|
|
"""Test complete HTML generation with media downloads."""
|
||
|
|
print("\n" + "=" * 60)
|
||
|
|
print("TEST: Complete HTML Generation with Media")
|
||
|
|
print("=" * 60)
|
||
|
|
|
||
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||
|
|
downloader = SnapshotDownloader(output_dir=temp_dir)
|
||
|
|
|
||
|
|
print("1. Setting up test environment...")
|
||
|
|
|
||
|
|
# Create test image files
|
||
|
|
test_images = ["image1.jpg", "image2.png"]
|
||
|
|
for img_name in test_images:
|
||
|
|
img_path = Path(temp_dir) / "assets" / img_name
|
||
|
|
img_path.write_bytes(f"fake_content_for_{img_name}".encode())
|
||
|
|
|
||
|
|
# Mock snapshots with media
|
||
|
|
mock_snapshots = [
|
||
|
|
{
|
||
|
|
"id": 100001,
|
||
|
|
"type": "Snapshot",
|
||
|
|
"child": {"forename": "Alice", "surname": "Smith"},
|
||
|
|
"author": {"forename": "Teacher", "surname": "One"},
|
||
|
|
"startTime": "2024-01-15T10:30:00",
|
||
|
|
"notes": "<p>Alice's first snapshot</p>",
|
||
|
|
"media": [
|
||
|
|
{
|
||
|
|
"id": 1001,
|
||
|
|
"fileName": "image1.jpg",
|
||
|
|
"type": "image",
|
||
|
|
"mimeType": "image/jpeg"
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"id": 100002,
|
||
|
|
"type": "Snapshot",
|
||
|
|
"child": {"forename": "Bob", "surname": "Johnson"},
|
||
|
|
"author": {"forename": "Teacher", "surname": "Two"},
|
||
|
|
"startTime": "2024-01-16T14:20:00",
|
||
|
|
"notes": "<p>Bob's creative work</p>",
|
||
|
|
"media": [
|
||
|
|
{
|
||
|
|
"id": 1002,
|
||
|
|
"fileName": "image2.png",
|
||
|
|
"type": "image",
|
||
|
|
"mimeType": "image/png"
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
]
|
||
|
|
|
||
|
|
# Mock the download_media_file method
|
||
|
|
async def mock_download_media(session, media):
|
||
|
|
filename = media.get('fileName', 'unknown.jpg')
|
||
|
|
if filename in test_images:
|
||
|
|
return f"assets/{filename}"
|
||
|
|
return None
|
||
|
|
|
||
|
|
downloader.download_media_file = mock_download_media
|
||
|
|
|
||
|
|
print("2. Generating complete HTML file...")
|
||
|
|
html_file = await downloader.generate_html_file(mock_snapshots, "2024-01-01", "2024-12-31")
|
||
|
|
|
||
|
|
if html_file and html_file.exists():
|
||
|
|
print(" ✅ HTML file generated successfully")
|
||
|
|
|
||
|
|
with open(html_file, 'r', encoding='utf-8') as f:
|
||
|
|
content = f.read()
|
||
|
|
|
||
|
|
print("3. Checking HTML content...")
|
||
|
|
|
||
|
|
# Check for local image references
|
||
|
|
checks = [
|
||
|
|
('src="assets/image1.jpg"', "Image 1 local reference"),
|
||
|
|
('src="assets/image2.png"', "Image 2 local reference"),
|
||
|
|
('Alice by Teacher One', "Snapshot 1 title"),
|
||
|
|
('Bob by Teacher Two', "Snapshot 2 title"),
|
||
|
|
('class="image-grid"', "Image grid structure"),
|
||
|
|
]
|
||
|
|
|
||
|
|
all_passed = True
|
||
|
|
for check_text, description in checks:
|
||
|
|
if check_text in content:
|
||
|
|
print(f" ✅ {description} found")
|
||
|
|
else:
|
||
|
|
print(f" ❌ {description} missing")
|
||
|
|
all_passed = False
|
||
|
|
|
||
|
|
if not all_passed:
|
||
|
|
return False
|
||
|
|
|
||
|
|
else:
|
||
|
|
print(" ❌ HTML file not generated")
|
||
|
|
return False
|
||
|
|
|
||
|
|
print("\n✅ Complete HTML generation with media test passed!")
|
||
|
|
return True
|
||
|
|
|
||
|
|
async def run_all_tests(self):
|
||
|
|
"""Run all media download tests."""
|
||
|
|
print("🚀 Starting Media Download Tests")
|
||
|
|
print("=" * 80)
|
||
|
|
|
||
|
|
try:
|
||
|
|
success = True
|
||
|
|
|
||
|
|
success &= self.test_assets_folder_creation()
|
||
|
|
success &= self.test_filename_sanitization()
|
||
|
|
success &= await self.test_media_download_mock()
|
||
|
|
success &= await self.test_media_formatting_integration()
|
||
|
|
success &= await self.test_complete_html_generation_with_media()
|
||
|
|
|
||
|
|
if success:
|
||
|
|
print("\n" + "=" * 80)
|
||
|
|
print("🎉 ALL MEDIA DOWNLOAD TESTS PASSED!")
|
||
|
|
print("=" * 80)
|
||
|
|
print("✅ Assets folder created correctly")
|
||
|
|
print("✅ Filename sanitization works properly")
|
||
|
|
print("✅ Media files download to assets subfolder")
|
||
|
|
print("✅ HTML references local files correctly")
|
||
|
|
print("✅ Complete integration working")
|
||
|
|
print("\n📁 Media Download Features:")
|
||
|
|
print("• Downloads images to assets/ subfolder")
|
||
|
|
print("• Downloads attachments to assets/ subfolder")
|
||
|
|
print("• Uses relative paths in HTML (assets/filename.jpg)")
|
||
|
|
print("• Fallback to API URLs if download fails")
|
||
|
|
print("• Sanitizes filenames for filesystem safety")
|
||
|
|
print("• Handles existing files (no re-download)")
|
||
|
|
else:
|
||
|
|
print("\n❌ SOME MEDIA DOWNLOAD TESTS FAILED")
|
||
|
|
|
||
|
|
return success
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
print(f"\n❌ MEDIA DOWNLOAD TESTS FAILED: {e}")
|
||
|
|
import traceback
|
||
|
|
traceback.print_exc()
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
def show_media_download_info():
|
||
|
|
"""Show information about media download functionality."""
|
||
|
|
print("\n" + "=" * 80)
|
||
|
|
print("📁 MEDIA DOWNLOAD FUNCTIONALITY")
|
||
|
|
print("=" * 80)
|
||
|
|
|
||
|
|
print("\n🎯 How It Works:")
|
||
|
|
print("1. Creates 'assets' subfolder in output directory")
|
||
|
|
print("2. Downloads media files (images, attachments) from API")
|
||
|
|
print("3. Saves files with sanitized filenames")
|
||
|
|
print("4. Updates HTML to reference local files")
|
||
|
|
print("5. Fallback to API URLs if download fails")
|
||
|
|
|
||
|
|
print("\n📋 Supported Media Types:")
|
||
|
|
print("• Images: JPEG, PNG, GIF, WebP, etc.")
|
||
|
|
print("• Documents: PDF, DOC, TXT, etc.")
|
||
|
|
print("• Any file type from ParentZone media API")
|
||
|
|
|
||
|
|
print("\n💾 File Organization:")
|
||
|
|
print("output_directory/")
|
||
|
|
print("├── snapshots_DATE_to_DATE.html")
|
||
|
|
print("├── snapshots.log")
|
||
|
|
print("└── assets/")
|
||
|
|
print(" ├── image1.jpeg")
|
||
|
|
print(" ├── document.pdf")
|
||
|
|
print(" └── attachment.txt")
|
||
|
|
|
||
|
|
print("\n🔗 HTML Integration:")
|
||
|
|
print("• Images: <img src=\"assets/filename.jpg\">")
|
||
|
|
print("• Attachments: <a href=\"assets/filename.pdf\">")
|
||
|
|
print("• Relative paths for portability")
|
||
|
|
print("• Self-contained reports (HTML + assets)")
|
||
|
|
|
||
|
|
print("\n✨ Benefits:")
|
||
|
|
print("• Offline viewing - images work without internet")
|
||
|
|
print("• Faster loading - no API requests for media")
|
||
|
|
print("• Portable reports - can be shared easily")
|
||
|
|
print("• Professional presentation with embedded media")
|
||
|
|
|
||
|
|
print("\n⚠️ Considerations:")
|
||
|
|
print("• Requires storage space for downloaded media")
|
||
|
|
print("• Download time increases with media count")
|
||
|
|
print("• Large files may take longer to process")
|
||
|
|
print("• API authentication required for media download")
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
"""Main test function."""
|
||
|
|
# Setup logging
|
||
|
|
logging.basicConfig(
|
||
|
|
level=logging.INFO,
|
||
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||
|
|
)
|
||
|
|
|
||
|
|
tester = MediaDownloadTester()
|
||
|
|
|
||
|
|
# Run tests
|
||
|
|
success = asyncio.run(tester.run_all_tests())
|
||
|
|
|
||
|
|
# Show information
|
||
|
|
if success:
|
||
|
|
show_media_download_info()
|
||
|
|
|
||
|
|
return 0 if success else 1
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
exit(main())
|