EU-Utility/tests/unit/test_data_store.py

397 lines
13 KiB
Python

"""
Unit tests for Data Store service.
Tests cover:
- Singleton pattern
- Data persistence (save/load/delete)
- File locking
- Backup and restore
- Cache management
- Cross-platform compatibility
"""
import pytest
import json
import os
from pathlib import Path
from unittest.mock import patch, mock_open, MagicMock
from datetime import datetime
from core.data_store import DataStore, get_data_store
@pytest.mark.unit
class TestDataStoreSingleton:
"""Test DataStore singleton behavior."""
def test_singleton_instance(self, reset_singletons):
"""Test that DataStore is a proper singleton."""
ds1 = get_data_store()
ds2 = get_data_store()
assert ds1 is ds2
assert isinstance(ds1, DataStore)
def test_singleton_reset(self, reset_singletons):
"""Test that singleton can be reset."""
ds1 = get_data_store()
DataStore._instance = None
ds2 = get_data_store()
assert ds1 is not ds2
@pytest.mark.unit
class TestDataStoreInitialization:
"""Test DataStore initialization."""
def test_default_data_dir(self, reset_singletons):
"""Test default data directory."""
ds = get_data_store()
assert ds.data_dir == Path("data/plugins")
def test_custom_data_dir(self, temp_data_dir):
"""Test custom data directory."""
ds = DataStore(data_dir=str(temp_data_dir))
assert ds.data_dir == temp_data_dir
assert ds.data_dir.exists()
def test_data_dir_creation(self, tmp_path):
"""Test that data directory is created if not exists."""
data_dir = tmp_path / "new_data_dir"
assert not data_dir.exists()
ds = DataStore(data_dir=str(data_dir))
assert data_dir.exists()
@pytest.mark.unit
class TestDataPersistence:
"""Test data persistence operations."""
def test_save_and_load(self, temp_data_dir):
"""Test saving and loading data."""
ds = DataStore(data_dir=str(temp_data_dir))
# Save data
result = ds.save("test_plugin", "key1", {"name": "Test", "value": 42})
assert result is True
# Load data
loaded = ds.load("test_plugin", "key1")
assert loaded == {"name": "Test", "value": 42}
def test_load_default(self, temp_data_dir):
"""Test loading with default value."""
ds = DataStore(data_dir=str(temp_data_dir))
# Load non-existent key with default
result = ds.load("test_plugin", "nonexistent", default="default_value")
assert result == "default_value"
def test_load_none_default(self, temp_data_dir):
"""Test loading with None default."""
ds = DataStore(data_dir=str(temp_data_dir))
result = ds.load("test_plugin", "nonexistent")
assert result is None
def test_delete(self, temp_data_dir):
"""Test deleting data."""
ds = DataStore(data_dir=str(temp_data_dir))
# Save then delete
ds.save("test_plugin", "key1", "value1")
result = ds.delete("test_plugin", "key1")
assert result is True
assert ds.load("test_plugin", "key1") is None
def test_delete_nonexistent(self, temp_data_dir):
"""Test deleting non-existent key."""
ds = DataStore(data_dir=str(temp_data_dir))
result = ds.delete("test_plugin", "nonexistent")
assert result is False
def test_multiple_keys(self, temp_data_dir):
"""Test saving multiple keys for same plugin."""
ds = DataStore(data_dir=str(temp_data_dir))
ds.save("test_plugin", "key1", "value1")
ds.save("test_plugin", "key2", "value2")
ds.save("test_plugin", "key3", "value3")
assert ds.load("test_plugin", "key1") == "value1"
assert ds.load("test_plugin", "key2") == "value2"
assert ds.load("test_plugin", "key3") == "value3"
def test_multiple_plugins(self, temp_data_dir):
"""Test saving data for multiple plugins."""
ds = DataStore(data_dir=str(temp_data_dir))
ds.save("plugin1", "key", "plugin1_value")
ds.save("plugin2", "key", "plugin2_value")
assert ds.load("plugin1", "key") == "plugin1_value"
assert ds.load("plugin2", "key") == "plugin2_value"
@pytest.mark.unit
class TestGetAllKeys:
"""Test getting all keys for a plugin."""
def test_get_all_keys(self, temp_data_dir):
"""Test getting all keys."""
ds = DataStore(data_dir=str(temp_data_dir))
ds.save("test_plugin", "key1", "value1")
ds.save("test_plugin", "key2", "value2")
ds.save("test_plugin", "key3", "value3")
keys = ds.get_all_keys("test_plugin")
assert len(keys) == 3
assert "key1" in keys
assert "key2" in keys
assert "key3" in keys
def test_get_all_keys_empty(self, temp_data_dir):
"""Test getting keys for plugin with no data."""
ds = DataStore(data_dir=str(temp_data_dir))
keys = ds.get_all_keys("nonexistent_plugin")
assert keys == []
@pytest.mark.unit
class TestClearPlugin:
"""Test clearing all data for a plugin."""
def test_clear_plugin(self, temp_data_dir):
"""Test clearing plugin data."""
ds = DataStore(data_dir=str(temp_data_dir))
ds.save("test_plugin", "key1", "value1")
ds.save("test_plugin", "key2", "value2")
result = ds.clear_plugin("test_plugin")
assert result is True
assert ds.load("test_plugin", "key1") is None
assert ds.load("test_plugin", "key2") is None
def test_clear_nonexistent_plugin(self, temp_data_dir):
"""Test clearing non-existent plugin."""
ds = DataStore(data_dir=str(temp_data_dir))
result = ds.clear_plugin("nonexistent_plugin")
assert result is True # Nothing to clear is still success
@pytest.mark.unit
class TestBackupAndRestore:
"""Test backup and restore functionality."""
def test_backup_created_on_save(self, temp_data_dir):
"""Test that backup is created when saving."""
ds = DataStore(data_dir=str(temp_data_dir))
# First save - no backup yet
ds.save("test_plugin", "key", "value1")
# Second save - should create backup
ds.save("test_plugin", "key", "value2")
backups = ds.get_backups("test_plugin")
assert len(backups) == 1
def test_multiple_backups(self, temp_data_dir):
"""Test multiple backup creation."""
ds = DataStore(data_dir=str(temp_data_dir))
ds.max_backups = 3
# Create multiple saves
for i in range(5):
ds.save("test_plugin", "key", f"value{i}")
backups = ds.get_backups("test_plugin")
# Should have max_backups (3) backups
assert len(backups) <= 3
def test_restore_backup(self, temp_data_dir):
"""Test restoring from backup."""
ds = DataStore(data_dir=str(temp_data_dir))
# Save initial value
ds.save("test_plugin", "key", "original_value")
# Get backup path
backups = ds.get_backups("test_plugin")
if backups:
backup_path = backups[0]
# Change value
ds.save("test_plugin", "key", "new_value")
assert ds.load("test_plugin", "key") == "new_value"
# Restore backup
result = ds.restore_backup("test_plugin", backup_path)
assert result is True
# Check restored value
assert ds.load("test_plugin", "key") == "original_value"
def test_restore_nonexistent_backup(self, temp_data_dir):
"""Test restoring from non-existent backup."""
ds = DataStore(data_dir=str(temp_data_dir))
result = ds.restore_backup("test_plugin", "/nonexistent/backup.json")
assert result is False
@pytest.mark.unit
class TestCacheManagement:
"""Test cache management."""
def test_cache_update_on_save(self, temp_data_dir):
"""Test that cache is updated on save."""
ds = DataStore(data_dir=str(temp_data_dir))
ds.save("test_plugin", "key", "value")
# Check cache
assert "test_plugin" in ds._cache
assert ds._cache["test_plugin"]["key"] == "value"
def test_cache_used_on_load(self, temp_data_dir):
"""Test that cache is used when loading."""
ds = DataStore(data_dir=str(temp_data_dir))
# Save and load to populate cache
ds.save("test_plugin", "key", "value")
ds.load("test_plugin", "key")
# Modify file directly (simulate external change)
file_path = ds._get_plugin_file("test_plugin")
with open(file_path, 'w') as f:
json.dump({"key": "modified_value"}, f)
# Load should return cached value
result = ds.load("test_plugin", "key")
assert result == "value" # From cache, not file
def test_cache_invalidation_on_restore(self, temp_data_dir):
"""Test that cache is invalidated on restore."""
ds = DataStore(data_dir=str(temp_data_dir))
ds.save("test_plugin", "key", "value")
ds.load("test_plugin", "key") # Populate cache
# Create and restore backup
backups = ds.get_backups("test_plugin")
if backups:
ds.restore_backup("test_plugin", backups[0])
# Cache should be invalidated
assert "test_plugin" not in ds._cache
@pytest.mark.unit
class TestFileNaming:
"""Test file naming and sanitization."""
def test_plugin_id_sanitization_dots(self, temp_data_dir):
"""Test sanitization of plugin IDs with dots."""
ds = DataStore(data_dir=str(temp_data_dir))
ds.save("plugin.name.with.dots", "key", "value")
file_path = ds._get_plugin_file("plugin.name.with.dots")
assert "plugin_name_with_dots" in str(file_path)
def test_plugin_id_sanitization_slashes(self, temp_data_dir):
"""Test sanitization of plugin IDs with slashes."""
ds = DataStore(data_dir=str(temp_data_dir))
ds.save("plugin/name/with/slashes", "key", "value")
file_path = ds._get_plugin_file("plugin/name/with/slashes")
assert "plugin_name_with_slashes" in str(file_path)
def test_plugin_id_sanitization_backslashes(self, temp_data_dir):
"""Test sanitization of plugin IDs with backslashes."""
ds = DataStore(data_dir=str(temp_data_dir))
ds.save("plugin\\name\\with\\backslashes", "key", "value")
file_path = ds._get_plugin_file("plugin\\name\\with\\backslashes")
assert "plugin_name_with_backslashes" in str(file_path)
@pytest.mark.unit
class TestErrorHandling:
"""Test error handling."""
def test_load_corrupted_file(self, temp_data_dir):
"""Test loading corrupted JSON file."""
ds = DataStore(data_dir=str(temp_data_dir))
# Create corrupted file
file_path = ds._get_plugin_file("test_plugin")
file_path.parent.mkdir(parents=True, exist_ok=True)
with open(file_path, 'w') as f:
f.write("not valid json")
# Should return empty dict, not raise
result = ds.load("test_plugin", "key")
assert result is None
def test_save_io_error(self, temp_data_dir):
"""Test handling IO error during save."""
ds = DataStore(data_dir=str(temp_data_dir))
with patch('builtins.open', side_effect=IOError("Disk full")):
result = ds.save("test_plugin", "key", "value")
assert result is False
@pytest.mark.unit
class TestThreadSafety:
"""Test thread safety of DataStore."""
def test_concurrent_saves(self, temp_data_dir):
"""Test concurrent saves from multiple threads."""
import threading
ds = DataStore(data_dir=str(temp_data_dir))
errors = []
def save_data(thread_id):
try:
for i in range(10):
ds.save("test_plugin", f"key_{thread_id}_{i}", f"value_{i}")
except Exception as e:
errors.append(e)
# Create and start threads
threads = [threading.Thread(target=save_data, args=(i,)) for i in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
# Check no errors occurred
assert len(errors) == 0
# Verify all data was saved
keys = ds.get_all_keys("test_plugin")
assert len(keys) == 50 # 5 threads * 10 saves each