397 lines
13 KiB
Python
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
|