10
10
11
11
class TestS3V2UnitTests :
12
12
"""Test that S3 v2 integration only uses safe_dumps and not json.dumps"""
13
+
13
14
def test_s3_v2_source_code_analysis (self ):
14
15
"""Test that S3 v2 source code only imports and uses safe_dumps"""
15
16
import inspect
@@ -18,7 +19,139 @@ def test_s3_v2_source_code_analysis(self):
18
19
19
20
# Get the source code of the s3_v2 module
20
21
source_code = inspect .getsource (s3_v2 )
21
-
22
+
22
23
# Verify that json.dumps is not used directly in the code
23
- assert "json.dumps(" not in source_code , \
24
- "S3 v2 should not use json.dumps directly"
24
+ assert (
25
+ "json.dumps(" not in source_code
26
+ ), "S3 v2 should not use json.dumps directly"
27
+
28
+ @patch ('asyncio.create_task' )
29
+ @patch ('litellm.integrations.s3_v2.CustomBatchLogger.periodic_flush' )
30
+ def test_s3_v2_endpoint_url (self , mock_periodic_flush , mock_create_task ):
31
+ """testing s3 endpoint url"""
32
+ from unittest .mock import AsyncMock , MagicMock
33
+ from litellm .types .integrations .s3_v2 import s3BatchLoggingElement
34
+
35
+ # Mock periodic_flush and create_task to prevent async task creation during init
36
+ mock_periodic_flush .return_value = None
37
+ mock_create_task .return_value = None
38
+
39
+ # Mock response for all tests
40
+ mock_response = MagicMock ()
41
+ mock_response .status_code = 200
42
+ mock_response .raise_for_status = MagicMock ()
43
+
44
+ # Create a test batch logging element
45
+ test_element = s3BatchLoggingElement (
46
+ s3_object_key = "2025-09-14/test-key.json" ,
47
+ payload = {"test" : "data" },
48
+ s3_object_download_filename = "test-file.json"
49
+ )
50
+
51
+ # Test 1: Custom endpoint URL with bucket name
52
+ s3_logger = S3Logger (
53
+ s3_bucket_name = "test-bucket" ,
54
+ s3_endpoint_url = "https://s3.amazonaws.com" ,
55
+ s3_aws_access_key_id = "test-key" ,
56
+ s3_aws_secret_access_key = "test-secret" ,
57
+ s3_region_name = "us-east-1"
58
+ )
59
+
60
+ s3_logger .async_httpx_client = AsyncMock ()
61
+ s3_logger .async_httpx_client .put .return_value = mock_response
62
+
63
+ asyncio .run (s3_logger .async_upload_data_to_s3 (test_element ))
64
+
65
+ call_args = s3_logger .async_httpx_client .put .call_args
66
+ assert call_args is not None
67
+ url = call_args [0 ][0 ]
68
+ expected_url = "https://s3.amazonaws.com/test-bucket/2025-09-14/test-key.json"
69
+ assert url == expected_url , f"Expected URL { expected_url } , got { url } "
70
+
71
+ # Test 2: MinIO-compatible endpoint
72
+ s3_logger_minio = S3Logger (
73
+ s3_bucket_name = "litellm-logs" ,
74
+ s3_endpoint_url = "https://minio.example.com:9000" ,
75
+ s3_aws_access_key_id = "minio-key" ,
76
+ s3_aws_secret_access_key = "minio-secret" ,
77
+ s3_region_name = "us-east-1"
78
+ )
79
+
80
+ s3_logger_minio .async_httpx_client = AsyncMock ()
81
+ s3_logger_minio .async_httpx_client .put .return_value = mock_response
82
+
83
+ asyncio .run (s3_logger_minio .async_upload_data_to_s3 (test_element ))
84
+
85
+ call_args_minio = s3_logger_minio .async_httpx_client .put .call_args
86
+ assert call_args_minio is not None
87
+ url_minio = call_args_minio [0 ][0 ]
88
+ expected_minio_url = "https://minio.example.com:9000/litellm-logs/2025-09-14/test-key.json"
89
+ assert url_minio == expected_minio_url , f"Expected MinIO URL { expected_minio_url } , got { url_minio } "
90
+
91
+ # Test 3: Custom endpoint without bucket name (should fall back to default)
92
+ s3_logger_no_bucket = S3Logger (
93
+ s3_endpoint_url = "https://s3.amazonaws.com" ,
94
+ s3_aws_access_key_id = "test-key" ,
95
+ s3_aws_secret_access_key = "test-secret" ,
96
+ s3_region_name = "us-east-1"
97
+ )
98
+
99
+ s3_logger_no_bucket .async_httpx_client = AsyncMock ()
100
+ s3_logger_no_bucket .async_httpx_client .put .return_value = mock_response
101
+
102
+ asyncio .run (s3_logger_no_bucket .async_upload_data_to_s3 (test_element ))
103
+
104
+ call_args_no_bucket = s3_logger_no_bucket .async_httpx_client .put .call_args
105
+ assert call_args_no_bucket is not None
106
+ url_no_bucket = call_args_no_bucket [0 ][0 ]
107
+ # Should use default S3 URL format when bucket is missing (bucket becomes None in URL)
108
+ assert "s3.us-east-1.amazonaws.com" in url_no_bucket
109
+ assert "https://" in url_no_bucket
110
+ # Should not include the custom endpoint since bucket is missing
111
+ assert "https://s3.amazonaws.com/" not in url_no_bucket
112
+
113
+ # Test 4: Sync upload method with custom endpoint
114
+ s3_logger_sync = S3Logger (
115
+ s3_bucket_name = "sync-bucket" ,
116
+ s3_endpoint_url = "https://custom.s3.endpoint.com" ,
117
+ s3_aws_access_key_id = "sync-key" ,
118
+ s3_aws_secret_access_key = "sync-secret" ,
119
+ s3_region_name = "us-east-1"
120
+ )
121
+
122
+ mock_sync_client = MagicMock ()
123
+ mock_sync_client .put .return_value = mock_response
124
+
125
+ with patch ('litellm.integrations.s3_v2._get_httpx_client' , return_value = mock_sync_client ):
126
+ s3_logger_sync .upload_data_to_s3 (test_element )
127
+
128
+ call_args_sync = mock_sync_client .put .call_args
129
+ assert call_args_sync is not None
130
+ url_sync = call_args_sync [0 ][0 ]
131
+ expected_sync_url = "https://custom.s3.endpoint.com/sync-bucket/2025-09-14/test-key.json"
132
+ assert url_sync == expected_sync_url , f"Expected sync URL { expected_sync_url } , got { url_sync } "
133
+
134
+ # Test 5: Download method with custom endpoint
135
+ s3_logger_download = S3Logger (
136
+ s3_bucket_name = "download-bucket" ,
137
+ s3_endpoint_url = "https://download.s3.endpoint.com" ,
138
+ s3_aws_access_key_id = "download-key" ,
139
+ s3_aws_secret_access_key = "download-secret" ,
140
+ s3_region_name = "us-east-1"
141
+ )
142
+
143
+ mock_download_response = MagicMock ()
144
+ mock_download_response .status_code = 200
145
+ mock_download_response .json = MagicMock (return_value = {"downloaded" : "data" })
146
+ s3_logger_download .async_httpx_client = AsyncMock ()
147
+ s3_logger_download .async_httpx_client .get .return_value = mock_download_response
148
+
149
+ result = asyncio .run (s3_logger_download ._download_object_from_s3 ("2025-09-14/download-test-key.json" ))
150
+
151
+ call_args_download = s3_logger_download .async_httpx_client .get .call_args
152
+ assert call_args_download is not None
153
+ url_download = call_args_download [0 ][0 ]
154
+ expected_download_url = "https://download.s3.endpoint.com/download-bucket/2025-09-14/download-test-key.json"
155
+ assert url_download == expected_download_url , f"Expected download URL { expected_download_url } , got { url_download } "
156
+
157
+ assert result == {"downloaded" : "data" }
0 commit comments