""" Unit tests for Clipboard Manager service. Tests cover: - Singleton pattern - Copy/paste operations - History management - Persistence (save/load) - Availability checking """ import sys import unittest import json import tempfile import shutil from pathlib import Path from unittest.mock import MagicMock, patch, mock_open from datetime import datetime # Add project root to path project_root = Path(__file__).parent.parent.parent if str(project_root) not in sys.path: sys.path.insert(0, str(project_root)) from core.clipboard import ClipboardManager, get_clipboard_manager, ClipboardEntry, copy_to_clipboard, paste_from_clipboard class TestClipboardManagerSingleton(unittest.TestCase): """Test ClipboardManager singleton behavior.""" def tearDown(self): """Reset singleton after each test.""" ClipboardManager._instance = None def test_singleton_instance(self): """Test that ClipboardManager is a proper singleton.""" cm1 = get_clipboard_manager() cm2 = get_clipboard_manager() self.assertIs(cm1, cm2) self.assertIsInstance(cm1, ClipboardManager) def test_singleton_reset(self): """Test that singleton can be reset.""" cm1 = get_clipboard_manager() ClipboardManager._instance = None cm2 = get_clipboard_manager() self.assertIsNot(cm1, cm2) class TestClipboardManagerInitialization(unittest.TestCase): """Test ClipboardManager initialization.""" def tearDown(self): """Reset singleton after each test.""" ClipboardManager._instance = None def test_initialization_default_values(self): """Test that initialization sets default values.""" cm = ClipboardManager() self.assertEqual(cm._max_history, 100) self.assertEqual(cm._history.maxlen, 100) self.assertEqual(cm._history_file, Path("data/clipboard_history.json")) self.assertFalse(cm._monitoring) def test_initialization_custom_values(self): """Test initialization with custom values.""" custom_path = Path("/custom/history.json") cm = ClipboardManager(max_history=50, history_file=custom_path) self.assertEqual(cm._max_history, 50) self.assertEqual(cm._history.maxlen, 50) self.assertEqual(cm._history_file, custom_path) class TestClipboardOperations(unittest.TestCase): """Test clipboard copy and paste operations.""" def tearDown(self): """Reset singleton after each test.""" ClipboardManager._instance = None @patch('pyperclip.copy') def test_copy_success(self, mock_copy): """Test successful copy operation.""" cm = ClipboardManager() cm._history_file = MagicMock() # Prevent file operations result = cm.copy("test text", source="test") self.assertTrue(result) mock_copy.assert_called_once_with("test text") self.assertEqual(len(cm._history), 1) @patch('pyperclip.copy') def test_copy_adds_to_history(self, mock_copy): """Test that copy adds entry to history.""" cm = ClipboardManager() cm._history_file = MagicMock() cm.copy("test text", source="test") entry = cm._history[0] self.assertEqual(entry.text, "test text") self.assertEqual(entry.source, "test") self.assertIsNotNone(entry.timestamp) @patch('pyperclip.copy') @patch('core.clipboard.print') def test_copy_failure(self, mock_print, mock_copy): """Test copy failure handling.""" mock_copy.side_effect = Exception("Copy failed") cm = ClipboardManager() result = cm.copy("test text") self.assertFalse(result) @patch('pyperclip.paste') def test_paste_success(self, mock_paste): """Test successful paste operation.""" mock_paste.return_value = "pasted text" cm = ClipboardManager() result = cm.paste() self.assertEqual(result, "pasted text") @patch('pyperclip.paste') def test_paste_empty(self, mock_paste): """Test paste with empty clipboard.""" mock_paste.return_value = None cm = ClipboardManager() result = cm.paste() self.assertEqual(result, "") @patch('pyperclip.paste') @patch('core.clipboard.print') def test_paste_failure(self, mock_print, mock_paste): """Test paste failure handling.""" mock_paste.side_effect = Exception("Paste failed") cm = ClipboardManager() result = cm.paste() self.assertEqual(result, "") class TestHistoryManagement(unittest.TestCase): """Test clipboard history management.""" def tearDown(self): """Reset singleton after each test.""" ClipboardManager._instance = None def test_get_history_empty(self): """Test getting history when empty.""" cm = ClipboardManager() history = cm.get_history() self.assertEqual(history, []) def test_get_history_with_limit(self): """Test getting history with limit.""" cm = ClipboardManager() # Add 5 entries for i in range(5): cm._history.append(ClipboardEntry(text=f"text{i}", timestamp=datetime.now().isoformat())) history = cm.get_history(limit=3) self.assertEqual(len(history), 3) # Should return newest first self.assertEqual(history[0].text, "text2") self.assertEqual(history[2].text, "text4") def test_get_history_order(self): """Test that history is returned newest first.""" cm = ClipboardManager() cm._history.append(ClipboardEntry(text="old", timestamp="2024-01-01T00:00:00")) cm._history.append(ClipboardEntry(text="new", timestamp="2024-01-02T00:00:00")) history = cm.get_history() self.assertEqual(history[0].text, "new") self.assertEqual(history[1].text, "old") def test_clear_history(self): """Test clearing history.""" cm = ClipboardManager() cm._history_file = MagicMock() cm._history.append(ClipboardEntry(text="test", timestamp=datetime.now().isoformat())) self.assertEqual(len(cm._history), 1) cm.clear_history() self.assertEqual(len(cm._history), 0) def test_history_max_size(self): """Test that history respects max size.""" cm = ClipboardManager(max_history=3) cm._history_file = MagicMock() # Add 5 entries for i in range(5): cm._history.append(ClipboardEntry(text=f"text{i}", timestamp=datetime.now().isoformat())) # Should only keep last 3 self.assertEqual(len(cm._history), 3) class TestPersistence(unittest.TestCase): """Test history persistence.""" def setUp(self): """Create temporary directory for tests.""" self.temp_dir = tempfile.mkdtemp() self.history_file = Path(self.temp_dir) / "history.json" def tearDown(self): """Clean up temporary directory.""" shutil.rmtree(self.temp_dir) ClipboardManager._instance = None def test_save_history(self): """Test saving history to file.""" cm = ClipboardManager(history_file=self.history_file) cm._history.append(ClipboardEntry(text="test", timestamp="2024-01-01T00:00:00", source="test")) cm._save_history() self.assertTrue(self.history_file.exists()) with open(self.history_file, 'r') as f: data = json.load(f) self.assertEqual(len(data), 1) self.assertEqual(data[0]['text'], "test") def test_load_history(self): """Test loading history from file.""" # Create history file data = [ {"text": "saved entry", "timestamp": "2024-01-01T00:00:00", "source": "file"} ] self.history_file.parent.mkdir(parents=True, exist_ok=True) with open(self.history_file, 'w') as f: json.dump(data, f) cm = ClipboardManager(history_file=self.history_file) self.assertEqual(len(cm._history), 1) self.assertEqual(cm._history[0].text, "saved entry") def test_load_history_file_not_exists(self): """Test loading when history file doesn't exist.""" cm = ClipboardManager(history_file=self.history_file) # Should not raise self.assertEqual(len(cm._history), 0) @patch('core.clipboard.print') def test_load_history_corrupted(self, mock_print): """Test loading corrupted history file.""" self.history_file.parent.mkdir(parents=True, exist_ok=True) with open(self.history_file, 'w') as f: f.write("not valid json") cm = ClipboardManager(history_file=self.history_file) # Should not raise, history should be empty self.assertEqual(len(cm._history), 0) @patch('core.clipboard.print') def test_save_history_error(self, mock_print): """Test handling save error.""" cm = ClipboardManager(history_file=self.history_file) cm._history.append(ClipboardEntry(text="test", timestamp=datetime.now().isoformat())) # Make directory read-only self.history_file.parent.mkdir(parents=True, exist_ok=True) with patch('builtins.open', side_effect=IOError("Permission denied")): cm._save_history() # Should not raise class TestAvailability(unittest.TestCase): """Test clipboard availability checking.""" def tearDown(self): """Reset singleton after each test.""" ClipboardManager._instance = None @patch('pyperclip.paste') def test_is_available_true(self, mock_paste): """Test availability when pyperclip works.""" mock_paste.return_value = "" cm = ClipboardManager() self.assertTrue(cm.is_available()) @patch('pyperclip.paste') def test_is_available_false(self, mock_paste): """Test availability when pyperclip fails.""" mock_paste.side_effect = Exception("Not available") cm = ClipboardManager() self.assertFalse(cm.is_available()) class TestClipboardEntry(unittest.TestCase): """Test ClipboardEntry dataclass.""" def test_entry_creation(self): """Test creating a clipboard entry.""" entry = ClipboardEntry( text="test text", timestamp="2024-01-01T00:00:00", source="plugin" ) self.assertEqual(entry.text, "test text") self.assertEqual(entry.timestamp, "2024-01-01T00:00:00") self.assertEqual(entry.source, "plugin") def test_entry_defaults(self): """Test entry default values.""" entry = ClipboardEntry(text="test", timestamp="2024-01-01T00:00:00") self.assertEqual(entry.source, "unknown") class TestConvenienceFunctions(unittest.TestCase): """Test convenience functions.""" def tearDown(self): """Reset singleton after each test.""" ClipboardManager._instance = None @patch.object(ClipboardManager, 'copy') def test_copy_to_clipboard(self, mock_copy): """Test copy_to_clipboard convenience function.""" mock_copy.return_value = True result = copy_to_clipboard("test text") self.assertTrue(result) mock_copy.assert_called_once_with("test text") @patch.object(ClipboardManager, 'paste') def test_paste_from_clipboard(self, mock_paste): """Test paste_from_clipboard convenience function.""" mock_paste.return_value = "pasted text" result = paste_from_clipboard() self.assertEqual(result, "pasted text") if __name__ == '__main__': unittest.main()