354 lines
9.7 KiB
TypeScript
354 lines
9.7 KiB
TypeScript
import { waitFor } from '@testing-library/react';
|
|
import { renderHook } from '@testing-library/react-hooks';
|
|
import { http, HttpResponse } from 'msw';
|
|
import { saveAs } from 'file-saver';
|
|
import { createElement, Fragment } from 'react';
|
|
|
|
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
|
import { server } from '@/setup-tests/server';
|
|
|
|
import {
|
|
useExportMutation,
|
|
exportImage,
|
|
getImagesNamesForDownload,
|
|
} from './useExportImageMutation';
|
|
|
|
function renderMutationHook() {
|
|
const Wrapper = withTestQueryProvider(({ children }) =>
|
|
createElement(Fragment, null, children)
|
|
);
|
|
|
|
return renderHook(() => useExportMutation(), {
|
|
wrapper: Wrapper,
|
|
});
|
|
}
|
|
|
|
vi.mock('file-saver', () => ({
|
|
saveAs: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@uirouter/react', async (importOriginal: () => Promise<object>) => ({
|
|
...(await importOriginal()),
|
|
useCurrentStateAndParams: vi.fn(() => ({
|
|
params: { endpointId: '1' },
|
|
})),
|
|
}));
|
|
|
|
describe('getImagesNamesForDownload', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should return tag names when images have tags', () => {
|
|
const images = [
|
|
{ id: 'sha256:abc123', tags: ['nginx:latest', 'nginx:1.21'] },
|
|
{ id: 'sha256:def456', tags: ['redis:alpine'] },
|
|
];
|
|
|
|
const result = getImagesNamesForDownload(images);
|
|
|
|
expect(result.names).toEqual(['nginx:latest', 'redis:alpine']);
|
|
});
|
|
|
|
it('should return image id when tags are undefined', () => {
|
|
const images = [
|
|
{ id: 'sha256:abc123', tags: undefined },
|
|
{ id: 'sha256:def456', tags: ['redis:alpine'] },
|
|
];
|
|
|
|
const result = getImagesNamesForDownload(images);
|
|
|
|
expect(result.names).toEqual(['sha256:abc123', 'redis:alpine']);
|
|
});
|
|
|
|
it('should return image id when tag is <none>:<none>', () => {
|
|
const images = [
|
|
{ id: 'sha256:abc123', tags: ['<none>:<none>'] },
|
|
{ id: 'sha256:def456', tags: ['redis:alpine'] },
|
|
];
|
|
|
|
const result = getImagesNamesForDownload(images);
|
|
|
|
expect(result.names).toEqual(['sha256:abc123', 'redis:alpine']);
|
|
});
|
|
|
|
it('should return image id when tags array is empty', () => {
|
|
const images = [
|
|
{ id: 'sha256:abc123', tags: [] },
|
|
{ id: 'sha256:def456', tags: ['redis:alpine'] },
|
|
];
|
|
|
|
const result = getImagesNamesForDownload(images);
|
|
|
|
expect(result.names).toEqual(['sha256:abc123', 'redis:alpine']);
|
|
});
|
|
});
|
|
|
|
describe('exportImage', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should export image and save file with correct filename', async () => {
|
|
const mockBlob = new Blob(['image data'], { type: 'application/x-tar' });
|
|
|
|
server.use(
|
|
http.get(
|
|
'/api/endpoints/:envId/docker/images/get',
|
|
() =>
|
|
new Response(mockBlob, {
|
|
headers: {
|
|
'content-disposition': 'attachment; filename=nginx-latest.tar',
|
|
},
|
|
})
|
|
)
|
|
);
|
|
|
|
await exportImage({
|
|
environmentId: 1,
|
|
nodeName: undefined,
|
|
images: [{ id: 'sha256:abc123', tags: ['nginx:latest'] }],
|
|
});
|
|
|
|
expect(saveAs).toHaveBeenCalledWith(mockBlob, 'nginx-latest.tar');
|
|
});
|
|
|
|
it('should include X-PortainerAgent-Target header when nodeName is provided', async () => {
|
|
let requestHeaders: Record<string, string | undefined> = {};
|
|
|
|
server.use(
|
|
http.get('/api/endpoints/:envId/docker/images/get', ({ request }) => {
|
|
requestHeaders = Object.fromEntries(request.headers.entries());
|
|
return new Response(new Blob(['image data']), {
|
|
headers: {
|
|
'content-disposition': 'attachment; filename=test.tar',
|
|
},
|
|
});
|
|
})
|
|
);
|
|
|
|
await exportImage({
|
|
environmentId: 1,
|
|
nodeName: 'worker-node-1',
|
|
images: [{ id: 'sha256:abc123', tags: ['nginx:latest'] }],
|
|
});
|
|
|
|
expect(requestHeaders['x-portaineragent-target']).toBe('worker-node-1');
|
|
});
|
|
|
|
it('should not include X-PortainerAgent-Target header when nodeName is undefined', async () => {
|
|
let requestHeaders: Record<string, string | undefined> = {};
|
|
|
|
server.use(
|
|
http.get('/api/endpoints/:envId/docker/images/get', ({ request }) => {
|
|
requestHeaders = Object.fromEntries(request.headers.entries());
|
|
return new Response(new Blob(['image data']), {
|
|
headers: {
|
|
'content-disposition': 'attachment; filename=test.tar',
|
|
},
|
|
});
|
|
})
|
|
);
|
|
|
|
await exportImage({
|
|
environmentId: 1,
|
|
nodeName: undefined,
|
|
images: [{ id: 'sha256:abc123', tags: ['nginx:latest'] }],
|
|
});
|
|
|
|
expect(requestHeaders['x-portaineragent-target']).toBeUndefined();
|
|
});
|
|
|
|
it('should send correct image names as query params', async () => {
|
|
let requestUrl = '';
|
|
|
|
server.use(
|
|
http.get('/api/endpoints/:envId/docker/images/get', ({ request }) => {
|
|
requestUrl = request.url;
|
|
|
|
return new Response(new Blob(['image data']), {
|
|
headers: {
|
|
'content-disposition': 'attachment; filename=test.tar',
|
|
},
|
|
});
|
|
})
|
|
);
|
|
|
|
await exportImage({
|
|
environmentId: 1,
|
|
nodeName: undefined,
|
|
images: [
|
|
{ id: 'sha256:abc123', tags: ['nginx:latest'] },
|
|
{ id: 'sha256:def456', tags: ['redis:alpine'] },
|
|
],
|
|
});
|
|
|
|
const url = new URL(requestUrl);
|
|
// Axios serializes array params with brackets: names[]=value1&names[]=value2
|
|
const names = url.searchParams.getAll('names[]');
|
|
expect(names).toEqual(['nginx:latest', 'redis:alpine']);
|
|
expect(saveAs).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should send mix of tags and IDs in query params for images without tags', async () => {
|
|
let requestUrl = '';
|
|
|
|
server.use(
|
|
http.get('/api/endpoints/:envId/docker/images/get', ({ request }) => {
|
|
requestUrl = request.url;
|
|
return new Response(new Blob(['image data']), {
|
|
headers: {
|
|
'content-disposition': 'attachment; filename=test.tar',
|
|
},
|
|
});
|
|
})
|
|
);
|
|
|
|
await exportImage({
|
|
environmentId: 1,
|
|
nodeName: undefined,
|
|
images: [
|
|
{ id: 'sha256:abc123', tags: ['nginx:latest'] },
|
|
{ id: 'sha256:def456', tags: undefined },
|
|
{ id: 'sha256:ghi789', tags: ['<none>:<none>'] },
|
|
{ id: 'sha256:jkl012', tags: [] },
|
|
],
|
|
});
|
|
|
|
const url = new URL(requestUrl);
|
|
const names = url.searchParams.getAll('names[]');
|
|
expect(names).toEqual([
|
|
'nginx:latest',
|
|
'sha256:def456',
|
|
'sha256:ghi789',
|
|
'sha256:jkl012',
|
|
]);
|
|
expect(saveAs).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should throw error when export fails', async () => {
|
|
server.use(
|
|
http.get('/api/endpoints/:envId/docker/images/get', () =>
|
|
HttpResponse.json({ message: 'Image not found' }, { status: 404 })
|
|
)
|
|
);
|
|
|
|
await expect(
|
|
exportImage({
|
|
environmentId: 1,
|
|
nodeName: undefined,
|
|
images: [{ id: 'sha256:abc123', tags: ['nginx:latest'] }],
|
|
})
|
|
).rejects.toThrow('Unable to export image');
|
|
});
|
|
|
|
it('should handle filename without content-disposition header', async () => {
|
|
const mockBlob = new Blob(['image data'], { type: 'application/x-tar' });
|
|
|
|
server.use(
|
|
http.get(
|
|
'/api/endpoints/:envId/docker/images/get',
|
|
() => new Response(mockBlob)
|
|
)
|
|
);
|
|
|
|
await exportImage({
|
|
environmentId: 1,
|
|
nodeName: undefined,
|
|
images: [{ id: 'sha256:abc123', tags: ['nginx:latest'] }],
|
|
});
|
|
|
|
expect(saveAs).toHaveBeenCalledWith(mockBlob, '');
|
|
});
|
|
});
|
|
|
|
describe('useExportMutation', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should successfully export images', async () => {
|
|
const mockBlob = new Blob(['image data'], { type: 'application/x-tar' });
|
|
|
|
server.use(
|
|
http.get(
|
|
'/api/endpoints/:envId/docker/images/get',
|
|
() =>
|
|
new Response(mockBlob, {
|
|
headers: {
|
|
'content-disposition': 'attachment; filename=nginx-latest.tar',
|
|
},
|
|
})
|
|
)
|
|
);
|
|
|
|
const { result } = renderMutationHook();
|
|
|
|
result.current.mutate({
|
|
images: [{ id: 'sha256:abc123', tags: ['nginx:latest'] }],
|
|
nodeName: undefined,
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isSuccess).toBe(true);
|
|
});
|
|
|
|
expect(saveAs).toHaveBeenCalledWith(mockBlob, 'nginx-latest.tar');
|
|
});
|
|
|
|
it('should handle export error', async () => {
|
|
server.use(
|
|
http.get('/api/endpoints/:envId/docker/images/get', () =>
|
|
HttpResponse.json({ message: 'Internal server error' }, { status: 500 })
|
|
)
|
|
);
|
|
|
|
const { result } = renderMutationHook();
|
|
|
|
result.current.mutate({
|
|
images: [{ id: 'sha256:abc123', tags: ['nginx:latest'] }],
|
|
nodeName: undefined,
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isError).toBe(true);
|
|
});
|
|
|
|
expect(result.current.error).toBeDefined();
|
|
});
|
|
|
|
it('should export multiple images with node name', async () => {
|
|
const mockBlob = new Blob(['image data'], { type: 'application/x-tar' });
|
|
let requestHeaders: Record<string, string | undefined> = {};
|
|
|
|
server.use(
|
|
http.get('/api/endpoints/:envId/docker/images/get', ({ request }) => {
|
|
requestHeaders = Object.fromEntries(request.headers.entries());
|
|
return new Response(mockBlob, {
|
|
headers: {
|
|
'content-disposition': 'attachment; filename=images.tar',
|
|
},
|
|
});
|
|
})
|
|
);
|
|
|
|
const { result } = renderMutationHook();
|
|
|
|
result.current.mutate({
|
|
images: [
|
|
{ id: 'sha256:abc123', tags: ['nginx:latest'] },
|
|
{ id: 'sha256:def456', tags: ['redis:alpine'] },
|
|
{ id: 'sha256:ghi789', tags: undefined },
|
|
],
|
|
nodeName: 'worker-node-1',
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isSuccess).toBe(true);
|
|
});
|
|
|
|
expect(saveAs).toHaveBeenCalledWith(mockBlob, 'images.tar');
|
|
expect(requestHeaders['x-portaineragent-target']).toBe('worker-node-1');
|
|
});
|
|
});
|