311 lines
8.5 KiB
TypeScript
311 lines
8.5 KiB
TypeScript
import { render, screen, waitFor } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { delay, http, HttpResponse } from 'msw';
|
|
|
|
import { server } from '@CE/setup-tests/server';
|
|
import { withTestQueryProvider } from '@CE/react/test-utils/withTestQuery';
|
|
import { withTestRouter } from '@CE/react/test-utils/withRouter';
|
|
import { withUserProvider } from '@CE/react/test-utils/withUserProvider';
|
|
import { createMockUsers } from '@CE/react-tools/test-mocks';
|
|
import { useSwarmId } from '@CE/react/docker/proxy/queries/useSwarm';
|
|
|
|
import { AssociateStackForm } from './AssociateStackForm';
|
|
|
|
// Mock the useSwarmId hook to avoid React Query complexity
|
|
vi.mock('@CE/react/docker/proxy/queries/useSwarm', () => ({
|
|
useSwarmId: vi.fn(),
|
|
}));
|
|
|
|
// Mock the AccessControlForm to simplify testing
|
|
vi.mock('@CE/react/portainer/access-control', () => ({
|
|
AccessControlForm: vi.fn(({ onChange, values }) => (
|
|
<div data-cy="access-control-form">
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
onChange({
|
|
...values,
|
|
ownership: 'private',
|
|
})
|
|
}
|
|
>
|
|
Change Access Control
|
|
</button>
|
|
</div>
|
|
)),
|
|
}));
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(useSwarmId).mockReturnValue({
|
|
data: undefined,
|
|
} as ReturnType<typeof useSwarmId>);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should render correctly', () => {
|
|
renderComponent();
|
|
|
|
expect(screen.getByText('Associate to this environment')).toBeVisible();
|
|
expect(
|
|
screen.getByText(/This feature allows you to re-associate this stack/i)
|
|
).toBeVisible();
|
|
|
|
expect(screen.getByTestId('access-control-form')).toBeVisible();
|
|
expect(screen.getByRole('button', { name: 'Associate' })).toBeVisible();
|
|
});
|
|
|
|
describe('form submission', () => {
|
|
it('should call mutation with correct payload on submit', async () => {
|
|
let requestUrl = '';
|
|
|
|
server.use(
|
|
http.put<{ id: string }>(
|
|
'/api/stacks/:id/associate',
|
|
async ({ request, params }) => {
|
|
requestUrl = request.url;
|
|
return HttpResponse.json(createMockStackResponse(params.id));
|
|
}
|
|
),
|
|
http.put('/api/resource_controls/:id', async ({ request }) => {
|
|
await request.json();
|
|
return HttpResponse.json({ success: true });
|
|
})
|
|
);
|
|
|
|
const user = userEvent.setup();
|
|
renderComponent({
|
|
environmentId: 5,
|
|
stackId: 123,
|
|
isOrphanedRunning: true,
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: 'Associate' })).toBeVisible();
|
|
});
|
|
|
|
const associateButton = screen.getByRole('button', { name: 'Associate' });
|
|
await user.click(associateButton);
|
|
|
|
await waitFor(() => {
|
|
expect(requestUrl).toContain('endpointId=5');
|
|
expect(requestUrl).toContain('orphanedRunning=true');
|
|
});
|
|
});
|
|
|
|
it('should show loading state during submission', async () => {
|
|
let associateCalled = false;
|
|
|
|
server.use(
|
|
http.put('/api/stacks/:id/associate', async () => {
|
|
associateCalled = true;
|
|
await delay(50);
|
|
return HttpResponse.json(createMockStackResponse());
|
|
}),
|
|
http.put('/api/resource_controls/:id', () =>
|
|
HttpResponse.json({ success: true })
|
|
)
|
|
);
|
|
|
|
const user = userEvent.setup();
|
|
renderComponent();
|
|
|
|
const associateButton = screen.getByRole('button', { name: 'Associate' });
|
|
await user.click(associateButton);
|
|
|
|
// Check for loading text
|
|
expect(screen.getByText(/association in progress/i)).toBeVisible();
|
|
|
|
// Wait for API call
|
|
await waitFor(
|
|
() => {
|
|
expect(associateCalled).toBe(true);
|
|
},
|
|
{ timeout: 2000 }
|
|
);
|
|
});
|
|
|
|
it('should complete association successfully', async () => {
|
|
let associateCalled = false;
|
|
let resourceControlCalled = false;
|
|
|
|
server.use(
|
|
http.put('/api/stacks/:id/associate', () => {
|
|
associateCalled = true;
|
|
return HttpResponse.json(createMockStackResponse());
|
|
}),
|
|
http.put('/api/resource_controls/:id', () => {
|
|
resourceControlCalled = true;
|
|
return HttpResponse.json({ success: true });
|
|
})
|
|
);
|
|
|
|
const user = userEvent.setup();
|
|
renderComponent({ stackName: 'my-stack' });
|
|
|
|
const associateButton = screen.getByRole('button', { name: 'Associate' });
|
|
await user.click(associateButton);
|
|
|
|
// Verify both API calls were made
|
|
await waitFor(
|
|
() => {
|
|
expect(associateCalled).toBe(true);
|
|
expect(resourceControlCalled).toBe(true);
|
|
},
|
|
{ timeout: 3000 }
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('swarmId integration', () => {
|
|
it('should pass swarmId when environment is in swarm mode', async () => {
|
|
let requestUrl = '';
|
|
|
|
vi.mocked(useSwarmId).mockReturnValue({
|
|
data: 'swarm-id-123',
|
|
} as ReturnType<typeof useSwarmId>);
|
|
|
|
server.use(
|
|
http.put('/api/stacks/:id/associate', async ({ request }) => {
|
|
requestUrl = request.url;
|
|
return HttpResponse.json(createMockStackResponse());
|
|
}),
|
|
http.put('/api/resource_controls/:id', () =>
|
|
HttpResponse.json({ success: true })
|
|
)
|
|
);
|
|
|
|
const user = userEvent.setup();
|
|
renderComponent({ environmentId: 1 });
|
|
|
|
const associateButton = screen.getByRole('button', { name: 'Associate' });
|
|
await user.click(associateButton);
|
|
|
|
await waitFor(() => {
|
|
expect(requestUrl).toContain('swarmId=swarm-id-123');
|
|
});
|
|
});
|
|
|
|
it('should not pass swarmId when environment is not in swarm mode', async () => {
|
|
let requestUrl = '';
|
|
|
|
vi.mocked(useSwarmId).mockReturnValue({
|
|
data: undefined,
|
|
} as ReturnType<typeof useSwarmId>);
|
|
|
|
server.use(
|
|
http.put('/api/stacks/:id/associate', async ({ request }) => {
|
|
requestUrl = request.url;
|
|
return HttpResponse.json(createMockStackResponse());
|
|
}),
|
|
http.put('/api/resource_controls/:id', () =>
|
|
HttpResponse.json({ success: true })
|
|
)
|
|
);
|
|
|
|
const user = userEvent.setup();
|
|
renderComponent();
|
|
|
|
const associateButton = screen.getByRole('button', { name: 'Associate' });
|
|
await user.click(associateButton);
|
|
|
|
// Verify swarmId is not included in the request
|
|
await waitFor(() => {
|
|
expect(requestUrl).not.toContain('swarmId');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('orphanedRunning parameter', () => {
|
|
it('should pass isOrphanedRunning=true when provided', async () => {
|
|
let requestUrl = '';
|
|
|
|
server.use(
|
|
http.put('/api/stacks/:id/associate', async ({ request }) => {
|
|
requestUrl = request.url;
|
|
return HttpResponse.json(createMockStackResponse());
|
|
}),
|
|
http.put('/api/resource_controls/:id', () =>
|
|
HttpResponse.json({ success: true })
|
|
)
|
|
);
|
|
|
|
const user = userEvent.setup();
|
|
renderComponent({ isOrphanedRunning: true });
|
|
|
|
const associateButton = screen.getByRole('button', { name: 'Associate' });
|
|
await user.click(associateButton);
|
|
|
|
await waitFor(() => {
|
|
expect(requestUrl).toContain('orphanedRunning=true');
|
|
});
|
|
});
|
|
|
|
it('should pass isOrphanedRunning=false when undefined', async () => {
|
|
let requestUrl = '';
|
|
|
|
server.use(
|
|
http.put('/api/stacks/:id/associate', async ({ request }) => {
|
|
requestUrl = request.url;
|
|
return HttpResponse.json(createMockStackResponse());
|
|
}),
|
|
http.put('/api/resource_controls/:id', () =>
|
|
HttpResponse.json({ success: true })
|
|
)
|
|
);
|
|
|
|
const user = userEvent.setup();
|
|
renderComponent({ isOrphanedRunning: undefined });
|
|
|
|
const associateButton = screen.getByRole('button', { name: 'Associate' });
|
|
await user.click(associateButton);
|
|
|
|
await waitFor(() => {
|
|
expect(requestUrl).toContain('orphanedRunning=false');
|
|
});
|
|
});
|
|
});
|
|
|
|
function renderComponent({
|
|
stackName = 'test-stack',
|
|
environmentId = 1,
|
|
stackId = 123,
|
|
isOrphanedRunning,
|
|
}: Partial<React.ComponentProps<typeof AssociateStackForm>> = {}) {
|
|
const users = createMockUsers(1, [1]);
|
|
|
|
server.use(
|
|
http.get('/api/users/:id', () => HttpResponse.json(users[0])),
|
|
http.get('/api/endpoints/:id/docker/swarm', () =>
|
|
HttpResponse.json({ message: 'Not in swarm mode' }, { status: 503 })
|
|
)
|
|
);
|
|
|
|
const Wrapped = withTestQueryProvider(
|
|
withTestRouter(withUserProvider(AssociateStackForm))
|
|
);
|
|
|
|
return render(
|
|
<Wrapped
|
|
stackName={stackName}
|
|
environmentId={environmentId}
|
|
stackId={stackId}
|
|
isOrphanedRunning={isOrphanedRunning}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function createMockStackResponse(stackId = '123') {
|
|
return {
|
|
Id: stackId,
|
|
Name: 'test-stack',
|
|
ResourceControl: {
|
|
Id: 1,
|
|
ResourceId: stackId,
|
|
Type: 6,
|
|
},
|
|
};
|
|
}
|