0adaaad971
- Servidor gRPC: Agregar interceptores de autenticación y UserRegistry para aislar sesiones por usuario.
- Contexto de Hilos: Corregir propagación de ContextVar _current_user a hilos secundarios en ExecutionServicer.
- Configuración Compartida: Implementar herencia y deep merge de settings de IA ('ai') y servidores MCP en configfile.
- Hot-Reload: Recarga automática en caliente de la configuración compartida global ante cambios en disco.
- CLI: Agregar comandos e interfaces de usuario para autenticación (login) y administración de usuarios.
- Pruebas: Desarrollar tests unitarios completos (test_shared_ai.py) y resolver regresiones en la suite existente.
135 lines
5.1 KiB
Python
135 lines
5.1 KiB
Python
import os
|
|
import pytest
|
|
from connpy.grpc_layer.user_registry import UserRegistry
|
|
from connpy.services.provider import ServiceProvider
|
|
|
|
@pytest.fixture
|
|
def test_config_dir(tmp_path):
|
|
"""Creates a temporary config directory for testing user registry."""
|
|
config_dir = tmp_path / "conn_config"
|
|
config_dir.mkdir()
|
|
return config_dir
|
|
|
|
@pytest.fixture
|
|
def registry(test_config_dir):
|
|
"""Initializes UserRegistry pointing to a temporary directory."""
|
|
return UserRegistry(str(test_config_dir))
|
|
|
|
|
|
class TestUserRegistry:
|
|
def test_has_users_empty(self, registry):
|
|
"""Verifies has_users is False when no users exist."""
|
|
assert registry.has_users() is False
|
|
|
|
def test_get_provider_returns_service_provider(self, registry):
|
|
"""Tests that get_provider lazy-loads a valid ServiceProvider instance."""
|
|
username = "alice"
|
|
registry.user_service.create_user(username, "password")
|
|
|
|
assert registry.has_users() is True
|
|
|
|
provider = registry.get_provider(username)
|
|
assert isinstance(provider, ServiceProvider)
|
|
assert provider.mode == "local"
|
|
|
|
def test_get_provider_cached(self, registry):
|
|
"""Verifies that subsequent calls return the cached singleton instance."""
|
|
username = "bob"
|
|
registry.user_service.create_user(username, "password")
|
|
|
|
p1 = registry.get_provider(username)
|
|
p2 = registry.get_provider(username)
|
|
|
|
assert p1 is p2 # must be exact same object reference
|
|
|
|
def test_two_users_isolated(self, registry):
|
|
"""Ensures different users get completely separate ServiceProviders and configs."""
|
|
u1 = "user1"
|
|
u2 = "user2"
|
|
|
|
registry.user_service.create_user(u1, "pass1")
|
|
registry.user_service.create_user(u2, "pass2")
|
|
|
|
p1 = registry.get_provider(u1)
|
|
p2 = registry.get_provider(u2)
|
|
|
|
assert p1 is not p2
|
|
assert p1.config is not p2.config
|
|
|
|
# Add a node for user1 and verify user2 is unaffected
|
|
p1.nodes.add_node("node1", {"host": "1.1.1.1"})
|
|
assert "node1" in p1.nodes.list_nodes()
|
|
assert "node1" not in p2.nodes.list_nodes()
|
|
|
|
def test_evict_clears_cache(self, registry):
|
|
"""Verifies that eviction deletes the cached provider from memory."""
|
|
username = "evictuser"
|
|
registry.user_service.create_user(username, "pass")
|
|
|
|
p1 = registry.get_provider(username)
|
|
assert username in registry._providers
|
|
|
|
registry.evict(username)
|
|
assert username not in registry._providers
|
|
|
|
# Calling get_provider again spawns a new instance
|
|
p2 = registry.get_provider(username)
|
|
assert p1 is not p2
|
|
|
|
def test_provider_hot_reload_on_external_change(self, registry):
|
|
"""Verifies that UserRegistry hot-reloads the provider if config.yaml is updated externally."""
|
|
username = "charlie"
|
|
registry.user_service.create_user(username, "password")
|
|
|
|
# Initial load (no nodes)
|
|
p1 = registry.get_provider(username)
|
|
assert len(p1.nodes.list_nodes()) == 0
|
|
|
|
# Resolve config.yaml file path
|
|
conf_file = os.path.join(registry.server_config_dir, "users", username, "config.yaml")
|
|
|
|
# Modify the config file physically on disk by appending a node
|
|
from connpy.configfile import configfile
|
|
cfg = configfile(conf=conf_file)
|
|
cfg._connections_add(id="testnode", host="8.8.8.8")
|
|
cfg._saveconfig(cfg.file)
|
|
|
|
# Artificially increase mtime to force reload
|
|
mtime = os.path.getmtime(conf_file)
|
|
os.utime(conf_file, (mtime + 5.0, mtime + 5.0))
|
|
|
|
# Fetch provider again
|
|
p2 = registry.get_provider(username)
|
|
|
|
# Verify it hot-reloaded and the new node is immediately visible
|
|
assert p1 is not p2
|
|
assert "testnode" in p2.nodes.list_nodes()
|
|
|
|
def test_provider_hot_reload_fails_on_corrupt_file_keeps_old_provider(self, registry):
|
|
"""Verifies that UserRegistry keeps serving the old provider if disk config is corrupt."""
|
|
username = "danny"
|
|
registry.user_service.create_user(username, "password")
|
|
|
|
# Initial load
|
|
p1 = registry.get_provider(username)
|
|
p1.nodes.add_node("nodeA", {"host": "2.2.2.2"})
|
|
assert "nodeA" in p1.nodes.list_nodes()
|
|
|
|
# Resolve config.yaml path
|
|
conf_file = os.path.join(registry.server_config_dir, "users", username, "config.yaml")
|
|
|
|
# Write corrupted content directly to config.yaml
|
|
with open(conf_file, "w") as f:
|
|
f.write("corrupt yaml content ::: invalid syntax :::")
|
|
|
|
# Artificially increase mtime to force reload attempt
|
|
mtime = os.path.getmtime(conf_file)
|
|
os.utime(conf_file, (mtime + 5.0, mtime + 5.0))
|
|
|
|
# Fetching provider again should fallback to old_provider instead of failing completely
|
|
p2 = registry.get_provider(username)
|
|
|
|
# Verify fallback
|
|
assert p1 is p2
|
|
assert "nodeA" in p2.nodes.list_nodes()
|