11"""
22Unit tests for backend.app module.
3+
4+ IMPORTANT: This test file MUST run in isolation from other backend tests.
5+ Run it separately: python -m pytest tests/backend/test_app.py
6+
7+ It uses sys.modules mocking that conflicts with other v4 tests when run together.
8+ The CI/CD workflow runs all backend tests together, where this file will work
9+ because it detects existing v4 imports and skips mocking.
310"""
411
512import pytest
613import sys
714import os
8- import platform
9- from unittest . mock import patch , MagicMock , AsyncMock , Mock
15+ from unittest . mock import Mock , AsyncMock , patch
16+ from types import ModuleType
1017
1118# Add src to path
1219src_path = os .path .join (os .path .dirname (__file__ ), '..' , '..' , '..' )
3239os .environ .setdefault ("APP_ENV" , "dev" )
3340os .environ .setdefault ("AZURE_OPENAI_RAI_DEPLOYMENT_NAME" , "test-rai-deployment" )
3441
35- # Skip all tests on Linux due to platform-specific Mock/FastAPI compatibility issues
36- # Tests run on Windows for development validation
37- pytestmark = pytest .mark .skipif (
38- platform .system () == "Linux" ,
39- reason = "Skipping on Linux CI/CD - FastAPI middleware validation incompatible with mocking approach. Tests validated on Windows."
40- )
4142
43+ # Check if v4 modules are already properly imported (means we're in a full test run)
44+ _router_module = sys .modules .get ('backend.v4.api.router' )
45+ _has_real_router = (_router_module is not None and
46+ hasattr (_router_module , 'PlanService' ))
4247
43- @pytest .fixture (scope = "module" , autouse = True )
44- def setup_mocks ():
45- """Set up mocks for backend.app imports."""
46- # Save original modules
47- original_router = sys .modules .get ('backend.v4.api.router' )
48- original_agent_registry = sys .modules .get ('backend.v4.config.agent_registry' )
48+ if not _has_real_router :
49+ # We're running in isolation - need to mock v4 imports
50+ # This prevents relative import issues from v4.api.router
4951
50- # Create APIRouter mock that doesn't trigger isinstance/issubclass
52+ # Create a real FastAPI router to avoid isinstance errors
5153 from fastapi import APIRouter
52- mock_app_v4 = APIRouter ()
53- mock_app_v4 .routes = []
5454
55- # Mock the router module
56- class MockRouterModule :
57- app_v4 = mock_app_v4
55+ # Mock azure.monitor.opentelemetry module
56+ mock_azure_monitor_module = ModuleType ('configure_azure_monitor' )
57+ mock_azure_monitor_module .configure_azure_monitor = lambda * args , ** kwargs : None
58+ sys .modules ['azure.monitor.opentelemetry' ] = mock_azure_monitor_module
5859
59- sys .modules ['backend.v4.api.router' ] = MockRouterModule ()
60+ # Mock v4.models.messages module
61+ mock_messages_module = ModuleType ('messages' )
62+ mock_messages_module .WebsocketMessageType = type ('WebsocketMessageType' , (), {})
63+ sys .modules ['backend.v4.models.messages' ] = mock_messages_module
6064
61- # Mock agent registry
65+ # Mock v4.api.router module with a real APIRouter
66+ mock_router_module = ModuleType ('router' )
67+ mock_router_module .app_v4 = APIRouter ()
68+ sys .modules ['backend.v4.api.router' ] = mock_router_module
69+
70+ # Mock v4.config.agent_registry module
6271 class MockAgentRegistry :
6372 async def cleanup_all_agents (self ):
6473 pass
6574
66- class MockAgentRegistryModule :
67- agent_registry = MockAgentRegistry ()
68-
69- sys .modules ['backend.v4.config.agent_registry' ] = MockAgentRegistryModule ()
70-
71- # Mock Azure monitor and import
72- with patch ('azure.monitor.opentelemetry.configure_azure_monitor' ):
73- # Now import backend.app
74- import backend .app
75- globals ()['app' ] = backend .app .app
76- globals ()['lifespan' ] = backend .app .lifespan
77- globals ()['user_browser_language_endpoint' ] = backend .app .user_browser_language_endpoint
78-
79- yield
80-
81- # Cleanup - restore original modules
82- if original_router is not None :
83- sys .modules ['backend.v4.api.router' ] = original_router
84- elif 'backend.v4.api.router' in sys .modules :
85- del sys .modules ['backend.v4.api.router' ]
86-
87- if original_agent_registry is not None :
88- sys .modules ['backend.v4.config.agent_registry' ] = original_agent_registry
89- elif 'backend.v4.config.agent_registry' in sys .modules :
90- del sys .modules ['backend.v4.config.agent_registry' ]
91-
92- # Remove backend.app from cache so it can be reimported fresh
93- if 'backend.app' in sys .modules :
94- del sys .modules ['backend.app' ]
75+ mock_agent_registry_module = ModuleType ('agent_registry' )
76+ mock_agent_registry_module .agent_registry = MockAgentRegistry ()
77+ sys .modules ['backend.v4.config.agent_registry' ] = mock_agent_registry_module
78+
79+ # Now import backend.app
80+ from backend .app import app , user_browser_language_endpoint , lifespan
81+ from backend .common .models .messages_af import UserLanguage
9582
9683
9784def test_app_initialization ():
9885 """Test that FastAPI app initializes correctly."""
99- from backend .app import app
10086 assert app is not None
10187 assert hasattr (app , 'routes' )
88+ assert app .title is not None
89+
90+
91+ def test_app_has_routes ():
92+ """Test that app has registered routes."""
93+ assert len (app .routes ) > 0
94+
95+
96+ def test_app_has_middleware ():
97+ """Test that app has middleware configured."""
98+ assert hasattr (app , 'middleware' )
99+ # Check middleware stack exists (may be None before first request)
100+ assert hasattr (app , 'middleware_stack' )
102101
103102
104103def test_app_has_cors_middleware ():
@@ -112,106 +111,90 @@ def test_app_has_cors_middleware():
112111 assert has_cors , "CORS middleware not found in app.user_middleware"
113112
114113
115- def test_user_browser_language_endpoint ():
116- """Test the user browser language endpoint exists."""
117- from backend .app import user_browser_language_endpoint
118- from backend .common .models .messages_af import UserLanguage
119-
120- # Verify endpoint function exists and is callable
121- assert callable (user_browser_language_endpoint )
122-
123- # Verify it can create UserLanguage object
114+ def test_user_language_model ():
115+ """Test UserLanguage model creation."""
124116 test_lang = UserLanguage (language = "en-US" )
125117 assert test_lang .language == "en-US"
118+
119+ test_lang2 = UserLanguage (language = "es-ES" )
120+ assert test_lang2 .language == "es-ES"
126121
127122
128- def test_user_browser_language_endpoint_different_languages ():
123+ def test_user_language_model_different_languages ():
129124 """Test UserLanguage model with different languages."""
130- from backend .common .models .messages_af import UserLanguage
131-
132- # Test that UserLanguage can be created with different languages
133- for lang in ["es-ES" , "fr-FR" , "ja-JP" ]:
125+ for lang in ["fr-FR" , "de-DE" , "ja-JP" , "zh-CN" ]:
134126 test_lang = UserLanguage (language = lang )
135127 assert test_lang .language == lang
136128
137129
138130@pytest .mark .asyncio
139- async def test_lifespan_context ():
140- """Test the lifespan context manager."""
141- from backend .app import lifespan
131+ async def test_user_browser_language_endpoint_function ():
132+ """Test the user_browser_language_endpoint function directly."""
133+ user_lang = UserLanguage (language = "fr-FR" )
134+ request = Mock ()
142135
143- async with lifespan (app ):
144- pass
145-
146-
147- def test_app_includes_v4_router ():
148- """Test that V4 router is included."""
149- assert len (app .routes ) > 0
136+ result = await user_browser_language_endpoint (user_lang , request )
137+
138+ assert result == {"status" : "Language received successfully" }
139+ assert isinstance (result , dict )
150140
151141
152- def test_logging_configured ():
153- """Test that logging is configured."""
154- import logging
142+ @pytest .mark .asyncio
143+ async def test_user_browser_language_endpoint_multiple_calls ():
144+ """Test the endpoint with multiple different languages."""
145+ request = Mock ()
155146
156- logger = logging .getLogger ("backend" )
157- assert logger is not None
147+ for lang_code in ["en-US" , "es-ES" , "fr-FR" ]:
148+ user_lang = UserLanguage (language = lang_code )
149+ result = await user_browser_language_endpoint (user_lang , request )
150+ assert result ["status" ] == "Language received successfully"
158151
159152
160- def test_fastapi_app_configuration ():
161- """Test FastAPI app is properly configured."""
162-
163- # Verify app has lifespan
153+ def test_app_router_lifespan ():
154+ """Test that app has lifespan configured."""
164155 assert app .router .lifespan_context is not None
165156
166157
167158@pytest .mark .asyncio
168- async def test_user_browser_language_endpoint_function ():
169- """Test the user_browser_language_endpoint function directly."""
170- from backend .app import user_browser_language_endpoint
171- from backend .common .models .messages_af import UserLanguage
172- from unittest .mock import Mock
173-
174- # Create test data
175- user_lang = UserLanguage (language = "fr-FR" )
176- request = Mock ()
177-
178- # Call the endpoint
179- result = await user_browser_language_endpoint (user_lang , request )
180-
181- # Verify result
182- assert result == {"status" : "Language received successfully" }
159+ async def test_lifespan_context ():
160+ """Test the lifespan context manager."""
161+ # The agent_registry is already mocked at module level
162+ # Just test that lifespan context works
163+ async with lifespan (app ):
164+ pass
165+ # If we get here without exception, the test passed
183166
184167
185168@pytest .mark .asyncio
186- async def test_lifespan_exception_handling ():
169+ async def test_lifespan_cleanup_exception_handling ():
187170 """Test lifespan context manager exception handling during cleanup."""
188- from backend .app import lifespan
189- from backend .v4 .config .agent_registry import agent_registry
190-
191- # Save original method
192- original_cleanup = agent_registry .cleanup_all_agents
193-
194- # Make cleanup raise an exception
195- async def mock_cleanup ():
196- raise Exception ("Test cleanup error" )
197-
198- agent_registry .cleanup_all_agents = mock_cleanup
171+ # Mock agent_registry with cleanup that raises
172+ with patch ('backend.v4.config.agent_registry.agent_registry' ) as mock_registry :
173+ mock_registry .cleanup_all_agents = AsyncMock (side_effect = Exception ("Test cleanup error" ))
174+
175+ # Should not raise, exception should be caught and logged
176+ try :
177+ async with lifespan (app ):
178+ pass
179+ except Exception :
180+ pytest .fail ("Lifespan should handle cleanup exceptions gracefully" )
181+
182+
183+ def test_app_logging_configured ():
184+ """Test that logging is configured."""
185+ import logging
199186
200- # Should not raise, exception should be caught
201- try :
202- async with lifespan (app ):
203- pass
204- except Exception :
205- pytest .fail ("Lifespan should handle cleanup exceptions gracefully" )
206- finally :
207- # Restore original method
208- agent_registry .cleanup_all_agents = original_cleanup
187+ logger = logging .getLogger ("backend" )
188+ assert logger is not None
209189
210190
211- def test_applicationinsights_not_configured ():
212- """Test that app handles missing Application Insights gracefully."""
213- # This test checks that the app can start even without AppInsights
214- # The warning log on line 59 was already executed during module import
215- assert app is not None
191+ def test_app_has_v4_router ():
192+ """Test that V4 router is included in app routes."""
193+ assert len (app .routes ) > 0
194+ # App should have routes from the v4 router
195+ route_paths = [route .path for route in app .routes if hasattr (route , 'path' )]
196+ # At least one route should exist
197+ assert len (route_paths ) > 0
198+
216199
217200
0 commit comments