Spaces:
Sleeping
Sleeping
| """ | |
| 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().""" | |
| 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 |