349 lines
10 KiB
TypeScript
349 lines
10 KiB
TypeScript
import { render, screen, waitFor } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { vi } from 'vitest';
|
|
import _ from 'lodash';
|
|
import { http, HttpResponse } from 'msw';
|
|
|
|
import { withUserProvider } from '@/react/test-utils/withUserProvider';
|
|
import { withTestRouter } from '@/react/test-utils/withRouter';
|
|
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
|
import { ContainerDetailsViewModel } from '@/docker/models/containerDetails';
|
|
import { ContainerEngine } from '@/react/portainer/environments/types';
|
|
import { server } from '@/setup-tests/server';
|
|
import {
|
|
createMockEnvironment,
|
|
createMockUser,
|
|
} from '@/react-tools/test-mocks';
|
|
|
|
import { ContainerActionsSection } from './ContainerActionsSection';
|
|
|
|
const mockStartMutate = vi.fn();
|
|
const mockStopMutate = vi.fn();
|
|
const mockKillMutate = vi.fn();
|
|
const mockRestartMutate = vi.fn();
|
|
const mockPauseMutate = vi.fn();
|
|
const mockUnpauseMutate = vi.fn();
|
|
const mockRemoveMutate = vi.fn();
|
|
|
|
vi.mock('@uirouter/react', async (importOriginal: () => Promise<object>) => ({
|
|
...(await importOriginal()),
|
|
useCurrentStateAndParams: vi.fn(() => ({
|
|
params: { endpointId: 1 }, // Must match environmentId in test props and user authorizations
|
|
})),
|
|
}));
|
|
|
|
vi.mock('./queries/useStartContainer', () => ({
|
|
useStartContainer: () => ({ mutate: mockStartMutate, isLoading: false }),
|
|
}));
|
|
|
|
vi.mock('./queries/useStopContainer', () => ({
|
|
useStopContainer: () => ({ mutate: mockStopMutate, isLoading: false }),
|
|
}));
|
|
|
|
vi.mock('./queries/useKillContainer', () => ({
|
|
useKillContainer: () => ({ mutate: mockKillMutate, isLoading: false }),
|
|
}));
|
|
|
|
vi.mock('./queries/useRestartContainer', () => ({
|
|
useRestartContainer: () => ({ mutate: mockRestartMutate, isLoading: false }),
|
|
}));
|
|
|
|
vi.mock('./queries/usePauseContainer', () => ({
|
|
usePauseContainer: () => ({ mutate: mockPauseMutate, isLoading: false }),
|
|
}));
|
|
|
|
vi.mock('./queries/useResumeContainer', () => ({
|
|
useResumeContainer: () => ({ mutate: mockUnpauseMutate, isLoading: false }),
|
|
}));
|
|
|
|
vi.mock('./queries/useRemoveContainer', () => ({
|
|
useRemoveContainer: () => ({ mutate: mockRemoveMutate, isLoading: false }),
|
|
}));
|
|
|
|
vi.mock('@/react/docker/proxy/queries/useInfo', () => ({
|
|
useIsSwarm: () => false,
|
|
}));
|
|
|
|
const mockConfirmContainerDeletion = vi.fn();
|
|
vi.mock(
|
|
'@/react/docker/containers/common/confirm-container-delete-modal',
|
|
() => ({
|
|
confirmContainerDeletion: (title: string) =>
|
|
mockConfirmContainerDeletion(title),
|
|
})
|
|
);
|
|
|
|
describe('ContainerActionsSection', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
|
|
// Setup MSW handler for environment endpoint
|
|
server.use(
|
|
http.get('/api/endpoints/:id', () =>
|
|
HttpResponse.json(
|
|
createMockEnvironment({
|
|
Id: 1,
|
|
ContainerEngine: ContainerEngine.Docker,
|
|
SecuritySettings: {
|
|
allowContainerCapabilitiesForRegularUsers: true,
|
|
allowBindMountsForRegularUsers: true,
|
|
allowDeviceMappingForRegularUsers: true,
|
|
allowSysctlSettingForRegularUsers: true,
|
|
allowHostNamespaceForRegularUsers: true,
|
|
allowPrivilegedModeForRegularUsers: true,
|
|
allowVolumeBrowserForRegularUsers: true,
|
|
allowStackManagementForRegularUsers: true,
|
|
allowSecurityOptForRegularUsers: true,
|
|
enableHostManagementFeatures: false,
|
|
},
|
|
})
|
|
)
|
|
)
|
|
);
|
|
});
|
|
|
|
it('should render all action buttons when authorized', async () => {
|
|
renderComponent();
|
|
|
|
// Wait for environment data to load
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: 'Start' })).toBeVisible();
|
|
});
|
|
|
|
expect(screen.getByRole('button', { name: 'Stop' })).toBeVisible();
|
|
expect(screen.getByRole('button', { name: 'Kill' })).toBeVisible();
|
|
expect(screen.getByRole('button', { name: 'Restart' })).toBeVisible();
|
|
expect(screen.getByRole('button', { name: 'Pause' })).toBeVisible();
|
|
expect(screen.getByRole('button', { name: 'Resume' })).toBeVisible();
|
|
expect(screen.getByRole('button', { name: 'Remove' })).toBeVisible();
|
|
});
|
|
|
|
it('should disable start button when container is running', async () => {
|
|
renderComponent({
|
|
container: mockContainer({
|
|
Id: 'test-container-id',
|
|
State: { Running: true, Paused: false },
|
|
Config: { Image: 'nginx:latest' },
|
|
}),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: 'Start' })).toBeDisabled();
|
|
});
|
|
});
|
|
|
|
it('should disable stop, kill, restart buttons when container is not running', async () => {
|
|
renderComponent({
|
|
container: mockContainer({
|
|
Id: 'test-container-id',
|
|
State: { Running: false, Paused: false },
|
|
Config: { Image: 'nginx:latest' },
|
|
}),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: 'Stop' })).toBeDisabled();
|
|
});
|
|
expect(screen.getByRole('button', { name: 'Kill' })).toBeDisabled();
|
|
expect(screen.getByRole('button', { name: 'Restart' })).toBeDisabled();
|
|
});
|
|
|
|
it('should disable pause button when container is paused', async () => {
|
|
renderComponent({
|
|
container: mockContainer({
|
|
Id: 'test-container-id',
|
|
State: { Running: true, Paused: true },
|
|
Config: { Image: 'nginx:latest' },
|
|
}),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: 'Pause' })).toBeDisabled();
|
|
});
|
|
});
|
|
|
|
it('should disable unpause button when container is not paused', async () => {
|
|
renderComponent({
|
|
container: mockContainer({
|
|
Id: 'test-container-id',
|
|
State: { Running: true, Paused: false },
|
|
Config: { Image: 'nginx:latest' },
|
|
}),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: 'Resume' })).toBeDisabled();
|
|
});
|
|
});
|
|
|
|
it('should disable all buttons when container is Portainer', async () => {
|
|
renderComponent({
|
|
container: mockContainer({
|
|
Id: 'test-container-id',
|
|
State: { Running: true, Paused: false },
|
|
Config: { Image: 'portainer:latest' },
|
|
IsPortainer: true,
|
|
}),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: 'Start' })).toBeDisabled();
|
|
});
|
|
expect(screen.getByRole('button', { name: 'Stop' })).toBeDisabled();
|
|
expect(screen.getByRole('button', { name: 'Kill' })).toBeDisabled();
|
|
expect(screen.getByRole('button', { name: 'Restart' })).toBeDisabled();
|
|
expect(screen.getByRole('button', { name: 'Pause' })).toBeDisabled();
|
|
expect(screen.getByRole('button', { name: 'Resume' })).toBeDisabled();
|
|
expect(screen.getByRole('button', { name: 'Remove' })).toBeDisabled();
|
|
});
|
|
|
|
it('should call start mutation when start button is clicked', async () => {
|
|
const user = userEvent.setup();
|
|
renderComponent();
|
|
|
|
const startButton = await screen.findByRole('button', { name: 'Start' });
|
|
await user.click(startButton);
|
|
|
|
expect(mockStartMutate).toHaveBeenCalledWith(
|
|
{
|
|
environmentId: 1,
|
|
containerId: 'test-container-id',
|
|
nodeName: 'node1',
|
|
},
|
|
expect.objectContaining({
|
|
onSuccess: expect.any(Function),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should call stop mutation when stop button is clicked', async () => {
|
|
const user = userEvent.setup();
|
|
renderComponent({
|
|
container: mockContainer({
|
|
Id: 'test-container-id',
|
|
State: { Running: true, Paused: false },
|
|
Config: { Image: 'nginx:latest' },
|
|
}),
|
|
});
|
|
|
|
const stopButton = await screen.findByRole('button', { name: 'Stop' });
|
|
await user.click(stopButton);
|
|
|
|
expect(mockStopMutate).toHaveBeenCalledWith(
|
|
{
|
|
environmentId: 1,
|
|
containerId: 'test-container-id',
|
|
nodeName: 'node1',
|
|
},
|
|
expect.objectContaining({
|
|
onSuccess: expect.any(Function),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should show confirmation modal and call remove mutation when confirmed', async () => {
|
|
const user = userEvent.setup();
|
|
mockConfirmContainerDeletion.mockResolvedValue({ removeVolumes: true });
|
|
|
|
renderComponent();
|
|
|
|
const removeButton = await screen.findByRole('button', { name: 'Remove' });
|
|
await user.click(removeButton);
|
|
|
|
await waitFor(() => {
|
|
expect(mockConfirmContainerDeletion).toHaveBeenCalledWith(
|
|
'You are about to remove a container.'
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(mockRemoveMutate).toHaveBeenCalledWith(
|
|
{
|
|
environmentId: 1,
|
|
containerId: 'test-container-id',
|
|
nodeName: 'node1',
|
|
removeVolumes: true,
|
|
},
|
|
expect.objectContaining({
|
|
onSuccess: expect.any(Function),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should not call remove mutation when confirmation is cancelled', async () => {
|
|
const user = userEvent.setup();
|
|
mockConfirmContainerDeletion.mockResolvedValue(undefined);
|
|
|
|
renderComponent();
|
|
|
|
const removeButton = await screen.findByRole('button', { name: 'Remove' });
|
|
await user.click(removeButton);
|
|
|
|
await waitFor(() => {
|
|
expect(mockConfirmContainerDeletion).toHaveBeenCalled();
|
|
});
|
|
|
|
expect(mockRemoveMutate).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
function renderComponent(
|
|
props: Partial<React.ComponentProps<typeof ContainerActionsSection>> & {
|
|
userAuthorizations?: Record<string, boolean>;
|
|
} = {}
|
|
) {
|
|
const { userAuthorizations, ...componentProps } = props;
|
|
|
|
const defaultProps: React.ComponentProps<typeof ContainerActionsSection> = {
|
|
environmentId: 1,
|
|
nodeName: 'node1',
|
|
container: mockContainer(),
|
|
...componentProps,
|
|
};
|
|
|
|
const defaultAuthorizations = {
|
|
DockerContainerStart: true,
|
|
DockerContainerStop: true,
|
|
DockerContainerKill: true,
|
|
DockerContainerRestart: true,
|
|
DockerContainerPause: true,
|
|
DockerContainerUnpause: true,
|
|
DockerContainerDelete: true,
|
|
DockerContainerCreate: true,
|
|
};
|
|
|
|
const mockUser = createMockUser({
|
|
EndpointAuthorizations: {
|
|
1: userAuthorizations || defaultAuthorizations,
|
|
},
|
|
Id: 1,
|
|
Role: 1,
|
|
});
|
|
|
|
const Wrapper = withTestQueryProvider(
|
|
withUserProvider(withTestRouter(ContainerActionsSection), mockUser)
|
|
);
|
|
|
|
return render(<Wrapper {...defaultProps} />);
|
|
}
|
|
|
|
function mockContainer(
|
|
overrides: Partial<ContainerDetailsViewModel> = {}
|
|
): ContainerDetailsViewModel {
|
|
return _.merge(
|
|
{
|
|
Id: 'test-container-id',
|
|
State: {
|
|
Running: false,
|
|
Paused: false,
|
|
},
|
|
Config: {
|
|
Image: 'nginx:latest',
|
|
},
|
|
IsPortainer: false,
|
|
} as ContainerDetailsViewModel,
|
|
overrides
|
|
);
|
|
}
|