Skip to content

Commit 9af478a

Browse files
authored
tests: Add comprehensive unit tests for routing guards.py module
tests: Add comprehensive unit tests for routing guards.py module
2 parents 1e1237a + 4a50b03 commit 9af478a

File tree

1 file changed

+228
-0
lines changed

1 file changed

+228
-0
lines changed

tests/test_routing_guards.py

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
"""
2+
Unit tests for FletX Route Guard System
3+
"""
4+
5+
import pytest
6+
from unittest.mock import Mock
7+
from typing import Optional
8+
9+
from fletx.core.routing.guards import RouteGuard
10+
from fletx.core.routing.models import RouteInfo
11+
12+
13+
class TestRouteGuard:
14+
"""Test RouteGuard abstract base class."""
15+
16+
def test_route_guard_is_abstract(self):
17+
"""Test that RouteGuard is an abstract class."""
18+
with pytest.raises(TypeError):
19+
RouteGuard()
20+
21+
def test_route_guard_has_abstract_methods(self):
22+
"""Test that RouteGuard has required abstract methods."""
23+
assert hasattr(RouteGuard, 'can_activate')
24+
assert hasattr(RouteGuard, 'can_deactivate')
25+
assert hasattr(RouteGuard, 'redirect_to')
26+
27+
28+
class ConcreteRouteGuard(RouteGuard):
29+
"""Concrete implementation for testing."""
30+
31+
def __init__(self, can_activate_result: bool = True, redirect_path: Optional[str] = None):
32+
self.can_activate_result = can_activate_result
33+
self.redirect_path = redirect_path
34+
35+
async def can_activate(self, route: RouteInfo) -> bool:
36+
return self.can_activate_result
37+
38+
async def can_deactivate(self, current_route: RouteInfo) -> bool:
39+
return True
40+
41+
async def redirect_to(self, route: RouteInfo) -> Optional[str]:
42+
return self.redirect_path
43+
44+
45+
class TestConcreteRouteGuard:
46+
"""Test concrete RouteGuard implementation."""
47+
48+
def setup_method(self):
49+
"""Set up test fixtures."""
50+
self.route_info = Mock(spec=RouteInfo)
51+
self.route_info.path = "/test"
52+
53+
@pytest.mark.asyncio
54+
async def test_can_activate_allows_access(self):
55+
"""Test can_activate when access is allowed."""
56+
guard = ConcreteRouteGuard(can_activate_result=True)
57+
result = await guard.can_activate(self.route_info)
58+
assert result is True
59+
60+
@pytest.mark.asyncio
61+
async def test_can_activate_blocks_access(self):
62+
"""Test can_activate when access is blocked."""
63+
guard = ConcreteRouteGuard(can_activate_result=False)
64+
result = await guard.can_activate(self.route_info)
65+
assert result is False
66+
67+
@pytest.mark.asyncio
68+
async def test_redirect_to_with_path(self):
69+
"""Test redirect_to when redirect path is provided."""
70+
guard = ConcreteRouteGuard(redirect_path="/login")
71+
result = await guard.redirect_to(self.route_info)
72+
assert result == "/login"
73+
74+
@pytest.mark.asyncio
75+
async def test_redirect_to_without_path(self):
76+
"""Test redirect_to when no redirect path is provided."""
77+
guard = ConcreteRouteGuard(redirect_path=None)
78+
result = await guard.redirect_to(self.route_info)
79+
assert result is None
80+
81+
82+
class AuthenticationGuard(RouteGuard):
83+
"""Authentication guard implementation."""
84+
85+
def __init__(self, is_authenticated: bool = False, login_path: str = "/login"):
86+
self.is_authenticated = is_authenticated
87+
self.login_path = login_path
88+
89+
async def can_activate(self, route: RouteInfo) -> bool:
90+
return self.is_authenticated
91+
92+
async def can_deactivate(self, current_route: RouteInfo) -> bool:
93+
return True
94+
95+
async def redirect_to(self, route: RouteInfo) -> Optional[str]:
96+
return self.login_path if not self.is_authenticated else None
97+
98+
99+
class TestAuthenticationGuard:
100+
"""Test AuthenticationGuard implementation."""
101+
102+
def setup_method(self):
103+
"""Set up test fixtures."""
104+
self.route_info = Mock(spec=RouteInfo)
105+
self.route_info.path = "/protected"
106+
107+
@pytest.mark.asyncio
108+
async def test_authenticated_user_can_access(self):
109+
"""Test that authenticated users can access protected routes."""
110+
guard = AuthenticationGuard(is_authenticated=True)
111+
result = await guard.can_activate(self.route_info)
112+
assert result is True
113+
114+
@pytest.mark.asyncio
115+
async def test_unauthenticated_user_cannot_access(self):
116+
"""Test that unauthenticated users cannot access protected routes."""
117+
guard = AuthenticationGuard(is_authenticated=False)
118+
result = await guard.can_activate(self.route_info)
119+
assert result is False
120+
121+
@pytest.mark.asyncio
122+
async def test_unauthenticated_user_redirected_to_login(self):
123+
"""Test that unauthenticated users are redirected to login."""
124+
guard = AuthenticationGuard(is_authenticated=False, login_path="/login")
125+
result = await guard.redirect_to(self.route_info)
126+
assert result == "/login"
127+
128+
129+
class PermissionGuard(RouteGuard):
130+
"""Permission-based guard implementation."""
131+
132+
def __init__(self, user_permissions: list = None, required_permission: str = None):
133+
self.user_permissions = user_permissions or []
134+
self.required_permission = required_permission
135+
136+
async def can_activate(self, route: RouteInfo) -> bool:
137+
if not self.required_permission:
138+
return True
139+
return self.required_permission in self.user_permissions
140+
141+
async def can_deactivate(self, current_route: RouteInfo) -> bool:
142+
return True
143+
144+
async def redirect_to(self, route: RouteInfo) -> Optional[str]:
145+
if self.required_permission and self.required_permission not in self.user_permissions:
146+
return "/unauthorized"
147+
return None
148+
149+
150+
class TestPermissionGuard:
151+
"""Test PermissionGuard implementation."""
152+
153+
def setup_method(self):
154+
"""Set up test fixtures."""
155+
self.route_info = Mock(spec=RouteInfo)
156+
self.route_info.path = "/admin"
157+
158+
@pytest.mark.asyncio
159+
async def test_user_with_permission_can_access(self):
160+
"""Test that users with required permission can access routes."""
161+
guard = PermissionGuard(
162+
user_permissions=["admin", "user"],
163+
required_permission="admin"
164+
)
165+
result = await guard.can_activate(self.route_info)
166+
assert result is True
167+
168+
@pytest.mark.asyncio
169+
async def test_user_without_permission_cannot_access(self):
170+
"""Test that users without required permission cannot access routes."""
171+
guard = PermissionGuard(
172+
user_permissions=["user"],
173+
required_permission="admin"
174+
)
175+
result = await guard.can_activate(self.route_info)
176+
assert result is False
177+
178+
@pytest.mark.asyncio
179+
async def test_user_without_permission_redirected(self):
180+
"""Test that users without permission are redirected."""
181+
guard = PermissionGuard(
182+
user_permissions=["user"],
183+
required_permission="admin"
184+
)
185+
result = await guard.redirect_to(self.route_info)
186+
assert result == "/unauthorized"
187+
188+
189+
class TestRouteGuardEdgeCases:
190+
"""Test edge cases and error conditions."""
191+
192+
def setup_method(self):
193+
"""Set up test fixtures."""
194+
self.route_info = Mock(spec=RouteInfo)
195+
self.route_info.path = "/test"
196+
197+
@pytest.mark.asyncio
198+
async def test_guard_with_exception(self):
199+
"""Test guard behavior when method raises an exception."""
200+
class ExceptionGuard(RouteGuard):
201+
async def can_activate(self, route: RouteInfo) -> bool:
202+
raise ValueError("Test exception")
203+
204+
async def can_deactivate(self, current_route: RouteInfo) -> bool:
205+
return True
206+
207+
async def redirect_to(self, route: RouteInfo) -> Optional[str]:
208+
return None
209+
210+
guard = ExceptionGuard()
211+
with pytest.raises(ValueError, match="Test exception"):
212+
await guard.can_activate(self.route_info)
213+
214+
def test_guard_inheritance(self):
215+
"""Test that guards properly inherit from RouteGuard."""
216+
class CustomGuard(RouteGuard):
217+
async def can_activate(self, route: RouteInfo) -> bool:
218+
return True
219+
220+
async def can_deactivate(self, current_route: RouteInfo) -> bool:
221+
return True
222+
223+
async def redirect_to(self, route: RouteInfo) -> Optional[str]:
224+
return None
225+
226+
guard = CustomGuard()
227+
assert isinstance(guard, RouteGuard)
228+
assert issubclass(CustomGuard, RouteGuard)

0 commit comments

Comments
 (0)