Skip to content

Commit eac9ec0

Browse files
Refactor test workflow to run app.py tests separately and enhance coverage for user language endpoint functionality
1 parent 8315bbe commit eac9ec0

2 files changed

Lines changed: 115 additions & 127 deletions

File tree

.github/workflows/test.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,15 @@ jobs:
5050
echo "skip_tests=false" >> $GITHUB_ENV
5151
fi
5252
53+
- name: Run app.py tests separately (requires isolation due to mocking)
54+
if: env.skip_tests == 'false'
55+
run: |
56+
python -m pytest src/tests/backend/test_app.py --cov=backend.app --cov-report=term --cov-config=.coveragerc
57+
5358
- name: Run tests with coverage
5459
if: env.skip_tests == 'false'
5560
run: |
56-
python -m pytest src/tests/backend --cov=backend --cov-report=term --cov-config=.coveragerc
61+
python -m pytest src/tests/backend --cov=backend --cov-report=term --cov-config=.coveragerc --ignore=src/tests/backend/test_app.py
5762
5863
# - name: Run tests with coverage
5964
# if: env.skip_tests == 'false'

src/tests/backend/test_app.py

Lines changed: 109 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
"""
22
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.
310
"""
411

512
import pytest
613
import sys
714
import 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
1219
src_path = os.path.join(os.path.dirname(__file__), '..', '..', '..')
@@ -32,73 +39,65 @@
3239
os.environ.setdefault("APP_ENV", "dev")
3340
os.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

9784
def 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

104103
def 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

Comments
 (0)