Aggiunta validazione dei provider nella configurazione API e tests

This commit is contained in:
2025-11-05 20:12:14 +01:00
parent 2996bd4616
commit 24d73b6bf8
3 changed files with 375 additions and 0 deletions

345
tests/utils/test_configs.py Normal file
View File

@@ -0,0 +1,345 @@
from typing import Any, Generator
import pytest
import os
import tempfile
import yaml
from unittest.mock import patch, MagicMock
from app.configs import AppConfig, ModelsConfig, APIConfig, AgentsConfigs, Strategy, AppModel, Model
@pytest.mark.configs
class TestAppConfig:
@pytest.fixture
def valid_config_data(self) -> dict[str, Any]:
return {
'port': 8080,
'gradio_share': True,
'logging_level': 'DEBUG',
'strategies': [
{'name': 'TestStrategy', 'label': 'Test', 'description': 'Test strategy'}
],
'models': {
'gemini': [{'name': 'gemini-test', 'label': 'Gemini Test'}],
'ollama': [{'name': 'test-model', 'label': 'Test Model'}]
},
'api': {
'retry_attempts': 5,
'market_providers': ['YFinanceWrapper'],
'news_providers': ['DuckDuckGoWrapper'],
'social_providers': ['RedditWrapper']
},
'agents': {
'strategy': 'TestStrategy',
'team_model': 'gemini-test'
}
}
@pytest.fixture
def temp_config_file(self, valid_config_data: dict[str, Any]) -> Generator[str, None, None]:
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
yaml.dump(valid_config_data, f)
yield f.name
os.unlink(f.name)
def test_load_valid_config(self, temp_config_file: str):
"""Test caricamento di un file di configurazione valido"""
with patch.object(APIConfig, 'validate_providers'), \
patch.object(ModelsConfig, 'validate_models'), \
patch.object(AgentsConfigs, 'validate_defaults'):
config = AppConfig.load(temp_config_file)
assert config.port == 8080
assert config.gradio_share is True
assert config.logging_level == 'DEBUG'
assert len(config.strategies) == 1
assert config.strategies[0].name == 'TestStrategy'
def test_load_nonexistent_file(self):
"""Test caricamento di un file inesistente"""
with pytest.raises(FileNotFoundError):
AppConfig.load("nonexistent_file.yaml")
def test_load_invalid_yaml(self):
"""Test caricamento di un file YAML malformato"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
f.write("invalid: yaml: content: [")
temp_file = f.name
try:
with pytest.raises(yaml.YAMLError):
AppConfig.load(temp_file)
finally:
os.unlink(temp_file)
def test_singleton_pattern(self, temp_config_file: str):
"""Test che AppConfig sia un singleton"""
with patch.object(APIConfig, 'validate_providers'), \
patch.object(ModelsConfig, 'validate_models'), \
patch.object(AgentsConfigs, 'validate_defaults'):
# Reset singleton for test
if hasattr(AppConfig, 'instance'):
delattr(AppConfig, 'instance')
config1 = AppConfig.load(temp_config_file)
config2 = AppConfig.load(temp_config_file)
assert config1 is config2
def test_get_model_by_name_success(self, valid_config_data: dict[str, Any]):
"""Test recupero modello esistente"""
with patch.object(APIConfig, 'validate_providers'), \
patch.object(ModelsConfig, 'validate_models'), \
patch.object(AgentsConfigs, 'validate_defaults'):
config = AppConfig(**valid_config_data)
model = config.get_model_by_name('gemini-test')
assert model.name == 'gemini-test'
assert model.label == 'Gemini Test'
def test_get_model_by_name_not_found(self, valid_config_data: dict[str, Any]):
"""Test recupero modello inesistente"""
with patch.object(APIConfig, 'validate_providers'), \
patch.object(ModelsConfig, 'validate_models'), \
patch.object(AgentsConfigs, 'validate_defaults'):
config = AppConfig(**valid_config_data)
with pytest.raises(ValueError, match="Model with name 'nonexistent' not found"):
config.get_model_by_name('nonexistent')
def test_get_strategy_by_name_success(self, valid_config_data: dict[str, Any]):
"""Test recupero strategia esistente"""
with patch.object(APIConfig, 'validate_providers'), \
patch.object(ModelsConfig, 'validate_models'), \
patch.object(AgentsConfigs, 'validate_defaults'):
config = AppConfig(**valid_config_data)
strategy = config.get_strategy_by_name('TestStrategy')
assert strategy.name == 'TestStrategy'
assert strategy.label == 'Test'
def test_get_strategy_by_name_not_found(self, valid_config_data: dict[str, Any]):
"""Test recupero strategia inesistente"""
with patch.object(APIConfig, 'validate_providers'), \
patch.object(ModelsConfig, 'validate_models'), \
patch.object(AgentsConfigs, 'validate_defaults'):
config = AppConfig(**valid_config_data)
with pytest.raises(ValueError, match="Strategy with name 'nonexistent' not found"):
config.get_strategy_by_name('nonexistent')
@pytest.mark.configs
class TestModelsConfig:
def test_all_models_property(self):
"""Test proprietà all_models che combina tutti i modelli"""
config = ModelsConfig(
gemini=[AppModel(name='gemini-1', label='G1')],
ollama=[AppModel(name='ollama-1', label='O1')],
gpt=[AppModel(name='gpt-1', label='GPT1')]
)
all_models = config.all_models
assert len(all_models) == 3
names = [m.name for m in all_models]
assert 'gemini-1' in names
assert 'ollama-1' in names
assert 'gpt-1' in names
@patch('app.configs.os.getenv')
def test_validate_online_models_with_api_key(self, mock_getenv: MagicMock):
"""Test validazione modelli online con API key presente"""
mock_getenv.return_value = "test_api_key"
config = ModelsConfig(gemini=[AppModel(name='gemini-test')])
config.validate_models()
assert config.gemini[0].model is not None
@patch('app.configs.os.getenv')
def test_validate_online_models_without_api_key(self, mock_getenv: MagicMock):
"""Test validazione modelli online senza API key"""
mock_getenv.return_value = None
config = ModelsConfig(gemini=[AppModel(name='gemini-test')])
config.validate_models()
assert len(config.gemini) == 0
@patch('app.configs.ollama.list')
def test_validate_ollama_models_available(self, mock_ollama_list: MagicMock):
"""Test validazione modelli Ollama disponibili"""
mock_ollama_list.return_value = {
'models': [{'model': 'test-model'}, {'model': 'another-model'}]
}
config = ModelsConfig(ollama=[
AppModel(name='test-model'),
AppModel(name='unavailable-model')
])
config._ModelsConfig__validate_ollama_models() # type: ignore
assert len(config.ollama) == 1
assert config.ollama[0].name == 'test-model'
assert config.ollama[0].model is not None
@patch('app.configs.ollama.list')
def test_validate_ollama_models_server_error(self, mock_ollama_list: MagicMock):
"""Test validazione modelli Ollama con nessun modello disponibile"""
mock_ollama_list.side_effect = Exception("Connection error")
config = ModelsConfig(ollama=[])
config._ModelsConfig__validate_ollama_models() # type: ignore
assert len(config.ollama) == 0
@pytest.mark.configs
class TestAPIConfig:
@patch('app.configs.importlib.import_module')
def test_validate_providers_success(self, mock_import: MagicMock):
"""Test validazione provider con provider validi"""
mock_module = MagicMock()
mock_module.__all__ = ['TestWrapper']
mock_module.TestWrapper = MagicMock()
mock_import.return_value = mock_module
config = APIConfig(
market_providers=['TestWrapper'],
news_providers=['TestWrapper'],
social_providers=['TestWrapper']
)
config.validate_providers() # Should not raise
@patch('app.configs.importlib.import_module')
def test_validate_providers_no_valid_providers(self, mock_import: MagicMock):
"""Test validazione provider senza provider validi"""
mock_module = MagicMock()
mock_module.__all__ = ['ValidWrapper']
mock_import.return_value = mock_module
config = APIConfig(market_providers=['InvalidWrapper'])
with pytest.raises(ValueError, match="No valid markets providers found"):
config.validate_providers()
@patch('app.configs.importlib.import_module')
def test_validate_providers_with_exceptions(self, mock_import: MagicMock):
"""Test validazione provider con eccezioni durante l'inizializzazione"""
mock_module = MagicMock()
mock_module.__all__ = ['TestWrapper']
mock_module.TestWrapper.side_effect = Exception("Init error")
mock_import.return_value = mock_module
config = APIConfig(market_providers=['TestWrapper'])
with pytest.raises(ValueError, match="No valid markets providers found"):
config.validate_providers()
@pytest.mark.configs
class TestAgentsConfigs:
def test_validate_defaults_success(self):
"""Test validazione defaults con configurazioni valide"""
mock_config = MagicMock()
mock_config.get_strategy_by_name.return_value = Strategy(name='TestStrategy')
mock_config.get_model_by_name.return_value = AppModel(name='test-model')
agents_config = AgentsConfigs(
strategy='TestStrategy',
team_model='test-model',
team_leader_model='test-model',
query_analyzer_model='test-model',
report_generation_model='test-model'
)
agents_config.validate_defaults(mock_config) # Should not raise
def test_validate_defaults_invalid_strategy(self):
"""Test validazione defaults con strategia inesistente"""
mock_config = MagicMock()
mock_config.get_strategy_by_name.side_effect = ValueError("Strategy not found")
agents_config = AgentsConfigs(strategy='InvalidStrategy')
with pytest.raises(ValueError, match="Strategy not found"):
agents_config.validate_defaults(mock_config)
def test_validate_defaults_invalid_model(self):
"""Test validazione defaults con modello inesistente"""
mock_config = MagicMock()
mock_config.get_strategy_by_name.return_value = Strategy(name='TestStrategy')
mock_config.get_model_by_name.side_effect = ValueError("Model not found")
agents_config = AgentsConfigs(
strategy='TestStrategy',
team_model='invalid-model'
)
with pytest.raises(ValueError, match="Model not found"):
agents_config.validate_defaults(mock_config)
@pytest.mark.configs
class TestAppModel:
@pytest.fixture
def mock_model_instance(self) -> tuple[MagicMock, type[Model]]:
mock_instance = MagicMock()
# Use a concrete subclass of the application's Model base so pydantic validation passes,
# and make instantiation return the mock instance.
class DummyModel(Model):
def __new__(cls, id: str, instructions: list[str]):
return mock_instance
return mock_instance, DummyModel
def test_get_model_success(self, mock_model_instance: tuple[MagicMock, type[Model]]):
"""Test creazione modello con classe impostata"""
app_model = AppModel(name='test-model', model=mock_model_instance[1])
result = app_model.get_model("test instructions")
assert result == mock_model_instance[0]
def test_get_model_no_class_set(self):
"""Test creazione modello senza classe impostata"""
app_model = AppModel(name='test-model')
with pytest.raises(ValueError, match="Model class for 'test-model' is not set"):
app_model.get_model("test instructions")
def test_get_agent_success(self, mock_model_instance: tuple[MagicMock, type[Model]]):
"""Test creazione agente con modello valido"""
with patch('app.configs.Agent') as mock_agent_class:
mock_agent_instance = MagicMock()
mock_agent_class.return_value = mock_agent_instance
app_model = AppModel(name='test-model', model=mock_model_instance[1])
result = app_model.get_agent(instructions="test instructions", name="agent_name")
mock_agent_class.assert_called_once()
assert result == mock_agent_instance
@pytest.mark.configs
class TestStrategy:
def test_strategy_defaults(self):
"""Test valori di default per Strategy"""
strategy = Strategy()
assert strategy.name == "Conservative"
assert strategy.label == "Conservative"
assert "low-risk" in strategy.description.lower()
def test_strategy_custom_values(self):
"""Test Strategy con valori personalizzati"""
strategy = Strategy(
name="Aggressive",
label="High Risk",
description="High-risk strategy"
)
assert strategy.name == "Aggressive"
assert strategy.label == "High Risk"
assert strategy.description == "High-risk strategy"