1+ """
2+ Code coverage tests for Alfresco MCP Server.
3+ These tests ensure maximum code coverage by testing edge cases and error paths.
4+ """
5+ import pytest
6+ import os
7+ import tempfile
8+ from unittest .mock import patch , Mock , AsyncMock
9+ from fastmcp import Client
10+ from alfresco_mcp_server .fastmcp_server import mcp
11+
12+
13+ @pytest .mark .unit
14+ class TestCodeCoverage :
15+ """Tests designed to maximize code coverage."""
16+
17+ @pytest .mark .asyncio
18+ async def test_ensure_connection_success (self , fastmcp_client ):
19+ """Test ensure_connection function success path."""
20+ with patch ('alfresco_mcp_server.fastmcp_server.alfresco_factory' , None ), \
21+ patch ('alfresco_mcp_server.fastmcp_server.auth_util' , None ), \
22+ patch ('alfresco_mcp_server.config.AlfrescoConfig' ) as mock_config_class , \
23+ patch ('python_alfresco_api.client_factory.ClientFactory' ) as mock_factory_class , \
24+ patch ('python_alfresco_api.auth_util.AuthUtil' ) as mock_auth_class :
25+
26+ # Setup mocks
27+ mock_config = Mock ()
28+ mock_config .alfresco_url = "http://localhost:8080"
29+ mock_config .username = "admin"
30+ mock_config .password = "admin"
31+ mock_config .verify_ssl = False
32+ mock_config .timeout = 30
33+ mock_config_class .return_value = mock_config
34+
35+ mock_factory = Mock ()
36+ mock_factory_class .return_value = mock_factory
37+
38+ mock_auth = AsyncMock ()
39+ mock_auth_class .return_value = mock_auth
40+
41+ # Import and call ensure_connection
42+ from alfresco_mcp_server .fastmcp_server import ensure_connection
43+
44+ # The function should complete without error when mocks are set up
45+ try :
46+ await ensure_connection ()
47+ # If we get here, the function completed successfully
48+ assert True
49+ except Exception as e :
50+ # Should not raise an exception with proper mocks
51+ pytest .fail (f"ensure_connection raised unexpected exception: { e } " )
52+
53+ @pytest .mark .asyncio
54+ async def test_ensure_connection_already_initialized (self , fastmcp_client ):
55+ """Test ensure_connection when already initialized."""
56+ with patch ('alfresco_mcp_server.fastmcp_server.alfresco_factory' , Mock ()), \
57+ patch ('alfresco_mcp_server.fastmcp_server.auth_util' , Mock ()):
58+
59+ from alfresco_mcp_server .fastmcp_server import ensure_connection
60+
61+ # Should return early since factory and auth_util are set
62+ await ensure_connection ()
63+ # No exception should be raised
64+
65+ @pytest .mark .asyncio
66+ async def test_search_models_import_error (self , fastmcp_client ):
67+ """Test search functionality when search models can't be imported."""
68+ with patch ('alfresco_mcp_server.fastmcp_server.ensure_connection' ), \
69+ patch ('alfresco_mcp_server.fastmcp_server.alfresco_factory' ) as mock_factory , \
70+ patch ('alfresco_mcp_server.fastmcp_server.search_models' , None ):
71+
72+ search_client = AsyncMock ()
73+ mock_factory .create_search_client .return_value = search_client
74+
75+ # This should handle the case where search_models is None
76+ result = await fastmcp_client .call_tool ("search_content" , {
77+ "query" : "test" ,
78+ "max_results" : 10
79+ })
80+
81+ assert len (result ) == 1
82+ # Should handle gracefully
83+
84+ @pytest .mark .asyncio
85+ async def test_all_error_paths (self , fastmcp_client ):
86+ """Test error paths in all tools to maximize coverage."""
87+
88+ # Test each tool with connection failure
89+ tools_to_test = [
90+ ("search_content" , {"query" : "test" , "max_results" : 10 }),
91+ ("upload_document" , {"filename" : "test.txt" , "content_base64" : "dGVzdA==" }),
92+ ("download_document" , {"node_id" : "test-123" }),
93+ ("checkout_document" , {"node_id" : "test-123" }),
94+ ("checkin_document" , {"node_id" : "test-123" }),
95+ ("delete_node" , {"node_id" : "test-123" }),
96+ ("get_node_properties" , {"node_id" : "test-123" }),
97+ ("update_node_properties" , {"node_id" : "test-123" , "name" : "new_name" }),
98+ ("create_folder" , {"folder_name" : "Test Folder" })
99+ ]
100+
101+ for tool_name , args in tools_to_test :
102+ with patch ('alfresco_mcp_server.fastmcp_server.ensure_connection' ,
103+ side_effect = Exception ("Connection error" )):
104+
105+ result = await fastmcp_client .call_tool (tool_name , args )
106+ assert len (result ) == 1
107+ assert any (keyword in result [0 ].text for keyword in [
108+ "failed" , "Error" , "error"
109+ ])
110+
111+ @pytest .mark .asyncio
112+ async def test_base64_edge_cases (self , fastmcp_client ):
113+ """Test base64 handling edge cases."""
114+ # Test various invalid base64 strings
115+ invalid_base64_cases = [
116+ "not_base64_at_all" ,
117+ "invalid base64 with spaces" ,
118+ "YWJjZA===" , # Too many padding characters
119+ "" , # Empty string
120+ "x" , # Too short
121+ ]
122+
123+ for invalid_b64 in invalid_base64_cases :
124+ result = await fastmcp_client .call_tool ("upload_document" , {
125+ "filename" : "test.txt" ,
126+ "content_base64" : invalid_b64
127+ })
128+
129+ # Should handle gracefully
130+ assert len (result ) == 1
131+
132+ @pytest .mark .asyncio
133+ async def test_search_edge_cases (self , fastmcp_client ):
134+ """Test search with various edge case inputs."""
135+ edge_cases = [
136+ {"query" : " " , "max_results" : 10 }, # Whitespace only
137+ {"query" : "test" , "max_results" : 1 }, # Minimum results
138+ {"query" : "test" , "max_results" : 100 }, # Maximum results
139+ {"query" : "a" * 1000 , "max_results" : 10 }, # Very long query
140+ ]
141+
142+ for case in edge_cases :
143+ result = await fastmcp_client .call_tool ("search_content" , case )
144+ assert len (result ) == 1
145+ # Should not crash
146+
147+
148+ @pytest .mark .unit
149+ class TestResourcesCoverage :
150+ """Tests for resource functionality coverage."""
151+
152+ @pytest .mark .asyncio
153+ async def test_all_resource_sections (self , fastmcp_client ):
154+ """Test all supported resource sections."""
155+ sections = ["info" , "health" , "stats" , "config" ]
156+
157+ for section in sections :
158+ uri = f"alfresco://repository/{ section } "
159+ result = await fastmcp_client .read_resource (uri )
160+
161+ assert len (result ) > 0
162+ # Should return valid JSON
163+ import json
164+ try :
165+ data = json .loads (result [0 ].text )
166+ assert isinstance (data , dict )
167+ except json .JSONDecodeError :
168+ pytest .fail (f"Resource { section } did not return valid JSON" )
169+
170+ @pytest .mark .asyncio
171+ async def test_resource_error_cases (self , fastmcp_client ):
172+ """Test resource error handling."""
173+ # Test unknown section
174+ with patch ('alfresco_mcp_server.fastmcp_server.alfresco_factory' ) as mock_factory , \
175+ patch ('alfresco_mcp_server.fastmcp_server.auth_util' ) as mock_auth :
176+
177+ mock_auth .is_authenticated .return_value = False
178+
179+ result = await fastmcp_client .read_resource ("alfresco://repository/unknown" )
180+ assert len (result ) > 0
181+ # Should handle unknown section gracefully
182+
183+
184+ @pytest .mark .unit
185+ class TestPromptsCoverage :
186+ """Tests for prompt functionality coverage."""
187+
188+ @pytest .mark .asyncio
189+ async def test_prompt_all_analysis_types (self , fastmcp_client ):
190+ """Test prompt with all analysis types."""
191+ analysis_types = ["summary" , "detailed" , "trends" , "compliance" ]
192+
193+ for analysis_type in analysis_types :
194+ result = await fastmcp_client .get_prompt ("search_and_analyze" , {
195+ "query" : "test documents" ,
196+ "analysis_type" : analysis_type
197+ })
198+
199+ assert len (result .messages ) > 0
200+ prompt_text = result .messages [0 ].content .text
201+ assert "test documents" in prompt_text
202+ assert analysis_type in prompt_text .lower ()
203+
204+ @pytest .mark .asyncio
205+ async def test_prompt_edge_cases (self , fastmcp_client ):
206+ """Test prompt with edge case inputs."""
207+ edge_cases = [
208+ {"query" : "" , "analysis_type" : "summary" }, # Empty query
209+ {"query" : "test" , "analysis_type" : "" }, # Empty analysis type
210+ {"query" : "a" * 500 , "analysis_type" : "summary" }, # Very long query
211+ ]
212+
213+ for case in edge_cases :
214+ result = await fastmcp_client .get_prompt ("search_and_analyze" , case )
215+ assert len (result .messages ) > 0
216+ # Should handle gracefully
217+
218+
219+ @pytest .mark .unit
220+ class TestConfigurationCoverage :
221+ """Tests for configuration and environment handling."""
222+
223+ def test_config_environment_variables (self ):
224+ """Test configuration with various environment variable combinations."""
225+ with patch .dict (os .environ , {
226+ 'ALFRESCO_URL' : 'http://test:8080' ,
227+ 'ALFRESCO_USERNAME' : 'testuser' ,
228+ 'ALFRESCO_PASSWORD' : 'testpass' ,
229+ 'ALFRESCO_VERIFY_SSL' : 'true' ,
230+ 'ALFRESCO_TIMEOUT' : '60'
231+ }):
232+ from alfresco_mcp_server .config import AlfrescoConfig
233+ config = AlfrescoConfig ()
234+
235+ assert config .alfresco_url == 'http://test:8080'
236+ assert config .username == 'testuser'
237+ assert config .password == 'testpass'
238+ assert config .verify_ssl == True
239+ assert config .timeout == 60
240+
241+ def test_config_defaults (self ):
242+ """Test configuration with default values."""
243+ # Clear environment variables by removing them completely
244+ env_vars = ['ALFRESCO_URL' , 'ALFRESCO_USERNAME' , 'ALFRESCO_PASSWORD' ,
245+ 'ALFRESCO_VERIFY_SSL' , 'ALFRESCO_TIMEOUT' ]
246+
247+ # Remove variables instead of setting to empty string
248+ with patch .dict (os .environ , {}, clear = True ):
249+ for var in env_vars :
250+ os .environ .pop (var , None )
251+
252+ from alfresco_mcp_server .config import AlfrescoConfig
253+ config = AlfrescoConfig ()
254+
255+ # Should use defaults
256+ assert config .alfresco_url == 'http://localhost:8080'
257+ assert config .username == 'admin'
258+ assert config .password == 'admin'
259+ assert config .verify_ssl == False
260+ assert config .timeout == 30
261+
262+
263+ @pytest .mark .unit
264+ class TestMainEntryPoint :
265+ """Tests for main entry points and CLI handling."""
266+
267+ def test_main_function_coverage (self ):
268+ """Test main function argument parsing."""
269+ from alfresco_mcp_server .fastmcp_server import main
270+
271+ # Test with different argument combinations
272+ test_args = [
273+ [],
274+ ['--transport' , 'stdio' ],
275+ ['--transport' , 'http' , '--port' , '8001' ],
276+ ['--log-level' , 'DEBUG' ],
277+ ]
278+
279+ for args in test_args :
280+ with patch ('sys.argv' , ['fastmcp_server.py' ] + args ), \
281+ patch ('alfresco_mcp_server.fastmcp_server.mcp.run' ) as mock_run :
282+
283+ try :
284+ main ()
285+ mock_run .assert_called_once ()
286+ except SystemExit :
287+ # Expected for help or invalid args
288+ pass
289+
290+
291+ @pytest .mark .unit
292+ class TestAsyncContextManagers :
293+ """Tests for async context managers and cleanup."""
294+
295+ @pytest .mark .asyncio
296+ async def test_client_context_manager (self ):
297+ """Test FastMCP client context manager."""
298+ # Test that client properly connects and disconnects
299+ async with Client (mcp ) as client :
300+ assert client .is_connected ()
301+
302+ # Test basic functionality
303+ tools = await client .list_tools ()
304+ assert len (tools ) >= 9
305+
306+ # Client should be disconnected after context
307+ # Note: FastMCP handles this internally
308+
309+
310+ @pytest .mark .unit
311+ class TestExceptionHandling :
312+ """Tests for comprehensive exception handling."""
313+
314+ @pytest .mark .asyncio
315+ async def test_network_timeout_simulation (self , fastmcp_client ):
316+ """Test handling of network timeouts."""
317+ import asyncio
318+ with patch ('alfresco_mcp_server.fastmcp_server.ensure_connection' ,
319+ side_effect = asyncio .TimeoutError ("Network timeout" )):
320+
321+ result = await fastmcp_client .call_tool ("search_content" , {
322+ "query" : "test" ,
323+ "max_results" : 10
324+ })
325+
326+ assert len (result ) == 1
327+ assert "failed" in result [0 ].text .lower ()
328+
329+ @pytest .mark .asyncio
330+ async def test_authentication_failure_simulation (self , fastmcp_client ):
331+ """Test handling of authentication failures."""
332+ with patch ('alfresco_mcp_server.fastmcp_server.ensure_connection' ,
333+ side_effect = Exception ("Authentication failed" )):
334+
335+ result = await fastmcp_client .call_tool ("get_node_properties" , {
336+ "node_id" : "test-123"
337+ })
338+
339+ assert len (result ) == 1
340+ assert "failed" in result [0 ].text .lower ()
341+
342+ @pytest .mark .asyncio
343+ async def test_malformed_response_handling (self , fastmcp_client ):
344+ """Test handling of malformed Alfresco responses."""
345+ with patch ('alfresco_mcp_server.fastmcp_server.ensure_connection' ), \
346+ patch ('alfresco_mcp_server.fastmcp_server.alfresco_factory' ) as mock_factory :
347+
348+ # Setup mock to return malformed data
349+ search_client = AsyncMock ()
350+ search_client .search .return_value = "not a proper response object"
351+ mock_factory .create_search_client .return_value = search_client
352+
353+ result = await fastmcp_client .call_tool ("search_content" , {
354+ "query" : "test" ,
355+ "max_results" : 10
356+ })
357+
358+ assert len (result ) == 1
359+ # Should handle gracefully without crashing
0 commit comments