Feat #5: choose additive or replacement import
This commit is contained in:
parent
48c9760e3c
commit
9040cf72a8
4 changed files with 251 additions and 22 deletions
|
|
@ -15,7 +15,12 @@ from hosts.core.import_export import (
|
|||
)
|
||||
from hosts.core.models import HostEntry, HostsFile
|
||||
from hosts.tui.app import HostsManagerApp
|
||||
from hosts.tui.import_export_modal import ExportModal, FileBrowserModal, ImportModal
|
||||
from hosts.tui.import_export_modal import (
|
||||
ExportModal,
|
||||
FileBrowserModal,
|
||||
ImportModal,
|
||||
ReplaceImportConfirmationModal,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -236,14 +241,45 @@ async def test_import_shortcut_requires_privileged_mode_and_surfaces_service_err
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_successful_import_replaces_entries_and_saves_through_the_manager():
|
||||
"""A valid import reaches the persistence seam without a real Hosts File."""
|
||||
async def test_import_form_asks_how_to_apply_entries_at_minimum_supported_size():
|
||||
"""Both import actions remain visible and reachable at 100 by 30."""
|
||||
app = HostsManagerApp()
|
||||
app.edit_mode = True
|
||||
app.manager.edit_mode = True
|
||||
app.import_export_service = Mock()
|
||||
app.import_export_service.get_supported_import_formats.return_value = list(
|
||||
ImportFormat
|
||||
)
|
||||
|
||||
async with app.run_test(size=(100, 30)) as pilot:
|
||||
app.action_import_entries()
|
||||
await pilot.pause()
|
||||
|
||||
assert isinstance(app.screen, ImportModal)
|
||||
actions = app.screen.query_one("#import-mode-select", RadioSet)
|
||||
assert [button.label.plain for button in actions.query(RadioButton)] == [
|
||||
"Add to Hosts File",
|
||||
"Replace Hosts File",
|
||||
]
|
||||
assert app.screen.query_one("#mode-add", RadioButton).value
|
||||
assert not app.screen.query_one("#mode-replace", RadioButton).value
|
||||
container = app.screen.query_one("#format-container")
|
||||
submit = app.screen.query_one("#import-button", Button)
|
||||
assert submit.region.bottom <= container.region.bottom
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_successful_import_adds_entries_and_saves_through_the_manager():
|
||||
"""The safe default appends imported Host Entries to the Hosts File."""
|
||||
app = HostsManagerApp()
|
||||
app.hosts_file = HostsFile(
|
||||
entries=[HostEntry(ip_address="192.0.2.10", hostnames=["original.test"])]
|
||||
entries=[HostEntry(ip_address="192.0.2.10", hostnames=["original.test"])],
|
||||
header_comments=["existing header"],
|
||||
footer_comments=["existing footer"],
|
||||
)
|
||||
app.edit_mode = True
|
||||
app.manager.edit_mode = True
|
||||
app.load_hosts_file = Mock()
|
||||
app.save_mutation = Mock(return_value=True)
|
||||
app.import_export_service = Mock()
|
||||
app.import_export_service.get_supported_import_formats.return_value = list(
|
||||
|
|
@ -269,9 +305,74 @@ async def test_successful_import_replaces_entries_and_saves_through_the_manager(
|
|||
await pilot.pause()
|
||||
|
||||
assert [entry.hostnames for entry in app.hosts_file.entries] == [
|
||||
["imported.test"]
|
||||
["original.test"],
|
||||
["imported.test"],
|
||||
]
|
||||
assert app.hosts_file.header_comments == ["existing header"]
|
||||
assert app.hosts_file.footer_comments == ["existing footer"]
|
||||
app.save_mutation.assert_called_once()
|
||||
assert "Imported and saved 1 Host Entries" in str(
|
||||
assert "Added and saved 1 Host Entry" in str(
|
||||
app.query_one("#message-rail", Static).render()
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_replace_import_requires_explicit_destructive_confirmation():
|
||||
"""Replacement cannot mutate until the warning modal is confirmed."""
|
||||
app = HostsManagerApp()
|
||||
original = HostEntry(ip_address="192.0.2.10", hostnames=["original.test"])
|
||||
imported = HostEntry(ip_address="198.51.100.3", hostnames=["imported.test"])
|
||||
app.hosts_file = HostsFile(entries=[original])
|
||||
app.edit_mode = True
|
||||
app.manager.edit_mode = True
|
||||
app.load_hosts_file = Mock()
|
||||
app.save_mutation = Mock(return_value=True)
|
||||
app.import_export_service = Mock()
|
||||
app.import_export_service.get_supported_import_formats.return_value = list(
|
||||
ImportFormat
|
||||
)
|
||||
app.import_export_service.import_json_format.return_value = ImportResult(
|
||||
success=True,
|
||||
entries=[imported],
|
||||
errors=[],
|
||||
warnings=[],
|
||||
total_processed=1,
|
||||
successfully_imported=1,
|
||||
)
|
||||
|
||||
async with app.run_test(size=(120, 40)) as pilot:
|
||||
|
||||
async def request_replacement() -> None:
|
||||
app.action_import_entries()
|
||||
await pilot.pause()
|
||||
app.screen.query_one("#path-input", Input).value = "/tmp/entries.json"
|
||||
app.screen.query_one("#format-json", RadioButton).value = True
|
||||
app.screen.query_one("#mode-replace", RadioButton).value = True
|
||||
await pilot.click("#import-button")
|
||||
await pilot.pause()
|
||||
|
||||
await request_replacement()
|
||||
|
||||
assert isinstance(app.screen, ReplaceImportConfirmationModal)
|
||||
assert app.focused is app.screen.query_one("#cancel-button", Button)
|
||||
assert "remove all 1 current Host Entry" in str(
|
||||
app.screen.query_one("#replace-warning", Static).render()
|
||||
)
|
||||
assert app.hosts_file.entries == [original]
|
||||
app.save_mutation.assert_not_called()
|
||||
|
||||
await pilot.click("#cancel-button")
|
||||
await pilot.pause()
|
||||
assert app.hosts_file.entries == [original]
|
||||
app.save_mutation.assert_not_called()
|
||||
|
||||
await request_replacement()
|
||||
await pilot.click("#replace-button")
|
||||
await pilot.pause()
|
||||
await pilot.pause()
|
||||
|
||||
assert app.hosts_file.entries == [imported]
|
||||
app.save_mutation.assert_called_once()
|
||||
assert "Replaced Hosts File with 1 imported Host Entry" in str(
|
||||
app.query_one("#message-rail", Static).render()
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue