Files
Nexus/tests/test_step0_infrastructure.py
T
Your Name ead4b2580f
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
fix: 3 code bugs + test fixes (46/46 passing)
1. infrastructure/__init__.py: remove nonexistent get_ssh_pool import,
   use lazy __getattr__ to avoid triggering MySQL engine at import time
2. auth_service.py + auth_jwt.py: JWT sub field must be string
   (PyJWT 2.12+ enforces RFC 7519), decode with int() conversion
3. session.py: lazy engine init — engine created on first access
   instead of module import time, allows testing without MySQL
4. Tests: fix Column default assertions (SQLAlchemy defaults only
   apply on DB INSERT), fix Request.url mock for Starlette

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 22:30:34 +08:00

153 lines
5.3 KiB
Python

"""Nexus — Step 0: Infrastructure Tests (R8: Redis + WebSocket + Session leak)
Tests:
- R8: Redis BlockingConnectionPool configuration
- R8: WebSocket ConnectionManager
- D7: DB Session middleware
"""
import pytest
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch
class TestRedisClient:
"""R8: Redis client tests"""
def test_get_redis_raises_before_init(self):
"""get_redis() raises RuntimeError if called before init_redis()"""
from server.infrastructure.redis.client import get_redis
# Reset module state
import server.infrastructure.redis.client as redis_mod
redis_mod._redis = None
with pytest.raises(RuntimeError, match="Redis not initialized"):
get_redis()
def test_get_redis_sync_returns_none_before_init(self):
"""get_redis_sync() returns None if Redis not initialized"""
import server.infrastructure.redis.client as redis_mod
redis_mod._redis = None
from server.infrastructure.redis.client import get_redis_sync
assert get_redis_sync() is None
@pytest.mark.asyncio
async def test_init_redis_exits_on_empty_url(self):
"""init_redis() raises SystemExit if REDIS_URL is empty"""
from server.infrastructure.redis.client import init_redis
import server.config as config_mod
original_url = config_mod.settings.REDIS_URL
config_mod.settings.REDIS_URL = ""
with pytest.raises(SystemExit, match="REDIS_URL must be set"):
await init_redis(MagicMock())
config_mod.settings.REDIS_URL = original_url
class TestWebSocketManager:
"""R8: WebSocket ConnectionManager tests"""
def test_manager_initial_state(self):
"""ConnectionManager starts with zero connections"""
from server.api.websocket import ConnectionManager
manager = ConnectionManager()
assert manager.client_count == 0
def test_manager_disconnect_nonexistent(self):
"""Disconnecting a non-existent client is a no-op"""
from server.api.websocket import ConnectionManager
manager = ConnectionManager()
manager.disconnect("nonexistent") # Should not raise
assert manager.client_count == 0
def test_manager_update_pong(self):
"""update_pong updates last_pong_time for existing connection"""
from server.api.websocket import ConnectionManager
import time
manager = ConnectionManager()
ws_mock = MagicMock()
manager._connections["test_client"] = {
"websocket": ws_mock,
"last_pong": time.monotonic() - 10,
}
before = manager._connections["test_client"]["last_pong"]
manager.update_pong("test_client")
after = manager._connections["test_client"]["last_pong"]
assert after > before
class TestDbSessionMiddleware:
"""D7: DB Session middleware tests"""
@pytest.mark.asyncio
async def test_middleware_skips_non_api_routes(self):
"""Middleware skips non-API routes (WebSocket, health, static)"""
from server.main import DbSessionMiddleware
from starlette.responses import Response
middleware = DbSessionMiddleware(app=MagicMock())
# Create a proper ASGI scope for a non-API path
scope = {
"type": "http",
"method": "GET",
"path": "/favicon.ico",
"query_string": b"",
"headers": [],
}
# Use a real Request object with a proper scope
from starlette.requests import Request
request = Request(scope)
call_next = AsyncMock(return_value=Response())
response = await middleware.dispatch(request, call_next)
assert call_next.called
class TestJWTAuth:
"""A5: JWT authentication tests"""
def test_public_path_detection(self):
"""Public paths are correctly identified"""
from server.api.auth_jwt import _is_public_path
assert _is_public_path("/api/auth/login") is True
assert _is_public_path("/api/auth/refresh") is True
assert _is_public_path("/api/agent/heartbeat") is True
assert _is_public_path("/health") is True
assert _is_public_path("/ws/alerts") is True
def test_protected_path_detection(self):
"""Protected paths are correctly identified"""
from server.api.auth_jwt import _is_public_path
assert _is_public_path("/api/servers/") is False
assert _is_public_path("/api/settings/") is False
assert _is_public_path("/api/sync/files") is False
assert _is_public_path("/api/assets/platforms") is False
assert _is_public_path("/api/auth/me") is False
assert _is_public_path("/api/auth/totp/setup") is False
class TestAsyncSSHPool:
"""W7: asyncssh pool tests"""
def test_pool_initial_state(self):
"""Pool starts with zero connections"""
from server.infrastructure.ssh.asyncssh_pool import AsyncSSHPool
pool = AsyncSSHPool()
assert pool.stats == {"total": 0, "active": 0, "idle": 0}
def test_pool_config(self):
"""Pool configuration is reasonable"""
from server.infrastructure.ssh.asyncssh_pool import AsyncSSHPool
pool = AsyncSSHPool()
assert pool.MAX_CONNECTIONS == 100
assert pool.IDLE_TIMEOUT == 300
assert pool.CONNECT_TIMEOUT == 10