hosts/tests/test_add_entry_modal.py
phg 1c8396f020 Add comprehensive tests for filtering and import/export functionality
- Created `test_filters.py` to test the EntryFilter and FilterOptions classes, covering default values, custom values, filtering by status, DNS type, resolution status, and search functionality.
- Implemented tests for combined filters and edge cases in filtering.
- Added `test_import_export.py` to test the ImportExportService class, including exporting to hosts, JSON, and CSV formats, as well as importing from these formats.
- Included tests for handling invalid formats, missing required columns, and warnings during import.
- Updated `uv.lock` to include `pytest-asyncio` as a dependency for asynchronous testing.
2025-08-18 10:32:52 +02:00

464 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, MagicMock
from textual.widgets import Input, Checkbox, RadioSet, RadioButton, Static
from textual.app import App
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")