Skip to content

Commit 221a8cf

Browse files
Refactor import paths to use relative imports for consistency across modules
1 parent d8a4ef7 commit 221a8cf

3 files changed

Lines changed: 1909 additions & 0 deletions

File tree

src/tests/backend/test_app.py

Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
"""
2+
Unit 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.
10+
"""
11+
12+
import pytest
13+
import sys
14+
import os
15+
from unittest.mock import Mock, AsyncMock, patch, MagicMock
16+
from types import ModuleType
17+
18+
# Add src to path
19+
src_path = os.path.join(os.path.dirname(__file__), '..', '..')
20+
src_path = os.path.abspath(src_path)
21+
if src_path not in sys.path:
22+
sys.path.insert(0, src_path)
23+
24+
# Add backend to path for relative imports
25+
backend_path = os.path.join(src_path, 'backend')
26+
if backend_path not in sys.path:
27+
sys.path.insert(0, backend_path)
28+
29+
# Set environment variables BEFORE importing backend.app
30+
os.environ.setdefault("APPLICATIONINSIGHTS_CONNECTION_STRING", "InstrumentationKey=test-key-12345")
31+
os.environ.setdefault("AZURE_OPENAI_API_KEY", "test-key")
32+
os.environ.setdefault("AZURE_OPENAI_ENDPOINT", "https://test.openai.azure.com")
33+
os.environ.setdefault("AZURE_OPENAI_DEPLOYMENT_NAME", "test-deployment")
34+
os.environ.setdefault("AZURE_OPENAI_API_VERSION", "2024-02-01")
35+
os.environ.setdefault("PROJECT_CONNECTION_STRING", "test-connection")
36+
os.environ.setdefault("AZURE_COSMOS_ENDPOINT", "https://test.cosmos.azure.com")
37+
os.environ.setdefault("AZURE_COSMOS_KEY", "test-key")
38+
os.environ.setdefault("AZURE_COSMOS_DATABASE_NAME", "test-db")
39+
os.environ.setdefault("AZURE_COSMOS_CONTAINER_NAME", "test-container")
40+
os.environ.setdefault("FRONTEND_SITE_NAME", "http://localhost:3000")
41+
os.environ.setdefault("AZURE_AI_SUBSCRIPTION_ID", "test-subscription-id")
42+
os.environ.setdefault("AZURE_AI_RESOURCE_GROUP", "test-resource-group")
43+
os.environ.setdefault("AZURE_AI_PROJECT_NAME", "test-project")
44+
os.environ.setdefault("AZURE_AI_AGENT_ENDPOINT", "https://test.endpoint.azure.com")
45+
os.environ.setdefault("APP_ENV", "dev")
46+
os.environ.setdefault("AZURE_OPENAI_RAI_DEPLOYMENT_NAME", "test-rai-deployment")
47+
48+
49+
# Check if v4 modules are already properly imported (means we're in a full test run)
50+
_router_module = sys.modules.get('backend.v4.api.router')
51+
_has_real_router = (_router_module is not None and
52+
hasattr(_router_module, 'PlanService'))
53+
54+
if not _has_real_router:
55+
# We're running in isolation - need to mock v4 imports
56+
# This prevents relative import issues from v4.api.router
57+
58+
# Create a real FastAPI router to avoid isinstance errors
59+
from fastapi import APIRouter
60+
61+
# Mock azure.monitor.opentelemetry module
62+
mock_azure_monitor_module = ModuleType('configure_azure_monitor')
63+
mock_azure_monitor_module.configure_azure_monitor = lambda *args, **kwargs: None
64+
sys.modules['azure.monitor.opentelemetry'] = mock_azure_monitor_module
65+
66+
# Mock v4.models.messages module (both backend. and relative paths)
67+
mock_messages_module = ModuleType('messages')
68+
mock_messages_module.WebsocketMessageType = type('WebsocketMessageType', (), {})
69+
sys.modules['backend.v4.models.messages'] = mock_messages_module
70+
sys.modules['v4.models.messages'] = mock_messages_module
71+
72+
# Mock v4.api.router module with a real APIRouter (both backend. and relative paths)
73+
mock_router_module = ModuleType('router')
74+
mock_router_module.app_v4 = APIRouter()
75+
sys.modules['backend.v4.api.router'] = mock_router_module
76+
sys.modules['v4.api.router'] = mock_router_module
77+
78+
# Mock v4.config.agent_registry module (both backend. and relative paths)
79+
class MockAgentRegistry:
80+
async def cleanup_all_agents(self):
81+
pass
82+
83+
mock_agent_registry_module = ModuleType('agent_registry')
84+
mock_agent_registry_module.agent_registry = MockAgentRegistry()
85+
sys.modules['backend.v4.config.agent_registry'] = mock_agent_registry_module
86+
sys.modules['v4.config.agent_registry'] = mock_agent_registry_module
87+
88+
# Mock middleware.health_check module (both backend. and relative paths)
89+
mock_health_check_module = ModuleType('health_check')
90+
mock_health_check_module.HealthCheckMiddleware = MagicMock()
91+
sys.modules['backend.middleware.health_check'] = mock_health_check_module
92+
sys.modules['middleware.health_check'] = mock_health_check_module
93+
94+
# Now import backend.app
95+
from backend.app import app, user_browser_language_endpoint, lifespan
96+
from backend.common.models.messages_af import UserLanguage
97+
98+
99+
def test_app_initialization():
100+
"""Test that FastAPI app initializes correctly."""
101+
assert app is not None
102+
assert hasattr(app, 'routes')
103+
assert app.title is not None
104+
105+
106+
def test_app_has_routes():
107+
"""Test that app has registered routes."""
108+
assert len(app.routes) > 0
109+
110+
111+
def test_app_has_middleware():
112+
"""Test that app has middleware configured."""
113+
assert hasattr(app, 'middleware')
114+
# Check middleware stack exists (may be None before first request)
115+
assert hasattr(app, 'middleware_stack')
116+
117+
118+
def test_app_has_cors_middleware():
119+
"""Test that CORS middleware is configured."""
120+
from starlette.middleware.cors import CORSMiddleware
121+
# Check if CORS middleware is in the middleware stack
122+
has_cors = any(
123+
hasattr(m, 'cls') and m.cls == CORSMiddleware
124+
for m in app.user_middleware
125+
)
126+
assert has_cors, "CORS middleware not found in app.user_middleware"
127+
128+
129+
def test_user_language_model():
130+
"""Test UserLanguage model creation."""
131+
test_lang = UserLanguage(language="en-US")
132+
assert test_lang.language == "en-US"
133+
134+
test_lang2 = UserLanguage(language="es-ES")
135+
assert test_lang2.language == "es-ES"
136+
137+
138+
def test_user_language_model_different_languages():
139+
"""Test UserLanguage model with different languages."""
140+
for lang in ["fr-FR", "de-DE", "ja-JP", "zh-CN"]:
141+
test_lang = UserLanguage(language=lang)
142+
assert test_lang.language == lang
143+
144+
145+
@pytest.mark.asyncio
146+
async def test_user_browser_language_endpoint_function():
147+
"""Test the user_browser_language_endpoint function directly."""
148+
user_lang = UserLanguage(language="fr-FR")
149+
request = Mock()
150+
151+
result = await user_browser_language_endpoint(user_lang, request)
152+
153+
assert result == {"status": "Language received successfully"}
154+
assert isinstance(result, dict)
155+
156+
157+
@pytest.mark.asyncio
158+
async def test_user_browser_language_endpoint_multiple_calls():
159+
"""Test the endpoint with multiple different languages."""
160+
request = Mock()
161+
162+
for lang_code in ["en-US", "es-ES", "fr-FR"]:
163+
user_lang = UserLanguage(language=lang_code)
164+
result = await user_browser_language_endpoint(user_lang, request)
165+
assert result["status"] == "Language received successfully"
166+
167+
168+
def test_app_router_lifespan():
169+
"""Test that app has lifespan configured."""
170+
assert app.router.lifespan_context is not None
171+
172+
173+
@pytest.mark.asyncio
174+
async def test_lifespan_context():
175+
"""Test the lifespan context manager."""
176+
# The agent_registry is already mocked at module level
177+
# Just test that lifespan context works
178+
async with lifespan(app):
179+
pass
180+
# If we get here without exception, the test passed
181+
182+
183+
@pytest.mark.asyncio
184+
async def test_lifespan_cleanup_exception_handling():
185+
"""Test lifespan context manager exception handling during cleanup."""
186+
# Patch at the location where agent_registry is used (backend.app module)
187+
import backend.app as app_module
188+
original_registry = app_module.agent_registry
189+
190+
try:
191+
# Create a mock registry that raises a general Exception
192+
mock_registry = Mock()
193+
mock_registry.cleanup_all_agents = AsyncMock(side_effect=Exception("Test cleanup error"))
194+
app_module.agent_registry = mock_registry
195+
196+
# Should not raise, exception should be caught and logged
197+
async with lifespan(app):
198+
pass
199+
# If we get here, exception was handled gracefully
200+
finally:
201+
# Restore original
202+
app_module.agent_registry = original_registry
203+
204+
205+
def test_app_logging_configured():
206+
"""Test that logging is configured."""
207+
import logging
208+
209+
logger = logging.getLogger("backend")
210+
assert logger is not None
211+
212+
213+
def test_app_has_v4_router():
214+
"""Test that V4 router is included in app routes."""
215+
assert len(app.routes) > 0
216+
# App should have routes from the v4 router
217+
route_paths = [route.path for route in app.routes if hasattr(route, 'path')]
218+
# At least one route should exist
219+
assert len(route_paths) > 0
220+
221+
222+
@pytest.mark.asyncio
223+
async def test_lifespan_cleanup_import_error_handling():
224+
"""Test lifespan context manager ImportError handling during cleanup."""
225+
# Patch at the location where agent_registry is used (backend.app module)
226+
import backend.app as app_module
227+
original_registry = app_module.agent_registry
228+
229+
try:
230+
# Create a mock registry that raises ImportError
231+
mock_registry = Mock()
232+
mock_registry.cleanup_all_agents = AsyncMock(side_effect=ImportError("Test import error"))
233+
app_module.agent_registry = mock_registry
234+
235+
# Should not raise, exception should be caught and logged
236+
async with lifespan(app):
237+
pass
238+
# If we get here, exception was handled gracefully
239+
finally:
240+
# Restore original
241+
app_module.agent_registry = original_registry
242+
243+
244+
@pytest.mark.asyncio
245+
async def test_lifespan_cleanup_success():
246+
"""Test lifespan context manager with successful cleanup."""
247+
# Create a mock registry
248+
mock_cleanup = AsyncMock(return_value=None)
249+
250+
# Patch at the module level where it's imported
251+
with patch.object(sys.modules.get('v4.config.agent_registry', sys.modules.get('backend.v4.config.agent_registry')),
252+
'agent_registry') as mock_registry:
253+
mock_registry.cleanup_all_agents = mock_cleanup
254+
255+
async with lifespan(app):
256+
# Startup phase
257+
pass
258+
# Shutdown phase completed without error
259+
260+
261+
def test_frontend_url_config():
262+
"""Test that frontend_url is configured from config."""
263+
from backend.app import frontend_url
264+
assert frontend_url is not None
265+
266+
267+
def test_app_includes_user_browser_language_route():
268+
"""Test that the user_browser_language endpoint is registered."""
269+
route_paths = [route.path for route in app.routes if hasattr(route, 'path')]
270+
assert "/api/user_browser_language" in route_paths
271+
272+
273+
@pytest.mark.asyncio
274+
async def test_user_browser_language_sets_config():
275+
"""Test that user_browser_language endpoint calls config method."""
276+
user_lang = UserLanguage(language="de-DE")
277+
request = Mock()
278+
279+
# Just test that it completes successfully and returns expected result
280+
result = await user_browser_language_endpoint(user_lang, request)
281+
assert result == {"status": "Language received successfully"}
282+
283+
284+
def test_app_configured_with_lifespan():
285+
"""Test that app is configured with lifespan context."""
286+
# Check that app.router has a lifespan_context attribute
287+
assert hasattr(app.router, 'lifespan_context')
288+
assert app.router.lifespan_context is not None
289+
290+
291+
class TestAppConfiguration:
292+
"""Test class for app configuration tests."""
293+
294+
def test_app_title_is_default(self):
295+
"""Test app has default title."""
296+
# FastAPI default title is "FastAPI"
297+
assert app.title == "FastAPI"
298+
299+
def test_app_middleware_stack_not_empty(self):
300+
"""Test that middleware stack is configured."""
301+
assert len(app.user_middleware) > 0
302+
303+
def test_cors_middleware_allows_all_origins(self):
304+
"""Test CORS middleware is configured to allow all origins."""
305+
from starlette.middleware.cors import CORSMiddleware
306+
cors_middleware = None
307+
for m in app.user_middleware:
308+
if hasattr(m, 'cls') and m.cls == CORSMiddleware:
309+
cors_middleware = m
310+
break
311+
312+
assert cors_middleware is not None
313+
# Check that allow_origins includes "*" - using kwargs attribute
314+
assert "*" in cors_middleware.kwargs.get('allow_origins', [])
315+
316+
def test_cors_middleware_allows_credentials(self):
317+
"""Test CORS middleware allows credentials."""
318+
from starlette.middleware.cors import CORSMiddleware
319+
for m in app.user_middleware:
320+
if hasattr(m, 'cls') and m.cls == CORSMiddleware:
321+
assert m.kwargs.get('allow_credentials') is True
322+
break
323+
324+
325+
class TestUserLanguageModel:
326+
"""Test class for UserLanguage model validation."""
327+
328+
def test_user_language_empty_string(self):
329+
"""Test UserLanguage with empty string."""
330+
lang = UserLanguage(language="")
331+
assert lang.language == ""
332+
333+
def test_user_language_with_underscore_format(self):
334+
"""Test UserLanguage with underscore format (e.g. en_US)."""
335+
lang = UserLanguage(language="en_US")
336+
assert lang.language == "en_US"
337+
338+
def test_user_language_lowercase(self):
339+
"""Test UserLanguage with lowercase language code."""
340+
lang = UserLanguage(language="en")
341+
assert lang.language == "en"
342+
343+
344+
@pytest.mark.asyncio
345+
async def test_user_browser_language_endpoint_logs_info(caplog):
346+
"""Test that user_browser_language endpoint logs the received language."""
347+
import logging
348+
349+
user_lang = UserLanguage(language="pt-BR")
350+
request = Mock()
351+
352+
with caplog.at_level(logging.INFO):
353+
await user_browser_language_endpoint(user_lang, request)
354+
355+
# Check that log contains the language info
356+
assert any("pt-BR" in record.message or "Received browser language" in record.message
357+
for record in caplog.records)
358+
359+
360+
def test_logging_configured_correctly():
361+
"""Test that logging is configured at module level."""
362+
import logging
363+
364+
# opentelemetry.sdk should be set to ERROR level
365+
otel_logger = logging.getLogger("opentelemetry.sdk")
366+
assert otel_logger.level == logging.ERROR
367+
368+
369+
def test_health_check_middleware_configured():
370+
"""Test that health check middleware is in the middleware stack."""
371+
# The middleware should be present
372+
assert len(app.user_middleware) >= 2 # CORS + HealthCheck minimum
373+
374+
375+

0 commit comments

Comments
 (0)