background-remover-api / tests /test_validators.py
Keramo's picture
Upload 13 files
4ba81a9 verified
Raw
History Blame Contribute Delete
7.41 kB
"""
tests/test_validators.py
Tests for the upload validation pipeline.
"""
import io
import pytest
from unittest.mock import MagicMock, patch
from PIL import Image
from fastapi import HTTPException
from services.validators import (
validate_file_type,
validate_file_size,
validate_image_dimensions,
validate_image_integrity,
)
from config.constants import (
ALLOWED_TYPES,
INVALID_FILE_TYPE,
FILE_TOO_LARGE,
INVALID_DIMENSIONS,
INVALID_IMAGE,
)
from config.settings import MAX_FILE_SIZE, MAX_WIDTH, MAX_HEIGHT
# ── Helpers ────────────────────────────────────────────────────────────────────
def make_upload_file(content_type: str, data: bytes, filename: str = "test.png"):
"""Create a mock UploadFile with content_type property."""
file_obj = MagicMock()
file_obj.filename = filename
file_obj.content_type = content_type
file_obj.file = io.BytesIO(data)
return file_obj
def make_test_image(
width: int = 100,
height: int = 100,
format: str = "PNG",
) -> bytes:
"""Create a valid test image and return its bytes."""
img = Image.new("RGBA", (width, height), (128, 128, 128, 255))
buf = io.BytesIO()
img.save(buf, format=format)
buf.seek(0)
return buf.getvalue()
# ── 1. File Type Validation ───────────────────────────────────────────────────
class TestValidateFileType:
"""Tests for validate_file_type()."""
@pytest.mark.parametrize("mime_type", ALLOWED_TYPES)
def test_valid_file_type(self, mime_type):
"""Accept allowed MIME types."""
data = make_test_image()
file = make_upload_file(mime_type, data)
# Should not raise
validate_file_type(file)
def test_invalid_file_type(self):
"""Reject disallowed MIME types."""
data = b"not an image"
file = make_upload_file("text/plain", data, "test.txt")
with pytest.raises(HTTPException) as exc_info:
validate_file_type(file)
assert exc_info.value.status_code == 415
assert exc_info.value.detail == INVALID_FILE_TYPE
def test_pdf_rejected(self):
"""Reject PDF files."""
data = b"%PDF-1.4 fake pdf"
file = make_upload_file("application/pdf", data, "test.pdf")
with pytest.raises(HTTPException) as exc_info:
validate_file_type(file)
assert exc_info.value.status_code == 415
# ── 2. File Size Validation ───────────────────────────────────────────────────
class TestValidateFileSize:
"""Tests for validate_file_size()."""
def test_valid_file_size(self):
"""Accept files under MAX_FILE_SIZE."""
data = b"x" * 1000 # 1 KB
# Should not raise
validate_file_size(data)
def test_exact_max_size(self):
"""Accept files exactly at MAX_FILE_SIZE."""
data = b"x" * MAX_FILE_SIZE
# Should not raise
validate_file_size(data)
def test_oversized_file(self):
"""Reject files over MAX_FILE_SIZE."""
data = b"x" * (MAX_FILE_SIZE + 1)
with pytest.raises(HTTPException) as exc_info:
validate_file_size(data)
assert exc_info.value.status_code == 413
assert exc_info.value.detail == FILE_TOO_LARGE
def test_large_oversized_file(self):
"""Reject significantly oversized files."""
data = b"x" * (10 * 1024 * 1024) # 10 MB
with pytest.raises(HTTPException) as exc_info:
validate_file_size(data)
assert exc_info.value.status_code == 413
# ── 3. Image Dimensions Validation ────────────────────────────────────────────
class TestValidateImageDimensions:
"""Tests for validate_image_dimensions()."""
def test_valid_dimensions(self):
"""Accept images within limits."""
img = Image.new("RGBA", (100, 100))
# Should not raise
validate_image_dimensions(img)
def test_max_dimensions(self):
"""Accept images at exactly max dimensions."""
img = Image.new("RGBA", (MAX_WIDTH, MAX_HEIGHT))
# Should not raise
validate_image_dimensions(img)
def test_width_exceeds_limit(self):
"""Reject images wider than MAX_WIDTH."""
img = Image.new("RGBA", (MAX_WIDTH + 1, 100))
with pytest.raises(HTTPException) as exc_info:
validate_image_dimensions(img)
assert exc_info.value.status_code == 400
assert INVALID_DIMENSIONS in exc_info.value.detail
def test_height_exceeds_limit(self):
"""Reject images taller than MAX_HEIGHT."""
img = Image.new("RGBA", (100, MAX_HEIGHT + 1))
with pytest.raises(HTTPException) as exc_info:
validate_image_dimensions(img)
assert exc_info.value.status_code == 400
def test_both_exceed_limit(self):
"""Reject images exceeding both limits."""
img = Image.new("RGBA", (MAX_WIDTH + 1, MAX_HEIGHT + 1))
with pytest.raises(HTTPException) as exc_info:
validate_image_dimensions(img)
assert exc_info.value.status_code == 400
# ── 4. Image Integrity Validation ─────────────────────────────────────────────
class TestValidateImageIntegrity:
"""Tests for validate_image_integrity()."""
def test_valid_png(self):
"""Accept a valid PNG image."""
data = make_test_image(format="PNG")
result = validate_image_integrity(data)
assert isinstance(result, Image.Image)
assert result.mode == "RGBA"
def test_valid_jpeg(self):
"""Accept a valid JPEG image."""
img = Image.new("RGB", (100, 100), (128, 128, 128))
buf = io.BytesIO()
img.save(buf, format="JPEG")
buf.seek(0)
data = buf.getvalue()
result = validate_image_integrity(data)
assert isinstance(result, Image.Image)
def test_corrupted_image(self):
"""Reject corrupted data."""
data = b"not an image at all"
with pytest.raises(HTTPException) as exc_info:
validate_image_integrity(data)
assert exc_info.value.status_code == 400
assert exc_info.value.detail == INVALID_IMAGE
def test_truncated_image(self):
"""Reject truncated image data."""
# Create a valid image but truncate it
data = make_test_image()
truncated = data[:len(data) // 2]
with pytest.raises(HTTPException) as exc_info:
validate_image_integrity(truncated)
assert exc_info.value.status_code == 400
def test_empty_data(self):
"""Reject empty data."""
with pytest.raises(HTTPException) as exc_info:
validate_image_integrity(b"")
assert exc_info.value.status_code == 400