tftsr-devops_investigation/tests/unit/VMList.test.tsx
Shaun Arman a9a063f786
Some checks failed
Test / frontend-tests (pull_request) Successful in 1m37s
Test / frontend-typecheck (pull_request) Successful in 1m49s
PR Review Automation / review (pull_request) Successful in 10m13s
Test / rust-fmt-check (pull_request) Failing after 12m20s
Test / rust-clippy (pull_request) Successful in 13m53s
Test / rust-tests (pull_request) Has been cancelled
fix(proxmox): fix VM actions, remove Disk column, add Create VM
Issue 1 — VM actions silently doing nothing:
The root cause was a missing <Toaster> mount in App.tsx. All
toast.success/error calls were no-ops because the sonner Toaster
component was never rendered. Added it at the App root.

Also added dialog:allow-confirm capability (was missing, caused VM
delete confirmation to throw silently).

Issue 2 — Remove Disk column:
PVE cluster/resources returns only static disk allocation, not actual
usage, making the column misleading. Removed from VMList header, row,
and the diskPercent calculation.

Issue 3 — Add VM creation:
- New list_proxmox_nodes Tauri command (GET /nodes) for real node list
- New create_proxmox_vm Tauri command with server-side input validation:
  vmid range, numeric bounds, node/storage/bridge path-safety check,
  ISO volume-ID format check to prevent comma-property injection
- CreateVmDialog component with node/storage discovery on open
- "Add VM" button wired into VMsPage

MigrationDialog now fetches real cluster nodes via list_proxmox_nodes
instead of inferring them from the VMs already in the list.

Added suspendProxmoxVm, resumeProxmoxVm, listProxmoxNodes,
createProxmoxVm client wrappers to proxmoxClient.ts.

Tests: 446 Rust + 405 frontend, all pass. 19 new VMList tests (TDD),
7 new Rust tests for security validation logic.
2026-06-21 18:01:37 -05:00

314 lines
8.5 KiB
TypeScript

