"""Tests for static file serving of uploaded images.""" import os import tempfile from pathlib import Path import pytest from fastapi.testclient import TestClient from backend.main import app from backend.services.image_storage import save_image, IMAGES_ROOT, ensure_image_directories @pytest.fixture(autouse=True) def setup_images_directory(): """Create and clean up images directory for each test.""" # Ensure directory exists IMAGES_ROOT.mkdir(parents=True, exist_ok=True) yield # Cleanup after test import shutil if IMAGES_ROOT.exists(): shutil.rmtree(IMAGES_ROOT) @pytest.fixture def client(): """FastAPI test client.""" return TestClient(app) @pytest.fixture def sample_jpeg_image(): """Create a minimal valid JPEG image for testing.""" # Minimal JPEG header + footer jpeg_data = ( b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00' b'\xff\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\t\t' b'\x08\n\x0c\x14\r\x0c\x0b\x0b\x0c\x19\x12\x13\x0f\x14\x1d\x1a' b'\x1f\x1e\x1d\x1a\x1c\x1c $.\' ",#\x1c\x1c(7),01444\x1f\'' b'\x9898_-282-\xff\xc0\x00\x0b\x08\x00\x01\x00\x01\x01\x11\x00' b'\xff\xc4\x00\x1f\x00\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00' b'\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08' b'\t\n\x0b\xff\xda\x00\x08\x01\x01\x00\x00?\x00\x7f\x00\xff\xd9' ) return jpeg_data @pytest.fixture def sample_png_image(): """Create a minimal valid PNG image for testing.""" # Create a minimal valid PNG programmatically using PIL try: from PIL import Image from io import BytesIO img = Image.new('RGB', (1, 1), color='white') buffer = BytesIO() img.save(buffer, format='PNG') return buffer.getvalue() except ImportError: # Fallback to hardcoded PNG if PIL not available # This is a valid minimal PNG that should be detected correctly png_data = ( b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00' b'\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDATx' b'\x9cc\xf8\xcf\xc0\x00\x00\x00\x03\x00\x01\xfc\x83\t\xbf\x00' b'\x00\x00\x00IEND\xaeB`\x82' ) return png_data class TestStaticFileServing: """Test static file serving for /images/ directory.""" def test_images_directory_exists_on_startup(self): """Verify /images directory is created on startup.""" assert IMAGES_ROOT.exists(), f"Images directory {IMAGES_ROOT} should exist" assert IMAGES_ROOT.is_dir(), f"{IMAGES_ROOT} should be a directory" def test_serve_uploaded_jpeg_image(self, client, sample_jpeg_image): """Test serving a JPEG image uploaded to /images/.""" # Save image directly using image storage utility image_path = save_image( file_bytes=sample_jpeg_image, category="test_category", filename_base="test_image", variant="original" ) # Request the image via static file mount response = client.get(image_path) # Verify response assert response.status_code == 200, f"Expected 200, got {response.status_code}" assert response.headers["content-type"] == "image/jpeg" assert response.content == sample_jpeg_image def test_serve_uploaded_png_image(self, client, sample_png_image): """Test serving a PNG image uploaded to /images/.""" image_path = save_image( file_bytes=sample_png_image, category="test_category", filename_base="test_png", variant="original" ) response = client.get(image_path) # Note: save_image always saves with .jpg extension assert response.status_code == 200 # Content is served back correctly (extension determines MIME type) assert response.content == sample_png_image def test_serve_thumbnail_variant(self, client, sample_jpeg_image): """Test serving thumbnail variant of an image.""" image_path = save_image( file_bytes=sample_jpeg_image, category="test_category", filename_base="test_image", variant="thumb" ) response = client.get(image_path) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" def test_404_for_nonexistent_image(self, client): """Test 404 response for non-existent image.""" response = client.get("/images/nonexistent/missing.jpg") assert response.status_code == 404 def test_directory_traversal_protection(self, client): """Test that directory traversal attempts are blocked.""" # Try various directory traversal patterns traversal_paths = [ "/images/../../../etc/passwd", "/images/test/../../etc/passwd", "/images/test/..%2F..%2Fetc%2Fpasswd", ] for path in traversal_paths: response = client.get(path) # Should either be 404 or 400, not 200 assert response.status_code in [400, 404], \ f"Path {path} should not be accessible, got {response.status_code}" def test_serve_multiple_categories(self, client, sample_jpeg_image, sample_png_image): """Test serving images from multiple categories.""" # Save images to different categories jpeg_path = save_image( file_bytes=sample_jpeg_image, category="memory", filename_base="ddr4_stick", variant="original" ) png_path = save_image( file_bytes=sample_png_image, category="networking", filename_base="sfp_transceiver", variant="original" ) # Verify both can be served jpeg_response = client.get(jpeg_path) png_response = client.get(png_path) assert jpeg_response.status_code == 200 assert jpeg_response.headers["content-type"] == "image/jpeg" assert png_response.status_code == 200 # Both saved as .jpg due to save_image implementation assert png_response.headers["content-type"] in ["image/jpeg", "image/jpg"] def test_content_matches_uploaded_file(self, client, sample_jpeg_image): """Verify that served content exactly matches uploaded file.""" image_path = save_image( file_bytes=sample_jpeg_image, category="test_category", filename_base="test_image", variant="original" ) response = client.get(image_path) # Content-length should match assert len(response.content) == len(sample_jpeg_image) # Content should match byte-for-byte assert response.content == sample_jpeg_image def test_mime_type_auto_detection_jpeg(self, client, sample_jpeg_image): """Test MIME type is auto-detected correctly for JPEG.""" image_path = save_image( file_bytes=sample_jpeg_image, category="test_category", filename_base="jpeg_file", variant="original" ) response = client.get(image_path) assert response.headers["content-type"] in ["image/jpeg", "image/jpg"] def test_mime_type_auto_detection_all_saved_as_jpeg(self, client, sample_png_image): """Test MIME type detection for images saved as JPEG (all images saved as .jpg).""" # Note: save_image always saves with .jpg extension regardless of input format image_path = save_image( file_bytes=sample_png_image, category="test_category", filename_base="png_file", variant="original" ) response = client.get(image_path) # File extension is .jpg, so MIME type will be image/jpeg assert response.headers["content-type"] in ["image/jpeg", "image/jpg"] def test_original_and_thumbnail_variants_accessible(self, client, sample_jpeg_image): """Test both original and thumbnail variants are accessible.""" original_path = save_image( file_bytes=sample_jpeg_image, category="test_category", filename_base="multi_variant", variant="original" ) thumb_path = save_image( file_bytes=sample_jpeg_image, category="test_category", filename_base="multi_variant", variant="thumb" ) # Both should be accessible original_response = client.get(original_path) thumb_response = client.get(thumb_path) assert original_response.status_code == 200 assert thumb_response.status_code == 200 def test_images_served_with_correct_path_structure(self, client, sample_jpeg_image): """Test images are served at /images/{category}/{filename} path.""" image_path = save_image( file_bytes=sample_jpeg_image, category="storage", filename_base="ssd_drive", variant="original" ) # Path should be /images/storage/ssd_drive_original.jpg assert image_path.startswith("/images/"), "Path should start with /images/" assert "storage" in image_path, "Path should include category" response = client.get(image_path) assert response.status_code == 200