11import pytest
22from PIL import Image
33import io
4+ import asyncio
5+ from concurrent .futures import ThreadPoolExecutor
6+ from unittest .mock import AsyncMock , MagicMock
47from utils .core .images import (
58 validate_and_process_image ,
9+ read_upload_with_size_limit ,
10+ reject_oversized_content_length ,
611 InvalidImageError ,
712 MAX_FILE_SIZE ,
13+ MAX_AVATAR_UPLOAD_BYTES ,
814 MIN_DIMENSION ,
915 MAX_DIMENSION ,
1016)
@@ -18,6 +24,12 @@ def create_test_image(width: int, height: int, format: str = "PNG") -> bytes:
1824 return output .getvalue ()
1925
2026
27+ def _run_async (coro ):
28+ """Run a coroutine when pytest may already have an event loop active."""
29+ with ThreadPoolExecutor (max_workers = 1 ) as executor :
30+ return executor .submit (asyncio .run , coro ).result ()
31+
32+
2133def test_webp_dependencies_are_installed ():
2234 """Test that webp dependencies are installed"""
2335 assert ".webp" in Image .registered_extensions (), (
@@ -89,6 +101,41 @@ def test_file_too_large():
89101 assert "File too large" in str (exc_info .value .detail )
90102
91103
104+ def test_read_upload_with_size_limit_rejects_oversized_stream ():
105+ upload = MagicMock ()
106+ chunk_size = 64 * 1024
107+ upload .read = AsyncMock (
108+ side_effect = [b"x" * chunk_size , b"x" * (MAX_FILE_SIZE - chunk_size + 1 ), b"" ]
109+ )
110+
111+ with pytest .raises (InvalidImageError ) as exc_info :
112+ _run_async (read_upload_with_size_limit (upload , MAX_FILE_SIZE ))
113+
114+ assert "File too large" in str (exc_info .value .detail )
115+ assert upload .read .await_count == 2
116+
117+
118+ def test_read_upload_with_size_limit_accepts_valid_stream ():
119+ upload = MagicMock ()
120+ upload .read = AsyncMock (side_effect = [b"abc" , b"def" , b"" ])
121+
122+ data = _run_async (read_upload_with_size_limit (upload , MAX_FILE_SIZE ))
123+
124+ assert data == b"abcdef"
125+
126+
127+ def test_reject_oversized_content_length ():
128+ with pytest .raises (InvalidImageError ):
129+ reject_oversized_content_length (
130+ str (MAX_AVATAR_UPLOAD_BYTES + 1 ), MAX_AVATAR_UPLOAD_BYTES
131+ )
132+
133+
134+ def test_reject_oversized_content_length_allows_missing_or_valid ():
135+ reject_oversized_content_length (None , MAX_AVATAR_UPLOAD_BYTES )
136+ reject_oversized_content_length (str (MAX_FILE_SIZE ), MAX_AVATAR_UPLOAD_BYTES )
137+
138+
92139def test_corrupt_image_data ():
93140 """Test that corrupt image data is rejected"""
94141 corrupt_data = b"not an image"
0 commit comments