463 lines
17 KiB
Python
463 lines
17 KiB
Python
"""
|
|
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")
|