feat(web): delete a template, not just back it up
POST /host/delete-template: same two-step confirm gate, CSRF check, and known-template containment as /host/backup, offloaded to a thread the same way (rmtree on a multi-GB template dir is not instant). No backup-required safety rail -- same confirm-gate as every other destructive action, nothing more; the confirm summary spells out that clones can no longer be made from the template until one is restored. Also cleaned up the per-backup Delete button it sits next to: name+meta grouped into one shrinkable unit so the row wraps as a block instead of the delete button drifting away from a name that wrapped, and each backup is now its own bordered line instead of a run-on flex list.
This commit is contained in:
@@ -918,6 +918,88 @@ def test_host_backup_execute_exception_never_500s(
|
||||
assert entry.result == "error"
|
||||
|
||||
|
||||
# ── POST /host/delete-template ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_host_delete_template_missing_template_field_is_400(tmp_path: Path) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
app = create_app(config)
|
||||
client = TestClient(app)
|
||||
csrf = _get_csrf(client)
|
||||
|
||||
resp = client.post("/host/delete-template", data={"csrf": csrf})
|
||||
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_host_delete_template_unknown_template_name_is_400_not_audited(tmp_path: Path) -> None:
|
||||
"""Same B6 containment discipline as ``/host/backup``: an arbitrary name
|
||||
is rejected before ``delete_template`` is ever called."""
|
||||
config = _make_config(tmp_path)
|
||||
app = create_app(config)
|
||||
client = TestClient(app)
|
||||
csrf = _get_csrf(client)
|
||||
|
||||
resp = client.post(
|
||||
"/host/delete-template", data={"csrf": csrf, "template": "NoSuchTemplate"}
|
||||
)
|
||||
|
||||
assert resp.status_code == 400
|
||||
assert not config.web.audit_log.exists()
|
||||
|
||||
|
||||
def test_host_delete_template_preview_then_execute_full_cycle(tmp_path: Path) -> None:
|
||||
template_dir = tmp_path / "templates" / "LinuxBuild2404"
|
||||
template_dir.mkdir(parents=True)
|
||||
(template_dir / "LinuxBuild2404.vmx").write_text("x", encoding="utf-8")
|
||||
|
||||
config = _make_config(tmp_path)
|
||||
app = create_app(config)
|
||||
client = TestClient(app)
|
||||
csrf = _get_csrf(client)
|
||||
|
||||
preview = client.post(
|
||||
"/host/delete-template", data={"csrf": csrf, "template": "LinuxBuild2404"}
|
||||
)
|
||||
assert preview.status_code == 200
|
||||
assert "Confirm" in preview.text
|
||||
assert "permanently delete" in preview.text
|
||||
assert _last_audit_entry(config.web.audit_log).result == "dry-run"
|
||||
assert template_dir.is_dir() # nothing removed on the preview stage
|
||||
|
||||
execute = client.post(
|
||||
"/host/delete-template",
|
||||
data={"csrf": csrf, "stage": "execute", "template": "LinuxBuild2404"},
|
||||
)
|
||||
assert execute.status_code == 200
|
||||
entry = _last_audit_entry(config.web.audit_log)
|
||||
assert entry.result == "ok"
|
||||
assert entry.action == "host.delete_template"
|
||||
assert not template_dir.exists()
|
||||
|
||||
|
||||
def test_host_delete_template_execute_exception_never_500s(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
def _boom(*args: Any, **kwargs: Any) -> Any:
|
||||
raise RuntimeError("device busy")
|
||||
|
||||
monkeypatch.setattr(routes_module, "delete_template", _boom)
|
||||
(tmp_path / "templates" / "X").mkdir(parents=True)
|
||||
config = _make_config(tmp_path)
|
||||
app = create_app(config)
|
||||
client = TestClient(app)
|
||||
csrf = _get_csrf(client)
|
||||
|
||||
resp = client.post(
|
||||
"/host/delete-template", data={"csrf": csrf, "stage": "execute", "template": "X"}
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
entry = _last_audit_entry(config.web.audit_log)
|
||||
assert entry.result == "error"
|
||||
|
||||
|
||||
# ── web/security.py: direct unit coverage of the CSRF primitives ──────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user