Files
portainer/app/react/docker/containers/update/resolveContainerUpdatePath.test.ts
T
claude code agent ccd5897915 fix(automation): gate stack redeploy on PortainerStackUpdate + bulk/name polish (#10 review)
F1: single-container "Update now" and bulk "Update" now require
PortainerStackUpdate when the resolved path is a stack, disabling the
action with a tooltip / skipping it rather than letting the click 403.

F2: resolveContainerUpdatePath only matches a Docker Compose stack; a
same-named swarm/kubernetes stack is treated as external.

F3: SecondaryActions no longer renders an empty ButtonGroup when all of
recreate/duplicate/update-now are hidden.

F4: bulk update reports an explicit no-op toast and counts containers vs
stacks honestly in the success summary.

F5: bulk toasts use trimmed container names (no leading slash).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 09:42:33 +03:00

100 lines
2.6 KiB
TypeScript

import { Stack, StackType } from '@/react/common/stacks/types';
import { COMPOSE_STACK_NAME_LABEL } from '@/react/constants';
import { resolveContainerUpdatePath } from './resolveContainerUpdatePath';
function buildStack(overrides: Partial<Stack>): Stack {
return {
Id: 1,
Name: 'my-stack',
EndpointId: 3,
Type: StackType.DockerCompose,
...overrides,
} as Stack;
}
describe('resolveContainerUpdatePath', () => {
it('returns standalone when there is no compose project label', () => {
expect(
resolveContainerUpdatePath({ labels: {}, environmentId: 3 }, [])
).toEqual({ kind: 'standalone' });
});
it('returns stack when the compose project matches a Portainer stack', () => {
const stack = buildStack({ Id: 7, Name: 'my-stack', EndpointId: 3 });
expect(
resolveContainerUpdatePath(
{
labels: { [COMPOSE_STACK_NAME_LABEL]: 'my-stack' },
environmentId: 3,
},
[stack]
)
).toEqual({ kind: 'stack', stackId: 7, isGitStack: false });
});
it('flags git stacks so they redeploy via the git path', () => {
const stack = buildStack({
Id: 9,
Name: 'git-stack',
EndpointId: 3,
GitConfig: { URL: 'https://example.com/repo.git' } as Stack['GitConfig'],
});
expect(
resolveContainerUpdatePath(
{
labels: { [COMPOSE_STACK_NAME_LABEL]: 'git-stack' },
environmentId: 3,
},
[stack]
)
).toEqual({ kind: 'stack', stackId: 9, isGitStack: true });
});
it('returns external when the compose project has no matching stack', () => {
expect(
resolveContainerUpdatePath(
{
labels: { [COMPOSE_STACK_NAME_LABEL]: 'unknown-project' },
environmentId: 3,
},
[buildStack({ Name: 'other' })]
)
).toEqual({ kind: 'external' });
});
it('returns external when a same-named stack is not a compose stack', () => {
const stack = buildStack({
Name: 'my-stack',
EndpointId: 3,
Type: StackType.Kubernetes,
});
expect(
resolveContainerUpdatePath(
{
labels: { [COMPOSE_STACK_NAME_LABEL]: 'my-stack' },
environmentId: 3,
},
[stack]
)
).toEqual({ kind: 'external' });
});
it('does not match a stack from a different endpoint', () => {
const stack = buildStack({ Name: 'my-stack', EndpointId: 99 });
expect(
resolveContainerUpdatePath(
{
labels: { [COMPOSE_STACK_NAME_LABEL]: 'my-stack' },
environmentId: 3,
},
[stack]
)
).toEqual({ kind: 'external' });
});
});