Files
tfm_ainventory/backend/tests/test_static_files.py

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