Feat #5: expose import and export workflows
This commit is contained in:
parent
024b08459c
commit
20e7d63665
7 changed files with 451 additions and 0 deletions
165
tests/test_import_export_workflow.py
Normal file
165
tests/test_import_export_workflow.py
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
"""User-visible import and export workflows."""
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from textual.widgets import Input, RadioButton, Static
|
||||
|
||||
from hosts.core.import_export import (
|
||||
ExportFormat,
|
||||
ExportResult,
|
||||
ImportFormat,
|
||||
ImportResult,
|
||||
)
|
||||
from hosts.core.models import HostEntry, HostsFile
|
||||
from hosts.tui.app import HostsManagerApp
|
||||
from hosts.tui.import_export_modal import ExportModal, ImportModal
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_export_shortcut_opens_a_format_chooser_and_reports_validation():
|
||||
"""Export presents every core format and keeps invalid input visible."""
|
||||
app = HostsManagerApp()
|
||||
app.hosts_file = HostsFile(
|
||||
entries=[HostEntry(ip_address="192.0.2.10", hostnames=["export.test"])]
|
||||
)
|
||||
app.load_hosts_file = Mock()
|
||||
|
||||
async with app.run_test(size=(120, 40)) as pilot:
|
||||
await pilot.press("ctrl+x")
|
||||
await pilot.pause()
|
||||
assert isinstance(app.screen, ExportModal)
|
||||
assert [button.label.plain for button in app.screen.query(RadioButton)] == [
|
||||
"Hosts file (.hosts)",
|
||||
"JSON (.json)",
|
||||
"CSV (.csv)",
|
||||
]
|
||||
|
||||
app.screen.query_one("#path-input", Input).value = ""
|
||||
await pilot.click("#continue-button")
|
||||
assert "Choose a file path" in str(
|
||||
app.screen.query_one("#workflow-error", Static).render()
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_export_requires_overwrite_confirmation_before_calling_service(tmp_path):
|
||||
"""An existing output file is not replaced until the user confirms it."""
|
||||
target = tmp_path / "hosts.json"
|
||||
target.write_text("existing", encoding="utf-8")
|
||||
app = HostsManagerApp()
|
||||
app.hosts_file = HostsFile(
|
||||
entries=[HostEntry(ip_address="192.0.2.10", hostnames=["export.test"])]
|
||||
)
|
||||
app.import_export_service = Mock()
|
||||
app.import_export_service.get_supported_export_formats.return_value = list(
|
||||
ExportFormat
|
||||
)
|
||||
app.import_export_service.validate_export_path.return_value = [
|
||||
f"File {target} already exists and will be overwritten"
|
||||
]
|
||||
app.import_export_service.export_json_format.return_value = ExportResult(
|
||||
success=True,
|
||||
file_path=target,
|
||||
entries_exported=1,
|
||||
errors=[],
|
||||
format=ExportFormat.JSON,
|
||||
)
|
||||
|
||||
async with app.run_test(size=(120, 40)) as pilot:
|
||||
app.action_export_entries()
|
||||
await pilot.pause()
|
||||
app.screen.query_one("#path-input", Input).value = str(target)
|
||||
app.screen.query_one("#format-json", RadioButton).value = True
|
||||
await pilot.click("#continue-button")
|
||||
assert "already exists" in str(
|
||||
app.screen.query_one("#workflow-warning", Static).render()
|
||||
)
|
||||
app.import_export_service.export_json_format.assert_not_called()
|
||||
|
||||
await pilot.click("#confirm-button")
|
||||
await pilot.pause()
|
||||
await pilot.pause()
|
||||
|
||||
app.import_export_service.export_json_format.assert_called_once_with(
|
||||
app.hosts_file, target
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_import_shortcut_requires_privileged_mode_and_surfaces_service_errors():
|
||||
"""Import cannot mutate in Read-only Mode and reports parser failures."""
|
||||
app = HostsManagerApp()
|
||||
app.hosts_file = HostsFile(
|
||||
entries=[HostEntry(ip_address="192.0.2.10", hostnames=["original.test"])]
|
||||
)
|
||||
app.load_hosts_file = Mock()
|
||||
|
||||
async with app.run_test(size=(120, 40)) as pilot:
|
||||
await pilot.press("ctrl+o")
|
||||
await pilot.pause()
|
||||
assert not isinstance(app.screen, ImportModal)
|
||||
assert "Read-only Mode" in str(app.query_one("#message-rail", Static).render())
|
||||
|
||||
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
|
||||
)
|
||||
app.import_export_service.import_csv_format.return_value = Mock(
|
||||
success=False, errors=["CSV column 'ip_address' is missing"]
|
||||
)
|
||||
app.action_import_entries()
|
||||
await pilot.pause()
|
||||
assert isinstance(app.screen, ImportModal)
|
||||
app.screen.query_one("#path-input", Input).value = "/tmp/broken.csv"
|
||||
csv_button = app.screen.query_one("#format-csv", RadioButton)
|
||||
csv_button.value = True
|
||||
await pilot.click("#import-button")
|
||||
await pilot.pause()
|
||||
assert "CSV column 'ip_address' is missing" in str(
|
||||
app.screen.query_one("#workflow-error", Static).render()
|
||||
)
|
||||
|
||||
|
||||
@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."""
|
||||
app = HostsManagerApp()
|
||||
app.hosts_file = HostsFile(
|
||||
entries=[HostEntry(ip_address="192.0.2.10", hostnames=["original.test"])]
|
||||
)
|
||||
app.edit_mode = True
|
||||
app.manager.edit_mode = True
|
||||
app.save_mutation = Mock(return_value=True)
|
||||
app.import_export_service = Mock()
|
||||
app.import_export_service.get_supported_import_formats.return_value = list(
|
||||
ImportFormat
|
||||
)
|
||||
imported = HostEntry(ip_address="198.51.100.3", hostnames=["imported.test"])
|
||||
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:
|
||||
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
|
||||
await pilot.click("#import-button")
|
||||
await pilot.pause()
|
||||
await pilot.pause()
|
||||
|
||||
assert [entry.hostnames for entry in app.hosts_file.entries] == [
|
||||
["imported.test"]
|
||||
]
|
||||
app.save_mutation.assert_called_once()
|
||||
assert "Imported and saved 1 Host Entries" in str(
|
||||
app.query_one("#message-rail", Static).render()
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue