""" 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