Initial working version
This commit is contained in:
parent
34a0627e76
commit
b6886cb34a
61 changed files with 4475 additions and 6 deletions
590
tests/test_app.py
Normal file
590
tests/test_app.py
Normal file
|
|
@ -0,0 +1,590 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from collections import Counter
|
||||
|
||||
import pytest
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.widgets import Button, Checkbox, Input, Select, Static
|
||||
|
||||
from pve_vm_setup.app import PveVmSetupApp
|
||||
from pve_vm_setup.models.workflow import WorkflowState
|
||||
from pve_vm_setup.screens.login import LoginView
|
||||
from pve_vm_setup.screens.wizard import NO_DISK_SELECTED, AutoStartConfirmModal, WizardView
|
||||
from pve_vm_setup.services.fake import FakeProxmoxService
|
||||
from pve_vm_setup.settings import AppSettings
|
||||
|
||||
|
||||
class LoginHarnessApp(App[None]):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield LoginView(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
WorkflowState(),
|
||||
FakeProxmoxService(),
|
||||
)
|
||||
|
||||
|
||||
async def wait_for_wizard_ready(
|
||||
pilot,
|
||||
app: App[None],
|
||||
*,
|
||||
attempts: int = 12,
|
||||
delay: float = 0.1,
|
||||
) -> None:
|
||||
for _ in range(attempts):
|
||||
await pilot.pause(delay)
|
||||
if (
|
||||
app.query_one("#general-vmid", Input).value == "123"
|
||||
and app.query_one("#general-node", Select).value == "fake-node-01"
|
||||
and app.query_one("#os-storage", Select).value == "cephfs"
|
||||
):
|
||||
return
|
||||
raise AssertionError("Timed out waiting for wizard reference data to load.")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_view_authenticates_with_pilot() -> None:
|
||||
app = LoginHarnessApp()
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
assert str(app.query_one("#title", Static).renderable) == "Proxmox Login"
|
||||
assert app.focused is app.query_one("#username", Input)
|
||||
|
||||
app.query_one("#username", Input).value = "junior"
|
||||
app.query_one("#password", Input).value = "secret"
|
||||
app.query_one("#connect", Button).press()
|
||||
await pilot.pause()
|
||||
|
||||
assert "Authenticated as junior@pam." == str(app.query_one("#status", Static).renderable)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_main_app_mounts_wizard_only_after_login() -> None:
|
||||
service = FakeProxmoxService()
|
||||
app = PveVmSetupApp(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
service=service,
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
assert app.query(LoginView)
|
||||
assert not app.query(WizardView)
|
||||
|
||||
login = app.query_one(LoginView)
|
||||
login.post_message(LoginView.Authenticated("junior@pam", "pam"))
|
||||
await pilot.pause()
|
||||
await pilot.pause()
|
||||
await wait_for_wizard_ready(pilot, app)
|
||||
|
||||
assert not app.query(LoginView)
|
||||
assert app.query(WizardView)
|
||||
assert app.focused is app.query_one("#general-name", Input)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wizard_activation_focuses_first_editable_field() -> None:
|
||||
service = FakeProxmoxService()
|
||||
app = PveVmSetupApp(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
service=service,
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
app.query_one(LoginView).remove()
|
||||
wizard = WizardView(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
WorkflowState(),
|
||||
service,
|
||||
)
|
||||
await app.query_one("#app-body").mount(wizard)
|
||||
wizard.activate()
|
||||
await wait_for_wizard_ready(pilot, app)
|
||||
|
||||
assert app.focused is app.query_one("#general-name", Input)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wizard_initial_activation_does_not_duplicate_live_reference_loads() -> None:
|
||||
class CountingService(FakeProxmoxService):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.calls: list[str] = []
|
||||
|
||||
def load_nodes(self):
|
||||
self.calls.append("load_nodes")
|
||||
return super().load_nodes()
|
||||
|
||||
def load_pools(self):
|
||||
self.calls.append("load_pools")
|
||||
return super().load_pools()
|
||||
|
||||
def load_existing_tags(self):
|
||||
self.calls.append("load_existing_tags")
|
||||
return super().load_existing_tags()
|
||||
|
||||
def load_next_vmid(self):
|
||||
self.calls.append("load_next_vmid")
|
||||
return super().load_next_vmid()
|
||||
|
||||
def load_storages(self, node: str):
|
||||
self.calls.append(f"load_storages:{node}")
|
||||
return super().load_storages(node)
|
||||
|
||||
def load_bridges(self, node: str):
|
||||
self.calls.append(f"load_bridges:{node}")
|
||||
return super().load_bridges(node)
|
||||
|
||||
def load_isos(self, node: str, storage: str):
|
||||
self.calls.append(f"load_isos:{node}:{storage}")
|
||||
return super().load_isos(node, storage)
|
||||
|
||||
service = CountingService()
|
||||
app = PveVmSetupApp(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
service=service,
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
app.query_one(LoginView).remove()
|
||||
wizard = WizardView(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
WorkflowState(),
|
||||
service,
|
||||
)
|
||||
await app.query_one("#app-body").mount(wizard)
|
||||
wizard.activate()
|
||||
for _ in range(6):
|
||||
await pilot.pause()
|
||||
|
||||
assert Counter(service.calls) == Counter(
|
||||
[
|
||||
"load_nodes",
|
||||
"load_pools",
|
||||
"load_existing_tags",
|
||||
"load_next_vmid",
|
||||
"load_storages:fake-node-01",
|
||||
"load_bridges:fake-node-01",
|
||||
"load_isos:fake-node-01:cephfs",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wizard_initial_activation_loads_reference_data_concurrently() -> None:
|
||||
class SlowService(FakeProxmoxService):
|
||||
delay = 0.15
|
||||
|
||||
def load_nodes(self):
|
||||
time.sleep(self.delay)
|
||||
return super().load_nodes()
|
||||
|
||||
def load_pools(self):
|
||||
time.sleep(self.delay)
|
||||
return super().load_pools()
|
||||
|
||||
def load_existing_tags(self):
|
||||
time.sleep(self.delay)
|
||||
return super().load_existing_tags()
|
||||
|
||||
def load_next_vmid(self):
|
||||
time.sleep(self.delay)
|
||||
return super().load_next_vmid()
|
||||
|
||||
def load_storages(self, node: str):
|
||||
time.sleep(self.delay)
|
||||
return super().load_storages(node)
|
||||
|
||||
def load_bridges(self, node: str):
|
||||
time.sleep(self.delay)
|
||||
return super().load_bridges(node)
|
||||
|
||||
def load_isos(self, node: str, storage: str):
|
||||
time.sleep(self.delay)
|
||||
return super().load_isos(node, storage)
|
||||
|
||||
service = SlowService()
|
||||
app = PveVmSetupApp(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
service=service,
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
app.query_one(LoginView).remove()
|
||||
wizard = WizardView(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
WorkflowState(),
|
||||
service,
|
||||
)
|
||||
await app.query_one("#app-body").mount(wizard)
|
||||
|
||||
started_at = time.perf_counter()
|
||||
wizard.activate()
|
||||
|
||||
await wait_for_wizard_ready(pilot, app)
|
||||
|
||||
elapsed = time.perf_counter() - started_at
|
||||
|
||||
assert elapsed < 1.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wizard_uses_scrollable_sections_with_border_titles() -> None:
|
||||
service = FakeProxmoxService()
|
||||
app = PveVmSetupApp(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
service=service,
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
app.query_one(LoginView).remove()
|
||||
wizard = WizardView(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
WorkflowState(),
|
||||
service,
|
||||
)
|
||||
await app.query_one("#app-body").mount(wizard)
|
||||
wizard.activate()
|
||||
await wait_for_wizard_ready(pilot, app)
|
||||
|
||||
general_section = app.query_one("#general-section", ScrollableContainer)
|
||||
os_section = app.query_one("#os-section", ScrollableContainer)
|
||||
|
||||
assert str(general_section.border_title).strip() == "General"
|
||||
assert str(os_section.border_title).strip() == "Operating System"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wizard_hides_os_fields_based_on_media_choice() -> None:
|
||||
service = FakeProxmoxService()
|
||||
app = PveVmSetupApp(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
service=service,
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
app.query_one(LoginView).remove()
|
||||
wizard = WizardView(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
WorkflowState(),
|
||||
service,
|
||||
)
|
||||
await app.query_one("#app-body").mount(wizard)
|
||||
wizard.activate()
|
||||
await wait_for_wizard_ready(pilot, app)
|
||||
|
||||
assert app.query_one("#os-storage", Select).display is True
|
||||
assert app.query_one("#os-iso", Select).display is True
|
||||
assert app.query_one("#os-physical-drive", Input).display is False
|
||||
|
||||
app.query_one("#os-media-choice", Select).value = "physical"
|
||||
await pilot.pause()
|
||||
|
||||
assert app.query_one("#os-storage", Select).display is False
|
||||
assert app.query_one("#os-iso", Select).display is False
|
||||
assert app.query_one("#os-physical-drive", Input).display is True
|
||||
|
||||
app.query_one("#os-media-choice", Select).value = "none"
|
||||
await pilot.pause()
|
||||
|
||||
assert app.query_one("#os-storage", Select).display is False
|
||||
assert app.query_one("#os-iso", Select).display is False
|
||||
assert app.query_one("#os-physical-drive", Input).display is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wizard_hides_dependent_system_memory_and_network_fields() -> None:
|
||||
service = FakeProxmoxService()
|
||||
app = PveVmSetupApp(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
service=service,
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
app.query_one(LoginView).remove()
|
||||
wizard = WizardView(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
WorkflowState(),
|
||||
service,
|
||||
)
|
||||
await app.query_one("#app-body").mount(wizard)
|
||||
wizard.activate()
|
||||
await wait_for_wizard_ready(pilot, app)
|
||||
|
||||
assert app.query_one("#system-efi-storage", Select).display is True
|
||||
assert app.query_one("#system-pre-enroll", Checkbox).display is True
|
||||
|
||||
app.query_one("#system-add-efi", Checkbox).value = False
|
||||
await pilot.pause()
|
||||
|
||||
assert app.query_one("#system-efi-storage", Select).display is False
|
||||
assert app.query_one("#system-pre-enroll", Checkbox).display is False
|
||||
|
||||
app.query_one("#system-tpm", Checkbox).value = True
|
||||
await pilot.pause()
|
||||
|
||||
assert app.query_one("#system-efi-storage", Select).display is True
|
||||
assert app.query_one("#system-pre-enroll", Checkbox).display is False
|
||||
|
||||
assert app.query_one("#memory-min-size", Input).display is True
|
||||
assert app.query_one("#memory-ksm", Checkbox).display is True
|
||||
|
||||
app.query_one("#memory-ballooning", Checkbox).value = False
|
||||
await pilot.pause()
|
||||
|
||||
assert app.query_one("#memory-min-size", Input).display is False
|
||||
assert app.query_one("#memory-ksm", Checkbox).display is False
|
||||
|
||||
assert app.query_one("#network-bridge", Select).display is True
|
||||
assert app.query_one("#network-rate", Input).display is True
|
||||
|
||||
app.query_one("#network-none", Checkbox).value = True
|
||||
await pilot.pause()
|
||||
|
||||
assert app.query_one("#network-bridge", Select).display is False
|
||||
assert app.query_one("#network-rate", Input).display is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wizard_tag_rows_keep_input_and_button_visible() -> None:
|
||||
service = FakeProxmoxService()
|
||||
app = PveVmSetupApp(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
service=service,
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
app.query_one(LoginView).remove()
|
||||
wizard = WizardView(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
WorkflowState(),
|
||||
service,
|
||||
)
|
||||
await app.query_one("#app-body").mount(wizard)
|
||||
wizard.activate()
|
||||
await wait_for_wizard_ready(pilot, app)
|
||||
|
||||
assert app.query_one("#general-tag-input", Input).display is True
|
||||
assert app.query_one("#general-tag-add", Button).display is True
|
||||
assert app.query_one("#general-tag-existing", Select).display is True
|
||||
assert app.query_one("#general-tag-use", Button).display is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wizard_add_tag_button_updates_current_tags() -> None:
|
||||
service = FakeProxmoxService()
|
||||
app = PveVmSetupApp(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
service=service,
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
app.query_one(LoginView).remove()
|
||||
wizard = WizardView(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
WorkflowState(),
|
||||
service,
|
||||
)
|
||||
await app.query_one("#app-body").mount(wizard)
|
||||
wizard.activate()
|
||||
await wait_for_wizard_ready(pilot, app)
|
||||
|
||||
app.query_one("#general-tag-input", Input).value = "alpha"
|
||||
app.query_one("#general-tag-add", Button).press()
|
||||
await pilot.pause()
|
||||
|
||||
assert wizard._workflow.config.general.tags == ["alpha"]
|
||||
current_tags = app.query_one("#general-tag-current", Select)
|
||||
assert current_tags.display is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wizard_hiding_select_collapses_open_overlay() -> None:
|
||||
service = FakeProxmoxService()
|
||||
app = PveVmSetupApp(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
service=service,
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
app.query_one(LoginView).remove()
|
||||
wizard = WizardView(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
WorkflowState(),
|
||||
service,
|
||||
)
|
||||
await app.query_one("#app-body").mount(wizard)
|
||||
wizard.activate()
|
||||
await wait_for_wizard_ready(pilot, app)
|
||||
|
||||
storage = app.query_one("#os-storage", Select)
|
||||
storage.expanded = True
|
||||
await pilot.pause()
|
||||
assert storage.expanded is True
|
||||
|
||||
app.query_one("#os-media-choice", Select).value = "physical"
|
||||
await pilot.pause()
|
||||
|
||||
assert storage.display is False
|
||||
assert storage.expanded is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disk_toolbar_buttons_render_left_of_disk_selector() -> None:
|
||||
service = FakeProxmoxService()
|
||||
app = PveVmSetupApp(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
service=service,
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
app.query_one(LoginView).remove()
|
||||
wizard = WizardView(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
WorkflowState(),
|
||||
service,
|
||||
)
|
||||
await app.query_one("#app-body").mount(wizard)
|
||||
wizard.activate()
|
||||
await wait_for_wizard_ready(pilot, app)
|
||||
|
||||
wizard._workflow.current_step_index = 3
|
||||
wizard._show_step()
|
||||
await pilot.pause()
|
||||
|
||||
add_button = app.query_one("#disks-add", Button)
|
||||
remove_button = app.query_one("#disks-remove", Button)
|
||||
selector = app.query_one("#disks-select", Select)
|
||||
|
||||
assert add_button.region.x < remove_button.region.x
|
||||
assert selector.region.x == add_button.region.x
|
||||
assert selector.region.width > remove_button.region.width * 3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disk_selector_switches_between_configured_disks_without_blank_option() -> None:
|
||||
service = FakeProxmoxService()
|
||||
app = PveVmSetupApp(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
service=service,
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
app.query_one(LoginView).remove()
|
||||
wizard = WizardView(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
WorkflowState(),
|
||||
service,
|
||||
)
|
||||
await app.query_one("#app-body").mount(wizard)
|
||||
wizard.activate()
|
||||
await wait_for_wizard_ready(pilot, app)
|
||||
|
||||
wizard._workflow.current_step_index = 3
|
||||
wizard._show_step()
|
||||
await pilot.pause()
|
||||
|
||||
app.query_one("#disks-add", Button).press()
|
||||
await pilot.pause()
|
||||
|
||||
selector = app.query_one("#disks-select", Select)
|
||||
option_values = [value for _, value in selector._options]
|
||||
|
||||
assert NO_DISK_SELECTED not in option_values
|
||||
assert selector.disabled is False
|
||||
assert selector.value == "1"
|
||||
|
||||
await asyncio.wait_for(pilot.click("#disks-select"), timeout=2)
|
||||
await pilot.pause()
|
||||
assert selector.expanded is True
|
||||
|
||||
await asyncio.wait_for(pilot.press("up"), timeout=2)
|
||||
await pilot.pause()
|
||||
await asyncio.wait_for(pilot.press("enter"), timeout=2)
|
||||
await pilot.pause()
|
||||
|
||||
assert selector.expanded is False
|
||||
assert selector.value == "0"
|
||||
assert wizard._selected_disk_index == 0
|
||||
assert app.focused is selector
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_confirm_step_replaces_create_with_exit_after_success() -> None:
|
||||
service = FakeProxmoxService()
|
||||
app = PveVmSetupApp(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
service=service,
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
app.query_one(LoginView).remove()
|
||||
wizard = WizardView(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
WorkflowState(),
|
||||
service,
|
||||
)
|
||||
await app.query_one("#app-body").mount(wizard)
|
||||
wizard.activate()
|
||||
await wait_for_wizard_ready(pilot, app)
|
||||
|
||||
wizard._workflow.current_step_index = 7
|
||||
wizard._workflow.submission.phase = "success"
|
||||
wizard._workflow.submission.message = "VM 123 created."
|
||||
wizard._show_step()
|
||||
await pilot.pause()
|
||||
|
||||
create_button = app.query_one("#wizard-create", Button)
|
||||
assert str(create_button.label) == "Exit"
|
||||
assert app.focused is create_button
|
||||
|
||||
exited: list[bool] = []
|
||||
app.exit = lambda *args, **kwargs: exited.append(True) # type: ignore[method-assign]
|
||||
create_button.press()
|
||||
await pilot.pause()
|
||||
|
||||
assert exited == [True]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_confirm_step_asks_whether_to_start_vm_before_submitting() -> None:
|
||||
service = FakeProxmoxService()
|
||||
app = PveVmSetupApp(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
service=service,
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
app.query_one(LoginView).remove()
|
||||
wizard = WizardView(
|
||||
AppSettings.from_env({}, load_dotenv_file=False),
|
||||
WorkflowState(),
|
||||
service,
|
||||
)
|
||||
await app.query_one("#app-body").mount(wizard)
|
||||
wizard.activate()
|
||||
await wait_for_wizard_ready(pilot, app)
|
||||
|
||||
app.query_one("#general-name", Input).value = "demo"
|
||||
wizard._workflow.current_step_index = 7
|
||||
wizard._show_step()
|
||||
await pilot.pause()
|
||||
|
||||
app.query_one("#wizard-create", Button).press()
|
||||
await pilot.pause()
|
||||
|
||||
assert isinstance(app.screen_stack[-1], AutoStartConfirmModal)
|
||||
assert service.created_vms == []
|
||||
|
||||
app.query_one("#auto-start-no", Button).press()
|
||||
|
||||
for _ in range(20):
|
||||
await pilot.pause(0.05)
|
||||
if service.created_vms:
|
||||
break
|
||||
|
||||
assert len(service.created_vms) == 1
|
||||
assert service.start_after_create_requests == [False]
|
||||
Loading…
Add table
Add a link
Reference in a new issue