Refactor test cases for improved readability and consistency

- Updated test_dns.py to enhance mock function definitions and improve spacing for better readability.
- Modified test_filters.py to streamline assertions and ensure consistent formatting across test cases.
- Cleaned up test_import_export.py by organizing imports and ensuring consistent formatting in CSV and JSON tests.
- Improved test_main.py by refining mock setups and ensuring consistent error handling in assertions.
This commit is contained in:
Philip Henning 2026-09-03 12:47:11 +02:00
parent 0710d10fac
commit 7872991e0b
42 changed files with 2376 additions and 2735 deletions

View file

@ -29,7 +29,7 @@ class TestAddEntryModalDNSSupport:
# 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 hasattr(self.modal, "compose")
assert callable(self.modal.compose)
def test_validate_input_ip_entry_valid(self):
@ -39,7 +39,7 @@ class TestAddEntryModalDNSSupport:
ip_address="192.168.1.1",
dns_name="",
hostnames_str="example.com",
is_dns_entry=False
is_dns_entry=False,
)
assert result is True
@ -47,12 +47,9 @@ class TestAddEntryModalDNSSupport:
"""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
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")
@ -63,7 +60,7 @@ class TestAddEntryModalDNSSupport:
ip_address="",
dns_name="example.com",
hostnames_str="www.example.com",
is_dns_entry=True
is_dns_entry=True,
)
assert result is True
@ -71,12 +68,9 @@ class TestAddEntryModalDNSSupport:
"""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
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")
@ -85,55 +79,58 @@ class TestAddEntryModalDNSSupport:
"""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
"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
is_dns_entry=True,
)
assert result is False
self.modal._show_error.assert_called_with("dns-error", "Invalid DNS name format")
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
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")
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
is_dns_entry=False,
)
assert result is False
self.modal._show_error.assert_called_with("hostnames-error", "Invalid hostname format: invalid hostname")
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."""
@ -141,7 +138,7 @@ class TestAddEntryModalDNSSupport:
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
@ -150,12 +147,12 @@ class TestAddEntryModalDNSSupport:
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("")
@ -166,10 +163,10 @@ class TestAddEntryModalDNSSupport:
# 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")
@ -178,7 +175,7 @@ class TestAddEntryModalDNSSupport:
"""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")
@ -199,7 +196,7 @@ class TestAddEntryModalRadioButtonLogic:
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
@ -208,25 +205,25 @@ class TestAddEntryModalRadioButtonLogic:
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")
@ -238,7 +235,7 @@ class TestAddEntryModalRadioButtonLogic:
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
@ -247,25 +244,25 @@ class TestAddEntryModalRadioButtonLogic:
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")
@ -285,26 +282,26 @@ class TestAddEntryModalSaveLogic:
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
@ -319,17 +316,17 @@ class TestAddEntryModalSaveLogic:
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]
@ -345,28 +342,28 @@ class TestAddEntryModalSaveLogic:
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
@ -381,23 +378,23 @@ class TestAddEntryModalSaveLogic:
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 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
@ -409,21 +406,21 @@ class TestAddEntryModalSaveLogic:
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()
@ -434,30 +431,33 @@ class TestAddEntryModalSaveLogic:
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")
self.modal._show_error.assert_called_once_with(
"hostnames-error", "Invalid IP address"
)

View file

@ -179,13 +179,13 @@ class TestUndoRedoHistory:
"""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()
@ -194,7 +194,7 @@ class TestUndoRedoHistory:
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
@ -205,7 +205,9 @@ class TestToggleEntryCommand:
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)
entry = HostEntry(
ip_address="192.168.1.1", hostnames=["test.local"], is_active=True
)
hosts_file.entries.append(entry)
command = ToggleEntryCommand(0)
@ -218,7 +220,9 @@ class TestToggleEntryCommand:
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)
entry = HostEntry(
ip_address="192.168.1.1", hostnames=["test.local"], is_active=False
)
hosts_file.entries.append(entry)
command = ToggleEntryCommand(0)
@ -231,7 +235,9 @@ class TestToggleEntryCommand:
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)
entry = HostEntry(
ip_address="192.168.1.1", hostnames=["test.local"], is_active=True
)
hosts_file.entries.append(entry)
command = ToggleEntryCommand(0)
@ -454,10 +460,17 @@ class TestUpdateEntryCommand:
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)
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 = UpdateEntryCommand(
0, "192.168.1.2", ["updated.local"], "new comment", False
)
result = command.execute(hosts_file)
assert result.success is True
@ -470,10 +483,17 @@ class TestUpdateEntryCommand:
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)
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 = UpdateEntryCommand(
0, "192.168.1.2", ["updated.local"], "new comment", False
)
command.execute(hosts_file)
result = command.undo(hosts_file)
@ -507,7 +527,7 @@ class TestHostsManagerIntegration:
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()
@ -519,7 +539,7 @@ class TestHostsManagerIntegration:
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
@ -528,46 +548,48 @@ class TestHostsManagerIntegration:
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')
@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)
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')
@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')
@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()
@ -575,7 +597,7 @@ class TestHostsManagerIntegration:
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

View file

