import { render, screen, waitFor } from '@testing-library/react';
import { setupUser } from '../../../../core/src/utils/testUtils/userEvent';
import React from 'react';
import { AccountService, EntityTagEditor, ManifestReader, SETTINGS } from '@spinnaker/core';
import {
KubernetesSecurityGroupActions,
KubernetesSecurityGroupDetailsComponent as KubernetesSecurityGroupDetails,
} from './KubernetesSecurityGroupDetails';
import type { IKubernetesSecurityGroupDetailsProps } from './KubernetesSecurityGroupDetails';
import { KubernetesV2SecurityGroupTransformer } from '../transformer';
import { KubernetesManifestCommandBuilder } from '../../manifest/manifestCommandBuilder.service';
import { ManifestWizard } from '../../manifest/wizard/ManifestWizard';
describe('', () => {
let originalAdHocInfraWritesEnabled: boolean;
let originalEntityTags: boolean;
let props: IKubernetesSecurityGroupDetailsProps;
let securityGroupReader: any;
beforeEach(() => {
originalAdHocInfraWritesEnabled = SETTINGS.kubernetesAdHocInfraWritesEnabled;
originalEntityTags = SETTINGS.feature.entityTags;
SETTINGS.kubernetesAdHocInfraWritesEnabled = true;
SETTINGS.feature.entityTags = false;
vi.spyOn(AccountService, 'challengeDestructiveActions').mockResolvedValue(false);
securityGroupReader = {
getSecurityGroupDetails: vi.fn().mockResolvedValue(securityGroup()),
};
props = {
app: appWithSecurityGroups(),
resolvedSecurityGroup: {
accountId: 'k8s-local',
name: 'networkPolicy backend-security-policy',
region: 'dev',
},
securityGroupReader,
} as IKubernetesSecurityGroupDetailsProps;
vi.spyOn(ManifestReader, 'getManifest').mockResolvedValue(manifestDetails() as any);
});
afterEach(() => {
SETTINGS.kubernetesAdHocInfraWritesEnabled = originalAdHocInfraWritesEnabled;
SETTINGS.feature.entityTags = originalEntityTags;
});
it('replaces missing details through the injected state service', () => {
const stateService = { go: vi.fn(), params: {} };
const component = new KubernetesSecurityGroupDetails({
...props,
router: {},
stateParams: {},
stateService,
} as any);
(component as any).autoClose();
expect(stateService.params.allowModalToStayOpen).toBe(true);
expect(stateService.go).toHaveBeenCalledWith('^', null, { location: 'replace' });
});
it('loads security group and manifest details before rendering the sections', async () => {
render();
expect(await screen.findByRole('heading', { name: 'backend-security-policy' })).toBeInTheDocument();
expect(securityGroupReader.getSecurityGroupDetails).toHaveBeenCalledWith(
props.app,
'k8s-local',
'kubernetes',
'dev',
'',
'networkPolicy backend-security-policy',
);
expect(ManifestReader.getManifest).toHaveBeenCalledWith(
'k8s-local',
'dev',
'networkPolicy backend-security-policy',
);
expect(screen.getByRole('heading', { name: 'Information' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Labels' })).toBeInTheDocument();
expect(screen.getByText('k8s-local')).toBeInTheDocument();
expect(screen.getByText('Account: k8s-local')).toBeInTheDocument();
expect(screen.getByText(/app.kubernetes.io\/name:\s*kubernetesapp/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Network Policy Actions' })).toBeInTheDocument();
});
it('auto-closes when the security group cannot be found', async () => {
const autoClose = vi.fn();
securityGroupReader.getSecurityGroupDetails.mockResolvedValue(null);
render();
await waitFor(() => expect(autoClose).toHaveBeenCalled());
});
it('renders notifications from the loaded security group tags', async () => {
SETTINGS.feature.entityTags = true;
const user = setupUser();
securityGroupReader.getSecurityGroupDetails.mockResolvedValue(
securityGroup({
entityTags: {
alerts: [],
entityRef: {
account: 'k8s-local',
cloudProvider: 'kubernetes',
entityId: 'networkPolicy backend-security-policy',
entityType: 'securityGroup',
region: 'dev',
},
id: 'security-group-tags',
notices: [
{
lastModified: 1753718892000,
name: 'backend-maintenance',
value: {
message: 'Backend policy maintenance tonight',
type: 'notice',
},
},
],
tags: [],
tagsMetadata: [],
},
}),
);
const { container } = render();
expect(await screen.findByRole('heading', { name: 'backend-security-policy' })).toBeInTheDocument();
const noticeMarker = container.querySelector('.notification.fa-flag');
expect(noticeMarker).toBeInTheDocument();
await user.hover(noticeMarker as HTMLElement);
expect(await screen.findByText('Backend policy maintenance tonight')).toBeVisible();
});
it('loads standalone security group details when the securityGroups data source is absent', async () => {
const app = {
...appWithSecurityGroups(),
isStandalone: true,
getDataSource: () => undefined,
};
render();
expect(await screen.findByRole('heading', { name: 'backend-security-policy' })).toBeInTheDocument();
expect(securityGroupReader.getSecurityGroupDetails).toHaveBeenCalledWith(
app,
'k8s-local',
'kubernetes',
'dev',
'',
'networkPolicy backend-security-policy',
);
});
});
describe('', () => {
let originalAdHocInfraWritesEnabled: boolean;
let originalEntityTags: boolean;
beforeEach(() => {
originalAdHocInfraWritesEnabled = SETTINGS.kubernetesAdHocInfraWritesEnabled;
originalEntityTags = SETTINGS.feature.entityTags;
SETTINGS.kubernetesAdHocInfraWritesEnabled = true;
SETTINGS.feature.entityTags = false;
vi.spyOn(AccountService, 'challengeDestructiveActions').mockResolvedValue(false);
});
afterEach(() => {
SETTINGS.kubernetesAdHocInfraWritesEnabled = originalAdHocInfraWritesEnabled;
SETTINGS.feature.entityTags = originalEntityTags;
});
const openActions = async (app = appWithSecurityGroups()) => {
const user = setupUser();
const resource = securityGroup();
render();
await user.click(screen.getByRole('button', { name: 'Network Policy Actions' }));
return { app, resource, user };
};
it('opens the delete modal from the actions menu', async () => {
const { user } = await openActions();
await user.click(screen.getByRole('menuitem', { name: /Delete/ }));
expect(screen.getByText('Delete NetworkPolicy backend-security-policy in dev')).toBeInTheDocument();
});
it('opens the manifest wizard from the actions menu', async () => {
const command = { manifest: {} };
vi.spyOn(KubernetesManifestCommandBuilder, 'buildNewManifestCommand').mockResolvedValue(command as any);
vi.spyOn(ManifestWizard, 'show').mockReturnValue(undefined);
const { app, user } = await openActions();
await user.click(screen.getByRole('menuitem', { name: /Edit/ }));
await waitFor(() =>
expect(KubernetesManifestCommandBuilder.buildNewManifestCommand).toHaveBeenCalledWith(
app,
manifestDetails().manifest,
securityGroup().moniker,
'k8s-local',
),
);
expect(ManifestWizard.show).toHaveBeenCalledWith({
title: 'Edit Manifest',
application: app,
command,
});
});
it('opens the entity tag editor with the security group context', async () => {
SETTINGS.feature.entityTags = true;
const showTagEditor = vi.spyOn(EntityTagEditor, 'show').mockResolvedValue(undefined);
const { app, resource, user } = await openActions();
await user.click(screen.getByText('Add notice'));
expect(showTagEditor).toHaveBeenCalledWith({
application: app,
entityRef: null,
entityType: 'securityGroup',
isNew: true,
onUpdate: expect.any(Function),
owner: resource,
ownerOptions: undefined,
tag: {
name: null,
value: {
message: null,
type: 'notice',
},
},
});
showTagEditor.mock.calls[0][0].onUpdate();
expect(app.securityGroups.refresh).toHaveBeenCalled();
});
it('does not render action controls when ad-hoc infrastructure writes are disabled', () => {
SETTINGS.kubernetesAdHocInfraWritesEnabled = false;
const { container } = render(
,
);
expect(container).toBeEmptyDOMElement();
expect(screen.queryByRole('button', { name: /Actions/ })).not.toBeInTheDocument();
});
});
describe('KubernetesV2SecurityGroupTransformer', () => {
it('normalizes Kubernetes security groups without modifying them', async () => {
const group = securityGroup();
await expectAsync(new KubernetesV2SecurityGroupTransformer().normalizeSecurityGroup(group)).toBeResolvedTo(group);
});
});
const appWithSecurityGroups = () =>
({
isStandalone: false,
getDataSource: () => ({
ready: () => Promise.resolve(),
onRefresh: () => () => null,
}),
securityGroups: {
refresh: vi.fn(),
},
serverGroups: {
refresh: vi.fn(),
},
} as any);
const securityGroup = (overrides: any = {}) =>
({
account: 'k8s-local',
apiVersion: 'networking.k8s.io/v1',
cloudProvider: 'kubernetes',
createdTime: 1753718892000,
displayName: 'backend-security-policy',
kind: 'networkPolicy',
moniker: { app: 'kubernetesapp', cluster: 'networkPolicy backend-security-policy' },
name: 'networkPolicy backend-security-policy',
namespace: 'dev',
region: 'dev',
...overrides,
} as any);
const manifestDetails = () =>
({
account: 'k8s-local',
manifest: {
metadata: {
annotations: {
'strategy.details.spinnaker.io/deployment-info': 'Account: {{ resource.account }}',
},
labels: {
'app.kubernetes.io/name': 'kubernetesapp',
},
name: 'backend-security-policy',
},
},
} as any);