mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
* fix(test): repair test infrastructure and mock fixtures - Add test/__init__.py for proper test collection - Fix ConfigManager instantiation to use config_path parameter - Route schedule config through config_service mock - Update mock to match get_raw_file_content endpoint change Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(test): correct get_main_config assertion per CodeRabbit review The endpoint calls load_config(), not get_raw_file_content('main'). Also set up load_config mock return value in the fixture so the test's data assertions pass correctly. Co-Authored-By: 5ymb01 <noreply@github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(test): correct plugin config test mock structure and schema returns - Plugin configs live at top-level keys, not under 'plugins' subkey - Mock schema_manager.generate_default_config to return a dict - Mock schema_manager.merge_with_defaults to merge dicts (not MagicMock) - Fixes test_get_plugin_config returning 500 due to non-serializable MagicMock Co-Authored-By: 5ymb01 <noreply@github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(test): use patch.object for config_service.get_config in schedule tests config_service.get_config is a real method, not a mock — can't set return_value on it directly. Use patch.object context manager instead. Co-Authored-By: 5ymb01 <noreply@github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: 5ymb01 <noreply@github.com>
306 lines
9.6 KiB
Python
306 lines
9.6 KiB
Python
"""
|
|
Tests for configuration validation edge cases.
|
|
|
|
Tests scenarios that commonly cause user configuration errors:
|
|
- Invalid JSON in config files
|
|
- Missing required fields
|
|
- Type mismatches
|
|
- Nested object validation
|
|
- Array validation
|
|
"""
|
|
|
|
import pytest
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
import tempfile
|
|
import os
|
|
|
|
# Add project root to path
|
|
import sys
|
|
project_root = Path(__file__).parent.parent
|
|
if str(project_root) not in sys.path:
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
from src.config_manager import ConfigManager
|
|
from src.exceptions import ConfigError
|
|
from src.plugin_system.schema_manager import SchemaManager
|
|
|
|
|
|
class TestInvalidJson:
|
|
"""Test handling of invalid JSON in config files."""
|
|
|
|
def test_invalid_json_syntax(self, tmp_path):
|
|
"""Config with invalid JSON syntax should raise ConfigError."""
|
|
config_file = tmp_path / "config.json"
|
|
config_file.write_text("{ invalid json }")
|
|
|
|
config_manager = ConfigManager(config_path=str(config_file))
|
|
with pytest.raises(ConfigError):
|
|
config_manager.load_config()
|
|
|
|
def test_truncated_json(self, tmp_path):
|
|
"""Config with truncated JSON should raise ConfigError."""
|
|
config_file = tmp_path / "config.json"
|
|
config_file.write_text('{"plugin": {"enabled": true') # Missing closing braces
|
|
|
|
config_manager = ConfigManager(config_path=str(config_file))
|
|
with pytest.raises(ConfigError):
|
|
config_manager.load_config()
|
|
|
|
def test_empty_config_file(self, tmp_path):
|
|
"""Empty config file should raise ConfigError."""
|
|
config_file = tmp_path / "config.json"
|
|
config_file.write_text("")
|
|
|
|
config_manager = ConfigManager(config_path=str(config_file))
|
|
with pytest.raises(ConfigError):
|
|
config_manager.load_config()
|
|
|
|
|
|
class TestTypeValidation:
|
|
"""Test type validation and coercion."""
|
|
|
|
def test_string_where_number_expected(self):
|
|
"""String value where number expected should be handled."""
|
|
schema_manager = SchemaManager()
|
|
|
|
schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"display_duration": {"type": "number", "default": 15}
|
|
}
|
|
}
|
|
|
|
config = {"display_duration": "invalid_string"}
|
|
|
|
# Validation should fail
|
|
is_valid, errors = schema_manager.validate_config_against_schema(
|
|
config, schema, "test-plugin"
|
|
)
|
|
assert not is_valid
|
|
assert len(errors) > 0
|
|
|
|
def test_number_where_string_expected(self):
|
|
"""Number value where string expected should be handled."""
|
|
schema_manager = SchemaManager()
|
|
|
|
schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"team_name": {"type": "string", "default": ""}
|
|
}
|
|
}
|
|
|
|
config = {"team_name": 12345}
|
|
|
|
is_valid, errors = schema_manager.validate_config_against_schema(
|
|
config, schema, "test-plugin"
|
|
)
|
|
assert not is_valid
|
|
assert len(errors) > 0
|
|
|
|
def test_null_value_for_required_field(self):
|
|
"""Null value for required field should be detected."""
|
|
schema_manager = SchemaManager()
|
|
|
|
# Schema that explicitly disallows null for api_key
|
|
schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"api_key": {"type": "string"} # string type doesn't allow null
|
|
},
|
|
"required": ["api_key"]
|
|
}
|
|
|
|
config = {"api_key": None}
|
|
|
|
is_valid, errors = schema_manager.validate_config_against_schema(
|
|
config, schema, "test-plugin"
|
|
)
|
|
# JSON Schema Draft 7: null is not a valid string type
|
|
assert not is_valid, "Null value should fail validation for string type"
|
|
assert errors, "Should have validation errors"
|
|
assert any("api_key" in str(e).lower() or "null" in str(e).lower() or "type" in str(e).lower() for e in errors), \
|
|
f"Error should mention api_key, null, or type issue: {errors}"
|
|
|
|
|
|
class TestNestedValidation:
|
|
"""Test validation of nested configuration objects."""
|
|
|
|
def test_nested_object_missing_required(self):
|
|
"""Missing required field in nested object should be detected."""
|
|
schema_manager = SchemaManager()
|
|
|
|
schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"nfl": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {"type": "boolean", "default": True},
|
|
"api_key": {"type": "string"}
|
|
},
|
|
"required": ["api_key"]
|
|
}
|
|
}
|
|
}
|
|
|
|
config = {"nfl": {"enabled": True}} # Missing api_key
|
|
|
|
is_valid, errors = schema_manager.validate_config_against_schema(
|
|
config, schema, "test-plugin"
|
|
)
|
|
assert not is_valid
|
|
|
|
def test_deeply_nested_validation(self):
|
|
"""Validation should work for deeply nested objects."""
|
|
schema_manager = SchemaManager()
|
|
|
|
schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"level1": {
|
|
"type": "object",
|
|
"properties": {
|
|
"level2": {
|
|
"type": "object",
|
|
"properties": {
|
|
"value": {"type": "number", "minimum": 0}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
config = {"level1": {"level2": {"value": -5}}} # Invalid: negative
|
|
|
|
is_valid, errors = schema_manager.validate_config_against_schema(
|
|
config, schema, "test-plugin"
|
|
)
|
|
assert not is_valid
|
|
|
|
|
|
class TestArrayValidation:
|
|
"""Test validation of array configurations."""
|
|
|
|
def test_array_min_items(self):
|
|
"""Array with fewer items than minItems should fail."""
|
|
schema_manager = SchemaManager()
|
|
|
|
schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"teams": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"minItems": 1
|
|
}
|
|
}
|
|
}
|
|
|
|
config = {"teams": []} # Empty array, minItems is 1
|
|
|
|
is_valid, errors = schema_manager.validate_config_against_schema(
|
|
config, schema, "test-plugin"
|
|
)
|
|
assert not is_valid
|
|
|
|
def test_array_max_items(self):
|
|
"""Array with more items than maxItems should fail."""
|
|
schema_manager = SchemaManager()
|
|
|
|
schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"teams": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"maxItems": 2
|
|
}
|
|
}
|
|
}
|
|
|
|
config = {"teams": ["A", "B", "C", "D"]} # 4 items, maxItems is 2
|
|
|
|
is_valid, errors = schema_manager.validate_config_against_schema(
|
|
config, schema, "test-plugin"
|
|
)
|
|
assert not is_valid
|
|
|
|
|
|
class TestCollisionDetection:
|
|
"""Test config key collision detection."""
|
|
|
|
def test_reserved_key_collision(self):
|
|
"""Plugin IDs that conflict with reserved keys should be detected."""
|
|
schema_manager = SchemaManager()
|
|
|
|
plugin_ids = ["display", "custom-plugin", "schedule"]
|
|
|
|
collisions = schema_manager.detect_config_key_collisions(plugin_ids)
|
|
|
|
# Should detect 'display' and 'schedule' as collisions
|
|
collision_types = [c["type"] for c in collisions]
|
|
collision_plugins = [c["plugin_id"] for c in collisions]
|
|
|
|
assert "reserved_key_collision" in collision_types
|
|
assert "display" in collision_plugins
|
|
assert "schedule" in collision_plugins
|
|
|
|
def test_case_collision(self):
|
|
"""Plugin IDs that differ only in case should be detected."""
|
|
schema_manager = SchemaManager()
|
|
|
|
plugin_ids = ["football-scoreboard", "Football-Scoreboard", "other-plugin"]
|
|
|
|
collisions = schema_manager.detect_config_key_collisions(plugin_ids)
|
|
|
|
case_collisions = [c for c in collisions if c["type"] == "case_collision"]
|
|
assert len(case_collisions) == 1
|
|
|
|
def test_no_collisions(self):
|
|
"""Unique plugin IDs should not trigger collisions."""
|
|
schema_manager = SchemaManager()
|
|
|
|
plugin_ids = ["football-scoreboard", "odds-ticker", "weather-display"]
|
|
|
|
collisions = schema_manager.detect_config_key_collisions(plugin_ids)
|
|
|
|
assert len(collisions) == 0
|
|
|
|
|
|
class TestDefaultMerging:
|
|
"""Test default value merging with user config."""
|
|
|
|
def test_defaults_applied_to_missing_fields(self):
|
|
"""Missing fields should get default values from schema."""
|
|
schema_manager = SchemaManager()
|
|
|
|
defaults = {
|
|
"enabled": True,
|
|
"display_duration": 15,
|
|
"nfl": {"enabled": True}
|
|
}
|
|
|
|
config = {"display_duration": 30} # Only override one field
|
|
|
|
merged = schema_manager.merge_with_defaults(config, defaults)
|
|
|
|
assert merged["enabled"] is True # From defaults
|
|
assert merged["display_duration"] == 30 # User override
|
|
assert merged["nfl"]["enabled"] is True # Nested default
|
|
|
|
def test_user_values_not_overwritten(self):
|
|
"""User-provided values should not be overwritten by defaults."""
|
|
schema_manager = SchemaManager()
|
|
|
|
defaults = {"enabled": True, "display_duration": 15}
|
|
config = {"enabled": False, "display_duration": 60}
|
|
|
|
merged = schema_manager.merge_with_defaults(config, defaults)
|
|
|
|
assert merged["enabled"] is False
|
|
assert merged["display_duration"] == 60
|