""" Tests for the AddEntryModal with DNS name support. This module tests the enhanced AddEntryModal functionality including DNS name entries, validation, and mutual exclusion logic. """ import pytest from unittest.mock import Mock from textual.widgets import Input, Checkbox, RadioSet, Static from src.hosts.tui.add_entry_modal import AddEntryModal from src.hosts.core.models import HostEntry class TestAddEntryModalDNSSupport: """Test cases for AddEntryModal DNS name support.""" def setup_method(self): """Set up test fixtures.""" self.modal = AddEntryModal() def test_modal_initialization(self): """Test that the modal initializes correctly.""" assert isinstance(self.modal, AddEntryModal) def test_compose_method_creates_dns_components(self): """Test that compose method creates DNS-related components.""" # Test that the compose method exists and can be called # We can't test the actual widget creation without mounting the modal # in a Textual app context, so we just verify the method exists assert hasattr(self.modal, 'compose') assert callable(self.modal.compose) def test_validate_input_ip_entry_valid(self): """Test validation for valid IP entry.""" # Test valid IP entry result = self.modal._validate_input( ip_address="192.168.1.1", dns_name="", hostnames_str="example.com", is_dns_entry=False ) assert result is True def test_validate_input_ip_entry_missing_ip(self): """Test validation for IP entry with missing IP address.""" # Mock the error display method self.modal._show_error = Mock() result = self.modal._validate_input( ip_address="", dns_name="", hostnames_str="example.com", is_dns_entry=False ) assert result is False self.modal._show_error.assert_called_with("ip-error", "IP address is required") def test_validate_input_dns_entry_valid(self): """Test validation for valid DNS entry.""" result = self.modal._validate_input( ip_address="", dns_name="example.com", hostnames_str="www.example.com", is_dns_entry=True ) assert result is True def test_validate_input_dns_entry_missing_dns_name(self): """Test validation for DNS entry with missing DNS name.""" # Mock the error display method self.modal._show_error = Mock() result = self.modal._validate_input( ip_address="", dns_name="", hostnames_str="example.com", is_dns_entry=True ) assert result is False self.modal._show_error.assert_called_with("dns-error", "DNS name is required") def test_validate_input_dns_entry_invalid_format(self): """Test validation for DNS entry with invalid DNS name format.""" # Mock the error display method self.modal._show_error = Mock() # Test various invalid DNS name formats invalid_dns_names = [ "example .com", # Contains space ".example.com", # Starts with dot "example.com.", # Ends with dot "example..com", # Double dots "ex@mple.com", # Invalid characters ] for invalid_dns in invalid_dns_names: result = self.modal._validate_input( ip_address="", dns_name=invalid_dns, hostnames_str="example.com", is_dns_entry=True ) assert result is False self.modal._show_error.assert_called_with("dns-error", "Invalid DNS name format") def test_validate_input_missing_hostnames(self): """Test validation for entries with missing hostnames.""" # Mock the error display method self.modal._show_error = Mock() # Test IP entry without hostnames result = self.modal._validate_input( ip_address="192.168.1.1", dns_name="", hostnames_str="", is_dns_entry=False ) assert result is False self.modal._show_error.assert_called_with("hostnames-error", "At least one hostname is required") def test_validate_input_invalid_hostnames(self): """Test validation for entries with invalid hostnames.""" # Mock the error display method self.modal._show_error = Mock() # Test with invalid hostname containing spaces result = self.modal._validate_input( ip_address="192.168.1.1", dns_name="", hostnames_str="invalid hostname", is_dns_entry=False ) assert result is False self.modal._show_error.assert_called_with("hostnames-error", "Invalid hostname format: invalid hostname") def test_clear_errors_includes_dns_error(self): """Test that clear_errors method includes DNS error clearing.""" # Mock the query_one method to return mock widgets mock_ip_error = Mock(spec=Static) mock_dns_error = Mock(spec=Static) mock_hostnames_error = Mock(spec=Static) def mock_query_one(selector, widget_type): if selector == "#ip-error": return mock_ip_error elif selector == "#dns-error": return mock_dns_error elif selector == "#hostnames-error": return mock_hostnames_error return Mock() self.modal.query_one = Mock(side_effect=mock_query_one) # Call clear_errors self.modal._clear_errors() # Verify all error widgets were cleared mock_ip_error.update.assert_called_with("") mock_dns_error.update.assert_called_with("") mock_hostnames_error.update.assert_called_with("") def test_show_error_displays_message(self): """Test that show_error method displays error messages correctly.""" # Mock the query_one method to return a mock widget mock_error_widget = Mock(spec=Static) self.modal.query_one = Mock(return_value=mock_error_widget) # Test showing an error self.modal._show_error("dns-error", "Test error message") # Verify the error widget was updated self.modal.query_one.assert_called_with("#dns-error", Static) mock_error_widget.update.assert_called_with("Test error message") def test_show_error_handles_missing_widget(self): """Test that show_error handles missing widgets gracefully.""" # Mock query_one to raise an exception self.modal.query_one = Mock(side_effect=Exception("Widget not found")) # This should not raise an exception try: self.modal._show_error("dns-error", "Test error message") except Exception: pytest.fail("_show_error should handle missing widgets gracefully") class TestAddEntryModalRadioButtonLogic: """Test cases for radio button logic in AddEntryModal.""" def setup_method(self): """Set up test fixtures.""" self.modal = AddEntryModal() def test_radio_button_change_to_ip_entry(self): """Test radio button change to IP entry mode.""" # Mock the query_one method for sections and inputs mock_ip_section = Mock() mock_dns_section = Mock() mock_ip_input = Mock(spec=Input) def mock_query_one(selector, widget_type=None): if selector == "#ip-section": return mock_ip_section elif selector == "#dns-section": return mock_dns_section elif selector == "#ip-address-input": return mock_ip_input return Mock() self.modal.query_one = Mock(side_effect=mock_query_one) # Create mock event mock_radio = Mock() mock_radio.id = "ip-entry-radio" mock_radio_set = Mock() mock_radio_set.id = "entry-type-radio" class MockEvent: def __init__(self): self.radio_set = mock_radio_set self.pressed = mock_radio event = MockEvent() # Call the event handler self.modal.on_radio_set_changed(event) # Verify IP section is shown and DNS section is hidden mock_ip_section.remove_class.assert_called_with("hidden") mock_dns_section.add_class.assert_called_with("hidden") mock_ip_input.focus.assert_called_once() def test_radio_button_change_to_dns_entry(self): """Test radio button change to DNS entry mode.""" # Mock the query_one method for sections and inputs mock_ip_section = Mock() mock_dns_section = Mock() mock_dns_input = Mock(spec=Input) def mock_query_one(selector, widget_type=None): if selector == "#ip-section": return mock_ip_section elif selector == "#dns-section": return mock_dns_section elif selector == "#dns-name-input": return mock_dns_input return Mock() self.modal.query_one = Mock(side_effect=mock_query_one) # Create mock event mock_radio = Mock() mock_radio.id = "dns-entry-radio" mock_radio_set = Mock() mock_radio_set.id = "entry-type-radio" class MockEvent: def __init__(self): self.radio_set = mock_radio_set self.pressed = mock_radio event = MockEvent() # Call the event handler self.modal.on_radio_set_changed(event) # Verify DNS section is shown and IP section is hidden mock_ip_section.add_class.assert_called_with("hidden") mock_dns_section.remove_class.assert_called_with("hidden") mock_dns_input.focus.assert_called_once() class TestAddEntryModalSaveLogic: """Test cases for save logic in AddEntryModal.""" def setup_method(self): """Set up test fixtures.""" self.modal = AddEntryModal() def test_action_save_ip_entry_creation(self): """Test saving a valid IP entry.""" # Mock validation to return True (not None) self.modal._validate_input = Mock(return_value=True) self.modal._clear_errors = Mock() self.modal.dismiss = Mock() # Mock form widgets mock_radio_set = Mock(spec=RadioSet) mock_radio_set.pressed_button = None # IP entry mode mock_ip_input = Mock(spec=Input) mock_ip_input.value = "192.168.1.1" mock_dns_input = Mock(spec=Input) mock_dns_input.value = "" mock_hostnames_input = Mock(spec=Input) mock_hostnames_input.value = "example.com, www.example.com" mock_comment_input = Mock(spec=Input) mock_comment_input.value = "Test comment" mock_active_checkbox = Mock(spec=Checkbox) mock_active_checkbox.value = True def mock_query_one(selector, widget_type): if selector == "#entry-type-radio": return mock_radio_set elif selector == "#ip-address-input": return mock_ip_input elif selector == "#dns-name-input": return mock_dns_input elif selector == "#hostnames-input": return mock_hostnames_input elif selector == "#comment-input": return mock_comment_input elif selector == "#active-checkbox": return mock_active_checkbox return Mock() self.modal.query_one = Mock(side_effect=mock_query_one) # Call action_save self.modal.action_save() # Verify validation was called self.modal._validate_input.assert_called_once_with( "192.168.1.1", "", "example.com, www.example.com", None ) # Verify modal was dismissed with a HostEntry self.modal.dismiss.assert_called_once() created_entry = self.modal.dismiss.call_args[0][0] assert isinstance(created_entry, HostEntry) assert created_entry.ip_address == "192.168.1.1" assert created_entry.hostnames == ["example.com", "www.example.com"] assert created_entry.comment == "Test comment" assert created_entry.is_active is True def test_action_save_dns_entry_creation(self): """Test saving a valid DNS entry.""" # Mock validation to return True self.modal._validate_input = Mock(return_value=True) self.modal._clear_errors = Mock() self.modal.dismiss = Mock() # Mock form widgets mock_radio_button = Mock() mock_radio_button.id = "dns-entry-radio" mock_radio_set = Mock(spec=RadioSet) mock_radio_set.pressed_button = mock_radio_button mock_ip_input = Mock(spec=Input) mock_ip_input.value = "" mock_dns_input = Mock(spec=Input) mock_dns_input.value = "example.com" mock_hostnames_input = Mock(spec=Input) mock_hostnames_input.value = "www.example.com" mock_comment_input = Mock(spec=Input) mock_comment_input.value = "" mock_active_checkbox = Mock(spec=Checkbox) mock_active_checkbox.value = True def mock_query_one(selector, widget_type): if selector == "#entry-type-radio": return mock_radio_set elif selector == "#ip-address-input": return mock_ip_input elif selector == "#dns-name-input": return mock_dns_input elif selector == "#hostnames-input": return mock_hostnames_input elif selector == "#comment-input": return mock_comment_input elif selector == "#active-checkbox": return mock_active_checkbox return Mock() self.modal.query_one = Mock(side_effect=mock_query_one) # Call action_save self.modal.action_save() # Verify validation was called self.modal._validate_input.assert_called_once_with( "", "example.com", "www.example.com", True ) # Verify modal was dismissed with a DNS HostEntry self.modal.dismiss.assert_called_once() created_entry = self.modal.dismiss.call_args[0][0] assert isinstance(created_entry, HostEntry) assert created_entry.ip_address == "0.0.0.0" # Placeholder IP for DNS entries assert hasattr(created_entry, 'dns_name') assert created_entry.dns_name == "example.com" assert created_entry.hostnames == ["www.example.com"] assert created_entry.comment is None assert created_entry.is_active is False # Inactive until DNS resolution def test_action_save_validation_failure(self): """Test save action when validation fails.""" # Mock validation to return False self.modal._validate_input = Mock(return_value=False) self.modal._clear_errors = Mock() self.modal.dismiss = Mock() # Mock form widgets (minimal setup since validation fails) mock_radio_set = Mock(spec=RadioSet) mock_radio_set.pressed_button = None def mock_query_one(selector, widget_type): if selector == "#entry-type-radio": return mock_radio_set return Mock(spec=Input, value="") self.modal.query_one = Mock(side_effect=mock_query_one) # Call action_save self.modal.action_save() # Verify validation was called and modal was not dismissed self.modal._validate_input.assert_called_once() self.modal.dismiss.assert_not_called() def test_action_save_exception_handling(self): """Test save action exception handling.""" # Mock validation to return True self.modal._validate_input = Mock(return_value=True) self.modal._clear_errors = Mock() self.modal._show_error = Mock() # Mock form widgets mock_radio_set = Mock(spec=RadioSet) mock_radio_set.pressed_button = None mock_input = Mock(spec=Input) mock_input.value = "invalid" def mock_query_one(selector, widget_type): if selector == "#entry-type-radio": return mock_radio_set return mock_input self.modal.query_one = Mock(side_effect=mock_query_one) # Mock HostEntry to raise ValueError with pytest.MonkeyPatch.context() as m: def mock_host_entry(*args, **kwargs): raise ValueError("Invalid IP address") m.setattr("src.hosts.tui.add_entry_modal.HostEntry", mock_host_entry) # Call action_save self.modal.action_save() # Verify error was shown self.modal._show_error.assert_called_once_with("hostnames-error", "Invalid IP address")