""" Tests for the command pattern implementation in the hosts TUI application. This module tests the undo/redo functionality, command pattern, and integration with the HostsManager. """ import pytest from unittest.mock import Mock, patch from src.hosts.core.commands import ( Command, OperationResult, UndoRedoHistory, ToggleEntryCommand, MoveEntryCommand, AddEntryCommand, DeleteEntryCommand, UpdateEntryCommand, ) from src.hosts.core.models import HostEntry, HostsFile from src.hosts.core.manager import HostsManager class TestOperationResult: """Test the OperationResult class.""" def test_operation_result_success(self): """Test successful operation result.""" result = OperationResult(True, "Operation successful") assert result.success is True assert result.message == "Operation successful" def test_operation_result_failure(self): """Test failed operation result.""" result = OperationResult(False, "Operation failed") assert result.success is False assert result.message == "Operation failed" def test_operation_result_with_data(self): """Test operation result with additional data.""" result = OperationResult(True, "Operation successful", {"key": "value"}) assert result.success is True assert result.message == "Operation successful" assert result.data == {"key": "value"} class TestUndoRedoHistory: """Test the UndoRedoHistory class.""" def test_initial_state(self): """Test initial state of undo/redo history.""" history = UndoRedoHistory() assert not history.can_undo() assert not history.can_redo() assert history.get_undo_description() is None assert history.get_redo_description() is None def test_execute_command(self): """Test executing a command adds it to history.""" history = UndoRedoHistory() hosts_file = HostsFile() command = Mock(spec=Command) command.execute.return_value = OperationResult(True, "Test executed") command.get_description.return_value = "Test command" result = history.execute_command(command, hosts_file) assert result.success is True assert result.message == "Test executed" assert history.can_undo() assert not history.can_redo() assert history.get_undo_description() == "Test command" command.execute.assert_called_once_with(hosts_file) def test_execute_failed_command(self): """Test executing a failed command does not add it to history.""" history = UndoRedoHistory() hosts_file = HostsFile() command = Mock(spec=Command) command.execute.return_value = OperationResult(False, "Test failed") result = history.execute_command(command, hosts_file) assert result.success is False assert result.message == "Test failed" assert not history.can_undo() assert not history.can_redo() def test_undo_operation(self): """Test undoing an operation.""" history = UndoRedoHistory() hosts_file = HostsFile() command = Mock(spec=Command) command.execute.return_value = OperationResult(True, "Test executed") command.undo.return_value = OperationResult(True, "Test undone") command.get_description.return_value = "Test command" # Execute then undo history.execute_command(command, hosts_file) result = history.undo(hosts_file) assert result.success is True assert result.message == "Test undone" assert not history.can_undo() assert history.can_redo() assert history.get_redo_description() == "Test command" command.undo.assert_called_once_with(hosts_file) def test_redo_operation(self): """Test redoing an operation.""" history = UndoRedoHistory() hosts_file = HostsFile() command = Mock(spec=Command) command.execute.return_value = OperationResult(True, "Test executed") command.undo.return_value = OperationResult(True, "Test undone") command.get_description.return_value = "Test command" # Execute, undo, then redo history.execute_command(command, hosts_file) history.undo(hosts_file) result = history.redo(hosts_file) assert result.success is True assert result.message == "Test executed" assert history.can_undo() assert not history.can_redo() assert history.get_undo_description() == "Test command" assert command.execute.call_count == 2 def test_undo_with_empty_history(self): """Test undo with empty history.""" history = UndoRedoHistory() hosts_file = HostsFile() result = history.undo(hosts_file) assert result.success is False assert "No operations to undo" in result.message def test_redo_with_empty_history(self): """Test redo with empty history.""" history = UndoRedoHistory() hosts_file = HostsFile() result = history.redo(hosts_file) assert result.success is False assert "No operations to redo" in result.message def test_max_history_limit(self): """Test that history respects the maximum limit.""" history = UndoRedoHistory(max_history=2) hosts_file = HostsFile() # Add 3 commands for i in range(3): command = Mock(spec=Command) command.execute.return_value = OperationResult(True, f"Command {i}") command.get_description.return_value = f"Command {i}" history.execute_command(command, hosts_file) # Should only keep the last 2 commands assert len(history.undo_stack) == 2 def test_clear_history(self): """Test clearing the history.""" history = UndoRedoHistory() hosts_file = HostsFile() command = Mock(spec=Command) command.execute.return_value = OperationResult(True, "Test executed") command.get_description.return_value = "Test command" history.execute_command(command, hosts_file) assert history.can_undo() history.clear_history() assert not history.can_undo() assert not history.can_redo() def test_new_command_clears_redo_stack(self): """Test that executing a new command clears the redo stack.""" history = UndoRedoHistory() hosts_file = HostsFile() # Execute and undo a command command1 = Mock(spec=Command) command1.execute.return_value = OperationResult(True, "Command 1") command1.undo.return_value = OperationResult(True, "Undo 1") command1.get_description.return_value = "Command 1" history.execute_command(command1, hosts_file) history.undo(hosts_file) assert history.can_redo() # Execute a new command command2 = Mock(spec=Command) command2.execute.return_value = OperationResult(True, "Command 2") command2.get_description.return_value = "Command 2" history.execute_command(command2, hosts_file) assert not history.can_redo() # Redo stack should be cleared class TestToggleEntryCommand: """Test the ToggleEntryCommand class.""" def test_toggle_active_to_inactive(self): """Test toggling an active entry to inactive.""" hosts_file = HostsFile() entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"], is_active=True) hosts_file.entries.append(entry) command = ToggleEntryCommand(0) result = command.execute(hosts_file) assert result.success is True assert not hosts_file.entries[0].is_active assert "deactivated" in result.message def test_toggle_inactive_to_active(self): """Test toggling an inactive entry to active.""" hosts_file = HostsFile() entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"], is_active=False) hosts_file.entries.append(entry) command = ToggleEntryCommand(0) result = command.execute(hosts_file) assert result.success is True assert hosts_file.entries[0].is_active assert "activated" in result.message def test_toggle_undo(self): """Test undoing a toggle operation.""" hosts_file = HostsFile() entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"], is_active=True) hosts_file.entries.append(entry) command = ToggleEntryCommand(0) command.execute(hosts_file) # Toggle to inactive result = command.undo(hosts_file) # Toggle back to active assert result.success is True assert hosts_file.entries[0].is_active assert "Undid toggle" in result.message def test_toggle_invalid_index(self): """Test toggle with invalid index.""" hosts_file = HostsFile() command = ToggleEntryCommand(0) result = command.execute(hosts_file) assert result.success is False assert "Invalid entry index" in result.message def test_toggle_get_description(self): """Test getting command description.""" command = ToggleEntryCommand(0) description = command.get_description() assert "Toggle entry at index 0" in description class TestMoveEntryCommand: """Test the MoveEntryCommand class.""" def test_move_entry_up(self): """Test moving an entry up.""" hosts_file = HostsFile() entry1 = HostEntry(ip_address="192.168.1.1", hostnames=["test1.local"]) entry2 = HostEntry(ip_address="192.168.1.2", hostnames=["test2.local"]) hosts_file.entries.extend([entry1, entry2]) command = MoveEntryCommand(1, 0) # Move from index 1 to index 0 result = command.execute(hosts_file) assert result.success is True assert hosts_file.entries[0] == entry2 assert hosts_file.entries[1] == entry1 assert "up" in result.message def test_move_entry_down(self): """Test moving an entry down.""" hosts_file = HostsFile() entry1 = HostEntry(ip_address="192.168.1.1", hostnames=["test1.local"]) entry2 = HostEntry(ip_address="192.168.1.2", hostnames=["test2.local"]) hosts_file.entries.extend([entry1, entry2]) command = MoveEntryCommand(0, 1) # Move from index 0 to index 1 result = command.execute(hosts_file) assert result.success is True assert hosts_file.entries[0] == entry2 assert hosts_file.entries[1] == entry1 assert "down" in result.message def test_move_undo(self): """Test undoing a move operation.""" hosts_file = HostsFile() entry1 = HostEntry(ip_address="192.168.1.1", hostnames=["test1.local"]) entry2 = HostEntry(ip_address="192.168.1.2", hostnames=["test2.local"]) hosts_file.entries.extend([entry1, entry2]) command = MoveEntryCommand(1, 0) # Move entry2 up command.execute(hosts_file) result = command.undo(hosts_file) # Move back down assert result.success is True assert hosts_file.entries[0] == entry1 assert hosts_file.entries[1] == entry2 assert "Undid move" in result.message def test_move_invalid_indices(self): """Test move with invalid indices.""" hosts_file = HostsFile() entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"]) hosts_file.entries.append(entry) command = MoveEntryCommand(0, 5) # Invalid target index result = command.execute(hosts_file) assert result.success is False assert "Invalid move" in result.message def test_move_same_position(self): """Test moving entry to same position.""" hosts_file = HostsFile() entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"]) hosts_file.entries.append(entry) command = MoveEntryCommand(0, 0) # Same position result = command.execute(hosts_file) assert result.success is True assert "No movement needed" in result.message def test_move_get_description(self): """Test getting command description.""" command = MoveEntryCommand(1, 0) description = command.get_description() assert "Move entry up" in description class TestAddEntryCommand: """Test the AddEntryCommand class.""" def test_add_entry(self): """Test adding an entry.""" hosts_file = HostsFile() entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"]) command = AddEntryCommand(entry) result = command.execute(hosts_file) assert result.success is True assert len(hosts_file.entries) == 1 assert hosts_file.entries[0] == entry assert "Added entry" in result.message def test_add_entry_at_specific_index(self): """Test adding an entry at a specific index.""" hosts_file = HostsFile() entry1 = HostEntry(ip_address="192.168.1.1", hostnames=["test1.local"]) entry2 = HostEntry(ip_address="192.168.1.2", hostnames=["test2.local"]) hosts_file.entries.append(entry1) command = AddEntryCommand(entry2, index=0) # Insert at beginning result = command.execute(hosts_file) assert result.success is True assert len(hosts_file.entries) == 2 assert hosts_file.entries[0] == entry2 assert hosts_file.entries[1] == entry1 def test_add_entry_undo(self): """Test undoing an add operation.""" hosts_file = HostsFile() entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"]) command = AddEntryCommand(entry) command.execute(hosts_file) result = command.undo(hosts_file) assert result.success is True assert len(hosts_file.entries) == 0 assert "Undid add" in result.message def test_add_entry_invalid_index(self): """Test adding entry with invalid index.""" hosts_file = HostsFile() entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"]) command = AddEntryCommand(entry, index=5) # Invalid index result = command.execute(hosts_file) assert result.success is False assert "Invalid insertion index" in result.message def test_add_get_description(self): """Test getting command description.""" entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"]) command = AddEntryCommand(entry) description = command.get_description() assert "Add entry: 192.168.1.1 test.local" in description class TestDeleteEntryCommand: """Test the DeleteEntryCommand class.""" def test_delete_entry(self): """Test deleting an entry.""" hosts_file = HostsFile() entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"]) hosts_file.entries.append(entry) command = DeleteEntryCommand(0) result = command.execute(hosts_file) assert result.success is True assert len(hosts_file.entries) == 0 assert "Deleted entry" in result.message def test_delete_entry_undo(self): """Test undoing a delete operation.""" hosts_file = HostsFile() entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"]) hosts_file.entries.append(entry) command = DeleteEntryCommand(0) command.execute(hosts_file) result = command.undo(hosts_file) assert result.success is True assert len(hosts_file.entries) == 1 assert hosts_file.entries[0].ip_address == "192.168.1.1" assert "Undid delete" in result.message def test_delete_invalid_index(self): """Test delete with invalid index.""" hosts_file = HostsFile() command = DeleteEntryCommand(0) result = command.execute(hosts_file) assert result.success is False assert "Invalid entry index" in result.message def test_delete_get_description(self): """Test getting command description.""" command = DeleteEntryCommand(0) description = command.get_description() assert "Delete entry at index 0" in description class TestUpdateEntryCommand: """Test the UpdateEntryCommand class.""" def test_update_entry(self): """Test updating an entry.""" hosts_file = HostsFile() old_entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"], comment="old comment", is_active=True) hosts_file.entries.append(old_entry) command = UpdateEntryCommand(0, "192.168.1.2", ["updated.local"], "new comment", False) result = command.execute(hosts_file) assert result.success is True assert hosts_file.entries[0].ip_address == "192.168.1.2" assert hosts_file.entries[0].hostnames == ["updated.local"] assert hosts_file.entries[0].comment == "new comment" assert hosts_file.entries[0].is_active is False assert "Updated entry" in result.message def test_update_entry_undo(self): """Test undoing an update operation.""" hosts_file = HostsFile() old_entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"], comment="old comment", is_active=True) hosts_file.entries.append(old_entry) command = UpdateEntryCommand(0, "192.168.1.2", ["updated.local"], "new comment", False) command.execute(hosts_file) result = command.undo(hosts_file) assert result.success is True assert hosts_file.entries[0].ip_address == "192.168.1.1" assert hosts_file.entries[0].hostnames == ["test.local"] assert hosts_file.entries[0].comment == "old comment" assert hosts_file.entries[0].is_active is True assert "Undid update" in result.message def test_update_invalid_index(self): """Test update with invalid index.""" hosts_file = HostsFile() command = UpdateEntryCommand(0, "192.168.1.2", ["updated.local"], None, True) result = command.execute(hosts_file) assert result.success is False assert "Invalid entry index" in result.message def test_update_get_description(self): """Test getting command description.""" command = UpdateEntryCommand(0, "192.168.1.2", ["updated.local"], None, True) description = command.get_description() assert "Update entry at index 0" in description class TestHostsManagerIntegration: """Test integration of commands with HostsManager.""" def test_manager_undo_redo_properties(self): """Test undo/redo availability properties.""" manager = HostsManager() # Initially no undo/redo available assert not manager.can_undo() assert not manager.can_redo() assert manager.get_undo_description() is None assert manager.get_redo_description() is None def test_manager_undo_without_edit_mode(self): """Test undo when not in edit mode.""" manager = HostsManager() hosts_file = HostsFile() result = manager.undo_last_operation(hosts_file) assert result.success is False assert "Not in edit mode" in result.message def test_manager_redo_without_edit_mode(self): """Test redo when not in edit mode.""" manager = HostsManager() hosts_file = HostsFile() result = manager.redo_last_operation(hosts_file) assert result.success is False assert "Not in edit mode" in result.message @patch('src.hosts.core.manager.HostsManager.enter_edit_mode') def test_manager_execute_toggle_command(self, mock_enter_edit): """Test executing a toggle command through the manager.""" mock_enter_edit.return_value = (True, "Edit mode enabled") manager = HostsManager() manager.edit_mode = True # Simulate edit mode hosts_file = HostsFile() entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"], is_active=True) hosts_file.entries.append(entry) result = manager.execute_toggle_command(hosts_file, 0) assert result.success is True assert not hosts_file.entries[0].is_active @patch('src.hosts.core.manager.HostsManager.enter_edit_mode') def test_manager_execute_move_command(self, mock_enter_edit): """Test executing a move command through the manager.""" mock_enter_edit.return_value = (True, "Edit mode enabled") manager = HostsManager() manager.edit_mode = True # Simulate edit mode hosts_file = HostsFile() entry1 = HostEntry(ip_address="192.168.1.1", hostnames=["test1.local"]) entry2 = HostEntry(ip_address="192.168.1.2", hostnames=["test2.local"]) hosts_file.entries.extend([entry1, entry2]) result = manager.execute_move_command(hosts_file, 1, "up") assert result.success is True assert hosts_file.entries[0] == entry2 @patch('src.hosts.core.manager.HostsManager.enter_edit_mode') def test_manager_command_not_in_edit_mode(self, mock_enter_edit): """Test executing commands when not in edit mode.""" manager = HostsManager() hosts_file = HostsFile() entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"]) result = manager.execute_add_command(hosts_file, entry) assert result.success is False assert "Not in edit mode" in result.message if __name__ == "__main__": pytest.main([__file__])