import { fireEvent, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { AccountService, DeckRuntimeContext, ReactModal, ServerGroupWriter, TaskExecutor, TaskReader, } from '@spinnaker/core'; import { renderWithRouter } from '../../../../../core/src/utils/testUtils/rtl'; import type { IAmazonServerGroup } from '../../../domain'; import { AmazonRollbackServerGroupModal, buildAmazonRollbackJob, getAmazonPreviousImageServerGroup, getAmazonRollbackType, getDefaultAmazonHealthyRollbackPercentage, validateAmazonRollbackValues, } from './index'; describe('AmazonRollbackServerGroupModal', () => { const serverGroup = { account: 'test-account', app: 'fnord', capacity: { desired: 10, max: 12, min: 2 }, cluster: 'fnord-main', instanceCounts: { total: 10 }, moniker: { app: 'fnord', cluster: 'fnord-main' }, name: 'fnord-main-v004', region: 'eu-west-1', type: 'aws', } as IAmazonServerGroup; const previousServerGroup = (name = 'fnord-main-v003') => ({ ...serverGroup, isDisabled: true, name }); const application = (attributes: any = {}) => ({ attributes, getDataSource: vi.fn(), name: 'fnord', serverGroups: { refresh: vi.fn() } } as any); const props = (app = application(), overrides: any = {}) => ({ allServerGroups: [previousServerGroup()], application: app, closeModal: vi.fn(), dismissModal: vi.fn(), previousServerGroup: previousServerGroup(), serverGroup, ...overrides, }); beforeEach(() => vi.spyOn(AccountService, 'challengeDestructiveActions').mockResolvedValue(true)); it('uses capacity-sensitive healthy percentage defaults', () => { expect([0, 9, 10, 19, 20].map(getDefaultAmazonHealthyRollbackPercentage)).toEqual([100, 100, 90, 90, 95]); }); it('normalizes previous-image metadata and selects that mode only without deployed candidates', () => { const withPreviousImage = { ...serverGroup, entityTags: { creationMetadata: { value: { previousServerGroup: { buildInfo: { jenkins: { number: 42 } }, imageId: 'ami-012345', imageName: 'fnord-20260712', name: 'fnord-main-v003', }, }, }, }, } as any; expect(getAmazonPreviousImageServerGroup(withPreviousImage)).toEqual({ buildNumber: 42, imageId: 'ami-012345', imageName: 'fnord-20260712', name: 'fnord-main-v003', }); expect(getAmazonRollbackType(withPreviousImage, [])).toBe('PREVIOUS_IMAGE'); expect(getAmazonRollbackType(withPreviousImage, [previousServerGroup()])).toBe('EXPLICIT'); expect(getAmazonRollbackType(serverGroup, [])).toBe('EXPLICIT'); expect( getAmazonPreviousImageServerGroup({ ...withPreviousImage, entityTags: { creationMetadata: { value: { previousServerGroup: { imageId: 'fnord-20260712', imageName: 'fnord-20260712', name: 'fnord-main-v003', }, }, }, }, } as any), ).toEqual({ imageId: undefined, imageName: 'fnord-20260712', name: 'fnord-main-v003' }); }); it('requires an eligible explicit target and valid health percentage and delay', () => { const valid = { delayBeforeDisableSeconds: 0, restoreServerGroupName: 'fnord-main-v003', targetHealthyRollbackPercentage: 95, }; expect(validateAmazonRollbackValues(valid, 'EXPLICIT', ['fnord-main-v003'])).toEqual({}); expect(validateAmazonRollbackValues({ ...valid, restoreServerGroupName: undefined }, 'EXPLICIT')).toEqual({ restoreServerGroupName: 'Select a server group to restore', }); expect(validateAmazonRollbackValues(valid, 'EXPLICIT', ['fnord-main-v002'])).toEqual({ restoreServerGroupName: 'Select an eligible server group to restore', }); [-1, 101, Number.NaN, Number.POSITIVE_INFINITY].forEach((targetHealthyRollbackPercentage) => expect(validateAmazonRollbackValues({ ...valid, targetHealthyRollbackPercentage }, 'EXPLICIT')).toEqual({ targetHealthyRollbackPercentage: 'Healthy threshold must be between 0 and 100', }), ); [-1, 1.5, Number.NaN, Number.POSITIVE_INFINITY].forEach((delayBeforeDisableSeconds) => expect(validateAmazonRollbackValues({ ...valid, delayBeforeDisableSeconds }, 'EXPLICIT')).toEqual({ delayBeforeDisableSeconds: 'Delay must be a non-negative whole number', }), ); expect(validateAmazonRollbackValues({ ...valid, restoreServerGroupName: undefined }, 'PREVIOUS_IMAGE', [])).toEqual( {}, ); }); it('builds the exact rollback writer command', () => { expect( buildAmazonRollbackJob(application({ platformHealthOnlyShowOverride: true }), serverGroup, 'EXPLICIT', { delayBeforeDisableSeconds: 15, interestingHealthProviderNames: ['Amazon'], reason: ' bad release ', restoreServerGroupName: 'fnord-main-v003', targetHealthyRollbackPercentage: 90, }), ).toEqual({ interestingHealthProviderNames: ['Amazon'], platformHealthOnlyShowOverride: true, reason: ' bad release ', rollbackContext: { delayBeforeDisableSeconds: 15, restoreServerGroupName: 'fnord-main-v003', rollbackServerGroupName: 'fnord-main-v004', targetHealthyRollbackPercentage: 90, }, rollbackType: 'EXPLICIT', }); }); it('passes the exact rollback job envelope from the writer to TaskExecutor', () => { const app = application(); const command = buildAmazonRollbackJob(app, serverGroup, 'PREVIOUS_IMAGE', { delayBeforeDisableSeconds: 0, targetHealthyRollbackPercentage: 90, }); vi.spyOn(TaskExecutor, 'executeTask').mockResolvedValue({} as any); new ServerGroupWriter(null).rollbackServerGroup(serverGroup, app, command); expect(TaskExecutor.executeTask).toHaveBeenCalledExactlyOnceWith({ application: app, description: 'Rollback Server Group: fnord-main-v004', job: [ { cloudProvider: 'aws', credentials: 'test-account', interestingHealthProviderNames: undefined, moniker: serverGroup.moniker, platformHealthOnlyShowOverride: undefined, reason: undefined, region: 'eu-west-1', rollbackContext: { delayBeforeDisableSeconds: 0, restoreServerGroupName: undefined, rollbackServerGroupName: 'fnord-main-v004', targetHealthyRollbackPercentage: 90, }, rollbackType: 'PREVIOUS_IMAGE', type: 'rollbackServerGroup', }, ], }); }); it('renders the controlled rollback form and submits exact values after verification', async () => { const app = application({ platformHealthOnly: true, platformHealthOnlyShowOverride: true }); const rollbackServerGroup = vi.fn().mockResolvedValue({ id: 'task-id' }); vi.spyOn(TaskReader, 'waitUntilTaskCompletes').mockResolvedValue({} as any); renderWithRouter( , ); expect(screen.getByRole('combobox', { name: 'Server group to restore' })).toHaveValue('fnord-main-v003'); expect(screen.getByRole('spinbutton', { name: 'Delay before disabling' })).toHaveValue(0); expect(screen.getByRole('spinbutton', { name: 'Target healthy rollback percentage' })).toHaveValue(90); expect(screen.getByText(/Type the name of the account/)).toHaveTextContent('test-account'); expect(screen.getByRole('button', { name: 'Submit' })).toBeDisabled(); fireEvent.change(screen.getByRole('spinbutton', { name: 'Delay before disabling' }), { target: { value: '30' }, }); fireEvent.change(screen.getByRole('spinbutton', { name: 'Target healthy rollback percentage' }), { target: { value: '95' }, }); await userEvent.type(screen.getByRole('textbox', { name: 'Reason' }), 'rollback requested'); await userEvent.type(screen.getByRole('textbox', { name: 'Confirm account test-account' }), 'test-account'); await waitFor(() => expect(screen.getByRole('button', { name: 'Submit' })).toBeEnabled()); fireEvent.click(screen.getByRole('button', { name: 'Submit' })); expect(rollbackServerGroup).toHaveBeenCalledWith(serverGroup, app, { interestingHealthProviderNames: ['Amazon'], platformHealthOnlyShowOverride: true, reason: 'rollback requested', rollbackContext: { delayBeforeDisableSeconds: 30, restoreServerGroupName: 'fnord-main-v003', rollbackServerGroupName: 'fnord-main-v004', targetHealthyRollbackPercentage: 95, }, rollbackType: 'EXPLICIT', }); await waitFor(() => expect(app.serverGroups.refresh).toHaveBeenCalledExactlyOnceWith()); }); it('dismisses through the public cancel action', async () => { const modalProps = props(); renderWithRouter( , ); await userEvent.click(screen.getByRole('button', { name: 'Cancel' })); expect(modalProps.dismissModal).toHaveBeenCalledExactlyOnceWith(); }); it('dismisses a submitted task through TaskMonitor close', async () => { const modalProps = props(); const rollbackServerGroup = vi.fn().mockReturnValue(new Promise(() => undefined)); const cancelPolling = vi.spyOn(TaskReader, 'cancelPolling'); renderWithRouter( , ); await userEvent.type(screen.getByRole('textbox', { name: 'Confirm account test-account' }), 'test-account'); await waitFor(() => expect(screen.getByRole('button', { name: 'Submit' })).toBeEnabled()); await userEvent.click(screen.getByRole('button', { name: 'Submit' })); expect(rollbackServerGroup).toHaveBeenCalledOnce(); cancelPolling.mockClear(); await userEvent.click(await screen.findByRole('button', { name: 'Close' })); expect(cancelPolling).toHaveBeenCalledExactlyOnceWith(null); expect(modalProps.dismissModal).toHaveBeenCalledExactlyOnceWith(); }); it('exposes a show primitive for actions integration', () => { const show = vi.spyOn(ReactModal, 'show').mockResolvedValue({} as any); const modalProps = props(); const runtimeServices = {} as any; AmazonRollbackServerGroupModal.show(modalProps, runtimeServices); expect(show).toHaveBeenCalledExactlyOnceWith( AmazonRollbackServerGroupModal, modalProps, undefined, runtimeServices, ); }); });