6aecdfbe46
Make the container image-status badge actionable, matching native Portainer: - Clicking "Update available" opens the update confirm dialog and runs the existing update flow (standalone recreate-with-pull / stack redeploy), gated and disabled while in flight to avoid a double submit. The confirm+apply logic is extracted from UpdateNowButton into a shared useApplyContainerImageUpdate hook so the details button and the list badge share one implementation. - Clicking "Up to date" re-queries the registry. Because the server caches image status (statusCache 5m + remoteDigestCache 5s), a plain refetch was a no-op, so the endpoint gains an optional ?force=true that bypasses BOTH caches for a manual re-check while still repopulating them; the default (auto badges + the auto-update daemon) keeps using the caches unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
108 lines
3.3 KiB
TypeScript
108 lines
3.3 KiB
TypeScript
import { renderHook } from '@testing-library/react-hooks';
|
|
import { http, HttpResponse } from 'msw';
|
|
|
|
import { server } from '@/setup-tests/server';
|
|
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
|
|
|
import {
|
|
ContainerImageStatus,
|
|
useContainerImageStatus,
|
|
useRecheckContainerImageStatus,
|
|
} from './useContainerImageStatus';
|
|
|
|
const environmentId = 3;
|
|
const containerId = 'abc123';
|
|
|
|
function renderImageStatusHook(nodeName?: string) {
|
|
return renderHook(
|
|
() => useContainerImageStatus(environmentId, containerId, nodeName),
|
|
{ wrapper: withTestQueryProvider(({ children }) => <>{children}</>) }
|
|
);
|
|
}
|
|
|
|
describe('useContainerImageStatus', () => {
|
|
it('calls the non-proxied image_status endpoint and returns the status', async () => {
|
|
const payload: ContainerImageStatus = { Status: 'outdated' };
|
|
|
|
server.use(
|
|
http.get(
|
|
`/api/docker/${environmentId}/containers/${containerId}/image_status`,
|
|
() => HttpResponse.json(payload)
|
|
)
|
|
);
|
|
|
|
const { result, waitFor } = renderImageStatusHook();
|
|
|
|
await waitFor(() => result.current.isSuccess);
|
|
expect(result.current.data).toEqual(payload);
|
|
});
|
|
|
|
it('forwards nodeName as a query parameter', async () => {
|
|
let receivedNodeName: string | null = null;
|
|
|
|
server.use(
|
|
http.get(
|
|
`/api/docker/${environmentId}/containers/${containerId}/image_status`,
|
|
({ request }) => {
|
|
receivedNodeName = new URL(request.url).searchParams.get('nodeName');
|
|
return HttpResponse.json<ContainerImageStatus>({ Status: 'updated' });
|
|
}
|
|
)
|
|
);
|
|
|
|
const { result, waitFor } = renderImageStatusHook('node-2');
|
|
|
|
await waitFor(() => result.current.isSuccess);
|
|
expect(receivedNodeName).toBe('node-2');
|
|
});
|
|
|
|
it('does not send the force parameter on the background query', async () => {
|
|
let receivedForce: string | null = 'unset';
|
|
|
|
server.use(
|
|
http.get(
|
|
`/api/docker/${environmentId}/containers/${containerId}/image_status`,
|
|
({ request }) => {
|
|
receivedForce = new URL(request.url).searchParams.get('force');
|
|
return HttpResponse.json<ContainerImageStatus>({ Status: 'updated' });
|
|
}
|
|
)
|
|
);
|
|
|
|
const { result, waitFor } = renderImageStatusHook();
|
|
|
|
await waitFor(() => result.current.isSuccess);
|
|
expect(receivedForce).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('useRecheckContainerImageStatus', () => {
|
|
it('sends force=true and updates the shared query cache with the fresh status', async () => {
|
|
let receivedForce: string | null = null;
|
|
|
|
server.use(
|
|
http.get(
|
|
`/api/docker/${environmentId}/containers/${containerId}/image_status`,
|
|
({ request }) => {
|
|
receivedForce = new URL(request.url).searchParams.get('force');
|
|
return HttpResponse.json<ContainerImageStatus>({
|
|
Status: 'outdated',
|
|
});
|
|
}
|
|
)
|
|
);
|
|
|
|
const wrapper = withTestQueryProvider(({ children }) => <>{children}</>);
|
|
const { result, waitFor } = renderHook(
|
|
() => useRecheckContainerImageStatus(environmentId, containerId),
|
|
{ wrapper }
|
|
);
|
|
|
|
result.current.mutate();
|
|
|
|
await waitFor(() => result.current.isSuccess);
|
|
expect(receivedForce).toBe('true');
|
|
expect(result.current.data).toEqual({ Status: 'outdated' });
|
|
});
|
|
});
|