test: add component tests for all 8 UI components
Add 92 new tests across 8 test files covering DriverStatusCard, SnapshotsPage, PVCDetailSection, StorageClassesPage, VolumesPage, MetricsPage, OverviewPage, and BenchmarkPage. Includes shared test-helpers.tsx with fixtures and a lightweight CommonComponents mock. Total tests: 67 → 159. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import { fireEvent, render, screen, waitFor, act } from '@testing-library/react';
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||
ApiProxy: {
|
||||
request: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
ConfigStore: class {
|
||||
get() { return {}; }
|
||||
set() {}
|
||||
update() {}
|
||||
useConfig() { return () => ({}); }
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () =>
|
||||
require('./__mocks__/commonComponents.ts')
|
||||
);
|
||||
|
||||
vi.mock('../api/TnsCsiDataContext');
|
||||
vi.mock('../api/kbench', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../api/kbench')>();
|
||||
return {
|
||||
...actual,
|
||||
createPvc: vi.fn().mockResolvedValue(undefined),
|
||||
createJob: vi.fn().mockResolvedValue(undefined),
|
||||
deleteJob: vi.fn().mockResolvedValue(undefined),
|
||||
deletePvc: vi.fn().mockResolvedValue(undefined),
|
||||
getJobPhase: vi.fn().mockResolvedValue({ phase: 'Active', job: {} }),
|
||||
fetchKbenchLogs: vi.fn().mockResolvedValue(''),
|
||||
listKbenchJobs: vi.fn().mockResolvedValue([]),
|
||||
generateJobName: vi.fn().mockReturnValue('kbench-abc123'),
|
||||
generatePvcName: vi.fn().mockReturnValue('kbench-abc123-pvc'),
|
||||
};
|
||||
});
|
||||
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import { ApiProxy } from '@kinvolk/headlamp-plugin/lib';
|
||||
import {
|
||||
createPvc,
|
||||
createJob,
|
||||
deleteJob,
|
||||
deletePvc,
|
||||
getJobPhase,
|
||||
fetchKbenchLogs,
|
||||
listKbenchJobs,
|
||||
parseKbenchLog,
|
||||
} from '../api/kbench';
|
||||
import { defaultContext, makeSampleStorageClass } from '../test-helpers';
|
||||
import BenchmarkPage from './BenchmarkPage';
|
||||
|
||||
function mockContext(overrides?: Parameters<typeof defaultContext>[0]) {
|
||||
vi.mocked(useTnsCsiContext).mockReturnValue(defaultContext(overrides));
|
||||
}
|
||||
|
||||
describe('BenchmarkPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(listKbenchJobs).mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it('shows loader when loading', () => {
|
||||
mockContext({ loading: true });
|
||||
render(<BenchmarkPage />);
|
||||
expect(screen.getByTestId('loader')).toHaveTextContent('Loading tns-csi data...');
|
||||
});
|
||||
|
||||
it('renders benchmark guide section', () => {
|
||||
mockContext();
|
||||
render(<BenchmarkPage />);
|
||||
expect(screen.getByText('Benchmark Guide')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Do not cancel mid-run/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Run New Benchmark form', () => {
|
||||
const sc = makeSampleStorageClass();
|
||||
mockContext({ storageClasses: [sc] });
|
||||
render(<BenchmarkPage />);
|
||||
expect(screen.getByText('Run New Benchmark')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Select storage class for benchmark')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('populates SC dropdown with storage class names', () => {
|
||||
const sc1 = makeSampleStorageClass({ metadata: { name: 'sc-a' } });
|
||||
const sc2 = makeSampleStorageClass({ metadata: { name: 'sc-b' } });
|
||||
mockContext({ storageClasses: [sc1, sc2] });
|
||||
render(<BenchmarkPage />);
|
||||
const select = screen.getByLabelText('Select storage class for benchmark') as HTMLSelectElement;
|
||||
expect(select.options.length).toBe(2);
|
||||
expect(select.options[0].value).toBe('sc-a');
|
||||
expect(select.options[1].value).toBe('sc-b');
|
||||
});
|
||||
|
||||
it('shows "No tns-csi storage classes found" when empty', () => {
|
||||
mockContext({ storageClasses: [] });
|
||||
render(<BenchmarkPage />);
|
||||
const select = screen.getByLabelText('Select storage class for benchmark') as HTMLSelectElement;
|
||||
expect(select.options[0].text).toContain('No tns-csi storage classes');
|
||||
});
|
||||
|
||||
it('shows confirmation dialog when Run Benchmark is clicked', () => {
|
||||
const sc = makeSampleStorageClass();
|
||||
mockContext({ storageClasses: [sc] });
|
||||
render(<BenchmarkPage />);
|
||||
fireEvent.click(screen.getByLabelText('Start kbench storage benchmark'));
|
||||
expect(screen.getByText('Confirm Benchmark')).toBeInTheDocument();
|
||||
expect(screen.getByText(/~33Gi PVC/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('cancels confirmation dialog', () => {
|
||||
const sc = makeSampleStorageClass();
|
||||
mockContext({ storageClasses: [sc] });
|
||||
render(<BenchmarkPage />);
|
||||
fireEvent.click(screen.getByLabelText('Start kbench storage benchmark'));
|
||||
fireEvent.click(screen.getByLabelText('Cancel benchmark'));
|
||||
expect(screen.queryByText('Confirm Benchmark')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('starts benchmark on confirmation and calls createPvc', async () => {
|
||||
vi.useFakeTimers();
|
||||
const sc = makeSampleStorageClass();
|
||||
mockContext({ storageClasses: [sc] });
|
||||
|
||||
// PVC bind check
|
||||
vi.mocked(ApiProxy.request).mockResolvedValue({ status: { phase: 'Bound' } });
|
||||
|
||||
render(<BenchmarkPage />);
|
||||
fireEvent.click(screen.getByLabelText('Start kbench storage benchmark'));
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByLabelText('Confirm and start benchmark'));
|
||||
});
|
||||
|
||||
expect(vi.mocked(createPvc)).toHaveBeenCalledTimes(1);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('shows failed state when PVC creation fails', async () => {
|
||||
const sc = makeSampleStorageClass();
|
||||
mockContext({ storageClasses: [sc] });
|
||||
vi.mocked(createPvc).mockRejectedValueOnce(new Error('quota exceeded'));
|
||||
|
||||
render(<BenchmarkPage />);
|
||||
fireEvent.click(screen.getByLabelText('Start kbench storage benchmark'));
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByLabelText('Confirm and start benchmark'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/quota exceeded/)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders past benchmarks section', async () => {
|
||||
mockContext();
|
||||
vi.mocked(listKbenchJobs).mockResolvedValueOnce([]);
|
||||
render(<BenchmarkPage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Past Benchmarks')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('No past benchmark jobs found.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders past benchmark jobs in table', async () => {
|
||||
mockContext();
|
||||
vi.mocked(listKbenchJobs).mockResolvedValueOnce([
|
||||
{
|
||||
jobName: 'kbench-old',
|
||||
namespace: 'default',
|
||||
storageClass: 'tns-nfs',
|
||||
phase: 'Complete',
|
||||
startedAt: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
]);
|
||||
render(<BenchmarkPage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('kbench-old')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Complete')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables Run Benchmark button when no storage classes', () => {
|
||||
mockContext({ storageClasses: [] });
|
||||
render(<BenchmarkPage />);
|
||||
const btn = screen.getByLabelText('Start kbench storage benchmark');
|
||||
expect(btn).toBeDisabled();
|
||||
});
|
||||
|
||||
it('shows confirmation dialog with selected SC and namespace', () => {
|
||||
const sc = makeSampleStorageClass();
|
||||
mockContext({ storageClasses: [sc] });
|
||||
render(<BenchmarkPage />);
|
||||
|
||||
// Change namespace
|
||||
const nsInput = screen.getByLabelText('Kubernetes namespace for benchmark job') as HTMLInputElement;
|
||||
fireEvent.change(nsInput, { target: { value: 'bench-ns' } });
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Start kbench storage benchmark'));
|
||||
// Confirm dialog shows SC and namespace in <strong> tags
|
||||
expect(screen.getByText('Confirm Benchmark')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Confirm and start benchmark')).toBeInTheDocument();
|
||||
// Namespace is shown in the dialog
|
||||
const dialogText = screen.getByText(/bench-ns/);
|
||||
expect(dialogText).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can change test size and mode', () => {
|
||||
const sc = makeSampleStorageClass();
|
||||
mockContext({ storageClasses: [sc] });
|
||||
render(<BenchmarkPage />);
|
||||
|
||||
const sizeInput = screen.getByLabelText('FIO test size') as HTMLInputElement;
|
||||
fireEvent.change(sizeInput, { target: { value: '10G' } });
|
||||
expect(sizeInput.value).toBe('10G');
|
||||
|
||||
const modeSelect = screen.getByLabelText('Benchmark mode') as HTMLSelectElement;
|
||||
fireEvent.change(modeSelect, { target: { value: 'quick' } });
|
||||
expect(modeSelect.value).toBe('quick');
|
||||
});
|
||||
|
||||
it('shows failed state when job creation fails', async () => {
|
||||
const sc = makeSampleStorageClass();
|
||||
mockContext({ storageClasses: [sc] });
|
||||
vi.mocked(createPvc).mockResolvedValueOnce(undefined);
|
||||
// PVC binds immediately
|
||||
vi.mocked(ApiProxy.request).mockResolvedValue({ status: { phase: 'Bound' } });
|
||||
vi.mocked(createJob).mockRejectedValueOnce(new Error('job already exists'));
|
||||
|
||||
render(<BenchmarkPage />);
|
||||
fireEvent.click(screen.getByLabelText('Start kbench storage benchmark'));
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByLabelText('Confirm and start benchmark'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/job already exists/)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Failed')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,183 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () =>
|
||||
require('./__mocks__/commonComponents.ts')
|
||||
);
|
||||
|
||||
import DriverStatusCard from './DriverStatusCard';
|
||||
import { makeSamplePod, sampleCSIDriver, makeSampleMetrics } from '../test-helpers';
|
||||
|
||||
describe('DriverStatusCard', () => {
|
||||
it('shows "Not detected" when no CSI driver is present', () => {
|
||||
render(
|
||||
<DriverStatusCard
|
||||
csiDriver={null}
|
||||
controllerPods={[]}
|
||||
nodePods={[]}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Not detected')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Degraded" when no pods are present', () => {
|
||||
render(
|
||||
<DriverStatusCard
|
||||
csiDriver={sampleCSIDriver}
|
||||
controllerPods={[]}
|
||||
nodePods={[]}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Degraded')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Metrics unavailable" when no metrics provided', () => {
|
||||
render(
|
||||
<DriverStatusCard
|
||||
csiDriver={sampleCSIDriver}
|
||||
controllerPods={[makeSamplePod()]}
|
||||
nodePods={[makeSamplePod({ name: 'tns-csi-node-abc' })]}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Metrics unavailable')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Healthy" and "Connected" when all pods ready and WS connected', () => {
|
||||
const metrics = makeSampleMetrics({ websocketConnected: 1 });
|
||||
render(
|
||||
<DriverStatusCard
|
||||
csiDriver={sampleCSIDriver}
|
||||
controllerPods={[makeSamplePod()]}
|
||||
nodePods={[makeSamplePod({ name: 'tns-csi-node-abc' })]}
|
||||
metrics={metrics}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Healthy')).toBeInTheDocument();
|
||||
expect(screen.getByText('Connected')).toBeInTheDocument();
|
||||
expect(screen.getByText('tns.csi.io installed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Disconnected" when WS is disconnected', () => {
|
||||
const metrics = makeSampleMetrics({ websocketConnected: 0 });
|
||||
render(
|
||||
<DriverStatusCard
|
||||
csiDriver={sampleCSIDriver}
|
||||
controllerPods={[makeSamplePod()]}
|
||||
nodePods={[makeSamplePod({ name: 'tns-csi-node-abc' })]}
|
||||
metrics={metrics}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Disconnected')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Unknown" when websocketConnected is null', () => {
|
||||
const metrics = makeSampleMetrics({ websocketConnected: null });
|
||||
render(
|
||||
<DriverStatusCard
|
||||
csiDriver={sampleCSIDriver}
|
||||
controllerPods={[makeSamplePod()]}
|
||||
nodePods={[makeSamplePod({ name: 'tns-csi-node-abc' })]}
|
||||
metrics={metrics}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Unknown')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders CSI capabilities section when driver is present', () => {
|
||||
render(
|
||||
<DriverStatusCard
|
||||
csiDriver={sampleCSIDriver}
|
||||
controllerPods={[]}
|
||||
nodePods={[]}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('CSI Driver Capabilities')).toBeInTheDocument();
|
||||
expect(screen.getByText('false')).toBeInTheDocument(); // attachRequired
|
||||
expect(screen.getByText('true')).toBeInTheDocument(); // podInfoOnMount
|
||||
expect(screen.getByText('Persistent')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render CSI capabilities when no driver', () => {
|
||||
render(
|
||||
<DriverStatusCard
|
||||
csiDriver={null}
|
||||
controllerPods={[]}
|
||||
nodePods={[]}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByText('CSI Driver Capabilities')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders pod rows with image, restarts, and ready status', () => {
|
||||
const pod = makeSamplePod({
|
||||
metadata: { name: 'ctrl-pod-1', creationTimestamp: '2025-01-01T00:00:00Z' },
|
||||
status: {
|
||||
phase: 'Running',
|
||||
conditions: [{ type: 'Ready', status: 'True' }],
|
||||
containerStatuses: [
|
||||
{ name: 'tns-csi', ready: true, restartCount: 2, image: 'fenio/tns-csi:v0.6.0' },
|
||||
],
|
||||
},
|
||||
});
|
||||
render(
|
||||
<DriverStatusCard
|
||||
csiDriver={sampleCSIDriver}
|
||||
controllerPods={[pod]}
|
||||
nodePods={[]}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('ctrl-pod-1')).toBeInTheDocument();
|
||||
expect(screen.getByText('fenio/tns-csi:v0.6.0')).toBeInTheDocument();
|
||||
expect(screen.getByText('2')).toBeInTheDocument(); // restarts
|
||||
expect(screen.getByText('Running')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "No controller pod found" when controllerPods is empty', () => {
|
||||
render(
|
||||
<DriverStatusCard
|
||||
csiDriver={sampleCSIDriver}
|
||||
controllerPods={[]}
|
||||
nodePods={[makeSamplePod({ name: 'node-1' })]}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('No controller pod found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "No node pods found" when nodePods is empty', () => {
|
||||
render(
|
||||
<DriverStatusCard
|
||||
csiDriver={sampleCSIDriver}
|
||||
controllerPods={[makeSamplePod()]}
|
||||
nodePods={[]}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('No node pods found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows WS reconnects when available in metrics', () => {
|
||||
const metrics = makeSampleMetrics({ websocketReconnectsTotal: 7 });
|
||||
render(
|
||||
<DriverStatusCard
|
||||
csiDriver={sampleCSIDriver}
|
||||
controllerPods={[makeSamplePod()]}
|
||||
nodePods={[makeSamplePod({ name: 'node-1' })]}
|
||||
metrics={metrics}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('WS Reconnects')).toBeInTheDocument();
|
||||
expect(screen.getByText('7')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows node pods count in section title', () => {
|
||||
const node1 = makeSamplePod({ name: 'tns-csi-node-1' });
|
||||
const node2 = makeSamplePod({ name: 'tns-csi-node-2' });
|
||||
render(
|
||||
<DriverStatusCard
|
||||
csiDriver={sampleCSIDriver}
|
||||
controllerPods={[makeSamplePod()]}
|
||||
nodePods={[node1, node2]}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Node Pods (2)')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () =>
|
||||
require('./__mocks__/commonComponents.ts')
|
||||
);
|
||||
|
||||
vi.mock('../api/TnsCsiDataContext');
|
||||
vi.mock('../api/metrics', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../api/metrics')>();
|
||||
return {
|
||||
...actual,
|
||||
fetchControllerMetrics: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import { fetchControllerMetrics } from '../api/metrics';
|
||||
import { defaultContext, makeSamplePod, makeSampleMetrics } from '../test-helpers';
|
||||
import MetricsPage from './MetricsPage';
|
||||
|
||||
function mockContext(overrides?: Parameters<typeof defaultContext>[0]) {
|
||||
vi.mocked(useTnsCsiContext).mockReturnValue(defaultContext(overrides));
|
||||
}
|
||||
|
||||
describe('MetricsPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(fetchControllerMetrics).mockReset();
|
||||
});
|
||||
|
||||
it('shows loader when context is loading', () => {
|
||||
mockContext({ loading: true });
|
||||
render(<MetricsPage />);
|
||||
expect(screen.getByTestId('loader')).toHaveTextContent('Loading tns-csi data...');
|
||||
});
|
||||
|
||||
it('shows "Driver Not Detected" when driver not installed', () => {
|
||||
mockContext({ driverInstalled: false });
|
||||
render(<MetricsPage />);
|
||||
expect(screen.getByText('Driver Not Detected')).toBeInTheDocument();
|
||||
expect(screen.getByText(/TNS-CSI driver not found/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "No controller pod found" when driver installed but no pods', () => {
|
||||
mockContext({ driverInstalled: true, controllerPods: [] });
|
||||
render(<MetricsPage />);
|
||||
expect(screen.getByText('Metrics Unavailable')).toBeInTheDocument();
|
||||
expect(screen.getByText(/No controller pod found/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows metrics error when fetch fails', async () => {
|
||||
const pod = makeSamplePod();
|
||||
mockContext({ driverInstalled: true, controllerPods: [pod] });
|
||||
vi.mocked(fetchControllerMetrics).mockRejectedValueOnce(new Error('connection refused'));
|
||||
render(<MetricsPage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('connection refused')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders three metric cards when fetch succeeds', async () => {
|
||||
const pod = makeSamplePod();
|
||||
const metrics = makeSampleMetrics();
|
||||
mockContext({ driverInstalled: true, controllerPods: [pod] });
|
||||
vi.mocked(fetchControllerMetrics).mockResolvedValueOnce(metrics);
|
||||
render(<MetricsPage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('WebSocket Health')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Volume Operations')).toBeInTheDocument();
|
||||
expect(screen.getByText('CSI Operations')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays correct WebSocket metric data', async () => {
|
||||
const pod = makeSamplePod();
|
||||
const metrics = makeSampleMetrics({
|
||||
websocketConnected: 1,
|
||||
websocketReconnectsTotal: 42,
|
||||
websocketMessagesTotal: [{ labels: {}, value: 250 }],
|
||||
// Zero out other metrics to avoid number collisions
|
||||
volumeOperationsTotal: [],
|
||||
volumeCapacityBytes: [],
|
||||
csiOperationsTotal: [],
|
||||
});
|
||||
mockContext({ driverInstalled: true, controllerPods: [pod] });
|
||||
vi.mocked(fetchControllerMetrics).mockResolvedValueOnce(metrics);
|
||||
render(<MetricsPage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Connected')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('42')).toBeInTheDocument(); // reconnects
|
||||
expect(screen.getByText('250')).toBeInTheDocument(); // messages
|
||||
});
|
||||
|
||||
it('displays CSI operations broken down by method', async () => {
|
||||
const pod = makeSamplePod();
|
||||
const metrics = makeSampleMetrics({
|
||||
csiOperationsTotal: [
|
||||
{ labels: { method: 'CreateVolume' }, value: 77 },
|
||||
{ labels: { method: 'DeleteVolume' }, value: 13 },
|
||||
],
|
||||
// Zero out other metrics to avoid number collisions
|
||||
volumeOperationsTotal: [],
|
||||
volumeCapacityBytes: [],
|
||||
websocketMessagesTotal: [],
|
||||
});
|
||||
mockContext({ driverInstalled: true, controllerPods: [pod] });
|
||||
vi.mocked(fetchControllerMetrics).mockResolvedValueOnce(metrics);
|
||||
render(<MetricsPage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('CreateVolume')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('77')).toBeInTheDocument();
|
||||
expect(screen.getByText('DeleteVolume')).toBeInTheDocument();
|
||||
expect(screen.getByText('13')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('refresh button triggers refetch', async () => {
|
||||
const pod = makeSamplePod();
|
||||
const metrics = makeSampleMetrics();
|
||||
mockContext({ driverInstalled: true, controllerPods: [pod] });
|
||||
vi.mocked(fetchControllerMetrics).mockResolvedValue(metrics);
|
||||
render(<MetricsPage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('WebSocket Health')).toBeInTheDocument();
|
||||
});
|
||||
const initialCallCount = vi.mocked(fetchControllerMetrics).mock.calls.length;
|
||||
fireEvent.click(screen.getByLabelText('Refresh metrics'));
|
||||
await waitFor(() => {
|
||||
expect(vi.mocked(fetchControllerMetrics).mock.calls.length).toBeGreaterThan(initialCallCount);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows "Updated" timestamp after successful fetch', async () => {
|
||||
const pod = makeSamplePod();
|
||||
const metrics = makeSampleMetrics();
|
||||
mockContext({ driverInstalled: true, controllerPods: [pod] });
|
||||
vi.mocked(fetchControllerMetrics).mockResolvedValueOnce(metrics);
|
||||
render(<MetricsPage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Updated:/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows volume operations grouped by protocol', async () => {
|
||||
const pod = makeSamplePod();
|
||||
const metrics = makeSampleMetrics({
|
||||
volumeOperationsTotal: [
|
||||
{ labels: { protocol: 'nfs' }, value: 15 },
|
||||
{ labels: { protocol: 'iscsi' }, value: 8 },
|
||||
],
|
||||
});
|
||||
mockContext({ driverInstalled: true, controllerPods: [pod] });
|
||||
vi.mocked(fetchControllerMetrics).mockResolvedValueOnce(metrics);
|
||||
render(<MetricsPage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Operations (nfs)')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Operations (iscsi)')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,276 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () =>
|
||||
require('./__mocks__/commonComponents.ts')
|
||||
);
|
||||
|
||||
vi.mock('../api/TnsCsiDataContext');
|
||||
vi.mock('../api/metrics', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../api/metrics')>();
|
||||
return {
|
||||
...actual,
|
||||
fetchControllerMetrics: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import { fetchControllerMetrics } from '../api/metrics';
|
||||
import {
|
||||
defaultContext,
|
||||
makeSamplePod,
|
||||
makeSamplePV,
|
||||
makeSamplePVC,
|
||||
makeSampleStorageClass,
|
||||
makeSampleMetrics,
|
||||
sampleCSIDriver,
|
||||
} from '../test-helpers';
|
||||
import OverviewPage from './OverviewPage';
|
||||
|
||||
function mockContext(overrides?: Parameters<typeof defaultContext>[0]) {
|
||||
vi.mocked(useTnsCsiContext).mockReturnValue(defaultContext(overrides));
|
||||
}
|
||||
|
||||
describe('OverviewPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(fetchControllerMetrics).mockReset();
|
||||
});
|
||||
|
||||
it('shows loader when loading', () => {
|
||||
mockContext({ loading: true });
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByTestId('loader')).toHaveTextContent('Loading TNS-CSI data...');
|
||||
});
|
||||
|
||||
it('shows "Driver Not Detected" when driver not installed', () => {
|
||||
mockContext({ driverInstalled: false });
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByText('Driver Not Detected')).toBeInTheDocument();
|
||||
expect(screen.getByText(/CSIDriver tns.csi.io not found/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error section when error is present', () => {
|
||||
mockContext({ error: 'cluster unavailable' });
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByText('cluster unavailable')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('always shows the development status notice', () => {
|
||||
mockContext({ driverInstalled: true });
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByText(/active early development/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders storage summary with SC/PV counts', () => {
|
||||
const sc = makeSampleStorageClass();
|
||||
const pv = makeSamplePV();
|
||||
const pvc = makeSamplePVC();
|
||||
mockContext({
|
||||
driverInstalled: true,
|
||||
csiDriver: sampleCSIDriver,
|
||||
storageClasses: [sc],
|
||||
persistentVolumes: [pv],
|
||||
persistentVolumeClaims: [pvc],
|
||||
controllerPods: [makeSamplePod()],
|
||||
nodePods: [makeSamplePod({ name: 'node-1' })],
|
||||
});
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByText('Storage Summary')).toBeInTheDocument();
|
||||
expect(screen.getByText('Storage Classes')).toBeInTheDocument();
|
||||
expect(screen.getByText('Persistent Volumes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders capacity aggregation from PVs', () => {
|
||||
const pv1 = makeSamplePV({
|
||||
metadata: { name: 'pv-1' },
|
||||
spec: { ...makeSamplePV().spec, capacity: { storage: '100Gi' } },
|
||||
});
|
||||
const pv2 = makeSamplePV({
|
||||
metadata: { name: 'pv-2' },
|
||||
spec: { ...makeSamplePV().spec, capacity: { storage: '50Gi' } },
|
||||
});
|
||||
mockContext({
|
||||
driverInstalled: true,
|
||||
csiDriver: sampleCSIDriver,
|
||||
storageClasses: [makeSampleStorageClass()],
|
||||
persistentVolumes: [pv1, pv2],
|
||||
persistentVolumeClaims: [],
|
||||
controllerPods: [makeSamplePod()],
|
||||
nodePods: [makeSamplePod({ name: 'node-1' })],
|
||||
});
|
||||
render(<OverviewPage />);
|
||||
// 150 GiB total
|
||||
expect(screen.getByText('150.0 GiB')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders protocol distribution bar', () => {
|
||||
const sc1 = makeSampleStorageClass({ parameters: { protocol: 'nfs' } });
|
||||
const sc2 = makeSampleStorageClass({
|
||||
metadata: { name: 'tns-nvmeof' },
|
||||
parameters: { protocol: 'nvmeof' },
|
||||
});
|
||||
mockContext({
|
||||
driverInstalled: true,
|
||||
csiDriver: sampleCSIDriver,
|
||||
storageClasses: [sc1, sc2],
|
||||
persistentVolumes: [],
|
||||
persistentVolumeClaims: [],
|
||||
controllerPods: [makeSamplePod()],
|
||||
nodePods: [makeSamplePod({ name: 'node-1' })],
|
||||
});
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByText('Protocol Distribution')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('percentage-bar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders pool capacity table when poolStats are present', () => {
|
||||
mockContext({
|
||||
driverInstalled: true,
|
||||
csiDriver: sampleCSIDriver,
|
||||
storageClasses: [],
|
||||
persistentVolumes: [],
|
||||
persistentVolumeClaims: [],
|
||||
controllerPods: [makeSamplePod()],
|
||||
nodePods: [makeSamplePod({ name: 'node-1' })],
|
||||
poolStats: [
|
||||
{ name: 'tank', status: 'ONLINE', size: 1e12, allocated: 5e11, free: 5e11 },
|
||||
],
|
||||
});
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByText('Pool Capacity')).toBeInTheDocument();
|
||||
expect(screen.getByText('tank')).toBeInTheDocument();
|
||||
expect(screen.getByText('ONLINE')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows pool stats error hint', () => {
|
||||
mockContext({
|
||||
driverInstalled: true,
|
||||
csiDriver: sampleCSIDriver,
|
||||
storageClasses: [],
|
||||
persistentVolumes: [],
|
||||
persistentVolumeClaims: [],
|
||||
controllerPods: [makeSamplePod()],
|
||||
nodePods: [makeSamplePod({ name: 'node-1' })],
|
||||
poolStatsError: 'API key invalid',
|
||||
});
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByText('Pool Capacity Unavailable')).toBeInTheDocument();
|
||||
expect(screen.getByText('API key invalid')).toBeInTheDocument();
|
||||
expect(screen.getByText(/TrueNAS API key/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Prometheus fallback capacity by pool when no poolStats and metrics available', async () => {
|
||||
const pod = makeSamplePod();
|
||||
const pv = makeSamplePV();
|
||||
const metrics = makeSampleMetrics({
|
||||
volumeCapacityBytes: [
|
||||
{ labels: { volume_id: 'tank/vol-001' }, value: 107374182400 },
|
||||
],
|
||||
});
|
||||
mockContext({
|
||||
driverInstalled: true,
|
||||
csiDriver: sampleCSIDriver,
|
||||
storageClasses: [makeSampleStorageClass()],
|
||||
persistentVolumes: [pv],
|
||||
persistentVolumeClaims: [],
|
||||
controllerPods: [pod],
|
||||
nodePods: [makeSamplePod({ name: 'node-1' })],
|
||||
poolStats: [],
|
||||
poolStatsError: null,
|
||||
});
|
||||
vi.mocked(fetchControllerMetrics).mockResolvedValueOnce(metrics);
|
||||
render(<OverviewPage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Provisioned Capacity by Pool')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders non-bound PVCs table', () => {
|
||||
const pendingPvc = makeSamplePVC({
|
||||
metadata: { name: 'pending-pvc', namespace: 'test', creationTimestamp: '2025-01-01T00:00:00Z' },
|
||||
status: { phase: 'Pending' },
|
||||
});
|
||||
mockContext({
|
||||
driverInstalled: true,
|
||||
csiDriver: sampleCSIDriver,
|
||||
storageClasses: [],
|
||||
persistentVolumes: [],
|
||||
persistentVolumeClaims: [pendingPvc],
|
||||
controllerPods: [makeSamplePod()],
|
||||
nodePods: [makeSamplePod({ name: 'node-1' })],
|
||||
});
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByText('Attention: Non-Bound PVCs')).toBeInTheDocument();
|
||||
expect(screen.getByText('pending-pvc')).toBeInTheDocument();
|
||||
expect(screen.getByText('Pending')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show non-bound PVCs section when all PVCs are bound', () => {
|
||||
const pvc = makeSamplePVC({ status: { phase: 'Bound' } });
|
||||
mockContext({
|
||||
driverInstalled: true,
|
||||
csiDriver: sampleCSIDriver,
|
||||
storageClasses: [],
|
||||
persistentVolumes: [],
|
||||
persistentVolumeClaims: [pvc],
|
||||
controllerPods: [makeSamplePod()],
|
||||
nodePods: [makeSamplePod({ name: 'node-1' })],
|
||||
});
|
||||
render(<OverviewPage />);
|
||||
expect(screen.queryByText('Attention: Non-Bound PVCs')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('refresh button calls context.refresh()', () => {
|
||||
const refreshFn = vi.fn();
|
||||
mockContext({
|
||||
driverInstalled: true,
|
||||
csiDriver: sampleCSIDriver,
|
||||
storageClasses: [],
|
||||
persistentVolumes: [],
|
||||
persistentVolumeClaims: [],
|
||||
controllerPods: [],
|
||||
nodePods: [],
|
||||
refresh: refreshFn,
|
||||
});
|
||||
render(<OverviewPage />);
|
||||
fireEvent.click(screen.getByLabelText('Refresh tns-csi data'));
|
||||
expect(refreshFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('shows metrics unavailable when fetchControllerMetrics fails', async () => {
|
||||
const pod = makeSamplePod();
|
||||
mockContext({
|
||||
driverInstalled: true,
|
||||
csiDriver: sampleCSIDriver,
|
||||
storageClasses: [],
|
||||
persistentVolumes: [],
|
||||
persistentVolumeClaims: [],
|
||||
controllerPods: [pod],
|
||||
nodePods: [makeSamplePod({ name: 'node-1' })],
|
||||
});
|
||||
vi.mocked(fetchControllerMetrics).mockRejectedValueOnce(new Error('timeout'));
|
||||
render(<OverviewPage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Metrics Unavailable')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('timeout')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows PVC status breakdown with Pending and Lost counts', () => {
|
||||
const boundPvc = makeSamplePVC({ metadata: { name: 'pvc-1', namespace: 'ns' }, status: { phase: 'Bound' } });
|
||||
const pendingPvc = makeSamplePVC({ metadata: { name: 'pvc-2', namespace: 'ns' }, status: { phase: 'Pending' } });
|
||||
const lostPvc = makeSamplePVC({ metadata: { name: 'pvc-3', namespace: 'ns' }, status: { phase: 'Lost' } });
|
||||
mockContext({
|
||||
driverInstalled: true,
|
||||
csiDriver: sampleCSIDriver,
|
||||
storageClasses: [],
|
||||
persistentVolumes: [],
|
||||
persistentVolumeClaims: [boundPvc, pendingPvc, lostPvc],
|
||||
controllerPods: [makeSamplePod()],
|
||||
nodePods: [makeSamplePod({ name: 'node-1' })],
|
||||
});
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByText('PVCs (Pending)')).toBeInTheDocument();
|
||||
expect(screen.getByText('PVCs (Lost)')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () =>
|
||||
require('./__mocks__/commonComponents.ts')
|
||||
);
|
||||
|
||||
vi.mock('../api/TnsCsiDataContext');
|
||||
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import { defaultContext, makeSamplePV, makeSamplePVC } from '../test-helpers';
|
||||
import PVCDetailSection from './PVCDetailSection';
|
||||
|
||||
function mockContext(overrides?: Parameters<typeof defaultContext>[0]) {
|
||||
vi.mocked(useTnsCsiContext).mockReturnValue(defaultContext(overrides));
|
||||
}
|
||||
|
||||
describe('PVCDetailSection', () => {
|
||||
it('returns null when loading', () => {
|
||||
mockContext({ loading: true });
|
||||
const { container } = render(
|
||||
<PVCDetailSection resource={{ metadata: { name: 'my-pvc', namespace: 'default' } }} />
|
||||
);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('returns null when PVC is not in filtered list', () => {
|
||||
mockContext({ persistentVolumeClaims: [] });
|
||||
const { container } = render(
|
||||
<PVCDetailSection resource={{ metadata: { name: 'other-pvc', namespace: 'default' } }} />
|
||||
);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('returns null when PVC has no bound PV', () => {
|
||||
const pvc = makeSamplePVC({ metadata: { name: 'orphan-pvc', namespace: 'default' } });
|
||||
mockContext({
|
||||
persistentVolumeClaims: [pvc],
|
||||
persistentVolumes: [], // no PVs to match
|
||||
});
|
||||
const { container } = render(
|
||||
<PVCDetailSection resource={{ metadata: { name: 'orphan-pvc', namespace: 'default' } }} />
|
||||
);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('renders storage details when PVC and PV are found', () => {
|
||||
const pvc = makeSamplePVC();
|
||||
const pv = makeSamplePV();
|
||||
mockContext({
|
||||
persistentVolumeClaims: [pvc],
|
||||
persistentVolumes: [pv],
|
||||
});
|
||||
render(
|
||||
<PVCDetailSection resource={{ metadata: { name: 'my-pvc', namespace: 'default' } }} />
|
||||
);
|
||||
expect(screen.getByText('TNS-CSI Storage Details')).toBeInTheDocument();
|
||||
expect(screen.getByText('tns.csi.io')).toBeInTheDocument();
|
||||
expect(screen.getByText('NFS')).toBeInTheDocument();
|
||||
expect(screen.getByText('10.0.0.1')).toBeInTheDocument();
|
||||
expect(screen.getByText('tns-nfs')).toBeInTheDocument();
|
||||
expect(screen.getByText('tank/vol-001')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders custom volume attributes (excluding protocol and server)', () => {
|
||||
const pv = makeSamplePV({
|
||||
spec: {
|
||||
...makeSamplePV().spec,
|
||||
csi: {
|
||||
driver: 'tns.csi.io',
|
||||
volumeHandle: 'tank/vol-001',
|
||||
volumeAttributes: {
|
||||
protocol: 'nfs',
|
||||
server: '10.0.0.1',
|
||||
pool: 'tank',
|
||||
customAttr: 'customValue',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const pvc = makeSamplePVC();
|
||||
mockContext({
|
||||
persistentVolumeClaims: [pvc],
|
||||
persistentVolumes: [pv],
|
||||
});
|
||||
render(
|
||||
<PVCDetailSection resource={{ metadata: { name: 'my-pvc', namespace: 'default' } }} />
|
||||
);
|
||||
expect(screen.getByText('pool')).toBeInTheDocument();
|
||||
expect(screen.getByText('tank')).toBeInTheDocument();
|
||||
expect(screen.getByText('customAttr')).toBeInTheDocument();
|
||||
expect(screen.getByText('customValue')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () =>
|
||||
require('./__mocks__/commonComponents.ts')
|
||||
);
|
||||
|
||||
vi.mock('../api/TnsCsiDataContext');
|
||||
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import { defaultContext, makeSampleSnapshot, makeSampleSnapshotClass } from '../test-helpers';
|
||||
import SnapshotsPage from './SnapshotsPage';
|
||||
|
||||
function mockContext(overrides?: Parameters<typeof defaultContext>[0]) {
|
||||
vi.mocked(useTnsCsiContext).mockReturnValue(defaultContext(overrides));
|
||||
}
|
||||
|
||||
describe('SnapshotsPage', () => {
|
||||
it('shows loader when loading', () => {
|
||||
mockContext({ loading: true });
|
||||
render(<SnapshotsPage />);
|
||||
expect(screen.getByTestId('loader')).toHaveTextContent('Loading snapshots...');
|
||||
});
|
||||
|
||||
it('shows error state', () => {
|
||||
mockContext({ error: 'something broke' });
|
||||
render(<SnapshotsPage />);
|
||||
expect(screen.getByText('something broke')).toBeInTheDocument();
|
||||
expect(screen.getByText('Error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows notice when snapshot CRD is not available', () => {
|
||||
mockContext({ snapshotCrdAvailable: false });
|
||||
render(<SnapshotsPage />);
|
||||
expect(screen.getByText('Volume Snapshot CRDs Not Installed')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/VolumeSnapshot CRDs.*not found/)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows empty message when snapshots list is empty', () => {
|
||||
mockContext({ snapshotCrdAvailable: true, volumeSnapshots: [] });
|
||||
render(<SnapshotsPage />);
|
||||
expect(screen.getByText('No tns-csi VolumeSnapshots found.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders snapshot classes when available', () => {
|
||||
const vsc = makeSampleSnapshotClass();
|
||||
mockContext({
|
||||
snapshotCrdAvailable: true,
|
||||
volumeSnapshotClasses: [vsc],
|
||||
volumeSnapshots: [],
|
||||
});
|
||||
render(<SnapshotsPage />);
|
||||
expect(screen.getByText('Snapshot Classes (1)')).toBeInTheDocument();
|
||||
expect(screen.getByText('tns-snap-class')).toBeInTheDocument();
|
||||
expect(screen.getByText('tns.csi.io')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders populated snapshots with readyToUse=true', () => {
|
||||
const snap = makeSampleSnapshot();
|
||||
mockContext({
|
||||
snapshotCrdAvailable: true,
|
||||
volumeSnapshots: [snap],
|
||||
});
|
||||
render(<SnapshotsPage />);
|
||||
expect(screen.getByText('snap-001')).toBeInTheDocument();
|
||||
expect(screen.getByText('my-pvc')).toBeInTheDocument();
|
||||
expect(screen.getByText('Yes')).toBeInTheDocument();
|
||||
expect(screen.getByText('100Gi')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders snapshot with readyToUse=false', () => {
|
||||
const snap = makeSampleSnapshot({
|
||||
status: { readyToUse: false, restoreSize: '50Gi' },
|
||||
});
|
||||
mockContext({
|
||||
snapshotCrdAvailable: true,
|
||||
volumeSnapshots: [snap],
|
||||
});
|
||||
render(<SnapshotsPage />);
|
||||
expect(screen.getByText('No')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders snapshot with readyToUse=undefined as Unknown', () => {
|
||||
const snap = makeSampleSnapshot({
|
||||
status: { readyToUse: undefined },
|
||||
});
|
||||
mockContext({
|
||||
snapshotCrdAvailable: true,
|
||||
volumeSnapshots: [snap],
|
||||
});
|
||||
render(<SnapshotsPage />);
|
||||
expect(screen.getByText('Unknown')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render snapshot classes section when empty', () => {
|
||||
mockContext({
|
||||
snapshotCrdAvailable: true,
|
||||
volumeSnapshotClasses: [],
|
||||
volumeSnapshots: [makeSampleSnapshot()],
|
||||
});
|
||||
render(<SnapshotsPage />);
|
||||
expect(screen.queryByText(/Snapshot Classes/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () =>
|
||||
require('./__mocks__/commonComponents.ts')
|
||||
);
|
||||
|
||||
let mockHash = '';
|
||||
const mockPush = vi.fn();
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useLocation: () => ({ pathname: '/tns-csi/storage-classes', hash: mockHash }),
|
||||
useHistory: () => ({ push: mockPush }),
|
||||
}));
|
||||
|
||||
vi.mock('../api/TnsCsiDataContext');
|
||||
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import { defaultContext, makeSampleStorageClass, makeSamplePV } from '../test-helpers';
|
||||
import StorageClassesPage from './StorageClassesPage';
|
||||
|
||||
function mockContext(overrides?: Parameters<typeof defaultContext>[0]) {
|
||||
vi.mocked(useTnsCsiContext).mockReturnValue(defaultContext(overrides));
|
||||
}
|
||||
|
||||
describe('StorageClassesPage', () => {
|
||||
beforeEach(() => {
|
||||
mockPush.mockClear();
|
||||
mockHash = '';
|
||||
});
|
||||
|
||||
it('shows loader when loading', () => {
|
||||
mockContext({ loading: true });
|
||||
render(<StorageClassesPage />);
|
||||
expect(screen.getByTestId('loader')).toHaveTextContent('Loading storage classes...');
|
||||
});
|
||||
|
||||
it('shows error state', () => {
|
||||
mockContext({ error: 'fetch failed' });
|
||||
render(<StorageClassesPage />);
|
||||
expect(screen.getByText('fetch failed')).toBeInTheDocument();
|
||||
expect(screen.getByText('Error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows empty message when no storage classes', () => {
|
||||
mockContext({ storageClasses: [] });
|
||||
render(<StorageClassesPage />);
|
||||
expect(screen.getByText('No tns-csi StorageClasses found.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders table with all columns populated', () => {
|
||||
const sc = makeSampleStorageClass();
|
||||
const pv = makeSamplePV();
|
||||
mockContext({
|
||||
storageClasses: [sc],
|
||||
persistentVolumes: [pv],
|
||||
});
|
||||
render(<StorageClassesPage />);
|
||||
expect(screen.getByText('tns-nfs')).toBeInTheDocument();
|
||||
expect(screen.getByText('NFS')).toBeInTheDocument();
|
||||
expect(screen.getByText('tank')).toBeInTheDocument();
|
||||
expect(screen.getByText('10.0.0.1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Delete')).toBeInTheDocument();
|
||||
expect(screen.getByText('Yes')).toBeInTheDocument(); // expansion
|
||||
expect(screen.getByText('1')).toBeInTheDocument(); // PV count
|
||||
});
|
||||
|
||||
it('opens detail panel when clicking SC name', () => {
|
||||
const sc = makeSampleStorageClass();
|
||||
mockContext({ storageClasses: [sc] });
|
||||
render(<StorageClassesPage />);
|
||||
fireEvent.click(screen.getByText('tns-nfs'));
|
||||
expect(mockPush).toHaveBeenCalledWith('/tns-csi/storage-classes#tns-nfs');
|
||||
});
|
||||
|
||||
it('renders detail panel when hash is set', () => {
|
||||
mockHash = '#tns-nfs';
|
||||
const sc = makeSampleStorageClass();
|
||||
mockContext({ storageClasses: [sc], persistentVolumes: [] });
|
||||
render(<StorageClassesPage />);
|
||||
expect(screen.getByText('StorageClass Details')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes panel via close button', () => {
|
||||
mockHash = '#tns-nfs';
|
||||
const sc = makeSampleStorageClass();
|
||||
mockContext({ storageClasses: [sc], persistentVolumes: [] });
|
||||
render(<StorageClassesPage />);
|
||||
fireEvent.click(screen.getByLabelText('Close panel'));
|
||||
expect(mockPush).toHaveBeenCalledWith('/tns-csi/storage-classes');
|
||||
});
|
||||
|
||||
it('closes panel via backdrop click', () => {
|
||||
mockHash = '#tns-nfs';
|
||||
const sc = makeSampleStorageClass();
|
||||
mockContext({ storageClasses: [sc], persistentVolumes: [] });
|
||||
render(<StorageClassesPage />);
|
||||
fireEvent.click(screen.getByLabelText('Close panel backdrop'));
|
||||
expect(mockPush).toHaveBeenCalledWith('/tns-csi/storage-classes');
|
||||
});
|
||||
|
||||
it('closes panel on Escape key', () => {
|
||||
mockHash = '#tns-nfs';
|
||||
const sc = makeSampleStorageClass();
|
||||
mockContext({ storageClasses: [sc], persistentVolumes: [] });
|
||||
render(<StorageClassesPage />);
|
||||
fireEvent.keyDown(window, { key: 'Escape' });
|
||||
expect(mockPush).toHaveBeenCalledWith('/tns-csi/storage-classes');
|
||||
});
|
||||
|
||||
it('shows NFS protocol notes in detail panel', () => {
|
||||
mockHash = '#tns-nfs';
|
||||
const sc = makeSampleStorageClass({ parameters: { protocol: 'nfs', pool: 'tank', server: '10.0.0.1' } });
|
||||
mockContext({ storageClasses: [sc], persistentVolumes: [] });
|
||||
render(<StorageClassesPage />);
|
||||
expect(screen.getByText('Protocol Notes')).toBeInTheDocument();
|
||||
expect(screen.getByText(/nfs-common/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows NVMe-oF protocol notes', () => {
|
||||
mockHash = '#tns-nvmeof';
|
||||
const sc = makeSampleStorageClass({
|
||||
metadata: { name: 'tns-nvmeof' },
|
||||
parameters: { protocol: 'nvmeof', pool: 'tank', server: '10.0.0.1' },
|
||||
});
|
||||
mockContext({ storageClasses: [sc], persistentVolumes: [] });
|
||||
render(<StorageClassesPage />);
|
||||
expect(screen.getByText(/nvme-cli/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows iSCSI protocol notes', () => {
|
||||
mockHash = '#tns-iscsi';
|
||||
const sc = makeSampleStorageClass({
|
||||
metadata: { name: 'tns-iscsi' },
|
||||
parameters: { protocol: 'iscsi', pool: 'tank', server: '10.0.0.1' },
|
||||
});
|
||||
mockContext({ storageClasses: [sc], persistentVolumes: [] });
|
||||
render(<StorageClassesPage />);
|
||||
expect(screen.getByText(/open-iscsi/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows PV count for each storage class', () => {
|
||||
const sc1 = makeSampleStorageClass({ metadata: { name: 'sc-a' } });
|
||||
const sc2 = makeSampleStorageClass({ metadata: { name: 'sc-b' } });
|
||||
const pv1 = makeSamplePV({ spec: { ...makeSamplePV().spec, storageClassName: 'sc-a' } });
|
||||
const pv2 = makeSamplePV({
|
||||
metadata: { name: 'pv-2' },
|
||||
spec: { ...makeSamplePV().spec, storageClassName: 'sc-a' },
|
||||
});
|
||||
mockContext({
|
||||
storageClasses: [sc1, sc2],
|
||||
persistentVolumes: [pv1, pv2],
|
||||
});
|
||||
render(<StorageClassesPage />);
|
||||
const cells = screen.getAllByRole('cell');
|
||||
const pvCells = cells.filter(c => c.textContent === '2');
|
||||
expect(pvCells.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () =>
|
||||
require('./__mocks__/commonComponents.ts')
|
||||
);
|
||||
|
||||
let mockHash = '';
|
||||
const mockPush = vi.fn();
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useLocation: () => ({ pathname: '/tns-csi/volumes', hash: mockHash }),
|
||||
useHistory: () => ({ push: mockPush }),
|
||||
}));
|
||||
|
||||
vi.mock('../api/TnsCsiDataContext');
|
||||
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import { defaultContext, makeSamplePV } from '../test-helpers';
|
||||
import VolumesPage from './VolumesPage';
|
||||
|
||||
function mockContext(overrides?: Parameters<typeof defaultContext>[0]) {
|
||||
vi.mocked(useTnsCsiContext).mockReturnValue(defaultContext(overrides));
|
||||
}
|
||||
|
||||
describe('VolumesPage', () => {
|
||||
beforeEach(() => {
|
||||
mockPush.mockClear();
|
||||
mockHash = '';
|
||||
});
|
||||
|
||||
it('shows loader when loading', () => {
|
||||
mockContext({ loading: true });
|
||||
render(<VolumesPage />);
|
||||
expect(screen.getByTestId('loader')).toHaveTextContent('Loading volumes...');
|
||||
});
|
||||
|
||||
it('shows error state', () => {
|
||||
mockContext({ error: 'api error' });
|
||||
render(<VolumesPage />);
|
||||
expect(screen.getByText('api error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows empty message when no PVs', () => {
|
||||
mockContext({ persistentVolumes: [] });
|
||||
render(<VolumesPage />);
|
||||
expect(screen.getByText('No tns-csi PersistentVolumes found.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders PV table with claim ref', () => {
|
||||
const pv = makeSamplePV();
|
||||
mockContext({ persistentVolumes: [pv] });
|
||||
render(<VolumesPage />);
|
||||
expect(screen.getByText('pv-test-001')).toBeInTheDocument();
|
||||
expect(screen.getByText('default/my-pvc')).toBeInTheDocument();
|
||||
expect(screen.getByText('NFS')).toBeInTheDocument();
|
||||
expect(screen.getByText('100Gi')).toBeInTheDocument();
|
||||
expect(screen.getByText('RWO')).toBeInTheDocument();
|
||||
expect(screen.getByText('Bound')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders "—" for PV without claimRef', () => {
|
||||
const pv = makeSamplePV({
|
||||
spec: {
|
||||
...makeSamplePV().spec,
|
||||
claimRef: undefined,
|
||||
},
|
||||
});
|
||||
mockContext({ persistentVolumes: [pv] });
|
||||
render(<VolumesPage />);
|
||||
const cells = screen.getAllByRole('cell');
|
||||
const dashCells = cells.filter(c => c.textContent === '—');
|
||||
expect(dashCells.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('opens detail panel when clicking PV name', () => {
|
||||
const pv = makeSamplePV();
|
||||
mockContext({ persistentVolumes: [pv] });
|
||||
render(<VolumesPage />);
|
||||
fireEvent.click(screen.getByText('pv-test-001'));
|
||||
expect(mockPush).toHaveBeenCalledWith('/tns-csi/volumes#pv-test-001');
|
||||
});
|
||||
|
||||
it('renders detail panel with CSI attributes', () => {
|
||||
mockHash = '#pv-test-001';
|
||||
const pv = makeSamplePV();
|
||||
mockContext({ persistentVolumes: [pv], persistentVolumeClaims: [] });
|
||||
render(<VolumesPage />);
|
||||
expect(screen.getByText('Volume Details')).toBeInTheDocument();
|
||||
expect(screen.getByText('CSI Attributes')).toBeInTheDocument();
|
||||
expect(screen.getByText('tank/vol-001')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Bound PVC section in detail panel when claimRef exists', () => {
|
||||
mockHash = '#pv-test-001';
|
||||
const pv = makeSamplePV();
|
||||
mockContext({ persistentVolumes: [pv] });
|
||||
render(<VolumesPage />);
|
||||
expect(screen.getByText('Bound PVC')).toBeInTheDocument();
|
||||
expect(screen.getByText('my-pvc')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Adoption section when annotation is present', () => {
|
||||
mockHash = '#pv-adoptable';
|
||||
const pv = makeSamplePV({
|
||||
metadata: {
|
||||
name: 'pv-adoptable',
|
||||
annotations: { 'tns-csi.io/adoptable': 'true' },
|
||||
},
|
||||
});
|
||||
mockContext({ persistentVolumes: [pv] });
|
||||
render(<VolumesPage />);
|
||||
expect(screen.getByText('Adoption')).toBeInTheDocument();
|
||||
expect(screen.getByText(/adopted cross-cluster/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes panel on Escape key', () => {
|
||||
mockHash = '#pv-test-001';
|
||||
const pv = makeSamplePV();
|
||||
mockContext({ persistentVolumes: [pv] });
|
||||
render(<VolumesPage />);
|
||||
fireEvent.keyDown(window, { key: 'Escape' });
|
||||
expect(mockPush).toHaveBeenCalledWith('/tns-csi/volumes');
|
||||
});
|
||||
|
||||
it('closes panel via backdrop click', () => {
|
||||
mockHash = '#pv-test-001';
|
||||
const pv = makeSamplePV();
|
||||
mockContext({ persistentVolumes: [pv] });
|
||||
render(<VolumesPage />);
|
||||
fireEvent.click(screen.getByLabelText('Close panel backdrop'));
|
||||
expect(mockPush).toHaveBeenCalledWith('/tns-csi/volumes');
|
||||
});
|
||||
|
||||
it('renders maximize/minimize button in panel', () => {
|
||||
mockHash = '#pv-test-001';
|
||||
const pv = makeSamplePV();
|
||||
mockContext({ persistentVolumes: [pv] });
|
||||
render(<VolumesPage />);
|
||||
const maxBtn = screen.getByLabelText('Maximize');
|
||||
expect(maxBtn).toBeInTheDocument();
|
||||
fireEvent.click(maxBtn);
|
||||
expect(screen.getByLabelText('Minimize')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Lightweight mock implementations of @kinvolk/headlamp-plugin/lib/CommonComponents.
|
||||
* Used via vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => commonComponentsMock).
|
||||
*
|
||||
* Uses React.createElement instead of JSX since this file is .ts (not .tsx).
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
type RC = React.ReactNode;
|
||||
|
||||
export const Loader = ({ title }: { title?: string }) =>
|
||||
React.createElement('div', { 'data-testid': 'loader' }, title);
|
||||
|
||||
export const SectionBox = ({ title, children }: { title?: string; children?: RC }) =>
|
||||
React.createElement('div', { 'data-testid': 'section-box', 'data-title': title },
|
||||
title ? React.createElement('h3', null, title) : null,
|
||||
children
|
||||
);
|
||||
|
||||
export const SectionHeader = ({ title }: { title: string }) =>
|
||||
React.createElement('h1', { 'data-testid': 'section-header' }, title);
|
||||
|
||||
export const SimpleTable = ({
|
||||
columns,
|
||||
data,
|
||||
emptyMessage,
|
||||
}: {
|
||||
columns: Array<{ label: string; getter: (item: unknown) => RC }>;
|
||||
data: unknown[];
|
||||
emptyMessage?: string;
|
||||
}) => {
|
||||
if (data.length === 0 && emptyMessage) {
|
||||
return React.createElement('div', { 'data-testid': 'empty-table' }, emptyMessage);
|
||||
}
|
||||
return React.createElement('table', { 'data-testid': 'simple-table' },
|
||||
React.createElement('thead', null,
|
||||
React.createElement('tr', null,
|
||||
columns.map(col => React.createElement('th', { key: col.label }, col.label))
|
||||
)
|
||||
),
|
||||
React.createElement('tbody', null,
|
||||
data.map((item, i) =>
|
||||
React.createElement('tr', { key: i },
|
||||
columns.map(col => React.createElement('td', { key: col.label }, col.getter(item)))
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const NameValueTable = ({
|
||||
rows,
|
||||
}: {
|
||||
rows: Array<{ name: string; value: RC }>;
|
||||
}) =>
|
||||
React.createElement('table', { 'data-testid': 'name-value-table' },
|
||||
React.createElement('tbody', null,
|
||||
rows.map(row =>
|
||||
React.createElement('tr', { key: row.name },
|
||||
React.createElement('td', null, row.name),
|
||||
React.createElement('td', null, row.value)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
export const StatusLabel = ({
|
||||
status,
|
||||
children,
|
||||
}: {
|
||||
status: string;
|
||||
children?: RC;
|
||||
}) =>
|
||||
React.createElement('span', { 'data-testid': 'status-label', 'data-status': status }, children);
|
||||
|
||||
export const PercentageBar = ({
|
||||
data,
|
||||
}: {
|
||||
data: Array<{ name: string; value: number }>;
|
||||
total: number;
|
||||
}) =>
|
||||
React.createElement('div', { 'data-testid': 'percentage-bar' },
|
||||
data.map(d =>
|
||||
React.createElement('span', { key: d.name }, `${d.name}: ${d.value}`)
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Shared test helpers: mock factories, fixtures, and context setup
|
||||
* for component tests.
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest';
|
||||
import type { TnsCsiContextValue } from './api/TnsCsiDataContext';
|
||||
import type {
|
||||
CSIDriver,
|
||||
TnsCsiPersistentVolume,
|
||||
TnsCsiPersistentVolumeClaim,
|
||||
TnsCsiPod,
|
||||
TnsCsiStorageClass,
|
||||
VolumeSnapshot,
|
||||
VolumeSnapshotClass,
|
||||
} from './api/k8s';
|
||||
import type { TnsCsiMetrics } from './api/metrics';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Default context value (everything empty / zeroed)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function defaultContext(overrides?: Partial<TnsCsiContextValue>): TnsCsiContextValue {
|
||||
return {
|
||||
csiDriver: null,
|
||||
driverInstalled: false,
|
||||
storageClasses: [],
|
||||
persistentVolumes: [],
|
||||
persistentVolumeClaims: [],
|
||||
controllerPods: [],
|
||||
nodePods: [],
|
||||
volumeSnapshots: [],
|
||||
volumeSnapshotClasses: [],
|
||||
snapshotCrdAvailable: false,
|
||||
poolStats: [],
|
||||
poolStatsError: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sample fixtures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const sampleCSIDriver: CSIDriver = {
|
||||
metadata: { name: 'tns.csi.io' },
|
||||
spec: {
|
||||
attachRequired: false,
|
||||
podInfoOnMount: true,
|
||||
volumeLifecycleModes: ['Persistent'],
|
||||
},
|
||||
};
|
||||
|
||||
export function makeSampleStorageClass(overrides?: Partial<TnsCsiStorageClass>): TnsCsiStorageClass {
|
||||
return {
|
||||
metadata: { name: 'tns-nfs', creationTimestamp: '2025-01-01T00:00:00Z' },
|
||||
provisioner: 'tns.csi.io',
|
||||
reclaimPolicy: 'Delete',
|
||||
volumeBindingMode: 'Immediate',
|
||||
allowVolumeExpansion: true,
|
||||
parameters: {
|
||||
protocol: 'nfs',
|
||||
pool: 'tank',
|
||||
server: '10.0.0.1',
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export const sampleStorageClass = makeSampleStorageClass();
|
||||
|
||||
export function makeSamplePV(overrides?: Partial<TnsCsiPersistentVolume>): TnsCsiPersistentVolume {
|
||||
return {
|
||||
metadata: {
|
||||
name: 'pv-test-001',
|
||||
creationTimestamp: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
spec: {
|
||||
csi: {
|
||||
driver: 'tns.csi.io',
|
||||
volumeHandle: 'tank/vol-001',
|
||||
volumeAttributes: {
|
||||
protocol: 'nfs',
|
||||
server: '10.0.0.1',
|
||||
pool: 'tank',
|
||||
},
|
||||
},
|
||||
capacity: { storage: '100Gi' },
|
||||
accessModes: ['ReadWriteOnce'],
|
||||
persistentVolumeReclaimPolicy: 'Delete',
|
||||
storageClassName: 'tns-nfs',
|
||||
claimRef: { name: 'my-pvc', namespace: 'default' },
|
||||
},
|
||||
status: { phase: 'Bound' },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export const samplePV = makeSamplePV();
|
||||
|
||||
export function makeSamplePVC(overrides?: Partial<TnsCsiPersistentVolumeClaim>): TnsCsiPersistentVolumeClaim {
|
||||
return {
|
||||
metadata: {
|
||||
name: 'my-pvc',
|
||||
namespace: 'default',
|
||||
creationTimestamp: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
spec: {
|
||||
storageClassName: 'tns-nfs',
|
||||
accessModes: ['ReadWriteOnce'],
|
||||
resources: { requests: { storage: '100Gi' } },
|
||||
volumeName: 'pv-test-001',
|
||||
},
|
||||
status: {
|
||||
phase: 'Bound',
|
||||
capacity: { storage: '100Gi' },
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export const samplePVC = makeSamplePVC();
|
||||
|
||||
export function makeSamplePod(overrides?: Partial<TnsCsiPod> & { name?: string }): TnsCsiPod {
|
||||
const name = overrides?.name ?? overrides?.metadata?.name ?? 'tns-csi-controller-abc';
|
||||
return {
|
||||
metadata: {
|
||||
name,
|
||||
creationTimestamp: '2025-01-01T00:00:00Z',
|
||||
...overrides?.metadata,
|
||||
},
|
||||
spec: {
|
||||
nodeName: 'node-1',
|
||||
...overrides?.spec,
|
||||
},
|
||||
status: {
|
||||
phase: 'Running',
|
||||
conditions: [{ type: 'Ready', status: 'True' }],
|
||||
containerStatuses: [
|
||||
{
|
||||
name: 'tns-csi',
|
||||
ready: true,
|
||||
restartCount: 0,
|
||||
image: 'fenio/tns-csi:v0.5.0',
|
||||
},
|
||||
],
|
||||
...overrides?.status,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const samplePod = makeSamplePod();
|
||||
|
||||
export function makeSampleSnapshot(overrides?: Partial<VolumeSnapshot>): VolumeSnapshot {
|
||||
return {
|
||||
metadata: {
|
||||
name: 'snap-001',
|
||||
namespace: 'default',
|
||||
creationTimestamp: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
spec: {
|
||||
source: { persistentVolumeClaimName: 'my-pvc' },
|
||||
volumeSnapshotClassName: 'tns-snap-class',
|
||||
},
|
||||
status: {
|
||||
readyToUse: true,
|
||||
restoreSize: '100Gi',
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeSampleSnapshotClass(overrides?: Partial<VolumeSnapshotClass>): VolumeSnapshotClass {
|
||||
return {
|
||||
metadata: {
|
||||
name: 'tns-snap-class',
|
||||
creationTimestamp: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
driver: 'tns.csi.io',
|
||||
deletionPolicy: 'Delete',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeSampleMetrics(overrides?: Partial<TnsCsiMetrics>): TnsCsiMetrics {
|
||||
return {
|
||||
websocketConnected: 1,
|
||||
websocketReconnectsTotal: 3,
|
||||
websocketMessagesTotal: [{ labels: {}, value: 100 }],
|
||||
websocketMessageDurationSeconds: [],
|
||||
volumeOperationsTotal: [
|
||||
{ labels: { protocol: 'nfs' }, value: 10 },
|
||||
{ labels: { protocol: 'iscsi' }, value: 5 },
|
||||
],
|
||||
volumeOperationsDurationSeconds: [],
|
||||
volumeCapacityBytes: [
|
||||
{ labels: { volume_id: 'tank/vol-001' }, value: 107374182400 },
|
||||
],
|
||||
csiOperationsTotal: [
|
||||
{ labels: { method: 'CreateVolume' }, value: 10 },
|
||||
{ labels: { method: 'DeleteVolume' }, value: 2 },
|
||||
],
|
||||
csiOperationsDurationSeconds: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user