@ -115,9 +115,10 @@ class TestResolveHostname:
@pytest.mark.asyncio
async def test_timeout_resolution(self):
"""Test hostname resolution timeout."""
async def mock_wait_for(*args, **kwargs):
raise asyncio.TimeoutError()
with patch("asyncio.wait_for", side_effect=mock_wait_for) as mock_wait_for:
resolution = await resolve_hostname("slow.example", timeout=1.0)
@ -142,9 +143,10 @@ class TestResolveHostname:
@pytest.mark.asyncio
async def test_empty_result_resolution(self):
"""Test hostname resolution with empty result."""
async def mock_wait_for(*args, **kwargs):
return []
with patch("asyncio.get_event_loop") as mock_loop:
mock_event_loop = AsyncMock()
mock_loop.return_value = mock_event_loop
@ -167,7 +169,9 @@ class TestResolveHostnamesBatch:
"""Test successful batch hostname resolution."""
hostnames = ["example.com", "test.example"]
with patch("src.hosts.core.dns.resolve_hostname", new_callable=AsyncMock) as mock_resolve:
with patch(
"src.hosts.core.dns.resolve_hostname", new_callable=AsyncMock
) as mock_resolve:
# Mock successful resolutions
mock_resolve.side_effect = [
DNSResolution(
@ -214,9 +218,8 @@ class TestResolveHostnamesBatch:
resolved_at=datetime.now(),
error_message="Name not found",
)
with patch("src.hosts.core.dns.resolve_hostname", mock_resolve_hostname):
with patch("src.hosts.core.dns.resolve_hostname", mock_resolve_hostname):
resolutions = await resolve_hostnames_batch(hostnames)
assert len(resolutions) == 2
@ -278,16 +281,20 @@ class TestDNSService:
"""Test async resolution when service is enabled."""
service = DNSService(enabled=True)
with patch("src.hosts.core.dns.resolve_hostname", new_callable=AsyncMock) as mock_resolve:
with patch(
"src.hosts.core.dns.resolve_hostname", new_callable=AsyncMock
) as mock_resolve:
mock_resolution = DNSResolution(
hostname="example.com",
resolved_ip="192.0.2.1",
status=DNSResolutionStatus.RESOLVED,
resolved_at=datetime.now(),
)
# Use proper async setup
async def mock_side_effect(hostname, timeout=5.0):
return mock_resolution
mock_resolve.side_effect = mock_side_effect
resolution = await service.resolve_entry_async("example.com")
@ -312,16 +319,20 @@ class TestDNSService:
"""Test manual entry refresh."""
service = DNSService(enabled=True)
with patch("src.hosts.core.dns.resolve_hostname", new_callable=AsyncMock) as mock_resolve:
with patch(
"src.hosts.core.dns.resolve_hostname", new_callable=AsyncMock
) as mock_resolve:
mock_resolution = DNSResolution(
hostname="example.com",
resolved_ip="192.0.2.1",
status=DNSResolutionStatus.RESOLVED,
resolved_at=datetime.now(),
)
# Use proper async setup
async def mock_side_effect(hostname, timeout=5.0):
return mock_resolution
mock_resolve.side_effect = mock_side_effect
result = await service.refresh_entry("example.com")

View file

@ -10,6 +10,7 @@ from datetime import datetime, timedelta
from src.hosts.core.filters import EntryFilter, FilterOptions
from src.hosts.core.models import HostEntry
class TestFilterOptions:
"""Test FilterOptions dataclass."""
@ -40,7 +41,7 @@ class TestFilterOptions:
active_only=True,
dns_only=True,
search_term="test",
preset_name="Active DNS Only"
preset_name="Active DNS Only",
)
assert options.active_only is True
assert options.dns_only is True
@ -50,60 +51,58 @@ class TestFilterOptions:
def test_to_dict(self):
"""Test converting FilterOptions to dictionary."""
options = FilterOptions(
active_only=True,
search_term="test",
preset_name="Test Preset"
active_only=True, search_term="test", preset_name="Test Preset"
)
result = options.to_dict()
expected = {
'show_active': True,
'show_inactive': True,
'active_only': True,
'inactive_only': False,
'show_dns_entries': True,
'show_ip_entries': True,
'dns_only': False,
'ip_only': False,
'show_resolved': True,
'show_unresolved': True,
'show_resolving': True,
'show_failed': True,
'show_mismatched': True,
'mismatch_only': False,
'resolved_only': False,
'search_term': 'test',
'search_in_hostnames': True,
'search_in_comments': True,
'search_in_ips': True,
'case_sensitive': False,
'preset_name': 'Test Preset'
"show_active": True,
"show_inactive": True,
"active_only": True,
"inactive_only": False,
"show_dns_entries": True,
"show_ip_entries": True,
"dns_only": False,
"ip_only": False,
"show_resolved": True,
"show_unresolved": True,
"show_resolving": True,
"show_failed": True,
"show_mismatched": True,
"mismatch_only": False,
"resolved_only": False,
"search_term": "test",
"search_in_hostnames": True,
"search_in_comments": True,
"search_in_ips": True,
"case_sensitive": False,
"preset_name": "Test Preset",
}
assert result == expected
def test_from_dict(self):
"""Test creating FilterOptions from dictionary."""
data = {
'active_only': True,
'dns_only': True,
'search_term': 'test',
'preset_name': 'Test Preset'
"active_only": True,
"dns_only": True,
"search_term": "test",
"preset_name": "Test Preset",
}
options = FilterOptions.from_dict(data)
assert options.active_only is True
assert options.dns_only is True
assert options.search_term == 'test'
assert options.preset_name == 'Test Preset'
assert options.search_term == "test"
assert options.preset_name == "Test Preset"
# Verify missing keys use defaults
assert options.inactive_only is False
def test_from_dict_partial(self):
"""Test creating FilterOptions from partial dictionary."""
data = {'active_only': True}
data = {"active_only": True}
options = FilterOptions.from_dict(data)
assert options.active_only is True
assert options.inactive_only is False # Default value
assert options.search_term is None # Default value
@ -113,15 +112,16 @@ class TestFilterOptions:
# Default options should be empty
options = FilterOptions()
assert options.is_empty() is True
# Options with search term should not be empty
options = FilterOptions(search_term="test")
assert options.is_empty() is False
# Options with any filter enabled should not be empty
options = FilterOptions(active_only=True)
assert options.is_empty() is False
class TestEntryFilter:
"""Test EntryFilter class."""
@ -129,45 +129,45 @@ class TestEntryFilter:
def sample_entries(self):
"""Create sample entries for testing."""
entries = []
# Active IP entry
entry1 = HostEntry("192.168.1.1", ["example.com"], "Test entry", True)
entries.append(entry1)
# Inactive IP entry
entry2 = HostEntry("192.168.1.2", ["inactive.com"], "Inactive entry", False)
entries.append(entry2)
# Active DNS entry - create with temporary IP then convert to DNS entry
entry3 = HostEntry("1.1.1.1", ["dns-only.com"], "DNS only entry", True)
entry3.ip_address = "" # Remove IP after creation
entry3.dns_name = "dns-only.com" # Set DNS name
entries.append(entry3)
# Inactive DNS entry - create with temporary IP then convert to DNS entry
entry4 = HostEntry("1.1.1.1", ["inactive-dns.com"], "Inactive DNS entry", False)
entry4.ip_address = "" # Remove IP after creation
entry4.dns_name = "inactive-dns.com" # Set DNS name
entries.append(entry4)
# Entry with DNS resolution data
entry5 = HostEntry("10.0.0.1", ["resolved.com"], "Resolved entry", True)
entry5.resolved_ip = "10.0.0.1"
entry5.last_resolved = datetime.now()
entry5.dns_resolution_status = "IP_MATCH"
entries.append(entry5)
# Entry with mismatched DNS
entry6 = HostEntry("10.0.0.2", ["mismatch.com"], "Mismatch entry", True)
entry6.resolved_ip = "10.0.0.3" # Different from IP address
entry6.last_resolved = datetime.now()
entry6.dns_resolution_status = "IP_MISMATCH"
entries.append(entry6)
# Entry without DNS resolution
entry7 = HostEntry("10.0.0.4", ["unresolved.com"], "Unresolved entry", True)
entries.append(entry7)
return entries
@pytest.fixture
@ -186,7 +186,7 @@ class TestEntryFilter:
"""Test filtering by active status only."""
options = FilterOptions(active_only=True)
result = entry_filter.filter_by_status(sample_entries, options)
active_entries = [e for e in result if e.is_active]
assert len(active_entries) == len(result)
assert all(entry.is_active for entry in result)
@ -195,7 +195,7 @@ class TestEntryFilter:
"""Test filtering by inactive status only."""
options = FilterOptions(inactive_only=True)
result = entry_filter.filter_by_status(sample_entries, options)
assert all(not entry.is_active for entry in result)
assert len(result) == 2 # entry2 and entry4
@ -203,7 +203,7 @@ class TestEntryFilter:
"""Test filtering by DNS entries only."""
options = FilterOptions(dns_only=True)
result = entry_filter.filter_by_dns_type(sample_entries, options)
assert all(entry.dns_name is not None for entry in result)
assert len(result) == 2 # entry3 and entry4
@ -211,7 +211,7 @@ class TestEntryFilter:
"""Test filtering by IP entries only."""
options = FilterOptions(ip_only=True)
result = entry_filter.filter_by_dns_type(sample_entries, options)
assert all(not entry.has_dns_name() for entry in result)
# Should exclude DNS-only entries (entry3, entry4)
expected_count = len(sample_entries) - 2
@ -221,8 +221,10 @@ class TestEntryFilter:
"""Test filtering by resolved entries only."""
options = FilterOptions(resolved_only=True)
result = entry_filter.filter_by_resolution_status(sample_entries, options)
assert all(entry.dns_resolution_status in ["IP_MATCH", "RESOLVED"] for entry in result)
assert all(
entry.dns_resolution_status in ["IP_MATCH", "RESOLVED"] for entry in result
)
assert len(result) == 1 # Only entry5 has resolved status
def test_filter_by_resolution_status_unresolved(self, entry_filter, sample_entries):
@ -231,18 +233,20 @@ class TestEntryFilter:
show_resolved=False,
show_resolving=False,
show_failed=False,
show_mismatched=False
show_mismatched=False,
)
result = entry_filter.filter_by_resolution_status(sample_entries, options)
assert all(entry.dns_resolution_status in [None, "NOT_RESOLVED"] for entry in result)
assert all(
entry.dns_resolution_status in [None, "NOT_RESOLVED"] for entry in result
)
assert len(result) == 5 # All except entry5 and entry6
def test_filter_by_resolution_status_mismatch(self, entry_filter, sample_entries):
"""Test filtering by DNS mismatch entries only."""
options = FilterOptions(mismatch_only=True)
result = entry_filter.filter_by_resolution_status(sample_entries, options)
# Should only return entry6 (mismatch between IP and resolved_ip)
assert len(result) == 1
assert result[0].hostnames[0] == "mismatch.com"
@ -251,7 +255,7 @@ class TestEntryFilter:
"""Test filtering by search term in hostname."""
options = FilterOptions(search_term="example")
result = entry_filter.filter_by_search(sample_entries, options)
assert len(result) == 1
assert result[0].hostnames[0] == "example.com"
@ -259,14 +263,14 @@ class TestEntryFilter:
"""Test filtering by search term in IP address."""
options = FilterOptions(search_term="192.168")
result = entry_filter.filter_by_search(sample_entries, options)
assert len(result) == 2 # entry1 and entry2
def test_filter_by_search_comment(self, entry_filter, sample_entries):
"""Test filtering by search term in comment."""
options = FilterOptions(search_term="DNS only")
result = entry_filter.filter_by_search(sample_entries, options)
assert len(result) == 1
assert result[0].comment == "DNS only entry"
@ -274,20 +278,16 @@ class TestEntryFilter:
"""Test search is case insensitive."""
options = FilterOptions(search_term="EXAMPLE")
result = entry_filter.filter_by_search(sample_entries, options)
assert len(result) == 1
assert result[0].hostnames[0] == "example.com"
def test_combined_filters(self, entry_filter, sample_entries):
"""Test applying multiple filters together."""
# Filter for active DNS entries containing "dns"
options = FilterOptions(
active_only=True,
dns_only=True,
search_term="dns"
)
options = FilterOptions(active_only=True, dns_only=True, search_term="dns")
result = entry_filter.apply_filters(sample_entries, options)
# Should only return entry3 (active DNS entry with "dns" in hostname)
assert len(result) == 1
assert result[0].hostnames[0] == "dns-only.com"
@ -298,14 +298,14 @@ class TestEntryFilter:
"""Test counting filtered entries."""
options = FilterOptions(active_only=True)
counts = entry_filter.count_filtered_entries(sample_entries, options)
assert counts['total'] == len(sample_entries)
assert counts['filtered'] == 5 # 5 active entries
assert counts["total"] == len(sample_entries)
assert counts["filtered"] == 5 # 5 active entries
def test_get_default_presets(self, entry_filter):
"""Test getting default filter presets."""
presets = entry_filter.get_default_presets()
# Check that default presets exist
assert "All Entries" in presets
assert "Active Only" in presets
@ -315,7 +315,7 @@ class TestEntryFilter:
assert "DNS Mismatches" in presets
assert "Resolved Entries" in presets
assert "Unresolved Entries" in presets
# Check that presets have correct structure
for preset_name, options in presets.items():
assert isinstance(options, FilterOptions)
@ -324,18 +324,16 @@ class TestEntryFilter:
"""Test saving and loading custom presets."""
# Create custom filter options
custom_options = FilterOptions(
active_only=True,
search_term="test",
preset_name="My Custom Filter"
active_only=True, search_term="test", preset_name="My Custom Filter"
)
# Save preset
entry_filter.save_preset("My Custom Filter", custom_options)
# Check it was saved
presets = entry_filter.get_saved_presets()
assert "My Custom Filter" in presets
# Load and verify
loaded_options = presets["My Custom Filter"]
assert loaded_options.active_only is True
@ -347,19 +345,19 @@ class TestEntryFilter:
# Save a preset first
custom_options = FilterOptions(active_only=True)
entry_filter.save_preset("To Delete", custom_options)
# Verify it exists
presets = entry_filter.get_saved_presets()
assert "To Delete" in presets
# Delete it
result = entry_filter.delete_preset("To Delete")
assert result is True
# Verify it's gone
presets = entry_filter.get_saved_presets()
assert "To Delete" not in presets
# Try to delete non-existent preset
result = entry_filter.delete_preset("Non Existent")
assert result is False
@ -370,7 +368,7 @@ class TestEntryFilter:
empty_options = FilterOptions()
result = entry_filter.apply_filters([], empty_options)
assert result == []
# None entries in list - filtering should handle None values gracefully
entries_with_none = [None, HostEntry("192.168.1.1", ["test.com"], "", True)]
# Filter out None values before applying filters
@ -382,9 +380,14 @@ class TestEntryFilter:
def test_search_multiple_hostnames(self, entry_filter):
"""Test search across multiple hostnames in single entry."""
# Create entry with multiple hostnames
entry = HostEntry("192.168.1.1", ["primary.com", "secondary.com", "alias.org"], "Multi-hostname entry", True)
entry = HostEntry(
"192.168.1.1",
["primary.com", "secondary.com", "alias.org"],
"Multi-hostname entry",
True,
)
entries = [entry]
# Search for each hostname
for hostname in ["primary", "secondary", "alias"]:
options = FilterOptions(search_term=hostname)
@ -397,7 +400,7 @@ class TestEntryFilter:
# Modify sample entries to have different resolution times
old_time = datetime.now() - timedelta(days=1)
recent_time = datetime.now() - timedelta(minutes=5)
# Make one entry have old resolution
for entry in sample_entries:
if entry.resolved_ip:
@ -405,7 +408,7 @@ class TestEntryFilter:
entry.last_resolved = recent_time
else:
entry.last_resolved = old_time
# Test that entries are still found regardless of age
# (Age filtering might be added in future versions)
options = FilterOptions(resolved_only=True)
@ -414,14 +417,11 @@ class TestEntryFilter:
def test_preset_name_preservation(self, entry_filter):
"""Test that preset names are preserved in FilterOptions."""
preset_options = FilterOptions(
active_only=True,
preset_name="Active Only"
)
preset_options = FilterOptions(active_only=True, preset_name="Active Only")
# Apply filters and check preset name is preserved
sample_entry = HostEntry("192.168.1.1", ["test.com"], "Test", True)
entry_filter.apply_filters([sample_entry], preset_options)
# The original preset name should be accessible
assert preset_options.preset_name == "Active Only"

View file

@ -12,10 +12,14 @@ import tempfile
from pathlib import Path
from datetime import datetime
from src.hosts.core.import_export import (
ImportExportService, ImportResult, ExportFormat, ImportFormat
ImportExportService,
ImportResult,
ExportFormat,
ImportFormat,
)
from src.hosts.core.models import HostEntry, HostsFile
class TestImportExportService:
"""Test ImportExportService class."""
@ -31,16 +35,16 @@ class TestImportExportService:
HostEntry("127.0.0.1", ["localhost"], "Local host", True),
HostEntry("192.168.1.1", ["router.local"], "Home router", True),
HostEntry("1.1.1.1", ["dns-only.com"], "DNS only entry", False), # Temp IP
HostEntry("10.0.0.1", ["test.example.com"], "Test server", True)
HostEntry("10.0.0.1", ["test.example.com"], "Test server", True),
]
# Convert to DNS entry and set DNS data for some entries
entries[2].ip_address = "" # Remove IP after creation
entries[2].dns_name = "dns-only.com"
entries[3].resolved_ip = "10.0.0.1"
entries[3].last_resolved = datetime(2024, 1, 15, 12, 0, 0)
entries[3].dns_resolution_status = "IP_MATCH"
hosts_file = HostsFile()
hosts_file.entries = entries
return hosts_file
@ -63,7 +67,7 @@ class TestImportExportService:
"""Test getting supported formats."""
export_formats = service.get_supported_export_formats()
import_formats = service.get_supported_import_formats()
assert len(export_formats) == 3
assert len(import_formats) == 3
assert ExportFormat.HOSTS in export_formats
@ -74,15 +78,15 @@ class TestImportExportService:
def test_export_hosts_format(self, service, sample_hosts_file, temp_dir):
"""Test exporting to hosts format."""
export_path = temp_dir / "test_hosts.txt"
result = service.export_hosts_format(sample_hosts_file, export_path)
assert result.success is True
assert result.entries_exported == 4
assert len(result.errors) == 0
assert result.format == ExportFormat.HOSTS
assert export_path.exists()
# Verify content
content = export_path.read_text()
assert "127.0.0.1" in content
@ -92,30 +96,30 @@ class TestImportExportService:
def test_export_json_format(self, service, sample_hosts_file, temp_dir):
"""Test exporting to JSON format."""
export_path = temp_dir / "test_export.json"
result = service.export_json_format(sample_hosts_file, export_path)
assert result.success is True
assert result.entries_exported == 4
assert len(result.errors) == 0
assert result.format == ExportFormat.JSON
assert export_path.exists()
# Verify JSON structure
with open(export_path, 'r') as f:
with open(export_path, "r") as f:
data = json.load(f)
assert "metadata" in data
assert "entries" in data
assert data["metadata"]["total_entries"] == 4
assert len(data["entries"]) == 4
# Check first entry
first_entry = data["entries"][0]
assert first_entry["ip_address"] == "127.0.0.1"
assert first_entry["hostnames"] == ["localhost"]
assert first_entry["is_active"] is True
# Check DNS entry
dns_entry = next((e for e in data["entries"] if e.get("dns_name")), None)
assert dns_entry is not None
@ -124,29 +128,35 @@ class TestImportExportService:
def test_export_csv_format(self, service, sample_hosts_file, temp_dir):
"""Test exporting to CSV format."""
export_path = temp_dir / "test_export.csv"
result = service.export_csv_format(sample_hosts_file, export_path)
assert result.success is True
assert result.entries_exported == 4
assert len(result.errors) == 0
assert result.format == ExportFormat.CSV
assert export_path.exists()
# Verify CSV structure
with open(export_path, 'r') as f:
with open(export_path, "r") as f:
reader = csv.DictReader(f)
rows = list(reader)
assert len(rows) == 4
# Check header
expected_fields = [
'ip_address', 'hostnames', 'comment', 'is_active',
'dns_name', 'resolved_ip', 'last_resolved', 'dns_resolution_status'
"ip_address",
"hostnames",
"comment",
"is_active",
"dns_name",
"resolved_ip",
"last_resolved",
"dns_resolution_status",
]
assert reader.fieldnames == expected_fields
# Check first row
first_row = rows[0]
assert first_row["ip_address"] == "127.0.0.1"
@ -156,9 +166,9 @@ class TestImportExportService:
def test_export_invalid_path(self, service, sample_hosts_file):
"""Test export with invalid path."""
invalid_path = Path("/invalid/path/test.json")
result = service.export_json_format(sample_hosts_file, invalid_path)
assert result.success is False
assert result.entries_exported == 0
assert len(result.errors) > 0
@ -176,17 +186,19 @@ class TestImportExportService:
"""
hosts_path = temp_dir / "test_hosts.txt"
hosts_path.write_text(hosts_content)
result = service.import_hosts_format(hosts_path)
assert result.success is True
assert result.total_processed >= 2
assert result.successfully_imported >= 2
assert len(result.errors) == 0
# Check imported entries
assert len(result.entries) >= 2
localhost_entry = next((e for e in result.entries if "localhost" in e.hostnames), None)
localhost_entry = next(
(e for e in result.entries if "localhost" in e.hostnames), None
)
assert localhost_entry is not None
assert localhost_entry.ip_address == "127.0.0.1"
assert localhost_entry.is_active is True
@ -198,21 +210,21 @@ class TestImportExportService:
"metadata": {
"exported_at": "2024-01-15T12:00:00",
"total_entries": 3,
"version": "1.0"
"version": "1.0",
},
"entries": [
{
"ip_address": "127.0.0.1",
"hostnames": ["localhost"],
"comment": "Local host",
"is_active": True
"is_active": True,
},
{
"ip_address": "",
"hostnames": ["dns-only.com"],
"comment": "DNS only",
"is_active": False,
"dns_name": "dns-only.com"
"dns_name": "dns-only.com",
},
{
"ip_address": "10.0.0.1",
@ -221,29 +233,29 @@ class TestImportExportService:
"is_active": True,
"resolved_ip": "10.0.0.1",
"last_resolved": "2024-01-15T12:00:00",
"dns_resolution_status": "IP_MATCH"
}
]
"dns_resolution_status": "IP_MATCH",
},
],
}
json_path = temp_dir / "test_import.json"
with open(json_path, 'w') as f:
with open(json_path, "w") as f:
json.dump(json_data, f)
result = service.import_json_format(json_path)
assert result.success is True
assert result.total_processed == 3
assert result.successfully_imported == 3
assert len(result.errors) == 0
assert len(result.entries) == 3
# Check DNS entry
dns_entry = next((e for e in result.entries if e.dns_name), None)
assert dns_entry is not None
assert dns_entry.dns_name == "dns-only.com"
assert dns_entry.ip_address == ""
# Check resolved entry
resolved_entry = next((e for e in result.entries if e.resolved_ip), None)
assert resolved_entry is not None
@ -254,35 +266,51 @@ class TestImportExportService:
"""Test importing from CSV format."""
# Create test CSV file
csv_path = temp_dir / "test_import.csv"
with open(csv_path, 'w', newline='') as f:
with open(csv_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow([
'ip_address', 'hostnames', 'comment', 'is_active',
'dns_name', 'resolved_ip', 'last_resolved', 'dns_resolution_status'
])
writer.writerow([
'127.0.0.1', 'localhost', 'Local host', 'true',
'', '', '', ''
])
writer.writerow([
'', 'dns-only.com', 'DNS only', 'false',
'dns-only.com', '', '', ''
])
writer.writerow([
'10.0.0.1', 'test.com example.com', 'Test server', 'true',
'', '10.0.0.1', '2024-01-15T12:00:00', 'IP_MATCH'
])
writer.writerow(
[
"ip_address",
"hostnames",
"comment",
"is_active",
"dns_name",
"resolved_ip",
"last_resolved",
"dns_resolution_status",
]
)
writer.writerow(
["127.0.0.1", "localhost", "Local host", "true", "", "", "", ""]
)
writer.writerow(
["", "dns-only.com", "DNS only", "false", "dns-only.com", "", "", ""]
)
writer.writerow(
[
"10.0.0.1",
"test.com example.com",
"Test server",
"true",
"",
"10.0.0.1",
"2024-01-15T12:00:00",
"IP_MATCH",
]
)
result = service.import_csv_format(csv_path)
assert result.success is True
assert result.total_processed == 3
assert result.successfully_imported == 3
assert len(result.errors) == 0
assert len(result.entries) == 3
# Check multiple hostnames entry
multi_hostname_entry = next((e for e in result.entries if "test.com" in e.hostnames), None)
multi_hostname_entry = next(
(e for e in result.entries if "test.com" in e.hostnames), None
)
assert multi_hostname_entry is not None
assert "example.com" in multi_hostname_entry.hostnames
assert len(multi_hostname_entry.hostnames) == 2
@ -292,11 +320,11 @@ class TestImportExportService:
# Create invalid JSON file
invalid_json = {"invalid": "format", "no_entries": True}
json_path = temp_dir / "invalid.json"
with open(json_path, 'w') as f:
with open(json_path, "w") as f:
json.dump(invalid_json, f)
result = service.import_json_format(json_path)
assert result.success is False
assert result.total_processed == 0
assert result.successfully_imported == 0
@ -307,9 +335,9 @@ class TestImportExportService:
"""Test importing malformed JSON."""
json_path = temp_dir / "malformed.json"
json_path.write_text("{invalid json content")
result = service.import_json_format(json_path)
assert result.success is False
assert result.total_processed == 0
assert result.successfully_imported == 0
@ -319,13 +347,13 @@ class TestImportExportService:
def test_import_csv_missing_required_columns(self, service, temp_dir):
"""Test importing CSV with missing required columns."""
csv_path = temp_dir / "missing_columns.csv"
with open(csv_path, 'w', newline='') as f:
with open(csv_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(['ip_address', 'comment']) # Missing 'hostnames'
writer.writerow(['127.0.0.1', 'test'])
writer.writerow(["ip_address", "comment"]) # Missing 'hostnames'
writer.writerow(["127.0.0.1", "test"])
result = service.import_csv_format(csv_path)
assert result.success is False
assert result.total_processed == 0
assert result.successfully_imported == 0
@ -341,17 +369,17 @@ class TestImportExportService:
"hostnames": ["localhost"],
"comment": "Test",
"is_active": True,
"last_resolved": "invalid-date-format"
"last_resolved": "invalid-date-format",
}
]
}
json_path = temp_dir / "warnings.json"
with open(json_path, 'w') as f:
with open(json_path, "w") as f:
json.dump(json_data, f)
result = service.import_json_format(json_path)
assert result.success is True
assert result.total_processed == 1
assert result.successfully_imported == 1
@ -361,9 +389,9 @@ class TestImportExportService:
def test_import_nonexistent_file(self, service):
"""Test importing non-existent file."""
nonexistent_path = Path("/nonexistent/file.json")
result = service.import_json_format(nonexistent_path)
assert result.success is False
assert result.total_processed == 0
assert result.successfully_imported == 0
@ -377,11 +405,11 @@ class TestImportExportService:
csv_file = temp_dir / "test.csv"
hosts_file = temp_dir / "hosts"
txt_file = temp_dir / "test.txt"
# Create empty files
for f in [json_file, csv_file, hosts_file, txt_file]:
f.touch()
assert service.detect_file_format(json_file) == ImportFormat.JSON
assert service.detect_file_format(csv_file) == ImportFormat.CSV
assert service.detect_file_format(hosts_file) == ImportFormat.HOSTS
@ -393,15 +421,15 @@ class TestImportExportService:
json_file = temp_dir / "no_extension"
json_file.write_text('{"entries": []}')
assert service.detect_file_format(json_file) == ImportFormat.JSON
# CSV content
csv_file = temp_dir / "csv_no_ext"
csv_file.write_text('ip_address,hostnames,comment')
csv_file.write_text("ip_address,hostnames,comment")
assert service.detect_file_format(csv_file) == ImportFormat.CSV
# Hosts content
hosts_file = temp_dir / "hosts_no_ext"
hosts_file.write_text('127.0.0.1 localhost')
hosts_file.write_text("127.0.0.1 localhost")
assert service.detect_file_format(hosts_file) == ImportFormat.HOSTS
def test_detect_file_format_nonexistent(self, service):
@ -415,13 +443,13 @@ class TestImportExportService:
valid_path = temp_dir / "export.json"
warnings = service.validate_export_path(valid_path, ExportFormat.JSON)
assert len(warnings) == 0
# Existing file
existing_file = temp_dir / "existing.json"
existing_file.touch()
warnings = service.validate_export_path(existing_file, ExportFormat.JSON)
assert any("already exists" in w for w in warnings)
# Wrong extension
wrong_ext = temp_dir / "file.txt"
warnings = service.validate_export_path(wrong_ext, ExportFormat.JSON)
@ -438,22 +466,22 @@ class TestImportExportService:
def test_export_import_roundtrip_json(self, service, sample_hosts_file, temp_dir):
"""Test export-import roundtrip for JSON format."""
export_path = temp_dir / "roundtrip.json"
# Export
export_result = service.export_json_format(sample_hosts_file, export_path)
assert export_result.success is True
# Import
import_result = service.import_json_format(export_path)
assert import_result.success is True
assert import_result.successfully_imported == len(sample_hosts_file.entries)
# Verify data integrity
original_entries = sample_hosts_file.entries
imported_entries = import_result.entries
assert len(imported_entries) == len(original_entries)
# Check specific entries
for orig, imported in zip(original_entries, imported_entries):
assert orig.ip_address == imported.ip_address
@ -465,11 +493,11 @@ class TestImportExportService:
def test_export_import_roundtrip_csv(self, service, sample_hosts_file, temp_dir):
"""Test export-import roundtrip for CSV format."""
export_path = temp_dir / "roundtrip.csv"
# Export
export_result = service.export_csv_format(sample_hosts_file, export_path)
assert export_result.success is True
# Import
import_result = service.import_csv_format(export_path)
assert import_result.success is True
@ -484,11 +512,11 @@ class TestImportExportService:
errors=["Error 1", "Error 2"],
warnings=[],
total_processed=5,
successfully_imported=0
successfully_imported=0,
)
assert result_with_errors.has_errors is True
assert result_with_errors.has_warnings is False
# Result with warnings
result_with_warnings = ImportResult(
success=True,
@ -496,7 +524,7 @@ class TestImportExportService:
errors=[],
warnings=["Warning 1"],
total_processed=5,
successfully_imported=5
successfully_imported=5,
)
assert result_with_warnings.has_errors is False
assert result_with_warnings.has_warnings is True
@ -505,15 +533,15 @@ class TestImportExportService:
"""Test exporting empty hosts file."""
empty_hosts_file = HostsFile()
export_path = temp_dir / "empty.json"
result = service.export_json_format(empty_hosts_file, export_path)
assert result.success is True
assert result.entries_exported == 0
assert export_path.exists()
# Verify empty file structure
with open(export_path, 'r') as f:
with open(export_path, "r") as f:
data = json.load(f)
assert data["metadata"]["total_entries"] == 0
assert len(data["entries"]) == 0
@ -521,24 +549,24 @@ class TestImportExportService:
def test_large_hostnames_list_csv(self, service, temp_dir):
"""Test CSV export/import with large hostnames list."""
entry = HostEntry(
"192.168.1.1",
"192.168.1.1",
["host1.com", "host2.com", "host3.com", "host4.com", "host5.com"],
"Multiple hostnames",
True
True,
)
hosts_file = HostsFile()
hosts_file.entries = [entry]
export_path = temp_dir / "multi_hostnames.csv"
# Export
export_result = service.export_csv_format(hosts_file, export_path)
assert export_result.success is True
# Import
import_result = service.import_csv_format(export_path)
assert import_result.success is True
imported_entry = import_result.entries[0]
assert len(imported_entry.hostnames) == 5
assert "host1.com" in imported_entry.hostnames

View file

@ -604,7 +604,7 @@ class TestHostsManagerApp:
mock_radio_set.id = "edit-entry-type-radio"
mock_pressed_radio = Mock()
mock_pressed_radio.id = "edit-ip-entry-radio"
event = Mock()
event.radio_set = mock_radio_set
event.pressed = mock_pressed_radio
@ -631,7 +631,7 @@ class TestHostsManagerApp:
mock_radio_set.id = "edit-entry-type-radio"
mock_pressed_radio = Mock()
mock_pressed_radio.id = "edit-dns-entry-radio"
event = Mock()
event.radio_set = mock_radio_set
event.pressed = mock_pressed_radio
@ -812,12 +812,12 @@ class TestHostsManagerApp:
mock_radio_set = Mock()
mock_ip_radio = Mock()
mock_dns_radio = Mock()
# Use a simple object to track value assignment
class MockDNSInput:
def __init__(self):
self.value = ""
mock_dns_input = MockDNSInput()
def mock_query_one(selector, widget_type=None):
@ -833,14 +833,16 @@ class TestHostsManagerApp:
app.query_one = mock_query_one
app.edit_handler.handle_entry_type_change = Mock()
# Mock the set_timer method to avoid event loop issues in tests
with patch.object(app, 'set_timer') as mock_set_timer:
with patch.object(app, "set_timer") as mock_set_timer:
app.edit_handler.populate_edit_form_with_type_detection()
# Verify timer was set with the correct callback
mock_set_timer.assert_called_once_with(0.1, app.edit_handler._delayed_radio_setup)
mock_set_timer.assert_called_once_with(
0.1, app.edit_handler._delayed_radio_setup
)
# Manually call the delayed setup to test the actual logic
app.edit_handler._delayed_radio_setup()
@ -946,21 +948,24 @@ class TestHostsManagerApp:
app.manager.save_hosts_file = Mock(return_value=(True, "Success"))
app.table_handler.populate_entries_table = Mock()
app.details_handler.update_entry_details = Mock()
# Create a mock that properly handles and closes coroutines
def consume_coro(coro, **kwargs):
# If it's a coroutine, close it to prevent warnings
if hasattr(coro, 'close'):
if hasattr(coro, "close"):
coro.close()
return None
app.run_worker = Mock(side_effect=consume_coro)
# Test action_refresh_dns in edit mode - should proceed
app.action_refresh_dns()
# Should not show error message about read-only mode
error_calls = [call for call in app.update_status.call_args_list
if "read-only mode" in str(call)]
error_calls = [
call
for call in app.update_status.call_args_list
if "read-only mode" in str(call)
]
assert len(error_calls) == 0
# Should start DNS resolution
app.run_worker.assert_called()
@ -972,8 +977,11 @@ class TestHostsManagerApp:
# Test action_update_single_dns in edit mode - should proceed
app.action_update_single_dns()
# Should not show error message about read-only mode
error_calls = [call for call in app.update_status.call_args_list
if "read-only mode" in str(call)]
error_calls = [
call
for call in app.update_status.call_args_list
if "read-only mode" in str(call)
]
assert len(error_calls) == 0
# Should start DNS resolution
app.run_worker.assert_called()