KI-Chat-Bot-Eugen/tests/test_config.py
Claude f6813b5fa5
Add comprehensive test suite with 97% code coverage
Implemented full test coverage for core bot components:

Test Coverage:
- MentionDetector: 98% (28 tests) - mention detection, nickname handling, content extraction
- Logger: 98% (15 tests) - file logging, log levels, unicode support
- ConversationMemory: 91% (25 tests) - history management, time filtering, JSON persistence
- PerplexityProvider: 100% (22 tests) - API calls, error handling, statistics
- Config: 100% (26 tests) - env loading, validation, JSON config

Infrastructure:
- Added pytest, pytest-asyncio, pytest-mock, pytest-cov to requirements.txt
- Created pytest.ini with coverage configuration
- Created .coveragerc to exclude non-production files
- Added conftest.py with shared fixtures and test isolation

Test Features:
- 116 total tests, all passing
- Isolated test environment (clean env vars, logging handlers)
- Async testing support for PerplexityProvider
- Mocked HTTP requests to avoid real API calls
- Comprehensive edge case coverage
- Unicode/German character support testing

Total: 97% code coverage across 273 statements
2026-01-02 22:10:14 +00:00

330 lines
12 KiB
Python

"""
Tests for Config class
"""
import pytest
import os
import json
from pathlib import Path
from config import Config
class TestConfig:
"""Test Config functionality"""
def test_config_loads_from_env_file(self, temp_dir, mock_env_file):
"""Test that config loads from .env file"""
config = Config(env_file=str(mock_env_file), config_file=str(temp_dir / "config.json"))
assert config.twitch_token == "oauth:test_token_12345"
assert config.twitch_channel == "#test_channel"
assert config.bot_name == "TestBot"
assert config.perplexity_key == "pplx-test-key-12345"
assert config.model == "sonar-pro"
assert config.max_tokens == 450
def test_config_default_values(self, temp_dir):
"""Test default values when env vars are missing"""
# Create empty env file
empty_env = temp_dir / ".env"
empty_env.write_text("")
config = Config(env_file=str(empty_env), config_file=str(temp_dir / "config.json"))
assert config.bot_name == "Eugen" # Default
assert config.model == "sonar-pro" # Default
assert config.max_tokens == 450 # Default
assert config.debug_mode is False # Default
assert config.auto_reconnect is True # Default
def test_config_debug_mode_true(self, temp_dir):
"""Test debug mode parsing when set to true"""
env_file = temp_dir / ".env"
env_file.write_text("DEBUG_MODE=true\n")
config = Config(env_file=str(env_file), config_file=str(temp_dir / "config.json"))
assert config.debug_mode is True
def test_config_debug_mode_false(self, temp_dir):
"""Test debug mode parsing when set to false"""
env_file = temp_dir / ".env"
env_file.write_text("DEBUG_MODE=false\n")
config = Config(env_file=str(env_file), config_file=str(temp_dir / "config.json"))
assert config.debug_mode is False
def test_config_debug_mode_case_insensitive(self, temp_dir):
"""Test debug mode parsing is case insensitive"""
env_file = temp_dir / ".env"
env_file.write_text("DEBUG_MODE=TRUE\n")
config = Config(env_file=str(env_file), config_file=str(temp_dir / "config.json"))
assert config.debug_mode is True
def test_config_auto_reconnect_parsing(self, temp_dir):
"""Test auto_reconnect boolean parsing"""
env_file = temp_dir / ".env"
env_file.write_text("AUTO_RECONNECT=false\n")
config = Config(env_file=str(env_file), config_file=str(temp_dir / "config.json"))
assert config.auto_reconnect is False
def test_config_integer_parsing(self, temp_dir):
"""Test integer parsing for numeric configs"""
env_file = temp_dir / ".env"
env_file.write_text("""MAX_TOKENS=300
RECONNECT_DELAY=20
CONTEXT_RETENTION_HOURS=2
""")
config = Config(env_file=str(env_file), config_file=str(temp_dir / "config.json"))
assert config.max_tokens == 300
assert config.reconnect_delay == 20
assert config.context_retention_hours == 2
def test_config_loads_from_json(self, temp_dir, mock_env_file, mock_config_json):
"""Test that config loads from JSON file"""
config = Config(env_file=str(mock_env_file), config_file=str(mock_config_json))
# JSON values should be loaded
# Note: .env takes precedence for overlapping keys
assert config.bot_name == "TestBot"
assert config.model == "sonar-pro"
def test_config_json_extends_env(self, temp_dir, mock_env_file):
"""Test that JSON config can extend env config with new keys"""
config_json_path = temp_dir / "config.json"
config_data = {
"custom_setting": "custom_value",
"another_setting": 123
}
with open(config_json_path, 'w') as f:
json.dump(config_data, f)
config = Config(env_file=str(mock_env_file), config_file=str(config_json_path))
assert hasattr(config, "custom_setting")
assert config.custom_setting == "custom_value"
assert hasattr(config, "another_setting")
assert config.another_setting == 123
def test_config_json_missing_doesnt_crash(self, temp_dir, mock_env_file):
"""Test that missing config.json doesn't crash"""
nonexistent = temp_dir / "nonexistent.json"
# Should not raise exception
config = Config(env_file=str(mock_env_file), config_file=str(nonexistent))
# Should still have env values
assert config.bot_name == "TestBot"
def test_config_json_invalid_doesnt_crash(self, temp_dir, mock_env_file):
"""Test that invalid JSON doesn't crash"""
invalid_json = temp_dir / "config.json"
invalid_json.write_text("{ invalid json }")
# Should not raise exception
config = Config(env_file=str(mock_env_file), config_file=str(invalid_json))
# Should still have env values
assert config.bot_name == "TestBot"
def test_is_configured_returns_true_when_valid(self, temp_dir, mock_env_file):
"""Test that is_configured returns True with valid config"""
config = Config(env_file=str(mock_env_file), config_file=str(temp_dir / "config.json"))
assert config.is_configured() is True
def test_is_configured_requires_oauth_prefix(self, temp_dir):
"""Test that is_configured checks for oauth: prefix"""
env_file = temp_dir / ".env"
env_file.write_text("""TWITCH_OAUTH_TOKEN=missing_oauth_prefix
TWITCH_CHANNEL=#channel
TWITCH_BOT_NICKNAME=Bot
PERPLEXITY_API_KEY=key
""")
config = Config(env_file=str(env_file), config_file=str(temp_dir / "config.json"))
assert config.is_configured() is False
def test_is_configured_requires_all_fields(self, temp_dir):
"""Test that is_configured checks all required fields"""
env_file = temp_dir / ".env"
# Missing perplexity key
env_file.write_text("""TWITCH_OAUTH_TOKEN=oauth:token
TWITCH_CHANNEL=#channel
TWITCH_BOT_NICKNAME=Bot
""")
config = Config(env_file=str(env_file), config_file=str(temp_dir / "config.json"))
assert config.is_configured() is False
def test_is_configured_with_empty_fields(self, temp_dir):
"""Test that is_configured rejects empty fields"""
env_file = temp_dir / ".env"
env_file.write_text("""TWITCH_OAUTH_TOKEN=oauth:token
TWITCH_CHANNEL=
TWITCH_BOT_NICKNAME=Bot
PERPLEXITY_API_KEY=key
""")
config = Config(env_file=str(env_file), config_file=str(temp_dir / "config.json"))
assert config.is_configured() is False
def test_save_to_json(self, temp_dir, mock_env_file):
"""Test saving config to JSON file"""
config_json_path = temp_dir / "saved_config.json"
config = Config(env_file=str(mock_env_file), config_file=str(config_json_path))
config.save_to_json()
assert config_json_path.exists()
with open(config_json_path, 'r') as f:
saved_data = json.load(f)
assert saved_data["bot_name"] == "TestBot"
assert saved_data["model"] == "sonar-pro"
assert saved_data["max_tokens"] == 450
assert saved_data["debug_mode"] is True
def test_save_to_json_creates_directory(self, temp_dir, mock_env_file):
"""Test that save_to_json creates parent directory if needed"""
nested_path = temp_dir / "nested" / "dir" / "config.json"
config = Config(env_file=str(mock_env_file), config_file=str(nested_path))
config.save_to_json()
assert nested_path.exists()
assert nested_path.parent.exists()
def test_save_to_json_uses_utf8_encoding(self, temp_dir, mock_env_file):
"""Test that save_to_json writes valid JSON with UTF-8 encoding"""
config_json_path = temp_dir / "config.json"
config = Config(env_file=str(mock_env_file), config_file=str(config_json_path))
config.save_to_json()
# Should be able to read as UTF-8 without errors
with open(config_json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# Should have valid JSON structure
assert isinstance(data, dict)
assert "bot_name" in data
def test_get_system_prompt_returns_string(self, temp_dir, mock_env_file):
"""Test that get_system_prompt returns a string"""
config = Config(env_file=str(mock_env_file), config_file=str(temp_dir / "config.json"))
prompt = config.get_system_prompt()
assert isinstance(prompt, str)
assert len(prompt) > 0
def test_get_system_prompt_contains_bot_context(self, temp_dir, mock_env_file):
"""Test that system prompt contains bot context"""
config = Config(env_file=str(mock_env_file), config_file=str(temp_dir / "config.json"))
prompt = config.get_system_prompt()
# Should contain relevant context about the bot
assert "Kene" in prompt or "AI" in prompt
assert "3D-Druck" in prompt or "Gaming" in prompt
def test_data_dir_configuration(self, temp_dir):
"""Test data directory configuration"""
env_file = temp_dir / ".env"
env_file.write_text("DATA_DIR=custom/data/path\n")
config = Config(env_file=str(env_file), config_file=str(temp_dir / "config.json"))
assert config.data_dir == "custom/data/path"
def test_log_dir_configuration(self, temp_dir):
"""Test log directory configuration"""
env_file = temp_dir / ".env"
env_file.write_text("LOG_DIR=custom/logs\n")
config = Config(env_file=str(env_file), config_file=str(temp_dir / "config.json"))
assert config.log_dir == "custom/logs"
def test_twitch_channel_with_hash(self, temp_dir):
"""Test that Twitch channel is stored with hash"""
env_file = temp_dir / ".env"
env_file.write_text("TWITCH_CHANNEL=#mychannel\n")
config = Config(env_file=str(env_file), config_file=str(temp_dir / "config.json"))
assert config.twitch_channel == "#mychannel"
def test_twitch_channel_without_hash(self, temp_dir):
"""Test Twitch channel without hash (should be accepted as-is)"""
env_file = temp_dir / ".env"
env_file.write_text("TWITCH_CHANNEL=mychannel\n")
config = Config(env_file=str(env_file), config_file=str(temp_dir / "config.json"))
# Config stores as-is, validation happens elsewhere
assert config.twitch_channel == "mychannel"
def test_perplexity_model_sonar(self, temp_dir):
"""Test Perplexity sonar model configuration"""
env_file = temp_dir / ".env"
env_file.write_text("PERPLEXITY_MODEL=sonar\n")
config = Config(env_file=str(env_file), config_file=str(temp_dir / "config.json"))
assert config.model == "sonar"
def test_perplexity_model_sonar_pro(self, temp_dir):
"""Test Perplexity sonar-pro model configuration"""
env_file = temp_dir / ".env"
env_file.write_text("PERPLEXITY_MODEL=sonar-pro\n")
config = Config(env_file=str(env_file), config_file=str(temp_dir / "config.json"))
assert config.model == "sonar-pro"
def test_config_immutable_after_load(self, temp_dir, mock_env_file):
"""Test that config values can be modified after load"""
config = Config(env_file=str(mock_env_file), config_file=str(temp_dir / "config.json"))
original_model = config.model
# Should be able to modify
config.model = "different-model"
assert config.model == "different-model"
assert config.model != original_model
def test_config_with_whitespace_in_values(self, temp_dir):
"""Test that whitespace in env values is handled"""
env_file = temp_dir / ".env"
env_file.write_text('TWITCH_OAUTH_TOKEN= oauth:token_with_space \n')
config = Config(env_file=str(env_file), config_file=str(temp_dir / "config.json"))
# dotenv should handle whitespace stripping
assert config.twitch_token.strip() == "oauth:token_with_space"
def test_save_to_json_doesnt_include_secrets(self, temp_dir, mock_env_file):
"""Test that save_to_json doesn't save API keys"""
config_json_path = temp_dir / "config.json"
config = Config(env_file=str(mock_env_file), config_file=str(config_json_path))
config.save_to_json()
with open(config_json_path, 'r') as f:
saved_data = json.load(f)
# Should NOT contain secrets
assert "twitch_token" not in saved_data
assert "perplexity_key" not in saved_data
assert "oauth" not in str(saved_data).lower()
# Should contain non-secret config
assert "bot_name" in saved_data
assert "model" in saved_data