Fix #12: roll back mutations when saving fails
This commit is contained in:
parent
2c5a8d969c
commit
d04ab6feaf
9 changed files with 677 additions and 184 deletions
|
|
@ -5,19 +5,31 @@ This module contains unit tests for the HostsManagerApp class,
|
|||
validating application behavior, navigation, and user interactions.
|
||||
"""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
from hosts.tui.app import HostsManagerApp
|
||||
from hosts.core.models import HostEntry, HostsFile
|
||||
from hosts.core.parser import HostsParser
|
||||
from hosts.core.config import Config
|
||||
from hosts.core.manager import HostsManager
|
||||
from hosts.core.dns import DNSResolution, DNSResolutionStatus
|
||||
|
||||
|
||||
class TestHostsManagerApp:
|
||||
"""Test cases for the HostsManagerApp class."""
|
||||
|
||||
@staticmethod
|
||||
def fail_saves(app: HostsManagerApp) -> None:
|
||||
"""Configure a TUI app with an isolated failed-persistence boundary."""
|
||||
app.manager.save_hosts_file = Mock(return_value=(False, "Permission denied"))
|
||||
app.table_handler.populate_entries_table = Mock()
|
||||
app.table_handler.move_cursor_to_entry_index = Mock()
|
||||
app.details_handler.update_entry_details = Mock()
|
||||
app.update_status = Mock()
|
||||
|
||||
def test_app_initialization(self):
|
||||
"""Test application initialization."""
|
||||
with patch("hosts.tui.app.HostsParser"), patch("hosts.tui.app.Config"):
|
||||
|
|
@ -537,8 +549,7 @@ class TestHostsManagerApp:
|
|||
app.update_status = Mock()
|
||||
|
||||
serialized_files = []
|
||||
app.manager = Mock(spec=HostsManager)
|
||||
app.manager.enter_edit_mode.return_value = (True, "Edit mode enabled")
|
||||
app.manager.enter_edit_mode = Mock(return_value=(True, "Edit mode enabled"))
|
||||
|
||||
def toggle_entry(hosts_file, index):
|
||||
hosts_file.toggle_entry(index)
|
||||
|
|
@ -548,8 +559,8 @@ class TestHostsManagerApp:
|
|||
serialized_files.append(HostsParser().serialize(hosts_file))
|
||||
return True, "Hosts file saved successfully"
|
||||
|
||||
app.manager.execute_toggle_command.side_effect = toggle_entry
|
||||
app.manager.save_hosts_file.side_effect = save_hosts_file
|
||||
app.manager.execute_toggle_command = Mock(side_effect=toggle_entry)
|
||||
app.manager.save_hosts_file = Mock(side_effect=save_hosts_file)
|
||||
|
||||
app.action_sort_by_ip()
|
||||
assert app.edit_mode is False
|
||||
|
|
@ -1138,20 +1149,16 @@ class TestHostsManagerApp:
|
|||
entry = HostEntry(ip_address="192.168.1.1", hostnames=["test.local"])
|
||||
app.hosts_file.add_entry(entry)
|
||||
app.manager.execute_toggle_command(app.hosts_file, 0)
|
||||
app.manager.save_hosts_file = Mock(
|
||||
return_value=(False, "Permission denied")
|
||||
)
|
||||
app.table_handler.populate_entries_table = Mock()
|
||||
app.details_handler.update_entry_details = Mock()
|
||||
app.update_status = Mock()
|
||||
self.fail_saves(app)
|
||||
|
||||
app.action_undo()
|
||||
|
||||
assert entry.is_active is False
|
||||
assert app.hosts_file.entries[0].is_active is False
|
||||
assert app.manager.can_undo()
|
||||
assert not app.manager.can_redo()
|
||||
app.table_handler.populate_entries_table.assert_not_called()
|
||||
app.details_handler.update_entry_details.assert_not_called()
|
||||
app.table_handler.populate_entries_table.assert_called_once()
|
||||
app.table_handler.move_cursor_to_entry_index.assert_called_once_with(0)
|
||||
app.details_handler.update_entry_details.assert_called_once()
|
||||
app.update_status.assert_called_once_with(
|
||||
"❌ Undo save failed; previous state restored: Permission denied"
|
||||
)
|
||||
|
|
@ -1190,22 +1197,449 @@ class TestHostsManagerApp:
|
|||
app.hosts_file.add_entry(entry)
|
||||
app.manager.execute_toggle_command(app.hosts_file, 0)
|
||||
app.manager.undo_last_operation(app.hosts_file)
|
||||
self.fail_saves(app)
|
||||
|
||||
app.action_redo()
|
||||
|
||||
assert app.hosts_file.entries[0].is_active is True
|
||||
assert not app.manager.can_undo()
|
||||
assert app.manager.can_redo()
|
||||
app.table_handler.populate_entries_table.assert_called_once()
|
||||
app.table_handler.move_cursor_to_entry_index.assert_called_once_with(0)
|
||||
app.details_handler.update_entry_details.assert_called_once()
|
||||
app.update_status.assert_called_once_with(
|
||||
"❌ Redo save failed; previous state restored: Permission denied"
|
||||
)
|
||||
|
||||
def test_toggle_save_failure_restores_model_selection_and_history(self):
|
||||
"""A failed toggle restores all user-visible and undoable state."""
|
||||
with patch("hosts.tui.app.HostsParser"), patch("hosts.tui.app.Config"):
|
||||
app = HostsManagerApp()
|
||||
app.edit_mode = True
|
||||
app.manager.edit_mode = True
|
||||
app.hosts_file = HostsFile(
|
||||
entries=[
|
||||
HostEntry(ip_address="192.0.2.1", hostnames=["one.test"]),
|
||||
HostEntry(ip_address="192.0.2.2", hostnames=["two.test"]),
|
||||
]
|
||||
)
|
||||
app.selected_entry_index = 0
|
||||
app.manager.execute_toggle_command(app.hosts_file, 1)
|
||||
app.manager.undo_last_operation(app.hosts_file)
|
||||
self.fail_saves(app)
|
||||
|
||||
app.action_toggle_entry()
|
||||
|
||||
assert app.hosts_file.entries[0].is_active is True
|
||||
assert app.selected_entry_index == 0
|
||||
assert not app.manager.can_undo()
|
||||
assert app.manager.can_redo()
|
||||
app.update_status.assert_called_once_with(
|
||||
"❌ Toggle save failed; previous state restored: Permission denied"
|
||||
)
|
||||
|
||||
def test_move_save_failure_restores_entry_order_and_selection(self):
|
||||
"""A failed move leaves the selected Host Entry in its original position."""
|
||||
with patch("hosts.tui.app.HostsParser"), patch("hosts.tui.app.Config"):
|
||||
app = HostsManagerApp()
|
||||
app.edit_mode = True
|
||||
app.manager.edit_mode = True
|
||||
app.hosts_file = HostsFile(
|
||||
entries=[
|
||||
HostEntry(ip_address="192.0.2.1", hostnames=["one.test"]),
|
||||
HostEntry(ip_address="192.0.2.2", hostnames=["two.test"]),
|
||||
]
|
||||
)
|
||||
app.selected_entry_index = 1
|
||||
self.fail_saves(app)
|
||||
|
||||
app.action_move_entry_up()
|
||||
|
||||
assert [entry.hostnames[0] for entry in app.hosts_file.entries] == [
|
||||
"one.test",
|
||||
"two.test",
|
||||
]
|
||||
assert app.selected_entry_index == 1
|
||||
assert not app.manager.can_undo()
|
||||
app.update_status.assert_called_once_with(
|
||||
"❌ Move save failed; previous state restored: Permission denied"
|
||||
)
|
||||
|
||||
def test_add_save_failure_removes_entry_and_restores_history(self):
|
||||
"""A failed add restores the prior entries and history."""
|
||||
with patch("hosts.tui.app.HostsParser"), patch("hosts.tui.app.Config"):
|
||||
app = HostsManagerApp()
|
||||
app.edit_mode = True
|
||||
app.manager.edit_mode = True
|
||||
existing = HostEntry(ip_address="192.0.2.1", hostnames=["one.test"])
|
||||
added = HostEntry(ip_address="192.0.2.2", hostnames=["two.test"])
|
||||
app.hosts_file = HostsFile(entries=[existing])
|
||||
app.selected_entry_index = 0
|
||||
self.fail_saves(app)
|
||||
app.push_screen = Mock(
|
||||
side_effect=lambda _screen, callback: callback(added)
|
||||
)
|
||||
|
||||
app.action_add_entry()
|
||||
|
||||
assert [entry.hostnames[0] for entry in app.hosts_file.entries] == [
|
||||
"one.test"
|
||||
]
|
||||
assert app.selected_entry_index == 0
|
||||
assert not app.manager.can_undo()
|
||||
app.update_status.assert_called_once_with(
|
||||
"❌ Add save failed; previous state restored: Permission denied"
|
||||
)
|
||||
|
||||
def test_later_mutation_does_not_persist_failed_add(self):
|
||||
"""A later successful save excludes a previously failed addition."""
|
||||
with patch("hosts.tui.app.HostsParser"), patch("hosts.tui.app.Config"):
|
||||
app = HostsManagerApp()
|
||||
app.edit_mode = True
|
||||
app.manager.edit_mode = True
|
||||
existing = HostEntry(ip_address="192.0.2.1", hostnames=["one.test"])
|
||||
added = HostEntry(ip_address="192.0.2.2", hostnames=["two.test"])
|
||||
app.hosts_file = HostsFile(entries=[existing])
|
||||
app.manager.save_hosts_file = Mock(
|
||||
return_value=(False, "Permission denied")
|
||||
)
|
||||
app.table_handler.populate_entries_table = Mock()
|
||||
app.table_handler.move_cursor_to_entry_index = Mock()
|
||||
app.table_handler.restore_cursor_position = Mock()
|
||||
app.details_handler.update_entry_details = Mock()
|
||||
app.update_status = Mock()
|
||||
app.set_timer = Mock()
|
||||
app.push_screen = Mock(
|
||||
side_effect=lambda _screen, callback: callback(added)
|
||||
)
|
||||
app.action_add_entry()
|
||||
|
||||
persisted = []
|
||||
|
||||
def save_hosts_file(hosts_file):
|
||||
persisted.append(HostsParser().serialize(hosts_file))
|
||||
return True, "Hosts file saved"
|
||||
|
||||
app.manager.save_hosts_file = Mock(side_effect=save_hosts_file)
|
||||
app.action_toggle_entry()
|
||||
|
||||
assert len(persisted) == 1
|
||||
assert "one.test" in persisted[0]
|
||||
assert "two.test" not in persisted[0]
|
||||
|
||||
def test_save_failure_restores_exact_duplicate_selection(self):
|
||||
"""Rollback keeps the second of two identical Host Entries selected."""
|
||||
with patch("hosts.tui.app.HostsParser"), patch("hosts.tui.app.Config"):
|
||||
app = HostsManagerApp()
|
||||
app.edit_mode = True
|
||||
app.manager.edit_mode = True
|
||||
app.hosts_file = HostsFile(
|
||||
entries=[
|
||||
HostEntry(ip_address="192.0.2.1", hostnames=["same.test"]),
|
||||
HostEntry(ip_address="192.0.2.1", hostnames=["same.test"]),
|
||||
]
|
||||
)
|
||||
app.selected_entry_index = 1
|
||||
app.manager.save_hosts_file = Mock(
|
||||
return_value=(False, "Permission denied")
|
||||
)
|
||||
table = Mock(row_count=2)
|
||||
app.query_one = Mock(return_value=table)
|
||||
app.table_handler.populate_entries_table = Mock()
|
||||
app.details_handler.update_entry_details = Mock()
|
||||
app.update_status = Mock()
|
||||
|
||||
app.action_redo()
|
||||
app.action_toggle_entry()
|
||||
|
||||
assert entry.is_active is True
|
||||
assert app.selected_entry_index == 1
|
||||
table.move_cursor.assert_called_once_with(row=1)
|
||||
|
||||
def test_delete_save_failure_restores_entry_and_selection(self):
|
||||
"""A failed delete restores the removed Host Entry and selection."""
|
||||
with patch("hosts.tui.app.HostsParser"), patch("hosts.tui.app.Config"):
|
||||
app = HostsManagerApp()
|
||||
app.edit_mode = True
|
||||
app.manager.edit_mode = True
|
||||
app.hosts_file = HostsFile(
|
||||
entries=[
|
||||
HostEntry(ip_address="192.0.2.1", hostnames=["one.test"]),
|
||||
HostEntry(ip_address="192.0.2.2", hostnames=["two.test"]),
|
||||
]
|
||||
)
|
||||
app.selected_entry_index = 1
|
||||
self.fail_saves(app)
|
||||
app.push_screen = Mock(side_effect=lambda _screen, callback: callback(True))
|
||||
|
||||
app.action_delete_entry()
|
||||
|
||||
assert [entry.hostnames[0] for entry in app.hosts_file.entries] == [
|
||||
"one.test",
|
||||
"two.test",
|
||||
]
|
||||
assert app.selected_entry_index == 1
|
||||
assert not app.manager.can_undo()
|
||||
assert app.manager.can_redo()
|
||||
app.table_handler.populate_entries_table.assert_not_called()
|
||||
app.details_handler.update_entry_details.assert_not_called()
|
||||
app.update_status.assert_called_once_with(
|
||||
"❌ Redo save failed; previous state restored: Permission denied"
|
||||
"❌ Delete save failed; previous state restored: Permission denied"
|
||||
)
|
||||
|
||||
def test_entry_editor_save_failure_restores_all_entry_fields(self):
|
||||
"""A failed editor save restores ordinary fields and DNS metadata."""
|
||||
with patch("hosts.tui.app.HostsParser"), patch("hosts.tui.app.Config"):
|
||||
app = HostsManagerApp()
|
||||
app.edit_mode = True
|
||||
app.manager.edit_mode = True
|
||||
resolved_at = datetime(2026, 9, 4, 10, 30)
|
||||
app.hosts_file = HostsFile(
|
||||
entries=[
|
||||
HostEntry(
|
||||
ip_address="192.0.2.1",
|
||||
hostnames=["one.test", "alias.test"],
|
||||
comment="original",
|
||||
is_active=False,
|
||||
dns_name="source.test",
|
||||
resolved_ip="192.0.2.1",
|
||||
last_resolved=resolved_at,
|
||||
dns_resolution_status="resolved",
|
||||
)
|
||||
]
|
||||
)
|
||||
app.selected_entry_index = 0
|
||||
app.entry_edit_mode = True
|
||||
app.manager.save_hosts_file = Mock(
|
||||
return_value=(False, "Permission denied")
|
||||
)
|
||||
app.table_handler.populate_entries_table = Mock()
|
||||
app.table_handler.move_cursor_to_entry_index = Mock()
|
||||
app.update_status = Mock()
|
||||
|
||||
ip_input = Mock(value="198.51.100.8")
|
||||
hostname_input = Mock(value="changed.test")
|
||||
comment_input = Mock(value="changed")
|
||||
active_checkbox = Mock(value=True)
|
||||
dns_input = Mock(value="")
|
||||
ip_radio = Mock(id="edit-ip-entry-radio", value=True)
|
||||
dns_radio = Mock(id="edit-dns-entry-radio", value=False)
|
||||
radio_set = Mock(pressed_button=ip_radio)
|
||||
widgets = {
|
||||
"#entry-details-display": Mock(),
|
||||
"#entry-edit-form": Mock(),
|
||||
"#ip-input": ip_input,
|
||||
"#hostname-input": hostname_input,
|
||||
"#comment-input": comment_input,
|
||||
"#active-checkbox": active_checkbox,
|
||||
"#dns-name-input": dns_input,
|
||||
"#edit-entry-type-radio": radio_set,
|
||||
"#edit-ip-entry-radio": ip_radio,
|
||||
"#edit-dns-entry-radio": dns_radio,
|
||||
"#edit-ip-section": Mock(),
|
||||
"#edit-dns-section": Mock(),
|
||||
}
|
||||
app.query_one = Mock(side_effect=lambda selector, *_args: widgets[selector])
|
||||
app.set_timer = Mock(side_effect=lambda _delay, callback: callback())
|
||||
|
||||
saved = app.edit_handler.validate_and_save_entry_changes()
|
||||
|
||||
entry = app.hosts_file.entries[0]
|
||||
assert saved is False
|
||||
assert entry.ip_address == "192.0.2.1"
|
||||
assert entry.hostnames == ["one.test", "alias.test"]
|
||||
assert entry.comment == "original"
|
||||
assert entry.is_active is False
|
||||
assert entry.dns_name == "source.test"
|
||||
assert entry.resolved_ip == "192.0.2.1"
|
||||
assert entry.last_resolved == resolved_at
|
||||
assert entry.dns_resolution_status == "resolved"
|
||||
assert ip_input.value == "192.0.2.1"
|
||||
assert hostname_input.value == "one.test, alias.test"
|
||||
assert comment_input.value == "original"
|
||||
assert active_checkbox.value is False
|
||||
assert dns_input.value == "source.test"
|
||||
app.update_status.assert_called_once_with(
|
||||
"❌ Edit save failed; previous state restored: Permission denied"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_batch_dns_save_failure_restores_mapping_and_metadata(self):
|
||||
"""A failed batch DNS refresh restores every changed DNS field."""
|
||||
with patch("hosts.tui.app.HostsParser"), patch("hosts.tui.app.Config"):
|
||||
app = HostsManagerApp()
|
||||
app.edit_mode = True
|
||||
app.manager.edit_mode = True
|
||||
original_time = datetime(2026, 9, 3, 8, 0)
|
||||
resolved_time = datetime(2026, 9, 4, 11, 0)
|
||||
app.hosts_file = HostsFile(
|
||||
entries=[
|
||||
HostEntry(
|
||||
ip_address="192.0.2.1",
|
||||
hostnames=["one.test"],
|
||||
dns_name="source.test",
|
||||
resolved_ip="192.0.2.1",
|
||||
last_resolved=original_time,
|
||||
dns_resolution_status="match",
|
||||
)
|
||||
]
|
||||
)
|
||||
app.selected_entry_index = 0
|
||||
app.dns_service.resolve_entry_async = AsyncMock(
|
||||
return_value=DNSResolution(
|
||||
hostname="source.test",
|
||||
resolved_ip="198.51.100.9",
|
||||
status=DNSResolutionStatus.RESOLVED,
|
||||
resolved_at=resolved_time,
|
||||
)
|
||||
)
|
||||
self.fail_saves(app)
|
||||
workers = []
|
||||
app.run_worker = Mock(
|
||||
side_effect=lambda worker, **_kwargs: workers.append(worker)
|
||||
)
|
||||
|
||||
app.action_refresh_dns()
|
||||
await workers[0]
|
||||
|
||||
entry = app.hosts_file.entries[0]
|
||||
assert entry.ip_address == "192.0.2.1"
|
||||
assert entry.resolved_ip == "192.0.2.1"
|
||||
assert entry.last_resolved == original_time
|
||||
assert entry.dns_resolution_status == "match"
|
||||
assert app.selected_entry_index == 0
|
||||
app.update_status.assert_any_call(
|
||||
"❌ DNS refresh save failed; previous state restored: Permission denied"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_single_dns_save_failure_restores_mapping_and_metadata(self):
|
||||
"""A failed selected-entry DNS refresh restores every changed field."""
|
||||
with patch("hosts.tui.app.HostsParser"), patch("hosts.tui.app.Config"):
|
||||
app = HostsManagerApp()
|
||||
app.edit_mode = True
|
||||
app.manager.edit_mode = True
|
||||
original_time = datetime(2026, 9, 3, 8, 0)
|
||||
app.hosts_file = HostsFile(
|
||||
entries=[
|
||||
HostEntry(
|
||||
ip_address="192.0.2.1",
|
||||
hostnames=["one.test"],
|
||||
dns_name="source.test",
|
||||
resolved_ip="192.0.2.1",
|
||||
last_resolved=original_time,
|
||||
dns_resolution_status="match",
|
||||
)
|
||||
]
|
||||
)
|
||||
app.dns_service.resolve_entry_async = AsyncMock(
|
||||
return_value=DNSResolution(
|
||||
hostname="source.test",
|
||||
resolved_ip="198.51.100.9",
|
||||
status=DNSResolutionStatus.RESOLVED,
|
||||
resolved_at=datetime(2026, 9, 4, 11, 0),
|
||||
)
|
||||
)
|
||||
self.fail_saves(app)
|
||||
workers = []
|
||||
app.run_worker = Mock(
|
||||
side_effect=lambda worker, **_kwargs: workers.append(worker)
|
||||
)
|
||||
|
||||
app.action_update_single_dns()
|
||||
await workers[0]
|
||||
|
||||
entry = app.hosts_file.entries[0]
|
||||
assert entry.ip_address == "192.0.2.1"
|
||||
assert entry.resolved_ip == "192.0.2.1"
|
||||
assert entry.last_resolved == original_time
|
||||
assert entry.dns_resolution_status == "match"
|
||||
app.update_status.assert_any_call(
|
||||
"❌ DNS refresh save failed; previous state restored: Permission denied"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_single_dns_refresh_updates_selected_duplicate(self):
|
||||
"""DNS refresh retains the selected entry identity across its await."""
|
||||
with patch("hosts.tui.app.HostsParser"), patch("hosts.tui.app.Config"):
|
||||
app = HostsManagerApp()
|
||||
app.edit_mode = True
|
||||
app.manager.edit_mode = True
|
||||
app.hosts_file = HostsFile(
|
||||
entries=[
|
||||
HostEntry(
|
||||
ip_address="192.0.2.1",
|
||||
hostnames=["same.test"],
|
||||
dns_name="source.test",
|
||||
),
|
||||
HostEntry(
|
||||
ip_address="192.0.2.2",
|
||||
hostnames=["same.test"],
|
||||
dns_name="source.test",
|
||||
),
|
||||
]
|
||||
)
|
||||
app.selected_entry_index = 1
|
||||
app.dns_service.resolve_entry_async = AsyncMock(
|
||||
return_value=DNSResolution(
|
||||
hostname="source.test",
|
||||
resolved_ip="198.51.100.9",
|
||||
status=DNSResolutionStatus.RESOLVED,
|
||||
resolved_at=datetime(2026, 9, 4, 11, 0),
|
||||
)
|
||||
)
|
||||
app.manager.save_hosts_file = Mock(return_value=(True, "Saved"))
|
||||
app.table_handler.populate_entries_table = Mock()
|
||||
app.table_handler.restore_cursor_position = Mock()
|
||||
app.details_handler.update_entry_details = Mock()
|
||||
app.update_status = Mock()
|
||||
workers = []
|
||||
app.run_worker = Mock(
|
||||
side_effect=lambda worker, **_kwargs: workers.append(worker)
|
||||
)
|
||||
|
||||
app.action_update_single_dns()
|
||||
await workers[0]
|
||||
|
||||
assert app.hosts_file.entries[0].ip_address == "192.0.2.1"
|
||||
assert app.hosts_file.entries[1].ip_address == "198.51.100.9"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_entry_dns_save_failure_keeps_saved_placeholder(self):
|
||||
"""Failed post-add DNS persistence keeps the already-saved DNS placeholder."""
|
||||
with patch("hosts.tui.app.HostsParser"), patch("hosts.tui.app.Config"):
|
||||
app = HostsManagerApp()
|
||||
app.edit_mode = True
|
||||
app.manager.edit_mode = True
|
||||
app.hosts_file = HostsFile()
|
||||
entry = HostEntry(
|
||||
ip_address="0.0.0.0",
|
||||
hostnames=["one.test"],
|
||||
is_active=False,
|
||||
dns_name="source.test",
|
||||
)
|
||||
app.manager.execute_add_command(app.hosts_file, entry)
|
||||
app.dns_service.resolve_entry_async = AsyncMock(
|
||||
return_value=DNSResolution(
|
||||
hostname="source.test",
|
||||
resolved_ip="198.51.100.9",
|
||||
status=DNSResolutionStatus.RESOLVED,
|
||||
resolved_at=datetime(2026, 9, 4, 11, 0),
|
||||
)
|
||||
)
|
||||
self.fail_saves(app)
|
||||
workers = []
|
||||
app.run_worker = Mock(
|
||||
side_effect=lambda worker, **_kwargs: workers.append(worker)
|
||||
)
|
||||
|
||||
app._resolve_new_dns_entry(entry)
|
||||
await workers[0]
|
||||
|
||||
restored = app.hosts_file.entries[0]
|
||||
assert restored.ip_address == "0.0.0.0"
|
||||
assert restored.resolved_ip is None
|
||||
assert restored.last_resolved is None
|
||||
assert restored.dns_resolution_status is None
|
||||
assert restored.is_active is False
|
||||
assert app.manager.can_undo()
|
||||
app.update_status.assert_any_call(
|
||||
"❌ DNS activation save failed; previous state restored: Permission denied"
|
||||
)
|
||||
|
||||
def test_main_function(self):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue