hosts/tests/test_import_export_workflow.py

165 lines
5.9 KiB
Python

"""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()
)