This commit includes comprehensive codebase cleanup and refactoring: ## Code Cleanup - Remove dead TypeScript code (types/monitoring.ts - 194 lines duplicate) - Remove unused Go functions (GetClusterNodes, MigratePassword, GetClusterHealthInfo) - Clean up commented-out code blocks across multiple files - Remove unused TypeScript exports (helpTextClass, private tag color helpers) - Delete obsolete test files and components ## localStorage Consolidation - Centralize all storage keys into STORAGE_KEYS constant - Update 5 files to use centralized keys: * utils/apiClient.ts (AUTH, LEGACY_TOKEN) * components/Dashboard/Dashboard.tsx (GUEST_METADATA) * components/Docker/DockerHosts.tsx (DOCKER_METADATA) * App.tsx (PLATFORMS_SEEN) * stores/updates.ts (UPDATES) - Benefits: Single source of truth, prevents typos, better maintainability ## Previous Work Committed - Docker monitoring improvements and disk metrics - Security enhancements and setup fixes - API refactoring and cleanup - Documentation updates - Build system improvements ## Testing - All frontend tests pass (29 tests) - All Go tests pass (15 packages) - Production build successful - Zero breaking changes Total: 186 files changed, 5825 insertions(+), 11602 deletions(-)
218 lines
7.2 KiB
TypeScript
218 lines
7.2 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { render, fireEvent, screen, waitFor, cleanup } from '@solidjs/testing-library';
|
|
import { createStore } from 'solid-js/store';
|
|
import { DockerAgents } from '../DockerAgents';
|
|
import type { DockerHost, RemovedDockerHost } from '@/types/api';
|
|
|
|
let mockWsStore: {
|
|
state: { dockerHosts: DockerHost[]; removedDockerHosts: RemovedDockerHost[] };
|
|
connected: () => boolean;
|
|
reconnecting: () => boolean;
|
|
activeAlerts: unknown[];
|
|
};
|
|
|
|
const allowDockerHostReenrollMock = vi.fn();
|
|
const deleteDockerHostMock = vi.fn();
|
|
const unhideDockerHostMock = vi.fn();
|
|
const notificationSuccessMock = vi.fn();
|
|
const notificationErrorMock = vi.fn();
|
|
const fetchMock = vi.fn();
|
|
|
|
vi.mock('@/App', () => ({
|
|
useWebSocket: () => mockWsStore,
|
|
}));
|
|
|
|
vi.mock('@/api/monitoring', () => ({
|
|
MonitoringAPI: {
|
|
allowDockerHostReenroll: (...args: unknown[]) => allowDockerHostReenrollMock(...args),
|
|
deleteDockerHost: (...args: unknown[]) => deleteDockerHostMock(...args),
|
|
unhideDockerHost: (...args: unknown[]) => unhideDockerHostMock(...args),
|
|
},
|
|
}));
|
|
|
|
vi.mock('@/api/security', () => ({
|
|
SecurityAPI: {
|
|
createToken: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock('@/stores/notifications', () => ({
|
|
notificationStore: {
|
|
success: (...args: unknown[]) => notificationSuccessMock(...args),
|
|
error: (...args: unknown[]) => notificationErrorMock(...args),
|
|
},
|
|
}));
|
|
|
|
const createDockerHost = (overrides?: Partial<DockerHost>): DockerHost => ({
|
|
id: 'host-1',
|
|
agentId: 'agent-1',
|
|
hostname: 'host-1.local',
|
|
displayName: 'Host One',
|
|
cpus: 4,
|
|
totalMemoryBytes: 8 * 1024 * 1024 * 1024,
|
|
uptimeSeconds: 12_345,
|
|
status: 'online',
|
|
lastSeen: Date.now(),
|
|
intervalSeconds: 30,
|
|
containers: [],
|
|
...overrides,
|
|
});
|
|
|
|
const createRemovedHost = (overrides?: Partial<RemovedDockerHost>): RemovedDockerHost => ({
|
|
id: 'host-removed',
|
|
hostname: 'retired-node.local',
|
|
displayName: 'Retired Node',
|
|
removedAt: Date.now() - 60_000,
|
|
...overrides,
|
|
});
|
|
|
|
const setupComponent = (hosts: DockerHost[], removedHosts: RemovedDockerHost[] = []) => {
|
|
const [state] = createStore({
|
|
dockerHosts: hosts,
|
|
removedDockerHosts: removedHosts,
|
|
});
|
|
|
|
mockWsStore = {
|
|
state,
|
|
connected: () => true,
|
|
reconnecting: () => false,
|
|
activeAlerts: [],
|
|
};
|
|
|
|
return render(() => <DockerAgents />);
|
|
};
|
|
|
|
beforeEach(() => {
|
|
allowDockerHostReenrollMock.mockReset();
|
|
deleteDockerHostMock.mockReset();
|
|
unhideDockerHostMock.mockReset();
|
|
notificationSuccessMock.mockReset();
|
|
notificationErrorMock.mockReset();
|
|
fetchMock.mockReset();
|
|
fetchMock.mockResolvedValue(
|
|
new Response(JSON.stringify({ requiresAuth: true, apiTokenConfigured: false }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe('DockerAgents removed hosts', () => {
|
|
it('renders removed hosts card and triggers allow/copy actions', async () => {
|
|
allowDockerHostReenrollMock.mockResolvedValue(undefined);
|
|
const clipboardSpy = vi.fn().mockResolvedValue(undefined);
|
|
|
|
vi.stubGlobal('navigator', { clipboard: { writeText: clipboardSpy } } as unknown as Navigator);
|
|
|
|
setupComponent([], [createRemovedHost()]);
|
|
|
|
expect(screen.getByText('Recently removed container hosts')).toBeInTheDocument();
|
|
const allowButton = screen.getByRole('button', { name: 'Allow re-enroll' });
|
|
fireEvent.click(allowButton);
|
|
|
|
await waitFor(() => expect(allowDockerHostReenrollMock).toHaveBeenCalledWith('host-removed'), { interval: 0 });
|
|
|
|
const copyButton = screen.getByRole('button', { name: 'Copy curl command' });
|
|
fireEvent.click(copyButton);
|
|
|
|
await waitFor(() => expect(clipboardSpy).toHaveBeenCalledTimes(1), { interval: 0 });
|
|
const copiedCommand = clipboardSpy.mock.calls.at(-1)?.[0];
|
|
expect(typeof copiedCommand).toBe('string');
|
|
expect(copiedCommand).toContain('/api/agents/docker/hosts/host-removed/allow-reenroll');
|
|
|
|
expect(notificationSuccessMock).toHaveBeenCalled();
|
|
expect(notificationErrorMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('shows progress stepper for an in-progress removal command', async () => {
|
|
const now = Date.now();
|
|
const host = createDockerHost({
|
|
command: {
|
|
id: 'cmd-1',
|
|
type: 'stop',
|
|
status: 'acknowledged',
|
|
createdAt: now - 60_000,
|
|
updatedAt: now - 20_000,
|
|
dispatchedAt: now - 40_000,
|
|
acknowledgedAt: now - 10_000,
|
|
},
|
|
});
|
|
|
|
setupComponent([host]);
|
|
|
|
const removeButton = screen.getByRole('button', { name: 'Remove' });
|
|
fireEvent.click(removeButton);
|
|
|
|
await screen.findByText('Remove container host "Host One"');
|
|
expect(screen.getByText('acknowledged')).toBeInTheDocument();
|
|
|
|
const progressHeading = screen.getByText('Progress');
|
|
const progressCard = progressHeading.parentElement?.parentElement;
|
|
if (!progressCard) {
|
|
throw new Error('Progress card not found');
|
|
}
|
|
|
|
const steps = Array.from(progressCard.querySelectorAll('li'));
|
|
expect(steps).toHaveLength(4);
|
|
|
|
expect(steps[0]).toHaveTextContent('Stop command queued');
|
|
expect(steps[1]).toHaveTextContent('Instruction delivered to the agent');
|
|
expect(steps[2]).toHaveTextContent('Agent acknowledged the stop request');
|
|
expect(steps[3]).toHaveTextContent('Agent disabled the service and removed autostart');
|
|
|
|
const indicators = steps.map((step) => step.querySelector('span'));
|
|
expect(indicators[0]?.className).toContain('bg-blue-500');
|
|
expect(indicators[1]?.className).toContain('bg-blue-500');
|
|
expect(indicators[2]?.className).toContain('animate-pulse');
|
|
expect(indicators[3]?.className).toContain('bg-gray-300');
|
|
});
|
|
|
|
it('shows waiting message once the agent has already stopped', async () => {
|
|
const host = createDockerHost({
|
|
status: 'online',
|
|
pendingUninstall: true,
|
|
command: undefined,
|
|
lastSeen: Date.now() - 45_000,
|
|
});
|
|
|
|
setupComponent([host]);
|
|
|
|
const viewButton = await screen.findByRole('button', { name: 'View progress' });
|
|
fireEvent.click(viewButton);
|
|
|
|
await screen.findByText('Pulse is waiting for', { exact: false });
|
|
|
|
const stopButton = screen.getByRole('button', { name: 'Waiting for host…' });
|
|
expect(stopButton).toBeDisabled();
|
|
expect(screen.queryByText('Progress')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('shows confirmation while waiting for the agent to acknowledge the stop command', async () => {
|
|
deleteDockerHostMock.mockResolvedValue({});
|
|
const host = createDockerHost();
|
|
|
|
setupComponent([host]);
|
|
|
|
const removeButton = screen.getByRole('button', { name: 'Remove' });
|
|
fireEvent.click(removeButton);
|
|
|
|
await screen.findByText('Remove container host "Host One"');
|
|
|
|
const stopButton = screen.getByRole('button', { name: 'Stop agent now' });
|
|
fireEvent.click(stopButton);
|
|
|
|
await waitFor(() => expect(deleteDockerHostMock).toHaveBeenCalledWith('host-1'), { interval: 0 });
|
|
|
|
const waitingButton = await screen.findByRole('button', { name: 'Waiting for agent…' });
|
|
expect(waitingButton).toBeDisabled();
|
|
|
|
expect(screen.getByText('Stop command sent.')).toBeInTheDocument();
|
|
expect(notificationSuccessMock).toHaveBeenCalledWith('Stop command sent to Host One', 3500);
|
|
});
|
|
});
|