1
1
import os
2
+ import sys
2
3
import unittest
3
4
from http .server import BaseHTTPRequestHandler
4
- from unittest .mock import MagicMock
5
+ from unittest .mock import MagicMock , patch , AsyncMock
6
+ from fastapi .testclient import TestClient
5
7
from aistore .sdk .etl .webserver import HTTPMultiThreadedServer
8
+ from aistore .sdk .etl .webserver .fastapi_server import FastAPIServer
6
9
7
10
8
11
class DummyETLServer (HTTPMultiThreadedServer ):
9
- """
10
- Dummy ETL server for testing transform and MIME type override.
11
- """
12
+ """Dummy ETL server for testing transform and MIME type override."""
12
13
13
14
def transform (self , data : bytes , path : str ) -> bytes :
14
15
return data .upper ()
@@ -17,12 +18,10 @@ def get_mime_type(self) -> str:
17
18
return "text/caps"
18
19
19
20
20
- # pylint: disable=super-init-not-called
21
21
class DummyRequestHandler (BaseHTTPRequestHandler ):
22
- """
23
- Fake handler that mocks HTTP methods for isolated testing of _set_headers().
24
- """
22
+ """Fake handler that mocks HTTP methods for isolated testing of `set_headers()`."""
25
23
24
+ # pylint: disable=super-init-not-called
26
25
def __init__ (self ):
27
26
# Don't call super().__init__(), just mock the necessary parts
28
27
self .send_response = MagicMock ()
@@ -38,6 +37,14 @@ def set_headers(self, status_code: int = 200):
38
37
self .end_headers ()
39
38
40
39
40
+ class DummyFastAPIServer (FastAPIServer ):
41
+ def transform (self , data : bytes , path : str ) -> bytes :
42
+ return data [::- 1 ] # Simple reverse transform for testing purposes
43
+
44
+ def get_mime_type (self ) -> str :
45
+ return "application/test"
46
+
47
+
41
48
class TestETLServerLogic (unittest .TestCase ):
42
49
def setUp (self ):
43
50
os .environ ["AIS_TARGET_URL" ] = "http://localhost:8080"
@@ -64,3 +71,108 @@ def test_set_headers_calls_expected_methods(self):
64
71
handler .send_response .assert_called_once_with (202 )
65
72
handler .send_header .assert_called_once_with ("Content-Type" , "application/test" )
66
73
handler .end_headers .assert_called_once ()
74
+
75
+
76
+ class TestFastAPIServer (unittest .IsolatedAsyncioTestCase ):
77
+ def setUp (self ):
78
+ os .environ ["AIS_TARGET_URL" ] = "http://localhost:8080"
79
+ self .etl_server = DummyFastAPIServer ()
80
+ self .client = TestClient (self .etl_server .app )
81
+
82
+ def test_health_check (self ):
83
+ response = self .client .get ("/health" )
84
+ self .assertEqual (response .status_code , 200 )
85
+ self .assertEqual (response .content , b"Running" )
86
+
87
+ # pylint: disable=protected-access
88
+ async def test_get_network_content (self ):
89
+ path = "test/path"
90
+ fake_content = b"fake data"
91
+
92
+ with patch .object (self .etl_server , "client" , AsyncMock ()) as mock_client :
93
+ mock_response = AsyncMock ()
94
+ mock_response .content = fake_content
95
+ mock_response .raise_for_status = MagicMock ()
96
+
97
+ mock_client .get .return_value = mock_response
98
+
99
+ result = await self .etl_server ._get_network_content (path )
100
+
101
+ self .assertEqual (result , fake_content )
102
+ mock_client .get .assert_called_once ()
103
+
104
+ @unittest .skipIf (sys .version_info < (3 , 9 ), "requires Python 3.9 or higher" )
105
+ async def test_handle_get_request (self ):
106
+ self .etl_server .arg_type = ""
107
+ path = "test/object"
108
+ original_content = b"original data"
109
+ transformed_content = original_content [::- 1 ]
110
+
111
+ with patch .object (
112
+ self .etl_server ,
113
+ "_get_network_content" ,
114
+ AsyncMock (return_value = original_content ),
115
+ ):
116
+ response = self .client .get (f"/{ path } " )
117
+
118
+ self .assertEqual (response .status_code , 200 )
119
+ self .assertEqual (response .content , transformed_content )
120
+
121
+ @unittest .skipIf (sys .version_info < (3 , 9 ), "requires Python 3.9 or higher" )
122
+ async def test_handle_put_request (self ):
123
+ self .etl_server .arg_type = ""
124
+ path = "test/object"
125
+ input_content = b"input data"
126
+ transformed_content = input_content [::- 1 ]
127
+
128
+ response = self .client .put (f"/{ path } " , content = input_content )
129
+
130
+ self .assertEqual (response .status_code , 200 )
131
+ self .assertEqual (response .content , transformed_content )
132
+
133
+
134
+ class TestBaseEnforcement (unittest .TestCase ):
135
+ def test_fastapi_server_without_target_url (self ):
136
+ if "AIS_TARGET_URL" in os .environ :
137
+ del os .environ ["AIS_TARGET_URL" ]
138
+
139
+ class MinimalFastAPIServer (FastAPIServer ):
140
+ def transform (self , data : bytes , path : str ) -> bytes :
141
+ return data
142
+
143
+ with self .assertRaises (EnvironmentError ) as context :
144
+ MinimalFastAPIServer ()
145
+ self .assertIn ("AIS_TARGET_URL" , str (context .exception ))
146
+
147
+ def test_http_server_server_without_target_url (self ):
148
+ if "AIS_TARGET_URL" in os .environ :
149
+ del os .environ ["AIS_TARGET_URL" ]
150
+
151
+ class MinimalHTTPServer (HTTPMultiThreadedServer ):
152
+ def transform (self , data : bytes , path : str ) -> bytes :
153
+ return data
154
+
155
+ with self .assertRaises (EnvironmentError ) as context :
156
+ MinimalHTTPServer ()
157
+ self .assertIn ("AIS_TARGET_URL" , str (context .exception ))
158
+
159
+ def test_http_multithreaded_server_without_transform (self ):
160
+ if "AIS_TARGET_URL" in os .environ :
161
+ del os .environ ["AIS_TARGET_URL" ]
162
+
163
+ class IncompleteETLServer (HTTPMultiThreadedServer ):
164
+ pass
165
+
166
+ with self .assertRaises (TypeError ) as context :
167
+ IncompleteETLServer () # pylint: disable=abstract-class-instantiated
168
+ self .assertIn ("Can't instantiate abstract class" , str (context .exception ))
169
+
170
+ def test_fastapi_server_without_transform (self ):
171
+ os .environ ["AIS_TARGET_URL" ] = "http://localhost"
172
+
173
+ class IncompleteFastAPIServer (FastAPIServer ):
174
+ pass
175
+
176
+ with self .assertRaises (TypeError ) as context :
177
+ IncompleteFastAPIServer () # pylint: disable=abstract-class-instantiated
178
+ self .assertIn ("Can't instantiate abstract class" , str (context .exception ))
0 commit comments