import React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { invoke } from "@tauri-apps/api/core";
import { VMList } from "@/components/Proxmox/VMList";
vi.mock("@tauri-apps/api/core");
vi.mock("sonner", () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
info: vi.fn(),
},
Toaster: () => null,
}));
type MockedInvoke = typeof invoke & {
mockResolvedValue: (v: unknown) => void;
mockRejectedValue: (e: unknown) => void;
mockResolvedValueOnce: (v: unknown) => void;
};
const mockInvoke = invoke as MockedInvoke;
const stoppedVm = {
id: 101,
name: "nginx",
node: "vmhost2",
status: "stopped",
cpu: 0,
mem: 0,
max_mem: 2 * 1024 * 1024 * 1024,
disk: 0,
max_disk: 20 * 1024 * 1024 * 1024,
uptime: 0,
};
const runningVm = {
id: 102,
name: "docker-01",
node: "vmhost2",
status: "running",
cpu: 0.35,
mem: 512 * 1024 * 1024,
max_mem: 2 * 1024 * 1024 * 1024,
disk: 0,
max_disk: 20 * 1024 * 1024 * 1024,
uptime: 3600,
};
const pausedVm = {
id: 103,
name: "test-vm",
node: "vmhost2",
status: "paused",
cpu: 0,
mem: 0,
max_mem: 1024 * 1024 * 1024,
disk: 0,
max_disk: 10 * 1024 * 1024 * 1024,
uptime: 0,
};
const mockClusters = [{ id: "cluster-1", name: "TFTSR" }];
function renderVMList(vms = [stoppedVm], clusterId = "cluster-1") {
const onRefresh = vi.fn();
return {
onRefresh,
...render(
<VMList
vms={vms}
clusterId={clusterId}
clusters={mockClusters as never}
onRefresh={onRefresh}
/>
),
};
}
describe("VMList — column rendering", () => {
it("renders VM name, VMID, node, status, CPU, memory and uptime columns", () => {
renderVMList([stoppedVm]);
expect(screen.getByText("Name")).toBeDefined();
expect(screen.getByText("VM ID")).toBeDefined();
expect(screen.getByText("Node")).toBeDefined();
expect(screen.getByText("Status")).toBeDefined();
expect(screen.getByText("CPU")).toBeDefined();
expect(screen.getByText("Memory")).toBeDefined();
expect(screen.getByText("Uptime")).toBeDefined();
});
it("does NOT render the Disk column", () => {
renderVMList([stoppedVm]);
expect(screen.queryByText("Disk")).toBeNull();
});
it("displays VM name in the list", () => {
renderVMList([stoppedVm]);
expect(screen.getByText("nginx")).toBeDefined();
});
it("displays the correct VMID", () => {
renderVMList([stoppedVm]);
expect(screen.getByText("101")).toBeDefined();
});
it("displays status badge for stopped VM", () => {
renderVMList([stoppedVm]);
expect(screen.getByText("stopped")).toBeDefined();
});
it("displays status badge for running VM", () => {
renderVMList([runningVm]);
expect(screen.getByText("running")).toBeDefined();
});
});
describe("VMList — action menu for stopped VM", () => {
beforeEach(() => {
vi.clearAllMocks();
mockInvoke.mockResolvedValue(undefined);
});
it("shows Start action for stopped VM", async () => {
renderVMList([stoppedVm]);
const menuBtn = screen.getAllByRole("button").find(
(b) => b.querySelector("svg")
);
fireEvent.click(menuBtn!);
expect(screen.getByText("Start")).toBeDefined();
});
it("does NOT show Stop/Reboot/Shutdown for stopped VM", async () => {
renderVMList([stoppedVm]);
const menuBtn = screen.getAllByRole("button").find(
(b) => b.querySelector("svg")
);
fireEvent.click(menuBtn!);
expect(screen.queryByText("Stop")).toBeNull();
expect(screen.queryByText("Reboot")).toBeNull();
expect(screen.queryByText("Shutdown")).toBeNull();
});
it("calls start_proxmox_vm when Start is clicked", async () => {
renderVMList([stoppedVm]);
const menuBtn = screen.getAllByRole("button").find(
(b) => b.querySelector("svg")
);
fireEvent.click(menuBtn!);
fireEvent.click(screen.getByText("Start"));
await waitFor(() => {
expect(mockInvoke).toHaveBeenCalledWith("start_proxmox_vm", {
clusterId: "cluster-1",
nodeId: "vmhost2",
vmId: 101,
});
});
});
});
describe("VMList — action menu for running VM", () => {
beforeEach(() => {
vi.clearAllMocks();
mockInvoke.mockResolvedValue(undefined);
});
it("shows Stop, Reboot, Shutdown, Suspend actions for running VM", async () => {
renderVMList([runningVm]);
const menuBtn = screen.getAllByRole("button").find(
(b) => b.querySelector("svg")
);
fireEvent.click(menuBtn!);
expect(screen.getByText("Stop")).toBeDefined();
expect(screen.getByText("Reboot")).toBeDefined();
expect(screen.getByText("Shutdown")).toBeDefined();
expect(screen.getByText("Suspend")).toBeDefined();
});
it("calls stop_proxmox_vm when Stop is clicked", async () => {
renderVMList([runningVm]);
const menuBtn = screen.getAllByRole("button").find(
(b) => b.querySelector("svg")
);
fireEvent.click(menuBtn!);
fireEvent.click(screen.getByText("Stop"));
await waitFor(() => {
expect(mockInvoke).toHaveBeenCalledWith("stop_proxmox_vm", {
clusterId: "cluster-1",
nodeId: "vmhost2",
vmId: 102,
});
});
});
it("calls reboot_proxmox_vm when Reboot is clicked", async () => {
renderVMList([runningVm]);
const menuBtn = screen.getAllByRole("button").find(
(b) => b.querySelector("svg")
);
fireEvent.click(menuBtn!);
fireEvent.click(screen.getByText("Reboot"));
await waitFor(() => {
expect(mockInvoke).toHaveBeenCalledWith("reboot_proxmox_vm", {
clusterId: "cluster-1",
nodeId: "vmhost2",
vmId: 102,
});
});
});
it("calls shutdown_proxmox_vm when Shutdown is clicked", async () => {
renderVMList([runningVm]);
const menuBtn = screen.getAllByRole("button").find(
(b) => b.querySelector("svg")
);
fireEvent.click(menuBtn!);
fireEvent.click(screen.getByText("Shutdown"));
await waitFor(() => {
expect(mockInvoke).toHaveBeenCalledWith("shutdown_proxmox_vm", {
clusterId: "cluster-1",
nodeId: "vmhost2",
vmId: 102,
});
});
});
it("calls suspend_proxmox_vm when Suspend is clicked", async () => {
renderVMList([runningVm]);
const menuBtn = screen.getAllByRole("button").find(
(b) => b.querySelector("svg")
);
fireEvent.click(menuBtn!);
fireEvent.click(screen.getByText("Suspend"));
await waitFor(() => {
expect(mockInvoke).toHaveBeenCalledWith("suspend_proxmox_vm", {
clusterId: "cluster-1",
nodeId: "vmhost2",
vmId: 102,
});
});
});
});
describe("VMList — action menu for paused VM", () => {
beforeEach(() => {
vi.clearAllMocks();
mockInvoke.mockResolvedValue(undefined);
});
it("shows Resume action for paused VM", async () => {
renderVMList([pausedVm]);
const menuBtn = screen.getAllByRole("button").find(
(b) => b.querySelector("svg")
);
fireEvent.click(menuBtn!);
expect(screen.getByText("Resume")).toBeDefined();
});
it("calls resume_proxmox_vm when Resume is clicked", async () => {
renderVMList([pausedVm]);
const menuBtn = screen.getAllByRole("button").find(
(b) => b.querySelector("svg")
);
fireEvent.click(menuBtn!);
fireEvent.click(screen.getByText("Resume"));
await waitFor(() => {
expect(mockInvoke).toHaveBeenCalledWith("resume_proxmox_vm", {
clusterId: "cluster-1",
nodeId: "vmhost2",
vmId: 103,
});
});
});
});
describe("VMList — migrate action", () => {
beforeEach(() => {
vi.clearAllMocks();
mockInvoke.mockResolvedValue([
{ node: "vmhost1", status: "online" },
{ node: "vmhost3", status: "online" },
]);
});
it("shows Migrate option in action menu", async () => {
renderVMList([runningVm]);
const menuBtn = screen.getAllByRole("button").find(
(b) => b.querySelector("svg")
);
fireEvent.click(menuBtn!);
expect(screen.getByText("Migrate")).toBeDefined();
});
it("opens migration dialog when Migrate is clicked", async () => {
renderVMList([runningVm]);
const menuBtn = screen.getAllByRole("button").find(
(b) => b.querySelector("svg")
);
fireEvent.click(menuBtn!);
fireEvent.click(screen.getByText("Migrate"));
await waitFor(() => {
expect(screen.getByText(/Migrate docker-01/i)).toBeDefined();
});
});
});
describe("VMList — empty state", () => {
it("renders empty table body with no VMs", () => {
renderVMList([]);
expect(screen.getByText("Virtual Machines")).toBeDefined();
});
});