Add guided env setup modal
This commit is contained in:
parent
c9859a5324
commit
8c90306c75
6 changed files with 474 additions and 9 deletions
|
|
@ -1,15 +1,19 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Container
|
||||
from textual.widgets import Footer, Header
|
||||
|
||||
from .models.workflow import WorkflowState
|
||||
from .screens.env_setup import EnvSetupModal, MissingEnvSetupPromptModal
|
||||
from .screens.login import LoginView
|
||||
from .screens.wizard import WizardView
|
||||
from .services.base import ProxmoxService
|
||||
from .services.factory import ProxmoxServiceFactory
|
||||
from .settings import AppSettings
|
||||
from .settings import ENV_VAR_SPECS, AppSettings, resolve_dotenv_paths, write_config_dotenv
|
||||
from .terminal_compat import build_driver_class
|
||||
|
||||
|
||||
|
|
@ -22,11 +26,22 @@ class PveVmSetupApp(App[None]):
|
|||
settings: AppSettings,
|
||||
*,
|
||||
service: ProxmoxService | None = None,
|
||||
prompt_for_missing_env_setup: bool | None = None,
|
||||
dotenv_path: str | Path = ".env",
|
||||
config_dotenv_path: str | Path | None = None,
|
||||
) -> None:
|
||||
super().__init__(driver_class=build_driver_class())
|
||||
self.settings = settings
|
||||
self.workflow = WorkflowState()
|
||||
self.service = service or ProxmoxServiceFactory.create(settings)
|
||||
self._prompt_for_missing_env_setup = (
|
||||
service is None
|
||||
if prompt_for_missing_env_setup is None
|
||||
else prompt_for_missing_env_setup
|
||||
)
|
||||
self._dotenv_path = dotenv_path
|
||||
self._config_dotenv_path = config_dotenv_path
|
||||
self._missing_env_prompted = False
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
|
|
@ -34,6 +49,9 @@ class PveVmSetupApp(App[None]):
|
|||
yield LoginView(self.settings, self.workflow, self.service)
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.call_after_refresh(self._maybe_prompt_for_missing_env_setup)
|
||||
|
||||
def on_unmount(self) -> None:
|
||||
close = getattr(self.service, "close", None)
|
||||
if callable(close):
|
||||
|
|
@ -44,3 +62,61 @@ class PveVmSetupApp(App[None]):
|
|||
wizard = WizardView(self.settings, self.workflow, self.service)
|
||||
await self.query_one("#app-body", Container).mount(wizard)
|
||||
wizard.activate()
|
||||
|
||||
def _maybe_prompt_for_missing_env_setup(self) -> None:
|
||||
if self._missing_env_prompted or not self._prompt_for_missing_env_setup:
|
||||
return
|
||||
dotenv_paths = resolve_dotenv_paths(
|
||||
dotenv_path=self._dotenv_path,
|
||||
config_dotenv_path=self._config_dotenv_path,
|
||||
)
|
||||
if dotenv_paths.any_exists:
|
||||
return
|
||||
self._missing_env_prompted = True
|
||||
self.push_screen(
|
||||
MissingEnvSetupPromptModal(dotenv_paths),
|
||||
self._handle_missing_env_prompt_result,
|
||||
)
|
||||
|
||||
def _handle_missing_env_prompt_result(self, should_setup: bool | None) -> None:
|
||||
if not should_setup:
|
||||
return
|
||||
self.push_screen(
|
||||
EnvSetupModal(self._current_env_values()),
|
||||
self._handle_env_setup_result,
|
||||
)
|
||||
|
||||
def _handle_env_setup_result(self, values: dict[str, str] | None) -> None:
|
||||
if not values:
|
||||
return
|
||||
write_config_dotenv(values, config_dotenv_path=self._config_dotenv_path)
|
||||
self._apply_runtime_env(values)
|
||||
self._reload_settings_and_service()
|
||||
|
||||
def _current_env_values(self) -> dict[str, str]:
|
||||
return {
|
||||
spec.name: os.environ.get(spec.name, "")
|
||||
for spec in ENV_VAR_SPECS
|
||||
if os.environ.get(spec.name, "")
|
||||
}
|
||||
|
||||
def _apply_runtime_env(self, values: dict[str, str]) -> None:
|
||||
for spec in ENV_VAR_SPECS:
|
||||
if spec.name in values:
|
||||
os.environ[spec.name] = values[spec.name]
|
||||
else:
|
||||
os.environ.pop(spec.name, None)
|
||||
|
||||
def _reload_settings_and_service(self) -> None:
|
||||
previous_service = self.service
|
||||
self.settings = AppSettings.from_env(
|
||||
dotenv_path=self._dotenv_path,
|
||||
config_dotenv_path=self._config_dotenv_path,
|
||||
)
|
||||
self.service = ProxmoxServiceFactory.create(self.settings)
|
||||
if self.query(LoginView):
|
||||
self.query_one(LoginView).reconfigure(self.settings, self.service)
|
||||
if previous_service is not self.service:
|
||||
close = getattr(previous_service, "close", None)
|
||||
if callable(close):
|
||||
close()
|
||||
|
|
|
|||
192
src/pve_vm_setup/screens/env_setup.py
Normal file
192
src/pve_vm_setup/screens/env_setup.py
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from textual import on
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import HorizontalGroup, ScrollableContainer, Vertical
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Button, Input, Static
|
||||
|
||||
from ..errors import SettingsError
|
||||
from ..settings import ENV_VAR_SPECS, AppSettings, DotenvPaths
|
||||
|
||||
|
||||
def _input_id(name: str) -> str:
|
||||
return f"env-{name.lower().replace('_', '-')}"
|
||||
|
||||
|
||||
class MissingEnvSetupPromptModal(ModalScreen[bool | None]):
|
||||
DEFAULT_CSS = """
|
||||
MissingEnvSetupPromptModal {
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
#missing-env-dialog {
|
||||
width: 68;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: round $accent;
|
||||
background: $surface;
|
||||
padding: 1 2;
|
||||
}
|
||||
|
||||
#missing-env-title {
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
#missing-env-actions {
|
||||
margin-top: 1;
|
||||
height: auto;
|
||||
width: 1fr;
|
||||
align-horizontal: right;
|
||||
}
|
||||
|
||||
#missing-env-actions Button {
|
||||
margin-left: 1;
|
||||
min-width: 14;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [("escape", "cancel", "Skip")]
|
||||
|
||||
def __init__(self, paths: DotenvPaths) -> None:
|
||||
super().__init__()
|
||||
self._paths = paths
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="missing-env-dialog"):
|
||||
yield Static("Set up a .env file?", id="missing-env-title")
|
||||
yield Static(
|
||||
"No .env file was found in the current directory or the standard config path."
|
||||
)
|
||||
yield Static(f"Current directory: {self._paths.cwd}")
|
||||
yield Static(f"Config file: {self._paths.config}")
|
||||
yield Static("PROXMOX_URL is required. All other values may be left blank.")
|
||||
with HorizontalGroup(id="missing-env-actions"):
|
||||
yield Button("Skip", id="missing-env-skip")
|
||||
yield Button("Set Up", id="missing-env-setup", variant="primary")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.set_focus(self.query_one("#missing-env-setup", Button))
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
self.dismiss(False)
|
||||
|
||||
@on(Button.Pressed, "#missing-env-skip")
|
||||
def on_skip_pressed(self) -> None:
|
||||
self.dismiss(False)
|
||||
|
||||
@on(Button.Pressed, "#missing-env-setup")
|
||||
def on_setup_pressed(self) -> None:
|
||||
self.dismiss(True)
|
||||
|
||||
|
||||
class EnvSetupModal(ModalScreen[dict[str, str] | None]):
|
||||
DEFAULT_CSS = """
|
||||
EnvSetupModal {
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
#env-setup-dialog {
|
||||
width: 92;
|
||||
max-width: 100%;
|
||||
height: 90%;
|
||||
border: round $accent;
|
||||
background: $surface;
|
||||
padding: 1 2;
|
||||
}
|
||||
|
||||
#env-setup-title {
|
||||
text-style: bold;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#env-setup-form {
|
||||
height: 1fr;
|
||||
padding-right: 1;
|
||||
}
|
||||
|
||||
.env-label {
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
#env-setup-status {
|
||||
margin-top: 1;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
#env-setup-actions {
|
||||
margin-top: 1;
|
||||
height: auto;
|
||||
width: 1fr;
|
||||
align-horizontal: right;
|
||||
}
|
||||
|
||||
#env-setup-actions Button {
|
||||
margin-left: 1;
|
||||
min-width: 14;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [("escape", "cancel", "Cancel")]
|
||||
|
||||
def __init__(self, initial_values: Mapping[str, str] | None = None) -> None:
|
||||
super().__init__()
|
||||
self._initial_values = dict(initial_values or {})
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="env-setup-dialog"):
|
||||
yield Static("Create Proxmox Environment File", id="env-setup-title")
|
||||
yield Static("Enter values for the supported variables. Leave optional values blank.")
|
||||
with ScrollableContainer(id="env-setup-form"):
|
||||
for spec in ENV_VAR_SPECS:
|
||||
label = spec.name if not spec.required else f"{spec.name} *"
|
||||
yield Static(label, classes="env-label")
|
||||
yield Input(
|
||||
value=self._initial_values.get(spec.name, ""),
|
||||
placeholder=spec.placeholder,
|
||||
password=spec.secret,
|
||||
id=_input_id(spec.name),
|
||||
)
|
||||
yield Static(
|
||||
"Values will be saved to ~/.config/pve-vm-setup/.env", id="env-setup-status"
|
||||
)
|
||||
with HorizontalGroup(id="env-setup-actions"):
|
||||
yield Button("Cancel", id="env-setup-cancel")
|
||||
yield Button("Save", id="env-setup-save", variant="primary")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.set_focus(self.query_one(f"#{_input_id('PROXMOX_URL')}", Input))
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
self.dismiss(None)
|
||||
|
||||
@on(Button.Pressed, "#env-setup-cancel")
|
||||
def on_cancel_pressed(self) -> None:
|
||||
self.dismiss(None)
|
||||
|
||||
@on(Button.Pressed, "#env-setup-save")
|
||||
def on_save_pressed(self) -> None:
|
||||
values = self._collect_values()
|
||||
if not values.get("PROXMOX_URL"):
|
||||
self._show_status("PROXMOX_URL is required.")
|
||||
return
|
||||
try:
|
||||
AppSettings.from_env(values, load_dotenv_file=False)
|
||||
except SettingsError as exc:
|
||||
self._show_status(str(exc))
|
||||
return
|
||||
self.dismiss(values)
|
||||
|
||||
def _collect_values(self) -> dict[str, str]:
|
||||
values: dict[str, str] = {}
|
||||
for spec in ENV_VAR_SPECS:
|
||||
widget = self.query_one(f"#{_input_id(spec.name)}", Input)
|
||||
value = widget.value if spec.secret else widget.value.strip()
|
||||
if value:
|
||||
values[spec.name] = value
|
||||
return values
|
||||
|
||||
def _show_status(self, message: str) -> None:
|
||||
self.query_one("#env-setup-status", Static).update(message)
|
||||
|
|
@ -86,6 +86,18 @@ class LoginView(Vertical):
|
|||
self.call_after_refresh(self.app.set_focus, username_input)
|
||||
self.run_worker(self._load_realms, thread=True, exclusive=True)
|
||||
|
||||
def reconfigure(self, settings: AppSettings, service: ProxmoxService) -> None:
|
||||
self._settings = settings
|
||||
self._service = service
|
||||
self.query_one("#mode", Static).update(
|
||||
f"Mode: {self._service.mode} on {self._settings.sanitized_host}"
|
||||
)
|
||||
self.query_one("#username", Input).value = self._settings.proxmox_user or ""
|
||||
self.query_one("#password", Input).value = self._settings.proxmox_password or ""
|
||||
self.query_one("#realm", Select).set_options([])
|
||||
self._show_status("Loading realms...")
|
||||
self.run_worker(self._load_realms, thread=True, exclusive=True)
|
||||
|
||||
def _load_realms(self) -> None:
|
||||
try:
|
||||
realms = self._service.load_realms()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,43 @@ from dotenv import dotenv_values
|
|||
from .errors import SettingsError
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EnvVarSpec:
|
||||
name: str
|
||||
placeholder: str = ""
|
||||
required: bool = False
|
||||
secret: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DotenvPaths:
|
||||
cwd: Path
|
||||
config: Path
|
||||
|
||||
@property
|
||||
def any_exists(self) -> bool:
|
||||
return self.cwd.exists() or self.config.exists()
|
||||
|
||||
|
||||
ENV_VAR_SPECS: tuple[EnvVarSpec, ...] = (
|
||||
EnvVarSpec("PROXMOX_URL", placeholder="https://pve.example.com:8006", required=True),
|
||||
EnvVarSpec("PROXMOX_REALM", placeholder="pam"),
|
||||
EnvVarSpec("PROXMOX_USER", placeholder="root"),
|
||||
EnvVarSpec("PROXMOX_PASSWORD", placeholder="Optional", secret=True),
|
||||
EnvVarSpec("PROXMOX_VERIFY_TLS", placeholder="true or false"),
|
||||
EnvVarSpec("PROXMOX_API_BASE", placeholder="/api2/json"),
|
||||
EnvVarSpec("PROXMOX_REQUEST_TIMEOUT_SECONDS", placeholder="15"),
|
||||
EnvVarSpec("PROXMOX_DEFAULT_ISO_SELECTOR", placeholder="*nixos*"),
|
||||
EnvVarSpec("PROXMOX_PREVENT_CREATE", placeholder="true or false"),
|
||||
EnvVarSpec("PROXMOX_ENABLE_TEST_MODE", placeholder="true or false"),
|
||||
EnvVarSpec("PROXMOX_TEST_NODE", placeholder="pve-test-01"),
|
||||
EnvVarSpec("PROXMOX_TEST_POOL", placeholder="sandbox"),
|
||||
EnvVarSpec("PROXMOX_TEST_TAG", placeholder="codex-e2e"),
|
||||
EnvVarSpec("PROXMOX_TEST_VM_NAME_PREFIX", placeholder="codex-e2e-"),
|
||||
EnvVarSpec("PROXMOX_KEEP_FAILED_VM", placeholder="true or false"),
|
||||
)
|
||||
|
||||
|
||||
def _load_dotenv(path: Path) -> dict[str, str]:
|
||||
return {
|
||||
key: value
|
||||
|
|
@ -20,6 +57,45 @@ def _load_dotenv(path: Path) -> dict[str, str]:
|
|||
}
|
||||
|
||||
|
||||
def resolve_dotenv_paths(
|
||||
*,
|
||||
dotenv_path: str | Path = ".env",
|
||||
config_dotenv_path: str | Path | None = None,
|
||||
) -> DotenvPaths:
|
||||
return DotenvPaths(
|
||||
cwd=Path(dotenv_path),
|
||||
config=(
|
||||
Path(config_dotenv_path).expanduser()
|
||||
if config_dotenv_path is not None
|
||||
else Path.home() / ".config" / "pve-vm-setup" / ".env"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _format_dotenv_value(value: str) -> str:
|
||||
if re.fullmatch(r"[A-Za-z0-9_./:@+-]+", value):
|
||||
return value
|
||||
escaped = value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def write_config_dotenv(
|
||||
values: Mapping[str, str | None],
|
||||
*,
|
||||
config_dotenv_path: str | Path | None = None,
|
||||
) -> Path:
|
||||
path = resolve_dotenv_paths(config_dotenv_path=config_dotenv_path).config
|
||||
lines = ["# Generated by pve-vm-setup"]
|
||||
for spec in ENV_VAR_SPECS:
|
||||
raw_value = values.get(spec.name)
|
||||
if raw_value is None or raw_value == "":
|
||||
continue
|
||||
lines.append(f"{spec.name}={_format_dotenv_value(raw_value)}")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def _parse_bool(value: str | None, *, default: bool) -> bool:
|
||||
if value is None or value == "":
|
||||
return default
|
||||
|
|
@ -105,14 +181,12 @@ class AppSettings:
|
|||
) -> AppSettings:
|
||||
raw: dict[str, str] = {}
|
||||
if load_dotenv_file:
|
||||
cwd_dotenv = Path(dotenv_path)
|
||||
home_config_dotenv = (
|
||||
Path(config_dotenv_path).expanduser()
|
||||
if config_dotenv_path is not None
|
||||
else Path.home() / ".config" / "pve-vm-setup" / ".env"
|
||||
dotenv_paths = resolve_dotenv_paths(
|
||||
dotenv_path=dotenv_path,
|
||||
config_dotenv_path=config_dotenv_path,
|
||||
)
|
||||
raw.update(_load_dotenv(cwd_dotenv))
|
||||
raw.update(_load_dotenv(home_config_dotenv))
|
||||
raw.update(_load_dotenv(dotenv_paths.cwd))
|
||||
raw.update(_load_dotenv(dotenv_paths.config))
|
||||
raw.update(os.environ if env is None else env)
|
||||
|
||||
api_base = raw.get("PROXMOX_API_BASE", "/api2/json").strip() or "/api2/json"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
import asyncio
|
||||
import time
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from textual.app import App, ComposeResult
|
||||
|
|
@ -11,8 +12,10 @@ 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.env_setup import EnvSetupModal, MissingEnvSetupPromptModal
|
||||
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.factory import ProxmoxServiceFactory
|
||||
from pve_vm_setup.services.fake import FakeProxmoxService
|
||||
from pve_vm_setup.settings import AppSettings
|
||||
|
||||
|
|
@ -85,6 +88,80 @@ async def test_main_app_mounts_wizard_only_after_login() -> None:
|
|||
assert app.focused is app.query_one("#general-name", Input)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_main_app_prompts_for_env_setup_when_no_dotenv_exists(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
home = tmp_path / "home"
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setenv("HOME", str(home))
|
||||
|
||||
app = PveVmSetupApp(
|
||||
AppSettings.from_env({}, load_dotenv_file=True),
|
||||
prompt_for_missing_env_setup=True,
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
|
||||
assert isinstance(app.screen_stack[-1], MissingEnvSetupPromptModal)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_main_app_can_save_env_setup_and_refresh_login_mode(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
class LiveLikeService(FakeProxmoxService):
|
||||
mode = "live"
|
||||
|
||||
home = tmp_path / "home"
|
||||
config_path = home / ".config" / "pve-vm-setup" / ".env"
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setenv("HOME", str(home))
|
||||
monkeypatch.delenv("PROXMOX_URL", raising=False)
|
||||
monkeypatch.delenv("PROXMOX_USER", raising=False)
|
||||
|
||||
def create_service(settings: AppSettings):
|
||||
if settings.proxmox_url:
|
||||
return LiveLikeService()
|
||||
return FakeProxmoxService()
|
||||
|
||||
monkeypatch.setattr(ProxmoxServiceFactory, "create", staticmethod(create_service))
|
||||
|
||||
app = PveVmSetupApp(
|
||||
AppSettings.from_env({}, load_dotenv_file=True),
|
||||
prompt_for_missing_env_setup=True,
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
assert isinstance(app.screen_stack[-1], MissingEnvSetupPromptModal)
|
||||
|
||||
app.query_one("#missing-env-setup", Button).press()
|
||||
await pilot.pause()
|
||||
assert isinstance(app.screen_stack[-1], EnvSetupModal)
|
||||
|
||||
app.query_one("#env-proxmox-url", Input).value = "https://pve.example.invalid:8006"
|
||||
app.query_one("#env-proxmox-user", Input).value = "root"
|
||||
app.query_one("#env-setup-save", Button).press()
|
||||
|
||||
for _ in range(10):
|
||||
await pilot.pause(0.05)
|
||||
if not app.screen_stack or not isinstance(app.screen_stack[-1], EnvSetupModal):
|
||||
break
|
||||
|
||||
assert config_path.exists()
|
||||
content = config_path.read_text(encoding="utf-8")
|
||||
assert "PROXMOX_URL=https://pve.example.invalid:8006" in content
|
||||
assert "PROXMOX_USER=root" in content
|
||||
assert "PROXMOX_PASSWORD" not in content
|
||||
|
||||
assert "Mode: live on pve.example.invalid:8006" == str(
|
||||
app.query_one("#mode", Static).renderable
|
||||
)
|
||||
assert app.query_one("#username", Input).value == "root"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wizard_activation_focuses_first_editable_field() -> None:
|
||||
service = FakeProxmoxService()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from pathlib import Path
|
|||
import pytest
|
||||
|
||||
from pve_vm_setup.errors import SettingsError
|
||||
from pve_vm_setup.settings import AppSettings
|
||||
from pve_vm_setup.settings import AppSettings, resolve_dotenv_paths, write_config_dotenv
|
||||
|
||||
|
||||
def test_settings_load_defaults_and_normalize_api_base() -> None:
|
||||
|
|
@ -152,3 +152,37 @@ def test_settings_prefers_environment_over_config_and_current_directory_dotenv(
|
|||
)
|
||||
|
||||
assert settings.proxmox_url == "https://env.example.invalid:8006"
|
||||
|
||||
|
||||
def test_resolve_dotenv_paths_uses_standard_config_location(tmp_path: Path, monkeypatch) -> None:
|
||||
home = tmp_path / "home"
|
||||
monkeypatch.setenv("HOME", str(home))
|
||||
|
||||
paths = resolve_dotenv_paths()
|
||||
|
||||
assert paths.cwd == Path(".env")
|
||||
assert paths.config == home / ".config" / "pve-vm-setup" / ".env"
|
||||
assert paths.any_exists is False
|
||||
|
||||
|
||||
def test_write_config_dotenv_creates_parent_directory_and_skips_blank_values(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
target = tmp_path / "home" / ".config" / "pve-vm-setup" / ".env"
|
||||
|
||||
written_path = write_config_dotenv(
|
||||
{
|
||||
"PROXMOX_URL": "https://pve.example.invalid:8006",
|
||||
"PROXMOX_USER": "root",
|
||||
"PROXMOX_PASSWORD": "",
|
||||
},
|
||||
config_dotenv_path=target,
|
||||
)
|
||||
|
||||
assert written_path == target
|
||||
assert target.exists()
|
||||
assert target.read_text(encoding="utf-8") == (
|
||||
"# Generated by pve-vm-setup\n"
|
||||
"PROXMOX_URL=https://pve.example.invalid:8006\n"
|
||||
"PROXMOX_USER=root\n"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue