257 lines
9.3 KiB
Python
257 lines
9.3 KiB
Python
"""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
|