Files
connpy/connpy/tests/test_user_registry.py
T
fluzzi32 0adaaad971 feat(multiuser): implementar sistema multiusuario gRPC y configuración compartida de IA/MCP
- 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.
2026-05-28 09:27:54 -03:00

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()