portainer/app/react/kubernetes/namespaces/ItemView/UpdateNamespaceForm.test.tsx

266 lines
7.4 KiB
TypeScript

import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { vi } from 'vitest';
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
import { withTestRouter } from '@/react/test-utils/withRouter';
import { withUserProvider } from '@/react/test-utils/withUserProvider';
import { UserViewModel } from '@/portainer/models/user';
import { server } from '@/setup-tests/server';
import { UpdateNamespaceForm } from './UpdateNamespaceForm';
const NAMESPACE_NAME = 'test-ns';
vi.mock('@uirouter/react', async (importOriginal: () => Promise<object>) => ({
...(await importOriginal()),
useCurrentStateAndParams: vi.fn(() => ({
params: { id: NAMESPACE_NAME },
})),
}));
vi.mock('@/react/hooks/useEnvironmentId', () => ({
useEnvironmentId: vi.fn(() => 1),
}));
const mockRegistries = [
{
Id: 1,
Type: 6,
Name: 'DockerHub',
URL: 'docker.io',
BaseURL: '',
Authentication: true,
Username: 'portainer',
Password: '',
RegistryAccesses: {
'1': {
UserAccessPolicies: null,
TeamAccessPolicies: null,
Namespaces: [NAMESPACE_NAME],
},
},
Gitlab: { ProjectId: 0, InstanceURL: '', ProjectPath: '' },
Quay: { OrganisationName: '', UseOrganisation: false },
Ecr: { Region: '' },
Github: { UseOrganisation: false, OrganisationName: '' },
},
{
Id: 2,
Type: 3,
Name: 'Private Registry',
URL: 'registry.example.com',
BaseURL: '',
Authentication: false,
Username: '',
Password: '',
RegistryAccesses: {
'1': {
UserAccessPolicies: null,
TeamAccessPolicies: null,
Namespaces: [],
},
},
Gitlab: { ProjectId: 0, InstanceURL: '', ProjectPath: '' },
Quay: { OrganisationName: '', UseOrganisation: false },
Ecr: { Region: '' },
Github: { UseOrganisation: false, OrganisationName: '' },
},
];
const mockNamespace = {
Id: NAMESPACE_NAME,
Name: NAMESPACE_NAME,
Status: { phase: 'Active' },
Annotations: {},
CreationDate: '2024-01-01T00:00:00Z',
NamespaceOwner: 'admin',
IsSystem: false,
IsDefault: false,
UnhealthyEventCount: 0,
ResourceQuota: {
spec: {
hard: {
'requests.memory': '256Mi',
'requests.cpu': '500m',
},
},
},
};
function buildEndpointResponse(overrides: Record<string, unknown> = {}) {
return {
Id: 1,
Name: 'test-cluster',
Type: 5,
Kubernetes: {
Configuration: {
EnableResourceOverCommit: false,
StorageClasses: [],
UseLoadBalancer: false,
IngressAvailabilityPerNamespace: false,
},
},
...overrides,
};
}
function setupDefaultHandlers(
overrides: {
endpoint?: Record<string, unknown>;
namespace?: Record<string, unknown>;
registries?: unknown[];
namespaceError?: boolean;
} = {}
) {
const handlers = [
http.get('/api/endpoints/:id', () =>
HttpResponse.json(buildEndpointResponse(overrides.endpoint))
),
http.get('/api/kubernetes/:id/max_resource_limits', () =>
HttpResponse.json({ Memory: 1024, CPU: 4 })
),
http.get('/api/kubernetes/:id/namespaces', () =>
// Return other namespaces only (not the current one) so that the
// name uniqueness validation doesn't trigger on mount.
HttpResponse.json({
'other-ns': { ...mockNamespace, Name: 'other-ns', Id: 'other-ns' },
})
),
http.get('/api/kubernetes/:id/namespaces/:namespace', () => {
if (overrides.namespaceError) {
return HttpResponse.json({ message: 'server error' }, { status: 500 });
}
return HttpResponse.json({
...mockNamespace,
...overrides.namespace,
});
}),
http.get('/api/endpoints/:id/registries', () =>
HttpResponse.json(overrides.registries ?? mockRegistries)
),
http.get('/api/kubernetes/:id/ingresscontrollers', () =>
HttpResponse.json([])
),
// Mutation handlers
http.put('/api/endpoints/:id/registries/:registryId', () =>
HttpResponse.json({})
),
http.put(
'/api/kubernetes/:id/namespaces/:namespace/ingresscontrollers',
() => HttpResponse.json([])
),
];
server.use(...handlers);
}
function renderComponent() {
const user = new UserViewModel({ Username: 'user', Role: 1 });
const Wrapped = withTestQueryProvider(
withUserProvider(withTestRouter(UpdateNamespaceForm), user)
);
return render(<Wrapped />);
}
describe('UpdateNamespaceForm', () => {
describe('form rendering', () => {
it('should load without errors, show read-only name, registries, and disabled button', async () => {
setupDefaultHandlers();
renderComponent();
// Wait for the form to load
await waitFor(() => {
expect(screen.getByText(NAMESPACE_NAME)).toBeVisible();
});
// No validation errors on initial load
const errors = screen.queryAllByRole('alert');
expect(errors).toHaveLength(0);
// In edit mode, name is text not an input
expect(
screen.queryByPlaceholderText('e.g. my-namespace')
).not.toBeInTheDocument();
// DockerHub should be shown as a selected registry (it has namespace access)
expect(screen.getByText('Registries')).toBeVisible();
expect(screen.getByText('DockerHub')).toBeVisible();
// Update button is disabled when form is pristine
expect(
screen.getByRole('button', { name: /Update namespace/i })
).toBeDisabled();
});
});
describe('error states', () => {
it('should show error alert when namespace query fails', async () => {
setupDefaultHandlers({ namespaceError: true });
renderComponent();
await waitFor(() => {
expect(screen.getByText('Error loading namespace')).toBeVisible();
});
});
});
describe('form submission', { timeout: 10_000 }, () => {
it('should submit correct namespace payload on update', async () => {
setupDefaultHandlers();
let namespacePutBody: unknown;
server.use(
http.put(
'/api/kubernetes/:id/namespaces/:namespace',
async ({ request }: { request: Request }) => {
namespacePutBody = await request.json();
return HttpResponse.json({});
}
)
);
const user = userEvent.setup();
renderComponent();
// Wait for form to be fully loaded with registries selector
await waitFor(() => {
expect(
screen.getByRole('button', { name: /Update namespace/i })
).toBeVisible();
});
// Dirty the form by selecting an additional registry via react-select
const registryInput = screen.getByRole('combobox');
await user.click(registryInput);
// Select the second registry that isn't already selected
await waitFor(() => {
expect(screen.getByText('Private Registry')).toBeVisible();
});
await user.click(screen.getByText('Private Registry'));
// Submit
const submitButton = screen.getByRole('button', {
name: /Update namespace/i,
});
await waitFor(() => {
expect(submitButton).not.toBeDisabled();
});
await user.click(submitButton);
await waitFor(() => {
expect(namespacePutBody).toBeDefined();
expect(namespacePutBody).toMatchObject({
Name: NAMESPACE_NAME,
Owner: 'user',
});
});
});
});
});