test_build_from_mapping.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import uuid
  2. from unittest.mock import MagicMock, patch
  3. import pytest
  4. from httpx import Response
  5. from factories.file_factory import (
  6. File,
  7. FileTransferMethod,
  8. FileType,
  9. FileUploadConfig,
  10. build_from_mapping,
  11. )
  12. from models import ToolFile, UploadFile
  13. # Test Data
  14. TEST_TENANT_ID = "test_tenant_id"
  15. TEST_UPLOAD_FILE_ID = str(uuid.uuid4())
  16. TEST_TOOL_FILE_ID = str(uuid.uuid4())
  17. TEST_REMOTE_URL = "http://example.com/test.jpg"
  18. # Test Config
  19. TEST_CONFIG = FileUploadConfig(
  20. allowed_file_types=[FileType.IMAGE, FileType.DOCUMENT],
  21. allowed_file_extensions=[".jpg", ".pdf"],
  22. allowed_file_upload_methods=[FileTransferMethod.LOCAL_FILE, FileTransferMethod.TOOL_FILE],
  23. number_limits=10,
  24. )
  25. # Fixtures
  26. @pytest.fixture
  27. def mock_upload_file():
  28. mock = MagicMock(spec=UploadFile)
  29. mock.id = TEST_UPLOAD_FILE_ID
  30. mock.tenant_id = TEST_TENANT_ID
  31. mock.name = "test.jpg"
  32. mock.extension = "jpg"
  33. mock.mime_type = "image/jpeg"
  34. mock.source_url = TEST_REMOTE_URL
  35. mock.size = 1024
  36. mock.key = "test_key"
  37. with patch("factories.file_factory.db.session.scalar", return_value=mock) as m:
  38. yield m
  39. @pytest.fixture
  40. def mock_tool_file():
  41. mock = MagicMock(spec=ToolFile)
  42. mock.id = TEST_TOOL_FILE_ID
  43. mock.tenant_id = TEST_TENANT_ID
  44. mock.name = "tool_file.pdf"
  45. mock.file_key = "tool_file.pdf"
  46. mock.mimetype = "application/pdf"
  47. mock.original_url = "http://example.com/tool.pdf"
  48. mock.size = 2048
  49. with patch("factories.file_factory.db.session.scalar", return_value=mock):
  50. yield mock
  51. @pytest.fixture
  52. def mock_http_head():
  53. def _mock_response(filename, size, content_type):
  54. return Response(
  55. status_code=200,
  56. headers={
  57. "Content-Disposition": f'attachment; filename="{filename}"',
  58. "Content-Length": str(size),
  59. "Content-Type": content_type,
  60. },
  61. )
  62. with patch("factories.file_factory.ssrf_proxy.head") as mock_head:
  63. mock_head.return_value = _mock_response("remote_test.jpg", 2048, "image/jpeg")
  64. yield mock_head
  65. # Helper functions
  66. def local_file_mapping(file_type="image"):
  67. return {
  68. "transfer_method": "local_file",
  69. "upload_file_id": TEST_UPLOAD_FILE_ID,
  70. "type": file_type,
  71. }
  72. def tool_file_mapping(file_type="document"):
  73. return {
  74. "transfer_method": "tool_file",
  75. "tool_file_id": TEST_TOOL_FILE_ID,
  76. "type": file_type,
  77. }
  78. # Tests
  79. def test_build_from_mapping_backward_compatibility(mock_upload_file):
  80. mapping = local_file_mapping(file_type="image")
  81. file = build_from_mapping(mapping=mapping, tenant_id=TEST_TENANT_ID)
  82. assert isinstance(file, File)
  83. assert file.transfer_method == FileTransferMethod.LOCAL_FILE
  84. assert file.type == FileType.IMAGE
  85. assert file.related_id == TEST_UPLOAD_FILE_ID
  86. @pytest.mark.parametrize(
  87. ("file_type", "should_pass", "expected_error"),
  88. [
  89. ("image", True, None),
  90. ("document", False, "Detected file type does not match"),
  91. ],
  92. )
  93. def test_build_from_local_file_strict_validation(mock_upload_file, file_type, should_pass, expected_error):
  94. mapping = local_file_mapping(file_type=file_type)
  95. if should_pass:
  96. file = build_from_mapping(mapping=mapping, tenant_id=TEST_TENANT_ID, strict_type_validation=True)
  97. assert file.type == FileType(file_type)
  98. else:
  99. with pytest.raises(ValueError, match=expected_error):
  100. build_from_mapping(mapping=mapping, tenant_id=TEST_TENANT_ID, strict_type_validation=True)
  101. @pytest.mark.parametrize(
  102. ("file_type", "should_pass", "expected_error"),
  103. [
  104. ("document", True, None),
  105. ("image", False, "Detected file type does not match"),
  106. ],
  107. )
  108. def test_build_from_tool_file_strict_validation(mock_tool_file, file_type, should_pass, expected_error):
  109. """Strict type validation for tool_file."""
  110. mapping = tool_file_mapping(file_type=file_type)
  111. if should_pass:
  112. file = build_from_mapping(mapping=mapping, tenant_id=TEST_TENANT_ID, strict_type_validation=True)
  113. assert file.type == FileType(file_type)
  114. else:
  115. with pytest.raises(ValueError, match=expected_error):
  116. build_from_mapping(mapping=mapping, tenant_id=TEST_TENANT_ID, strict_type_validation=True)
  117. def test_build_from_remote_url(mock_http_head):
  118. mapping = {
  119. "transfer_method": "remote_url",
  120. "url": TEST_REMOTE_URL,
  121. "type": "image",
  122. }
  123. file = build_from_mapping(mapping=mapping, tenant_id=TEST_TENANT_ID)
  124. assert file.transfer_method == FileTransferMethod.REMOTE_URL
  125. assert file.type == FileType.IMAGE
  126. assert file.filename == "remote_test.jpg"
  127. assert file.size == 2048
  128. def test_tool_file_not_found():
  129. """Test ToolFile not found in database."""
  130. with patch("factories.file_factory.db.session.scalar", return_value=None):
  131. mapping = tool_file_mapping()
  132. with pytest.raises(ValueError, match=f"ToolFile {TEST_TOOL_FILE_ID} not found"):
  133. build_from_mapping(mapping=mapping, tenant_id=TEST_TENANT_ID)
  134. def test_local_file_not_found():
  135. """Test UploadFile not found in database."""
  136. with patch("factories.file_factory.db.session.scalar", return_value=None):
  137. mapping = local_file_mapping()
  138. with pytest.raises(ValueError, match="Invalid upload file"):
  139. build_from_mapping(mapping=mapping, tenant_id=TEST_TENANT_ID)
  140. def test_build_without_type_specification(mock_upload_file):
  141. """Test the situation where no file type is specified"""
  142. mapping = {
  143. "transfer_method": "local_file",
  144. "upload_file_id": TEST_UPLOAD_FILE_ID,
  145. # type field is intentionally omitted
  146. }
  147. file = build_from_mapping(mapping=mapping, tenant_id=TEST_TENANT_ID)
  148. # Should automatically infer the type as "image" based on the file extension
  149. assert file.type == FileType.IMAGE
  150. @pytest.mark.parametrize(
  151. ("file_type", "should_pass", "expected_error"),
  152. [
  153. ("image", True, None),
  154. ("video", False, "File validation failed"),
  155. ],
  156. )
  157. def test_file_validation_with_config(mock_upload_file, file_type, should_pass, expected_error):
  158. """Test the validation of files and configurations"""
  159. mapping = local_file_mapping(file_type=file_type)
  160. if should_pass:
  161. file = build_from_mapping(mapping=mapping, tenant_id=TEST_TENANT_ID, config=TEST_CONFIG)
  162. assert file is not None
  163. else:
  164. with pytest.raises(ValueError, match=expected_error):
  165. build_from_mapping(mapping=mapping, tenant_id=TEST_TENANT_ID, config=TEST_CONFIG)
  166. def test_invalid_transfer_method():
  167. """Test that invalid transfer method raises ValueError."""
  168. mapping = {
  169. "transfer_method": "invalid_method",
  170. "upload_file_id": TEST_UPLOAD_FILE_ID,
  171. "type": "image",
  172. }
  173. with pytest.raises(ValueError, match="No matching enum found for value 'invalid_method'"):
  174. build_from_mapping(mapping=mapping, tenant_id=TEST_TENANT_ID)
  175. def test_invalid_uuid_format():
  176. """Test that invalid UUID format raises ValueError."""
  177. mapping = {
  178. "transfer_method": "local_file",
  179. "upload_file_id": "not-a-valid-uuid",
  180. "type": "image",
  181. }
  182. with pytest.raises(ValueError, match="Invalid upload file id format"):
  183. build_from_mapping(mapping=mapping, tenant_id=TEST_TENANT_ID)
  184. def test_tenant_mismatch():
  185. """Test that tenant mismatch raises security error."""
  186. # Create a mock upload file with a different tenant_id
  187. mock_file = MagicMock(spec=UploadFile)
  188. mock_file.id = TEST_UPLOAD_FILE_ID
  189. mock_file.tenant_id = "different_tenant_id"
  190. mock_file.name = "test.jpg"
  191. mock_file.extension = "jpg"
  192. mock_file.mime_type = "image/jpeg"
  193. mock_file.source_url = TEST_REMOTE_URL
  194. mock_file.size = 1024
  195. mock_file.key = "test_key"
  196. # Mock the database query to return None (no file found for this tenant)
  197. with patch("factories.file_factory.db.session.scalar", return_value=None):
  198. mapping = local_file_mapping()
  199. with pytest.raises(ValueError, match="Invalid upload file"):
  200. build_from_mapping(mapping=mapping, tenant_id=TEST_TENANT_ID)
  201. def test_disallowed_file_types(mock_upload_file):
  202. """Test that disallowed file types are rejected."""
  203. # Config that only allows image and document types
  204. restricted_config = FileUploadConfig(
  205. allowed_file_types=[FileType.IMAGE, FileType.DOCUMENT],
  206. )
  207. # Try to upload a video file
  208. mapping = local_file_mapping(file_type="video")
  209. with pytest.raises(ValueError, match="File validation failed"):
  210. build_from_mapping(mapping=mapping, tenant_id=TEST_TENANT_ID, config=restricted_config)
  211. def test_disallowed_extensions(mock_upload_file):
  212. """Test that disallowed file extensions are rejected for custom type."""
  213. # Mock a file with .exe extension
  214. mock_upload_file.return_value.extension = "exe"
  215. mock_upload_file.return_value.name = "malicious.exe"
  216. mock_upload_file.return_value.mime_type = "application/x-msdownload"
  217. # Config that only allows specific extensions for custom files
  218. restricted_config = FileUploadConfig(
  219. allowed_file_extensions=[".txt", ".csv", ".json"],
  220. )
  221. # Mapping without specifying type (will be detected as custom)
  222. mapping = {
  223. "transfer_method": "local_file",
  224. "upload_file_id": TEST_UPLOAD_FILE_ID,
  225. "type": "custom",
  226. }
  227. with pytest.raises(ValueError, match="File validation failed"):
  228. build_from_mapping(mapping=mapping, tenant_id=TEST_TENANT_ID, config=restricted_config)