Files
Nexus/tests/test_step0_infrastructure.py
T
Nexus Deploy a2ae74d582 release: BUG fixes batch 1-6, full bug scan docs, Ubuntu/Linux dev
- Backend: auth refresh reuse, schedule/retry/sync/agent/files fixes
- Frontend: push WS, files ETag browse, vite build assets
- Docs: audit/changelog, production deploy gate, bug scan registry B7-77
- Deploy: deploy-production.sh, prune assets, gate logs
2026-06-04 14:01:14 +08:00

157 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
inner = AsyncMock()
middleware = DbSessionMiddleware(app=inner)
# Create a proper ASGI scope for a non-API path
scope = {
"type": "http",
"method": "GET",
"path": "/favicon.ico",
"query_string": b"",
"headers": [],
}
async def receive():
return {"type": "http.request", "body": b"", "more_body": False}
sent = []
async def send(message):
sent.append(message)
await middleware(scope, receive, send)
inner.assert_called_once()
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