1+ import pytest
2+ import asyncio
3+ from unittest .mock import AsyncMock , patch , MagicMock
4+ from src .badfish .main import Badfish , BadfishException , badfish_factory
5+
6+
7+ class MockLogger :
8+ def __init__ (self ):
9+ self .debug_calls = []
10+ self .warning_calls = []
11+ self .info_calls = []
12+ self .error_calls = []
13+
14+ def debug (self , message ):
15+ self .debug_calls .append (message )
16+
17+ def warning (self , message ):
18+ self .warning_calls .append (message )
19+
20+ def info (self , message ):
21+ self .info_calls .append (message )
22+
23+ def error (self , message ):
24+ self .error_calls .append (message )
25+
26+
27+ MOCK_HOST = "test-host.example.com"
28+ MOCK_USERNAME = "test_user"
29+ MOCK_PASSWORD = "test_password"
30+ MOCK_RETRIES = 5
31+
32+
33+ class TestContextManager :
34+ """Test cases for the async context manager methods."""
35+
36+ @pytest .mark .asyncio
37+ async def test_context_manager_successful_entry_exit (self ):
38+ """Test successful entry and exit of the context manager."""
39+ logger = MockLogger ()
40+ badfish_instance = Badfish (MOCK_HOST , MOCK_USERNAME , MOCK_PASSWORD , logger , MOCK_RETRIES )
41+
42+ # Mock the init method
43+ with patch .object (badfish_instance , 'init' , new_callable = AsyncMock ) as mock_init :
44+ with patch .object (badfish_instance , 'delete_session' , new_callable = AsyncMock ) as mock_delete :
45+ async with badfish_instance as bf :
46+ assert bf is badfish_instance
47+ mock_init .assert_called_once ()
48+
49+ mock_delete .assert_called_once ()
50+
51+ @pytest .mark .asyncio
52+ async def test_context_manager_exception_handling (self ):
53+ """Test that exceptions are properly handled and logged."""
54+ logger = MockLogger ()
55+ badfish_instance = Badfish (MOCK_HOST , MOCK_USERNAME , MOCK_PASSWORD , logger , MOCK_RETRIES )
56+
57+ # Mock the init method to raise an exception
58+ with patch .object (badfish_instance , 'init' , new_callable = AsyncMock ) as mock_init :
59+ mock_init .side_effect = BadfishException ("Test exception" )
60+
61+ with patch .object (badfish_instance , 'delete_session' , new_callable = AsyncMock ) as mock_delete :
62+ with pytest .raises (BadfishException ):
63+ async with badfish_instance :
64+ pass
65+
66+ # When init fails, __aexit__ is not called because the exception is raised before entering the context
67+ mock_delete .assert_not_called ()
68+
69+ @pytest .mark .asyncio
70+ async def test_context_manager_direct_method_calls (self ):
71+ """Test direct calls to __aenter__ and __aexit__ methods."""
72+ logger = MockLogger ()
73+ badfish_instance = Badfish (MOCK_HOST , MOCK_USERNAME , MOCK_PASSWORD , logger , MOCK_RETRIES )
74+
75+ with patch .object (badfish_instance , 'init' , new_callable = AsyncMock ) as mock_init :
76+ with patch .object (badfish_instance , 'delete_session' , new_callable = AsyncMock ) as mock_delete :
77+ # Test direct __aenter__ call
78+ result = await badfish_instance .__aenter__ ()
79+ assert result is badfish_instance
80+ mock_init .assert_called_once ()
81+
82+ # Test direct __aexit__ call
83+ await badfish_instance .__aexit__ (None , None , None )
84+ mock_delete .assert_called_once ()
85+
86+ @pytest .mark .asyncio
87+ async def test_context_manager_nested_usage (self ):
88+ """Test nested usage of the context manager."""
89+ logger = MockLogger ()
90+ badfish_instance = Badfish (MOCK_HOST , MOCK_USERNAME , MOCK_PASSWORD , logger , MOCK_RETRIES )
91+
92+ with patch .object (badfish_instance , 'init' , new_callable = AsyncMock ) as mock_init :
93+ with patch .object (badfish_instance , 'delete_session' , new_callable = AsyncMock ) as mock_delete :
94+ # First context manager usage
95+ async with badfish_instance as bf1 :
96+ assert bf1 is badfish_instance
97+
98+ # Nested context manager usage (should call init again)
99+ async with badfish_instance as bf2 :
100+ assert bf2 is badfish_instance
101+
102+ # delete_session should be called twice (once for each context)
103+ assert mock_delete .call_count == 2
104+ # init should be called twice (once for each context entry)
105+ assert mock_init .call_count == 2
106+
107+ @pytest .mark .asyncio
108+ async def test_context_manager_integration_with_factory (self ):
109+ """Test context manager integration with badfish_factory."""
110+ logger = MockLogger ()
111+
112+ with patch .object (Badfish , 'init' , new_callable = AsyncMock ) as mock_init :
113+ with patch .object (Badfish , 'delete_session' , new_callable = AsyncMock ) as mock_delete :
114+ async with await badfish_factory (MOCK_HOST , MOCK_USERNAME , MOCK_PASSWORD , logger , MOCK_RETRIES ) as badfish :
115+ assert isinstance (badfish , Badfish )
116+ assert badfish .host == MOCK_HOST
117+ assert badfish .username == MOCK_USERNAME
118+ assert badfish .password == MOCK_PASSWORD
119+ # badfish_factory calls init, then context manager calls it again
120+ assert mock_init .call_count == 2
121+
122+ mock_delete .assert_called_once ()
123+
124+ @pytest .mark .asyncio
125+ async def test_context_manager_session_cleanup (self ):
126+ """Test that session cleanup happens even with exceptions."""
127+ logger = MockLogger ()
128+ badfish_instance = Badfish (MOCK_HOST , MOCK_USERNAME , MOCK_PASSWORD , logger , MOCK_RETRIES )
129+
130+ with patch .object (badfish_instance , 'init' , new_callable = AsyncMock ) as mock_init :
131+ with patch .object (badfish_instance , 'delete_session' , new_callable = AsyncMock ) as mock_delete :
132+ try :
133+ async with badfish_instance :
134+ raise ValueError ("Unexpected error" )
135+ except ValueError :
136+ pass
137+
138+ # delete_session should still be called despite the exception
139+ mock_delete .assert_called_once ()
140+
141+ @pytest .mark .asyncio
142+ async def test_context_manager_init_failure (self ):
143+ """Test context manager behavior when init fails."""
144+ logger = MockLogger ()
145+ badfish_instance = Badfish (MOCK_HOST , MOCK_USERNAME , MOCK_PASSWORD , logger , MOCK_RETRIES )
146+
147+ with patch .object (badfish_instance , 'init' , new_callable = AsyncMock ) as mock_init :
148+ mock_init .side_effect = BadfishException ("Init failed" )
149+
150+ with patch .object (badfish_instance , 'delete_session' , new_callable = AsyncMock ) as mock_delete :
151+ with pytest .raises (BadfishException ):
152+ async with badfish_instance :
153+ pass
154+
155+ # When init fails, __aexit__ is not called
156+ mock_delete .assert_not_called ()
157+
158+ @pytest .mark .asyncio
159+ async def test_context_manager_delete_session_failure (self ):
160+ """Test context manager behavior when delete_session fails."""
161+ logger = MockLogger ()
162+ badfish_instance = Badfish (MOCK_HOST , MOCK_USERNAME , MOCK_PASSWORD , logger , MOCK_RETRIES )
163+
164+ with patch .object (badfish_instance , 'init' , new_callable = AsyncMock ) as mock_init :
165+ with patch .object (badfish_instance , 'delete_session' , new_callable = AsyncMock ) as mock_delete :
166+ mock_delete .side_effect = BadfishException ("Delete session failed" )
167+
168+ # The exception should be re-raised from __aexit__
169+ with pytest .raises (BadfishException ):
170+ async with badfish_instance :
171+ pass
172+
173+
174+ class TestDeleteSession :
175+ """Test cases for the delete_session method."""
176+
177+ @pytest .mark .asyncio
178+ async def test_delete_session_success (self ):
179+ """Test successful session deletion."""
180+ logger = MockLogger ()
181+ badfish_instance = Badfish (MOCK_HOST , MOCK_USERNAME , MOCK_PASSWORD , logger , MOCK_RETRIES )
182+ badfish_instance .session_id = "/redfish/v1/SessionService/Sessions/123"
183+ badfish_instance .token = "test_token"
184+
185+ # Mock the delete_request method
186+ with patch .object (badfish_instance , 'delete_request' , new_callable = AsyncMock ) as mock_delete_request :
187+ # Mock response with status 200
188+ mock_response = MagicMock ()
189+ mock_response .status = 200
190+ mock_delete_request .return_value = mock_response
191+
192+ await badfish_instance .delete_session ()
193+
194+ # Verify delete_request was called with correct parameters
195+ mock_delete_request .assert_called_once ()
196+ call_args = mock_delete_request .call_args
197+ assert call_args [0 ][0 ] == f"https://{ MOCK_HOST } /redfish/v1/SessionService/Sessions/123"
198+ assert call_args [1 ]['headers' ] == {"content-type" : "application/json" }
199+
200+ # Verify session_id and token are cleared
201+ assert badfish_instance .session_id is None
202+ assert badfish_instance .token is None
203+
204+ # Verify success message was logged
205+ assert any ("Session successfully deleted" in call for call in logger .debug_calls )
206+
207+ @pytest .mark .asyncio
208+ async def test_delete_session_404_status (self ):
209+ """Test session deletion with 404 status (session not found)."""
210+ logger = MockLogger ()
211+ badfish_instance = Badfish (MOCK_HOST , MOCK_USERNAME , MOCK_PASSWORD , logger , MOCK_RETRIES )
212+ badfish_instance .session_id = "/redfish/v1/SessionService/Sessions/123"
213+ badfish_instance .token = "test_token"
214+
215+ with patch .object (badfish_instance , 'delete_request' , new_callable = AsyncMock ) as mock_delete_request :
216+ # Mock response with status 404
217+ mock_response = MagicMock ()
218+ mock_response .status = 404
219+ mock_delete_request .return_value = mock_response
220+
221+ await badfish_instance .delete_session ()
222+
223+ # Verify session_id and token are still cleared
224+ assert badfish_instance .session_id is None
225+ assert badfish_instance .token is None
226+
227+ # Verify appropriate message was logged
228+ assert any ("Session not found (404)" in call for call in logger .debug_calls )
229+
230+ @pytest .mark .asyncio
231+ async def test_delete_session_unexpected_status (self ):
232+ """Test session deletion with unexpected status code."""
233+ logger = MockLogger ()
234+ badfish_instance = Badfish (MOCK_HOST , MOCK_USERNAME , MOCK_PASSWORD , logger , MOCK_RETRIES )
235+ badfish_instance .session_id = "/redfish/v1/SessionService/Sessions/123"
236+ badfish_instance .token = "test_token"
237+
238+ with patch .object (badfish_instance , 'delete_request' , new_callable = AsyncMock ) as mock_delete_request :
239+ # Mock response with unexpected status
240+ mock_response = MagicMock ()
241+ mock_response .status = 500
242+ mock_delete_request .return_value = mock_response
243+
244+ await badfish_instance .delete_session ()
245+
246+ # Verify session_id and token are still cleared
247+ assert badfish_instance .session_id is None
248+ assert badfish_instance .token is None
249+
250+ # Verify warning was logged
251+ assert any ("Unexpected status 500" in call for call in logger .warning_calls )
252+
253+ @pytest .mark .asyncio
254+ async def test_delete_session_exception_handling (self ):
255+ """Test session deletion when delete_request raises an exception."""
256+ logger = MockLogger ()
257+ badfish_instance = Badfish (MOCK_HOST , MOCK_USERNAME , MOCK_PASSWORD , logger , MOCK_RETRIES )
258+ badfish_instance .session_id = "/redfish/v1/SessionService/Sessions/123"
259+ badfish_instance .token = "test_token"
260+
261+ with patch .object (badfish_instance , 'delete_request' , new_callable = AsyncMock ) as mock_delete_request :
262+ mock_delete_request .side_effect = BadfishException ("Network error" )
263+
264+ await badfish_instance .delete_session ()
265+
266+ # Verify session_id and token are still cleared despite the exception
267+ assert badfish_instance .session_id is None
268+ assert badfish_instance .token is None
269+
270+ # Verify exception was logged as warning
271+ assert any ("Failed to delete session" in call for call in logger .warning_calls )
272+ assert any ("Network error" in call for call in logger .warning_calls )
273+
274+ @pytest .mark .asyncio
275+ async def test_delete_session_no_session_id (self ):
276+ """Test delete_session when no session_id is set."""
277+ logger = MockLogger ()
278+ badfish_instance = Badfish (MOCK_HOST , MOCK_USERNAME , MOCK_PASSWORD , logger , MOCK_RETRIES )
279+ badfish_instance .session_id = None
280+ badfish_instance .token = "test_token"
281+
282+ with patch .object (badfish_instance , 'delete_request' , new_callable = AsyncMock ) as mock_delete_request :
283+ await badfish_instance .delete_session ()
284+
285+ # delete_request should not be called
286+ mock_delete_request .assert_not_called ()
287+
288+ # token should still be cleared
289+ assert badfish_instance .token is None
290+
291+ # Verify appropriate message was logged
292+ assert any ("No session ID found" in call for call in logger .debug_calls )
293+
294+ @pytest .mark .asyncio
295+ async def test_delete_session_cleanup_always_executes (self ):
296+ """Test that cleanup (clearing session_id and token) always executes."""
297+ logger = MockLogger ()
298+ badfish_instance = Badfish (MOCK_HOST , MOCK_USERNAME , MOCK_PASSWORD , logger , MOCK_RETRIES )
299+ badfish_instance .session_id = "/redfish/v1/SessionService/Sessions/123"
300+ badfish_instance .token = "test_token"
301+
302+ with patch .object (badfish_instance , 'delete_request' , new_callable = AsyncMock ) as mock_delete_request :
303+ # Mock response to raise an exception
304+ mock_delete_request .side_effect = Exception ("Unexpected error" )
305+
306+ # The method should not raise the exception due to try/finally
307+ await badfish_instance .delete_session ()
308+
309+ # Verify session_id and token are still cleared despite the exception
310+ assert badfish_instance .session_id is None
311+ assert badfish_instance .token is None
312+
313+ @pytest .mark .asyncio
314+ async def test_delete_session_other_exception (self ):
315+ """Test delete_session with non-BadfishException."""
316+ logger = MockLogger ()
317+ badfish_instance = Badfish (MOCK_HOST , MOCK_USERNAME , MOCK_PASSWORD , logger , MOCK_RETRIES )
318+ badfish_instance .session_id = "/redfish/v1/SessionService/Sessions/123"
319+ badfish_instance .token = "test_token"
320+
321+ with patch .object (badfish_instance , 'delete_request' , new_callable = AsyncMock ) as mock_delete_request :
322+ # Mock response to raise a different exception
323+ mock_delete_request .side_effect = ValueError ("Unexpected error" )
324+
325+ # The method should not raise the exception due to try/finally
326+ await badfish_instance .delete_session ()
327+
328+ # Verify session_id and token are still cleared
329+ assert badfish_instance .session_id is None
330+ assert badfish_instance .token is None
0 commit comments