tftsr-devops_investigation/tests/unit/NamespaceActionFix.test.tsx

664 lines
22 KiB
TypeScript
Raw Permalink Normal View History

/**
* TDD tests: action IPC calls in network/config/storage/access-control list
* components must use the item's own .namespace, never the filter prop (which
* can be "all" when the user is viewing all namespaces).
*/
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 { ServiceList } from "@/components/Kubernetes/ServiceList";
import { IngressList } from "@/components/Kubernetes/IngressList";
import { ConfigMapList } from "@/components/Kubernetes/ConfigMapList";
import { SecretList } from "@/components/Kubernetes/SecretList";
import { HPAList } from "@/components/Kubernetes/HPAList";
import { PVCList } from "@/components/Kubernetes/PVCList";
import { ServiceAccountList } from "@/components/Kubernetes/ServiceAccountList";
import { RoleList } from "@/components/Kubernetes/RoleList";
import { RoleBindingList } from "@/components/Kubernetes/RoleBindingList";
import { NetworkPolicyList } from "@/components/Kubernetes/NetworkPolicyList";
import { ResourceQuotaList } from "@/components/Kubernetes/ResourceQuotaList";
import { LimitRangeList } from "@/components/Kubernetes/LimitRangeList";
import type {
ServiceInfo,
IngressInfo,
ConfigMapInfo,
SecretInfo,
HorizontalPodAutoscalerInfo,
PersistentVolumeClaimInfo,
ServiceAccountInfo,
RoleInfo,
RoleBindingInfo,
NetworkPolicyInfo,
ResourceQuotaInfo,
LimitRangeInfo,
} from "@/lib/tauriCommands";
type MockedInvoke = typeof invoke & {
mockResolvedValue: (v: unknown) => void;
mockImplementation: (fn: (cmd: string, args?: unknown) => Promise<unknown>) => void;
};
const mockInvoke = invoke as MockedInvoke;
// ─── helpers ─────────────────────────────────────────────────────────────────
/** Open the action menu for the first row whose name cell matches `name`. */
function openActionMenu(name: string) {
const cell = screen.getByText(name);
const row = cell.closest("tr")!;
const btn = row.querySelector("button")!;
fireEvent.click(btn);
}
/** Click the first menu item that contains `label`. */
function clickMenuItem(label: string) {
const item = screen.getByText(label);
fireEvent.click(item);
}
// ─── ServiceList ─────────────────────────────────────────────────────────────
describe("ServiceList action IPC uses item.namespace", () => {
const svc: ServiceInfo = {
name: "my-svc",
namespace: "production",
type: "ClusterIP",
cluster_ip: "10.0.0.1",
ports: [],
age: "1d",
selector: {},
};
beforeEach(() => vi.clearAllMocks());
it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue("yaml: content");
render(
<ServiceList services={[svc]} clusterId="c1" namespace="all" />
);
openActionMenu("my-svc");
clickMenuItem("Edit");
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", {
clusterId: "c1",
resourceType: "services",
namespace: "production",
resourceName: "my-svc",
})
);
});
it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue(undefined);
render(
<ServiceList services={[svc]} clusterId="c1" namespace="all" onRefresh={() => {}} />
);
openActionMenu("my-svc");
clickMenuItem("Delete");
const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i });
fireEvent.click(confirmBtn);
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("delete_resource", {
clusterId: "c1",
resourceType: "services",
namespace: "production",
resourceName: "my-svc",
})
);
});
});
// ─── IngressList ─────────────────────────────────────────────────────────────
describe("IngressList action IPC uses item.namespace", () => {
const ing: IngressInfo = {
name: "my-ingress",
namespace: "staging",
host: "example.com",
addresses: [],
age: "2d",
};
beforeEach(() => vi.clearAllMocks());
it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue("yaml: content");
render(
<IngressList ingresses={[ing]} clusterId="c1" namespace="all" />
);
openActionMenu("my-ingress");
clickMenuItem("Edit");
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", {
clusterId: "c1",
resourceType: "ingresses",
namespace: "staging",
resourceName: "my-ingress",
})
);
});
it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue(undefined);
render(
<IngressList ingresses={[ing]} clusterId="c1" namespace="all" onRefresh={() => {}} />
);
openActionMenu("my-ingress");
clickMenuItem("Delete");
const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i });
fireEvent.click(confirmBtn);
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("delete_resource", {
clusterId: "c1",
resourceType: "ingresses",
namespace: "staging",
resourceName: "my-ingress",
})
);
});
});
// ─── ConfigMapList ────────────────────────────────────────────────────────────
describe("ConfigMapList action IPC uses item.namespace", () => {
const cm: ConfigMapInfo = {
name: "app-config",
namespace: "kube-system",
data_keys: 3,
age: "10d",
};
beforeEach(() => vi.clearAllMocks());
it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue("yaml: content");
render(
<ConfigMapList configmaps={[cm]} clusterId="c1" namespace="all" />
);
openActionMenu("app-config");
clickMenuItem("Edit");
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", {
clusterId: "c1",
resourceType: "configmaps",
namespace: "kube-system",
resourceName: "app-config",
})
);
});
it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue(undefined);
render(
<ConfigMapList configmaps={[cm]} clusterId="c1" namespace="all" onRefresh={() => {}} />
);
openActionMenu("app-config");
clickMenuItem("Delete");
const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i });
fireEvent.click(confirmBtn);
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("delete_resource", {
clusterId: "c1",
resourceType: "configmaps",
namespace: "kube-system",
resourceName: "app-config",
})
);
});
});
// ─── SecretList ───────────────────────────────────────────────────────────────
describe("SecretList action IPC uses item.namespace", () => {
const secret: SecretInfo = {
name: "db-creds",
namespace: "production",
type: "Opaque",
data_keys: 2,
age: "5d",
};
beforeEach(() => vi.clearAllMocks());
it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue("yaml: content");
render(
<SecretList secrets={[secret]} clusterId="c1" namespace="all" />
);
openActionMenu("db-creds");
clickMenuItem("Edit");
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", {
clusterId: "c1",
resourceType: "secrets",
namespace: "production",
resourceName: "db-creds",
})
);
});
it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue(undefined);
render(
<SecretList secrets={[secret]} clusterId="c1" namespace="all" onRefresh={() => {}} />
);
openActionMenu("db-creds");
clickMenuItem("Delete");
const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i });
fireEvent.click(confirmBtn);
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("delete_resource", {
clusterId: "c1",
resourceType: "secrets",
namespace: "production",
resourceName: "db-creds",
})
);
});
});
// ─── HPAList ──────────────────────────────────────────────────────────────────
describe("HPAList action IPC uses item.namespace", () => {
const hpa: HorizontalPodAutoscalerInfo = {
name: "web-hpa",
namespace: "default",
min_replicas: 1,
max_replicas: 10,
current_replicas: 3,
desired_replicas: 3,
age: "7d",
};
beforeEach(() => vi.clearAllMocks());
it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue("yaml: content");
render(
<HPAList hpas={[hpa]} clusterId="c1" namespace="all" />
);
openActionMenu("web-hpa");
clickMenuItem("Edit");
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", {
clusterId: "c1",
resourceType: "horizontalpodautoscalers",
namespace: "default",
resourceName: "web-hpa",
})
);
});
it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue(undefined);
render(
<HPAList hpas={[hpa]} clusterId="c1" namespace="all" onRefresh={() => {}} />
);
openActionMenu("web-hpa");
clickMenuItem("Delete");
const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i });
fireEvent.click(confirmBtn);
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("delete_resource", {
clusterId: "c1",
resourceType: "horizontalpodautoscalers",
namespace: "default",
resourceName: "web-hpa",
})
);
});
});
// ─── PVCList ──────────────────────────────────────────────────────────────────
describe("PVCList action IPC uses item.namespace", () => {
const pvc: PersistentVolumeClaimInfo = {
name: "data-pvc",
namespace: "staging",
status: "Bound",
volume: "pv-001",
capacity: "10Gi",
access_modes: ["ReadWriteOnce"],
age: "3d",
};
beforeEach(() => vi.clearAllMocks());
it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue("yaml: content");
render(
<PVCList pvcs={[pvc]} clusterId="c1" namespace="all" />
);
openActionMenu("data-pvc");
clickMenuItem("Edit");
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", {
clusterId: "c1",
resourceType: "persistentvolumeclaims",
namespace: "staging",
resourceName: "data-pvc",
})
);
});
it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue(undefined);
render(
<PVCList pvcs={[pvc]} clusterId="c1" namespace="all" onRefresh={() => {}} />
);
openActionMenu("data-pvc");
clickMenuItem("Delete");
const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i });
fireEvent.click(confirmBtn);
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("delete_resource", {
clusterId: "c1",
resourceType: "persistentvolumeclaims",
namespace: "staging",
resourceName: "data-pvc",
})
);
});
});
// ─── ServiceAccountList ───────────────────────────────────────────────────────
describe("ServiceAccountList action IPC uses item.namespace", () => {
const sa: ServiceAccountInfo = {
name: "app-sa",
namespace: "production",
secrets: 1,
age: "30d",
};
beforeEach(() => vi.clearAllMocks());
it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue("yaml: content");
render(
<ServiceAccountList serviceAccounts={[sa]} clusterId="c1" namespace="all" />
);
openActionMenu("app-sa");
clickMenuItem("Edit");
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", {
clusterId: "c1",
resourceType: "serviceaccounts",
namespace: "production",
resourceName: "app-sa",
})
);
});
it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue(undefined);
render(
<ServiceAccountList serviceAccounts={[sa]} clusterId="c1" namespace="all" onRefresh={() => {}} />
);
openActionMenu("app-sa");
clickMenuItem("Delete");
const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i });
fireEvent.click(confirmBtn);
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("delete_resource", {
clusterId: "c1",
resourceType: "serviceaccounts",
namespace: "production",
resourceName: "app-sa",
})
);
});
});
// ─── RoleList ─────────────────────────────────────────────────────────────────
describe("RoleList action IPC uses item.namespace", () => {
const role: RoleInfo = {
name: "pod-reader",
namespace: "default",
age: "14d",
};
beforeEach(() => vi.clearAllMocks());
it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue("yaml: content");
render(
<RoleList roles={[role]} clusterId="c1" namespace="all" />
);
openActionMenu("pod-reader");
clickMenuItem("Edit");
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", {
clusterId: "c1",
resourceType: "roles",
namespace: "default",
resourceName: "pod-reader",
})
);
});
it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue(undefined);
render(
<RoleList roles={[role]} clusterId="c1" namespace="all" onRefresh={() => {}} />
);
openActionMenu("pod-reader");
clickMenuItem("Delete");
const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i });
fireEvent.click(confirmBtn);
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("delete_resource", {
clusterId: "c1",
resourceType: "roles",
namespace: "default",
resourceName: "pod-reader",
})
);
});
});
// ─── RoleBindingList ──────────────────────────────────────────────────────────
describe("RoleBindingList action IPC uses item.namespace", () => {
const rb: RoleBindingInfo = {
name: "pod-reader-binding",
namespace: "default",
role: "pod-reader",
age: "10d",
};
beforeEach(() => vi.clearAllMocks());
it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue("yaml: content");
render(
<RoleBindingList roleBindings={[rb]} clusterId="c1" namespace="all" />
);
openActionMenu("pod-reader-binding");
clickMenuItem("Edit");
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", {
clusterId: "c1",
resourceType: "rolebindings",
namespace: "default",
resourceName: "pod-reader-binding",
})
);
});
it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue(undefined);
render(
<RoleBindingList roleBindings={[rb]} clusterId="c1" namespace="all" onRefresh={() => {}} />
);
openActionMenu("pod-reader-binding");
clickMenuItem("Delete");
const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i });
fireEvent.click(confirmBtn);
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("delete_resource", {
clusterId: "c1",
resourceType: "rolebindings",
namespace: "default",
resourceName: "pod-reader-binding",
})
);
});
});
// ─── NetworkPolicyList ────────────────────────────────────────────────────────
describe("NetworkPolicyList action IPC uses item.namespace", () => {
const np: NetworkPolicyInfo = {
name: "deny-all",
namespace: "production",
pod_selector: "{}",
policy_types: ["Ingress"],
age: "3d",
};
beforeEach(() => vi.clearAllMocks());
it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue("yaml: content");
render(
<NetworkPolicyList networkpolicies={[np]} clusterId="c1" namespace="all" />
);
openActionMenu("deny-all");
clickMenuItem("Edit");
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", {
clusterId: "c1",
resourceType: "networkpolicies",
namespace: "production",
resourceName: "deny-all",
})
);
});
it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue(undefined);
render(
<NetworkPolicyList networkpolicies={[np]} clusterId="c1" namespace="all" onRefresh={() => {}} />
);
openActionMenu("deny-all");
clickMenuItem("Delete");
const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i });
fireEvent.click(confirmBtn);
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("delete_resource", {
clusterId: "c1",
resourceType: "networkpolicies",
namespace: "production",
resourceName: "deny-all",
})
);
});
});
// ─── ResourceQuotaList ────────────────────────────────────────────────────────
describe("ResourceQuotaList action IPC uses item.namespace", () => {
const rq: ResourceQuotaInfo = {
name: "compute-resources",
namespace: "default",
request_cpu: "4",
request_memory: "8Gi",
limit_cpu: "8",
limit_memory: "16Gi",
age: "7d",
};
beforeEach(() => vi.clearAllMocks());
it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue("yaml: content");
render(
<ResourceQuotaList resourcequotas={[rq]} clusterId="c1" namespace="all" />
);
openActionMenu("compute-resources");
clickMenuItem("Edit");
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", {
clusterId: "c1",
resourceType: "resourcequotas",
namespace: "default",
resourceName: "compute-resources",
})
);
});
it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue(undefined);
render(
<ResourceQuotaList resourcequotas={[rq]} clusterId="c1" namespace="all" onRefresh={() => {}} />
);
openActionMenu("compute-resources");
clickMenuItem("Delete");
const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i });
fireEvent.click(confirmBtn);
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("delete_resource", {
clusterId: "c1",
resourceType: "resourcequotas",
namespace: "default",
resourceName: "compute-resources",
})
);
});
});
// ─── LimitRangeList ───────────────────────────────────────────────────────────
describe("LimitRangeList action IPC uses item.namespace", () => {
const lr: LimitRangeInfo = {
name: "cpu-mem-limits",
namespace: "default",
limit_count: 3,
age: "14d",
};
beforeEach(() => vi.clearAllMocks());
it("getResourceYamlCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue("yaml: content");
render(
<LimitRangeList limitranges={[lr]} clusterId="c1" namespace="all" />
);
openActionMenu("cpu-mem-limits");
clickMenuItem("Edit");
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", {
clusterId: "c1",
resourceType: "limitranges",
namespace: "default",
resourceName: "cpu-mem-limits",
})
);
});
it("deleteResourceCmd receives item.namespace, not filter prop 'all'", async () => {
mockInvoke.mockResolvedValue(undefined);
render(
<LimitRangeList limitranges={[lr]} clusterId="c1" namespace="all" onRefresh={() => {}} />
);
openActionMenu("cpu-mem-limits");
clickMenuItem("Delete");
const confirmBtn = await screen.findByRole("button", { name: /confirm|delete/i });
fireEvent.click(confirmBtn);
await waitFor(() =>
expect(mockInvoke).toHaveBeenCalledWith("delete_resource", {
clusterId: "c1",
resourceType: "limitranges",
namespace: "default",
resourceName: "cpu-mem-limits",
})
);
});
});