""" Unit tests for FileUploadValidator. Tests cover all validation methods: - File size validation - File count validation - MIME type validation - PDF structure validation - Comprehensive upload validation """ from unittest.mock import patch from local_deep_research.security.file_upload_validator import ( FileUploadValidator, ) class TestValidateFileSize: """Tests for validate_file_size method. Oversized-path tests patch ``MAX_FILE_SIZE`` to a small value so they can exercise the rejection branch without allocating multi-GB buffers in CI. """ def test_valid_size_via_content_length(self): """Test that valid size passes via Content-Length header.""" is_valid, error = FileUploadValidator.validate_file_size( content_length=1024 * 1024 # 1MB ) assert is_valid is True assert error is None def test_valid_size_via_file_content(self): """Test that valid size passes via actual file content.""" content = b"x" * (1024 * 1024) # 1MB is_valid, error = FileUploadValidator.validate_file_size( content_length=None, file_content=content ) assert is_valid is True assert error is None def test_oversized_via_content_length(self): """Test that oversized file is rejected via Content-Length.""" with patch.object( FileUploadValidator, "MAX_FILE_SIZE", 50 * 1024 * 1024 ): is_valid, error = FileUploadValidator.validate_file_size( content_length=51 * 1024 * 1024 # 51MB over a 50MB cap ) assert is_valid is False assert error is not None assert "too large" in error.lower() def test_oversized_via_file_content(self): """Test that oversized file is rejected via actual content.""" with patch.object(FileUploadValidator, "MAX_FILE_SIZE", 1024): content = b"x" * 2048 # 2KB over a 1KB cap is_valid, error = FileUploadValidator.validate_file_size( content_length=None, file_content=content ) assert is_valid is False assert error is not None assert "too large" in error.lower() def test_exactly_at_limit(self): """Test that file exactly at limit passes.""" is_valid, error = FileUploadValidator.validate_file_size( content_length=FileUploadValidator.MAX_FILE_SIZE ) assert is_valid is True assert error is None def test_one_byte_over_limit(self): """Test that file one byte over limit is rejected.""" is_valid, error = FileUploadValidator.validate_file_size( content_length=FileUploadValidator.MAX_FILE_SIZE + 1 ) assert is_valid is False assert error is not None def test_both_none_returns_valid(self): """Test that both None values return valid (no info to validate).""" is_valid, error = FileUploadValidator.validate_file_size( content_length=None, file_content=None ) assert is_valid is True assert error is None class TestValidateFileCount: """Tests for validate_file_count method.""" def test_valid_file_count(self): """Test that valid file count passes.""" is_valid, error = FileUploadValidator.validate_file_count(10) assert is_valid is True assert error is None def test_single_file_valid(self): """Test that single file passes.""" is_valid, error = FileUploadValidator.validate_file_count(1) assert is_valid is True assert error is None def test_max_files_valid(self): """Test that exactly max files passes.""" is_valid, error = FileUploadValidator.validate_file_count( FileUploadValidator.MAX_FILES_PER_REQUEST ) assert is_valid is True assert error is None def test_too_many_files(self): """Test that exceeding max files is rejected.""" is_valid, error = FileUploadValidator.validate_file_count( FileUploadValidator.MAX_FILES_PER_REQUEST + 1 ) assert is_valid is False assert error is not None assert "too many" in error.lower() def test_zero_files(self): """Test that zero files is rejected.""" is_valid, error = FileUploadValidator.validate_file_count(0) assert is_valid is False assert error is not None assert "no files" in error.lower() def test_negative_count(self): """Test that negative count is rejected.""" is_valid, error = FileUploadValidator.validate_file_count(-1) assert is_valid is False assert error is not None class TestValidateMimeType: """Tests for validate_mime_type method.""" def test_valid_pdf_extension_and_magic(self): """Test that valid PDF passes.""" content = b"%PDF-1.4\ntest content" is_valid, error = FileUploadValidator.validate_mime_type( "test.pdf", content ) assert is_valid is True assert error is None def test_uppercase_extension_valid(self): """Test that uppercase .PDF extension passes.""" content = b"%PDF-1.4\ntest content" is_valid, error = FileUploadValidator.validate_mime_type( "test.PDF", content ) assert is_valid is True assert error is None def test_mixed_case_extension_valid(self): """Test that mixed case .Pdf extension passes.""" content = b"%PDF-1.4\ntest content" is_valid, error = FileUploadValidator.validate_mime_type( "test.PdF", content ) assert is_valid is True assert error is None def test_wrong_extension_rejected(self): """Test that wrong extension is rejected.""" content = b"%PDF-1.4\ntest content" is_valid, error = FileUploadValidator.validate_mime_type( "test.txt", content ) assert is_valid is False assert error is not None assert "only pdf" in error.lower() def test_no_extension_rejected(self): """Test that file without extension is rejected.""" content = b"%PDF-1.4\ntest content" is_valid, error = FileUploadValidator.validate_mime_type( "testfile", content ) assert is_valid is False assert error is not None def test_wrong_magic_bytes_rejected(self): """Test that wrong magic bytes are rejected.""" content = b"This is not a PDF" is_valid, error = FileUploadValidator.validate_mime_type( "test.pdf", content ) assert is_valid is False assert error is not None assert "signature mismatch" in error.lower() def test_empty_content_rejected(self): """Test that empty content is rejected.""" content = b"" is_valid, error = FileUploadValidator.validate_mime_type( "test.pdf", content ) assert is_valid is False assert error is not None def test_exe_disguised_as_pdf_rejected(self): """Test that executable disguised as PDF is rejected.""" # MZ header for executable content = b"MZ" + b"\x00" * 100 is_valid, error = FileUploadValidator.validate_mime_type( "malware.pdf", content ) assert is_valid is False assert error is not None class TestValidatePdfStructure: """Tests for validate_pdf_structure method.""" def test_malformed_pdf_structure(self): """Test that malformed PDF structure is rejected.""" # Has magic bytes but invalid structure content = b"%PDF-1.4\nthis is not valid pdf structure" is_valid, error = FileUploadValidator.validate_pdf_structure( "test.pdf", content ) assert is_valid is False assert error is not None assert "invalid" in error.lower() or "corrupted" in error.lower() def test_truncated_pdf_rejected(self): """Test that truncated PDF is rejected.""" content = b"%PDF-1.4" # Just the header is_valid, error = FileUploadValidator.validate_pdf_structure( "test.pdf", content ) assert is_valid is False assert error is not None class TestValidateUpload: """Tests for validate_upload comprehensive method.""" def test_all_validations_run(self): """Test that all validations are executed.""" # Invalid size should fail first. Patch the cap to keep memory low. with patch.object(FileUploadValidator, "MAX_FILE_SIZE", 1024): large_content = b"%PDF-1.4\n" + (b"x" * 2048) # 2KB over 1KB cap is_valid, error = FileUploadValidator.validate_upload( filename="test.pdf", file_content=large_content, ) assert is_valid is False assert "too large" in error.lower() def test_invalid_mime_fails_after_size(self): """Test that MIME validation runs after size validation.""" content = b"not a pdf" is_valid, error = FileUploadValidator.validate_upload( filename="test.txt", # Wrong extension file_content=content, ) assert is_valid is False assert "pdf" in error.lower() def test_returns_first_error(self): """Test that first validation error stops processing.""" # Large file with wrong extension - should fail on size first with patch.object(FileUploadValidator, "MAX_FILE_SIZE", 1024): large_content = b"x" * 2048 # 2KB over 1KB cap is_valid, error = FileUploadValidator.validate_upload( filename="test.txt", file_content=large_content, ) assert is_valid is False # Should fail on size before checking extension assert "too large" in error.lower() class TestConstants: """Tests for class constants.""" def test_max_file_size_default_is_3gb(self): """Default MAX_FILE_SIZE matches the 3 GB server-config default.""" expected = 3 * 1024 * 1024 * 1024 assert FileUploadValidator.MAX_FILE_SIZE == expected def test_max_files_is_200(self): """Verify MAX_FILES_PER_REQUEST is 200.""" assert FileUploadValidator.MAX_FILES_PER_REQUEST == 200 def test_pdf_magic_bytes(self): """Verify PDF magic bytes.""" assert FileUploadValidator.PDF_MAGIC_BYTES == b"%PDF" def test_allowed_mime_types(self): """Verify allowed MIME types.""" assert "application/pdf" in FileUploadValidator.ALLOWED_MIME_TYPES