@spinnaker/google 2026.3.2 → 2026.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/autoscalingPolicy/GceAutoScalingFieldLayout.d.ts +2 -2
- package/dist/domain/loadBalancer.d.ts +6 -3
- package/dist/image/ImageSelect.d.ts +1 -1
- package/dist/index.js +159 -1
- package/dist/index.js.map +1 -1
- package/dist/loadBalancer/configure/common/gceLoadBalancerModels.d.ts +13 -2
- package/dist/loadBalancer/configure/http/GceHttpLoadBalancerEditor.d.ts +3 -0
- package/dist/loadBalancer/configure/http/GceHttpLoadBalancerModal.d.ts +1 -1
- package/dist/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.d.ts +30 -0
- package/dist/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.d.ts +53 -0
- package/dist/loadBalancer/httpLoadBalancerUtils.service.d.ts +3 -1
- package/dist/serverGroup/configure/wizard/GceCloneServerGroupModal.d.ts +3 -0
- package/dist/serverGroup/configure/wizard/GceServerGroupWizard.types.d.ts +1 -0
- package/dist/serverGroup/configure/wizard/customInstance/CustomInstanceConfigurer.d.ts +1 -1
- package/dist/serverGroup/configure/wizard/customInstance/GceCustomInstanceBuilder.d.ts +2 -2
- package/dist/serverGroup/configure/wizard/pages/AdvancedSettings.d.ts +3 -0
- package/dist/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.d.ts +1 -0
- package/dist/serverGroup/configure/wizard/pages/Policies.d.ts +21 -3
- package/dist/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.d.ts +1 -0
- package/dist/serverGroup/configure/wizard/pages/ServerGroupCapacity.d.ts +1 -0
- package/dist/serverGroup/configure/wizard/pages/ServerGroupImageSettings.d.ts +1 -0
- package/dist/serverGroup/configure/wizard/pages/ServerGroupInstanceType.d.ts +1 -0
- package/dist/serverGroup/configure/wizard/zones/GceInstanceFlexibilityConfigurer.d.ts +2 -2
- package/dist/serverGroup/details/GceInstanceFlexibilityPolicyDetails.d.ts +2 -2
- package/dist/serverGroup/details/stateful/MarkDiskStatefulButton.d.ts +2 -2
- package/dist/serverGroup/details/stateful/UpdateBootImageButton.d.ts +2 -2
- package/package.json +5 -5
- package/src/autoHealingPolicy/GceAutoHealingPolicyEditor.spec.tsx +68 -52
- package/src/autoscalingPolicy/GceAutoscalingPolicyEditor.spec.tsx +43 -50
- package/src/autoscalingPolicy/GceAutoscalingPolicyEditor.tsx +1 -1
- package/src/autoscalingPolicy/GceAutoscalingPolicyWriter.spec.ts +1 -1
- package/src/domain/loadBalancer.ts +6 -3
- package/src/gce.module.spec.ts +132 -153
- package/src/instance/details/GceInstanceDetails.spec.tsx +86 -83
- package/src/loadBalancer/configure/choice/GceLoadBalancerChoiceModal.spec.tsx +162 -121
- package/src/loadBalancer/configure/choice/GceLoadBalancerChoiceModal.tsx +13 -1
- package/src/loadBalancer/configure/common/GceProxyLoadBalancerEditor.spec.tsx +47 -30
- package/src/loadBalancer/configure/common/GceProxyLoadBalancerEditor.tsx +2 -1
- package/src/loadBalancer/configure/common/GceProxyLoadBalancerModal.spec.tsx +85 -57
- package/src/loadBalancer/configure/common/gceLoadBalancerData.spec.ts +17 -16
- package/src/loadBalancer/configure/common/gceLoadBalancerModels.spec.ts +230 -5
- package/src/loadBalancer/configure/common/gceLoadBalancerModels.ts +186 -22
- package/src/loadBalancer/configure/common/gceLoadBalancerSubmission.spec.ts +298 -2
- package/src/loadBalancer/configure/common/gceLoadBalancerSubmission.ts +35 -5
- package/src/loadBalancer/configure/common/useGceLoadBalancerData.spec.tsx +37 -46
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerEditor.spec.tsx +399 -58
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerEditor.tsx +148 -41
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerListenerEditor.spec.tsx +169 -36
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerListenerEditor.tsx +80 -12
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerModal.spec.tsx +224 -124
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerModal.tsx +8 -2
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerResourceEditors.spec.tsx +57 -22
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerResourceEditors.tsx +21 -0
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerRoutingEditor.spec.tsx +24 -20
- package/src/loadBalancer/configure/network/GceNetworkLoadBalancerEditor.spec.tsx +41 -32
- package/src/loadBalancer/configure/network/GceNetworkLoadBalancerModal.spec.tsx +35 -30
- package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.spec.tsx +443 -0
- package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.tsx +401 -0
- package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.spec.tsx +330 -0
- package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.tsx +348 -0
- package/src/loadBalancer/details/gceLoadBalancerDetails.spec.tsx +439 -35
- package/src/loadBalancer/details/gceLoadBalancerDetails.tsx +104 -21
- package/src/loadBalancer/httpLoadBalancerUtils.service.spec.ts +126 -0
- package/src/loadBalancer/httpLoadBalancerUtils.service.ts +27 -11
- package/src/loadBalancer/loadBalancer.setTransformer.spec.ts +165 -0
- package/src/loadBalancer/loadBalancer.setTransformer.ts +15 -2
- package/src/pipeline/stages/googleStageConfig.spec.js +22 -16
- package/src/securityGroup/configure/GceSecurityGroupModal.spec.tsx +177 -193
- package/src/securityGroup/configure/GceSecurityGroupModal.tsx +14 -2
- package/src/securityGroup/details/GceSecurityGroupDetails.spec.tsx +69 -78
- package/src/serverGroup/configure/serverGroupCommandBuilder.service.spec.js +35 -15
- package/src/serverGroup/configure/serverGroupConfiguration.service.js +7 -1
- package/src/serverGroup/configure/serverGroupConfiguration.service.spec.js +74 -4
- package/src/serverGroup/configure/wizard/GceCloneServerGroupModal.spec.tsx +593 -855
- package/src/serverGroup/configure/wizard/GceCloneServerGroupModal.tsx +120 -38
- package/src/serverGroup/configure/wizard/GceServerGroupWizard.types.ts +1 -0
- package/src/serverGroup/configure/wizard/GceServerGroupWizardPage.spec.tsx +53 -50
- package/src/serverGroup/configure/wizard/customInstance/GceCustomInstanceBuilder.spec.tsx +65 -49
- package/src/serverGroup/configure/wizard/hiddenMetadataKeys.value.js +1 -0
- package/src/serverGroup/configure/wizard/pages/AdvancedSettings.spec.tsx +101 -108
- package/src/serverGroup/configure/wizard/pages/AdvancedSettings.tsx +32 -28
- package/src/serverGroup/configure/wizard/pages/GceServerGroupFirewalls.spec.tsx +60 -59
- package/src/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.spec.tsx +240 -114
- package/src/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.tsx +152 -63
- package/src/serverGroup/configure/wizard/pages/Policies.spec.tsx +93 -48
- package/src/serverGroup/configure/wizard/pages/Policies.tsx +80 -55
- package/src/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.spec.tsx +123 -94
- package/src/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.tsx +24 -18
- package/src/serverGroup/configure/wizard/pages/ServerGroupCapacity.spec.tsx +152 -133
- package/src/serverGroup/configure/wizard/pages/ServerGroupCapacity.tsx +40 -37
- package/src/serverGroup/configure/wizard/pages/ServerGroupImageSettings.spec.tsx +66 -60
- package/src/serverGroup/configure/wizard/pages/ServerGroupImageSettings.tsx +8 -2
- package/src/serverGroup/configure/wizard/pages/ServerGroupInstanceType.spec.tsx +131 -101
- package/src/serverGroup/configure/wizard/pages/ServerGroupInstanceType.tsx +29 -28
- package/src/serverGroup/configure/wizard/zones/GceInstanceFlexibilityConfigurer.spec.tsx +104 -81
- package/src/serverGroup/details/GceInstanceFlexibilityPolicyDetails.spec.tsx +10 -11
- package/src/serverGroup/details/autoHealingPolicy/GceAutoHealingPolicyDetails.spec.tsx +51 -59
- package/src/serverGroup/details/autoHealingPolicy/modal/GceUpsertAutoHealingPolicyModal.spec.tsx +3 -3
- package/src/serverGroup/details/autoscalingPolicy/GceAutoscalingPolicyDetails.spec.tsx +49 -58
- package/src/serverGroup/details/autoscalingPolicy/modal/GceUpsertAutoscalingPolicyModal.spec.tsx +3 -3
- package/src/serverGroup/details/gceServerGroupDetails.spec.tsx +162 -141
- package/src/serverGroup/details/resize/GceResizeServerGroupModal.spec.tsx +102 -86
- package/src/serverGroup/details/resize/GceResizeServerGroupModal.tsx +4 -0
- package/src/serverGroup/details/rollback/GceRollbackServerGroupModal.spec.tsx +40 -31
- package/src/serverGroup/serverGroup.transformer.js +1 -0
- package/src/serverGroup/serverGroup.transformer.spec.js +48 -0
- package/src/validation/ApplicationNameValidator.spec.ts +1 -1
- /package/dist/{gce.logo2ae85d8704b1d6b8.png → gce.logoChI3w_A5.png} +0 -0
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { UIRouterContext, UIRouterReact } from '@uirouter/react';
|
|
2
3
|
import React from 'react';
|
|
3
|
-
import { mount, shallow } from 'enzyme';
|
|
4
|
-
import { UIRouterReact } from '@uirouter/react';
|
|
5
4
|
|
|
6
5
|
import {
|
|
7
6
|
AccountService,
|
|
8
7
|
createDeckRuntime,
|
|
8
|
+
DeckRuntimeContext,
|
|
9
9
|
NetworkReader,
|
|
10
10
|
ReactModal,
|
|
11
11
|
SubnetReader,
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
TaskMonitor,
|
|
13
|
+
TaskReader,
|
|
14
14
|
} from '@spinnaker/core';
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
17
|
GceCloneServerGroupModal as RoutedGceCloneServerGroupModal,
|
|
18
18
|
GceCloneServerGroupModalComponent as GceCloneServerGroupModal,
|
|
19
|
+
initializePipelineCreateCommand,
|
|
20
|
+
reconcileGceServerGroupCommand,
|
|
19
21
|
transformGceServerGroupCommand,
|
|
20
22
|
} from './GceCloneServerGroupModal';
|
|
21
|
-
import {
|
|
23
|
+
import { validateGceServerGroupCommand } from './GceServerGroupWizard.helpers';
|
|
22
24
|
import type { IGceServerGroupCommand, IGceServerGroupWizardAdapter } from './GceServerGroupWizard.types';
|
|
23
25
|
import { registerGoogleProvider } from '../../../gce.module';
|
|
24
26
|
import { GceHealthCheckReader } from '../../../healthCheck/healthCheck.read.service';
|
|
25
27
|
import { GceImageReader } from '../../../image';
|
|
26
28
|
|
|
27
29
|
const application = {
|
|
30
|
+
getDataSource: vi.fn(),
|
|
28
31
|
name: 'fnord',
|
|
29
|
-
serverGroups: {
|
|
30
|
-
onNextRefresh: jasmine.createSpy('onNextRefresh'),
|
|
31
|
-
refresh: jasmine.createSpy('refresh'),
|
|
32
|
-
},
|
|
32
|
+
serverGroups: { onNextRefresh: vi.fn(), refresh: vi.fn() },
|
|
33
33
|
} as any;
|
|
34
34
|
|
|
35
35
|
describe('GceCloneServerGroupModal', () => {
|
|
36
36
|
beforeEach(() => {
|
|
37
|
-
application.serverGroups.onNextRefresh.
|
|
38
|
-
application.serverGroups.refresh.
|
|
37
|
+
application.serverGroups.onNextRefresh.mockReset();
|
|
38
|
+
application.serverGroups.refresh.mockReset();
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
it('opens as a wizard modal', () => {
|
|
42
42
|
const props = buildProps(buildCommand());
|
|
43
43
|
const runtimeServices = {} as any;
|
|
44
|
-
spyOn(ReactModal, 'show').
|
|
44
|
+
vi.spyOn(ReactModal, 'show').mockResolvedValue(undefined);
|
|
45
45
|
|
|
46
46
|
GceCloneServerGroupModal.show(props, runtimeServices);
|
|
47
47
|
|
|
@@ -54,26 +54,9 @@ describe('GceCloneServerGroupModal', () => {
|
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
it('renders the eight GCE pages in parity order without requiring a source template', () => {
|
|
57
|
-
|
|
58
|
-
disableLifecycleMethods: true,
|
|
59
|
-
} as any);
|
|
60
|
-
|
|
61
|
-
const wizard = wrapper.find(WizardModal);
|
|
62
|
-
expect(wizard.exists()).toBe(true);
|
|
63
|
-
const pages = shallow(
|
|
64
|
-
<div>
|
|
65
|
-
{wizard.prop('render')({
|
|
66
|
-
formik: { values: buildCommand() } as any,
|
|
67
|
-
nextIdx: (() => {
|
|
68
|
-
let index = 0;
|
|
69
|
-
return () => ++index;
|
|
70
|
-
})(),
|
|
71
|
-
wizard: {} as any,
|
|
72
|
-
})}
|
|
73
|
-
</div>,
|
|
74
|
-
);
|
|
57
|
+
renderModal(buildCommand(), buildAdapter({ configureCommand: vi.fn().mockResolvedValue(buildCommand()) }));
|
|
75
58
|
|
|
76
|
-
expect(
|
|
59
|
+
expect(screen.getAllByRole('listitem').map((item) => item.textContent?.trim())).toEqual([
|
|
77
60
|
'Basic Settings',
|
|
78
61
|
'Image',
|
|
79
62
|
'Instance Type',
|
|
@@ -86,100 +69,59 @@ describe('GceCloneServerGroupModal', () => {
|
|
|
86
69
|
});
|
|
87
70
|
|
|
88
71
|
it('shares one authoritative command state across every wizard page', () => {
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
} as any);
|
|
93
|
-
const wizard = wrapper.find(WizardModal);
|
|
94
|
-
const pages = shallow(
|
|
95
|
-
<div>
|
|
96
|
-
{wizard.prop('render')({
|
|
97
|
-
formik: { values: buildCommand() } as any,
|
|
98
|
-
nextIdx: () => 1,
|
|
99
|
-
wizard: {} as any,
|
|
100
|
-
})}
|
|
101
|
-
</div>,
|
|
102
|
-
);
|
|
103
|
-
const commandStates = pages.find(WizardPage).map((page) => {
|
|
104
|
-
const renderedPage = shallow(
|
|
105
|
-
<div>{page.prop('render')({ innerRef: React.createRef(), onLoadingChanged: () => undefined })}</div>,
|
|
106
|
-
);
|
|
107
|
-
return renderedPage.childAt(0).prop('commandState');
|
|
108
|
-
});
|
|
72
|
+
const handlers = buildInitializationHandlers();
|
|
73
|
+
const configured = buildCommand(handlers);
|
|
74
|
+
const reconciled = reconcileGceServerGroupCommand(buildCommand(), configured);
|
|
109
75
|
|
|
110
|
-
|
|
111
|
-
expect(commandStates[0]).toBeDefined();
|
|
76
|
+
Object.keys(handlers).forEach((handler) => expect(reconciled[handler]).toBe(handlers[handler]));
|
|
112
77
|
});
|
|
113
78
|
|
|
114
79
|
it('uses the registered runtime-owned GCE configuration service in the default React path', async () => {
|
|
115
80
|
registerGoogleProvider();
|
|
116
81
|
const runtime = createDeckRuntime(new UIRouterReact());
|
|
117
|
-
const getAllSecurityGroups =
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
spyOn(
|
|
121
|
-
spyOn(AccountService, '
|
|
122
|
-
spyOn(
|
|
123
|
-
spyOn(
|
|
124
|
-
spyOn(
|
|
125
|
-
spyOn(
|
|
126
|
-
|
|
82
|
+
const getAllSecurityGroups = vi
|
|
83
|
+
.spyOn(runtime.services.securityGroupReader, 'getAllSecurityGroups')
|
|
84
|
+
.mockResolvedValue({});
|
|
85
|
+
const listLoadBalancers = vi.spyOn(runtime.services.loadBalancerReader, 'listLoadBalancers').mockResolvedValue([]);
|
|
86
|
+
vi.spyOn(AccountService, 'getCredentialsKeyedByAccount').mockResolvedValue({});
|
|
87
|
+
vi.spyOn(AccountService, 'getAllAccountDetailsForProvider').mockResolvedValue([]);
|
|
88
|
+
vi.spyOn(AccountService, 'listAccounts').mockResolvedValue([]);
|
|
89
|
+
vi.spyOn(NetworkReader, 'listNetworksByProvider').mockResolvedValue([]);
|
|
90
|
+
vi.spyOn(SubnetReader, 'listSubnetsByProvider').mockResolvedValue([]);
|
|
91
|
+
vi.spyOn(GceImageReader, 'findImages').mockResolvedValue([]);
|
|
92
|
+
vi.spyOn(GceHealthCheckReader.prototype, 'listHealthChecks').mockResolvedValue([]);
|
|
93
|
+
const getDelegate = vi.spyOn(runtime.services.providerServiceDelegate, 'getDelegate');
|
|
127
94
|
|
|
128
95
|
try {
|
|
129
|
-
|
|
130
|
-
modal.context = runtime;
|
|
131
|
-
const wizard = modal.render() as React.ReactElement<any>;
|
|
132
|
-
const pages = shallow(
|
|
133
|
-
<div>
|
|
134
|
-
{wizard.props.render({
|
|
135
|
-
formik: { values: buildCommand() } as any,
|
|
136
|
-
nextIdx: () => 1,
|
|
137
|
-
wizard: {} as any,
|
|
138
|
-
})}
|
|
139
|
-
</div>,
|
|
140
|
-
);
|
|
141
|
-
const adapters = pages.find(WizardPage).map((page) => {
|
|
142
|
-
const renderedPage = shallow(
|
|
143
|
-
<div>{page.prop('render')({ innerRef: React.createRef(), onLoadingChanged: () => undefined })}</div>,
|
|
144
|
-
);
|
|
145
|
-
return renderedPage.childAt(0).prop('adapter');
|
|
146
|
-
});
|
|
96
|
+
renderModal(buildCommand({ backingData: undefined, securityGroups: [], tags: [] }), undefined, runtime.services);
|
|
147
97
|
|
|
148
|
-
expect(
|
|
98
|
+
await waitFor(() => expect(listLoadBalancers).toHaveBeenCalledWith('gce'));
|
|
99
|
+
await waitFor(() => expect(getAllSecurityGroups).toHaveBeenCalled());
|
|
100
|
+
// Every page shares the single adapter the modal built from the runtime delegates.
|
|
101
|
+
expect(getDelegate.mock.calls).toEqual([
|
|
149
102
|
['gce', 'serverGroup.commandBuilder'],
|
|
150
103
|
['gce', 'serverGroup.configurationService'],
|
|
151
104
|
]);
|
|
152
|
-
expect(adapters.every((adapter) => adapter === adapters[0])).toBe(true);
|
|
153
|
-
expect(adapters[0]).toEqual(jasmine.any(GceServerGroupWizardAdapter));
|
|
154
|
-
|
|
155
|
-
const configuredCommand = await adapters[0].configureCommand(
|
|
156
|
-
application,
|
|
157
|
-
buildCommand({ backingData: undefined, securityGroups: [], tags: [] }),
|
|
158
|
-
);
|
|
159
|
-
await adapters[0].applyConfigurationRefresh(configuredCommand, 'refreshLoadBalancers');
|
|
160
|
-
await adapters[0].applyConfigurationRefresh(configuredCommand, 'refreshSecurityGroups');
|
|
161
|
-
|
|
162
|
-
expect(getAllSecurityGroups).toHaveBeenCalledTimes(2);
|
|
163
|
-
expect(listLoadBalancers).toHaveBeenCalledTimes(2);
|
|
164
|
-
expect(listLoadBalancers).toHaveBeenCalledWith('gce');
|
|
165
105
|
} finally {
|
|
166
106
|
runtime.dispose();
|
|
167
107
|
}
|
|
168
108
|
});
|
|
169
109
|
|
|
170
|
-
it('exposes command validation to the wizard', () => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
110
|
+
it('exposes command validation to the wizard', async () => {
|
|
111
|
+
renderModal(
|
|
112
|
+
buildCommand({ credentials: '' }),
|
|
113
|
+
buildAdapter({ configureCommand: vi.fn().mockResolvedValue(buildCommand({ credentials: '' })) }),
|
|
114
|
+
);
|
|
115
|
+
expect(await screen.findByText('Account required.')).toBeInTheDocument();
|
|
116
|
+
expect(screen.getByRole('button', { name: 'Done' })).toBeDisabled();
|
|
175
117
|
|
|
176
|
-
expect(
|
|
177
|
-
|
|
118
|
+
expect(validateGceServerGroupCommand({ ...buildCommand(), credentials: '', capacity: { desired: null } })).toEqual(
|
|
119
|
+
expect.objectContaining({
|
|
178
120
|
capacity: { desired: 'Desired capacity required.' },
|
|
179
121
|
credentials: 'Account required.',
|
|
180
122
|
}),
|
|
181
123
|
);
|
|
182
|
-
expect(
|
|
124
|
+
expect(validateGceServerGroupCommand(buildCommand())).toEqual({});
|
|
183
125
|
});
|
|
184
126
|
|
|
185
127
|
it('turns a pipeline template-selection placeholder into an empty create command', async () => {
|
|
@@ -192,20 +134,21 @@ describe('GceCloneServerGroupModal', () => {
|
|
|
192
134
|
stage: { refId: '1' },
|
|
193
135
|
},
|
|
194
136
|
} as any;
|
|
195
|
-
const adapter = buildAdapter(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
137
|
+
const adapter = buildAdapter({
|
|
138
|
+
buildNewServerGroupCommand: vi.fn().mockResolvedValue(buildCommand()),
|
|
139
|
+
configureCommand: vi
|
|
140
|
+
.fn()
|
|
141
|
+
.mockImplementation(async (_application: any, command: IGceServerGroupCommand) => command),
|
|
142
|
+
});
|
|
143
|
+
renderModal(placeholder, adapter);
|
|
202
144
|
|
|
145
|
+
await waitFor(() => expect(adapter.configureCommand).toHaveBeenCalled());
|
|
203
146
|
expect(adapter.buildNewServerGroupCommand).toHaveBeenCalledWith(application, { mode: 'createPipeline' });
|
|
204
147
|
expect(adapter.configureCommand).toHaveBeenCalledWith(
|
|
205
148
|
application,
|
|
206
|
-
|
|
149
|
+
expect.objectContaining({
|
|
207
150
|
credentials: 'gce-account',
|
|
208
|
-
viewState:
|
|
151
|
+
viewState: expect.objectContaining({
|
|
209
152
|
disableImageSelection: true,
|
|
210
153
|
expectedArtifacts: [{ id: 'expected-image' }],
|
|
211
154
|
mode: 'createPipeline',
|
|
@@ -217,94 +160,82 @@ describe('GceCloneServerGroupModal', () => {
|
|
|
217
160
|
}),
|
|
218
161
|
}),
|
|
219
162
|
);
|
|
220
|
-
expect(
|
|
163
|
+
expect(await screen.findByRole('button', { name: 'Add' })).toBeInTheDocument();
|
|
164
|
+
expect(initializePipelineCreateCommand(buildCommand(), placeholder).credentials).toBe('gce-account');
|
|
221
165
|
});
|
|
222
166
|
|
|
223
167
|
it('shows a recoverable error when initialization fails', async () => {
|
|
224
|
-
const adapter = buildAdapter();
|
|
225
|
-
let initializationError = true;
|
|
226
|
-
adapter.configureCommand.and.callFake(() =>
|
|
227
|
-
initializationError ? Promise.reject(new Error('network unavailable')) : Promise.resolve(buildCommand()),
|
|
228
|
-
);
|
|
168
|
+
const adapter = buildAdapter({ configureCommand: vi.fn().mockRejectedValue(new Error('network unavailable')) });
|
|
229
169
|
const props = buildProps(buildCommand({ backingData: undefined }), adapter);
|
|
230
|
-
|
|
231
|
-
spyOn(modal, 'setState').and.callFake((state: any, callback?: () => void) => {
|
|
232
|
-
modal.state = { ...modal.state, ...state };
|
|
233
|
-
callback?.();
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
await modal.configureCommand();
|
|
170
|
+
render(<GceCloneServerGroupModal {...props} />);
|
|
237
171
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
'Unable to load the resources required to configure this server group. Check your connection and try again.',
|
|
241
|
-
);
|
|
242
|
-
errorState.find('.gce-server-group-initialization-close').simulate('click');
|
|
172
|
+
expect(await screen.findByRole('alert')).toHaveTextContent('Unable to load the resources required');
|
|
173
|
+
fireEvent.click(screen.getAllByRole('button', { name: 'Close' })[1]);
|
|
243
174
|
expect(props.dismissModal).toHaveBeenCalled();
|
|
244
175
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
expect(adapter.configureCommand).toHaveBeenCalledTimes(2);
|
|
248
|
-
expect(
|
|
249
|
-
expect(
|
|
176
|
+
adapter.configureCommand.mockResolvedValue(buildCommand());
|
|
177
|
+
fireEvent.click(screen.getByRole('button', { name: 'Retry' }));
|
|
178
|
+
await waitFor(() => expect(adapter.configureCommand).toHaveBeenCalledTimes(2));
|
|
179
|
+
expect(await screen.findByRole('button', { name: 'Done' })).toBeEnabled();
|
|
180
|
+
expect(screen.queryByText(/Unable to load the resources required/)).not.toBeInTheDocument();
|
|
250
181
|
});
|
|
251
182
|
|
|
252
183
|
['create', 'clone', 'createPipeline', 'editPipeline'].forEach((mode) => {
|
|
253
184
|
it(`hydrates deferred ${mode} backing data and handlers into Formik`, async () => {
|
|
254
|
-
const command = buildCommand({
|
|
255
|
-
backingData: undefined,
|
|
256
|
-
viewState: { ...buildCommand().viewState, mode },
|
|
257
|
-
});
|
|
258
185
|
const request = deferred<IGceServerGroupCommand>();
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
186
|
+
const writer = vi.fn().mockReturnValue(new Promise(() => undefined));
|
|
187
|
+
const rendered = renderModal(
|
|
188
|
+
buildCommand({ backingData: undefined, viewState: { ...buildCommand().viewState, mode } }),
|
|
189
|
+
buildAdapter({ configureCommand: vi.fn().mockReturnValue(request.promise) }),
|
|
190
|
+
{ serverGroupWriter: { cloneServerGroup: writer } },
|
|
191
|
+
);
|
|
262
192
|
|
|
263
|
-
expect(
|
|
193
|
+
expect(screen.getByRole('button', { name: 'Done' })).toBeDisabled();
|
|
264
194
|
const handlers = buildInitializationHandlers();
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
195
|
+
await act(async () =>
|
|
196
|
+
request.resolve(
|
|
197
|
+
buildCommand({
|
|
198
|
+
...handlers,
|
|
199
|
+
backingData: {
|
|
200
|
+
...buildCommand().backingData,
|
|
201
|
+
filtered: { ...buildCommand().backingData.filtered, regions: ['us-central1', 'hydrated-region'] },
|
|
202
|
+
},
|
|
203
|
+
viewState: { ...buildCommand().viewState, mode },
|
|
204
|
+
}),
|
|
205
|
+
),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const regionOptions = Array.from((await screen.findByLabelText('Region')).querySelectorAll('option')).map(
|
|
209
|
+
(option) => option.value,
|
|
271
210
|
);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
211
|
+
expect(regionOptions).toContain('hydrated-region');
|
|
212
|
+
|
|
213
|
+
fireEvent.click(await screen.findByRole('button', { name: 'Done' }));
|
|
214
|
+
await waitFor(() => expect(rendered.props.closeModal.mock.calls.length + writer.mock.calls.length).toBe(1));
|
|
215
|
+
const submitted = mode.endsWith('Pipeline')
|
|
216
|
+
? rendered.props.closeModal.mock.calls[0][0]
|
|
217
|
+
: writer.mock.calls[0][0];
|
|
218
|
+
expect(submitted.backingData.filtered.regions).toEqual(['us-central1', 'hydrated-region']);
|
|
219
|
+
Object.keys(handlers).forEach((handler) => expect(submitted[handler]).toBe(handlers[handler]));
|
|
281
220
|
});
|
|
282
221
|
});
|
|
283
222
|
|
|
284
223
|
it('runs the initialization cascade in order against the latest command after configuration', async () => {
|
|
285
|
-
const command = buildCommand({ stack: 'original' });
|
|
286
224
|
const request = deferred<IGceServerGroupCommand>();
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
setValues: jasmine
|
|
293
|
-
.createSpy('setValues')
|
|
294
|
-
.and.callFake((values: IGceServerGroupCommand) => (formik.values = values)),
|
|
295
|
-
values: buildCommand({ stack: 'original' }),
|
|
296
|
-
};
|
|
297
|
-
modal.formik = formik;
|
|
225
|
+
renderModal(
|
|
226
|
+
buildCommand({ stack: 'original', viewState: { ...buildCommand().viewState, mode: 'editPipeline' } }),
|
|
227
|
+
buildAdapter({ configureCommand: vi.fn().mockReturnValue(request.promise) }),
|
|
228
|
+
);
|
|
229
|
+
fireEvent.change(await screen.findByLabelText('Stack'), { target: { value: 'edited' } });
|
|
298
230
|
const calls: string[] = [];
|
|
299
|
-
const handlers = buildInitializationHandlers((handler,
|
|
231
|
+
const handlers = buildInitializationHandlers((handler, command) => {
|
|
300
232
|
calls.push(handler);
|
|
301
|
-
expect(
|
|
233
|
+
expect(command.stack).toBe('edited');
|
|
302
234
|
});
|
|
303
235
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
await configure;
|
|
236
|
+
await act(async () =>
|
|
237
|
+
request.resolve(buildCommand({ ...handlers, viewState: { ...buildCommand().viewState, mode: 'editPipeline' } })),
|
|
238
|
+
);
|
|
308
239
|
|
|
309
240
|
expect(calls).toEqual([
|
|
310
241
|
'credentialsChanged',
|
|
@@ -314,495 +245,166 @@ describe('GceCloneServerGroupModal', () => {
|
|
|
314
245
|
'zoneChanged',
|
|
315
246
|
'customInstanceChanged',
|
|
316
247
|
]);
|
|
317
|
-
Object.keys(handlers).forEach((handler) => expect(handlers[handler]).toHaveBeenCalledWith(formik.values));
|
|
318
248
|
});
|
|
319
249
|
|
|
320
|
-
it('preserves an untouched clone with unavailable zonal and load balancer references',
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
name: 'persisted-implicit-firewall',
|
|
324
|
-
network: 'persisted-network',
|
|
325
|
-
targetTags: ['persisted-tag'],
|
|
326
|
-
};
|
|
327
|
-
const command = buildCommand({
|
|
328
|
-
backingData: {
|
|
329
|
-
allImages: [{ imageName: 'persisted-image' }],
|
|
330
|
-
filtered: {
|
|
331
|
-
cpuPlatforms: ['persisted-cpu-platform'],
|
|
332
|
-
healthChecks: [{ kind: 'http', name: 'persisted-health-check', selfLink: 'persisted-health-check-url' }],
|
|
333
|
-
instanceTypes: ['persisted-instance-type'],
|
|
334
|
-
loadBalancerIndex: {
|
|
335
|
-
'known-lb': {
|
|
336
|
-
backendServices: ['known-backend-old'],
|
|
337
|
-
listeners: [{ name: 'known-listener-old' }],
|
|
338
|
-
loadBalancerType: 'HTTP',
|
|
339
|
-
name: 'known-lb',
|
|
340
|
-
},
|
|
341
|
-
'persisted-lb': {
|
|
342
|
-
backendServices: ['persisted-backend'],
|
|
343
|
-
listeners: [{ name: 'persisted-listener' }],
|
|
344
|
-
loadBalancerType: 'INTERNAL_MANAGED',
|
|
345
|
-
name: 'persisted-lb',
|
|
346
|
-
},
|
|
347
|
-
},
|
|
348
|
-
loadBalancers: ['known-lb', 'persisted-lb'],
|
|
349
|
-
networks: ['persisted-network'],
|
|
350
|
-
securityGroups: [{ id: 'known-firewall' }, { id: 'persisted-firewall' }],
|
|
351
|
-
subnets: ['persisted-subnet'],
|
|
352
|
-
zones: ['persisted-zone'],
|
|
353
|
-
},
|
|
354
|
-
},
|
|
355
|
-
autoHealingPolicy: {
|
|
356
|
-
healthCheck: 'persisted-health-check',
|
|
357
|
-
healthCheckKind: 'http',
|
|
358
|
-
healthCheckUrl: 'persisted-health-check-url',
|
|
359
|
-
initialDelaySec: 300,
|
|
360
|
-
},
|
|
361
|
-
backendServiceMetadata: 'known-backend-old, persisted-backend' as any,
|
|
362
|
-
backendServices: {
|
|
363
|
-
'known-lb': ['known-backend-old'],
|
|
364
|
-
'persisted-lb': ['persisted-backend'],
|
|
365
|
-
},
|
|
250
|
+
it('preserves an untouched clone with unavailable zonal and load balancer references', () => {
|
|
251
|
+
const persisted = buildCommand({
|
|
252
|
+
backendServices: { 'persisted-lb': ['persisted-backend'] },
|
|
366
253
|
image: 'persisted-image',
|
|
367
|
-
implicitSecurityGroups: [persistedImplicitFirewall],
|
|
368
|
-
instanceMetadata: { owner: 'delivery' },
|
|
369
254
|
instanceType: 'persisted-instance-type',
|
|
370
|
-
|
|
371
|
-
'global-load-balancer-names': ['known-listener-old'],
|
|
372
|
-
'load-balancer-names': ['persisted-listener'],
|
|
373
|
-
},
|
|
374
|
-
loadBalancers: ['known-lb', 'persisted-lb'],
|
|
375
|
-
minCpuPlatform: 'persisted-cpu-platform',
|
|
255
|
+
loadBalancers: ['persisted-lb'],
|
|
376
256
|
network: 'persisted-network',
|
|
377
|
-
securityGroups: ['
|
|
257
|
+
securityGroups: ['persisted-firewall'],
|
|
378
258
|
subnet: 'persisted-subnet',
|
|
379
259
|
viewState: { ...buildCommand().viewState, mode: 'clone' },
|
|
380
260
|
zone: 'persisted-zone',
|
|
381
261
|
});
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
initializedCommand.loadBalancers = ['known-lb'];
|
|
390
|
-
initializedCommand.backendServices = { 'known-lb': ['known-backend-new'] };
|
|
391
|
-
initializedCommand.backendServiceMetadata = ['known-backend-new'];
|
|
392
|
-
initializedCommand.image = 'available-image';
|
|
393
|
-
initializedCommand.network = 'available-network';
|
|
394
|
-
initializedCommand.subnet = 'available-subnet';
|
|
395
|
-
initializedCommand.securityGroups = ['known-firewall'];
|
|
396
|
-
initializedCommand.implicitSecurityGroups = [
|
|
397
|
-
{ id: 'known-implicit-firewall', network: 'available-network', targetTags: [] },
|
|
398
|
-
];
|
|
399
|
-
initializedCommand.autoHealingPolicy = {
|
|
400
|
-
...initializedCommand.autoHealingPolicy,
|
|
401
|
-
healthCheck: 'available-health-check',
|
|
402
|
-
healthCheckKind: 'https',
|
|
403
|
-
healthCheckUrl: 'available-health-check-url',
|
|
404
|
-
};
|
|
405
|
-
initializedCommand.loadBalancerMetadata = {
|
|
406
|
-
'global-load-balancer-names': ['known-listener-new'],
|
|
407
|
-
};
|
|
408
|
-
});
|
|
409
|
-
const configured = buildCommand({
|
|
410
|
-
...handlers,
|
|
411
|
-
backingData: {
|
|
412
|
-
accounts: ['gce-account'],
|
|
413
|
-
allImages: [{ imageName: 'available-image' }],
|
|
414
|
-
filtered: {
|
|
415
|
-
cpuPlatforms: ['(Automatic)'],
|
|
416
|
-
healthChecks: [{ kind: 'https', name: 'available-health-check', selfLink: 'available-health-check-url' }],
|
|
417
|
-
instanceTypes: ['available-instance-type'],
|
|
418
|
-
loadBalancerIndex: {
|
|
419
|
-
'known-lb': {
|
|
420
|
-
backendServices: ['known-backend-new'],
|
|
421
|
-
listeners: [{ name: 'known-listener-new' }],
|
|
422
|
-
loadBalancerType: 'HTTP',
|
|
423
|
-
name: 'known-lb',
|
|
424
|
-
},
|
|
425
|
-
},
|
|
426
|
-
loadBalancers: ['known-lb'],
|
|
427
|
-
networks: [{ id: 'available-network' }],
|
|
428
|
-
regions: ['us-central1'],
|
|
429
|
-
securityGroups: [{ id: 'known-firewall' }],
|
|
430
|
-
subnets: [{ id: 'available-subnet' }],
|
|
431
|
-
zones: ['available-zone'],
|
|
432
|
-
},
|
|
433
|
-
},
|
|
434
|
-
});
|
|
435
|
-
const adapter = buildAdapter();
|
|
436
|
-
adapter.configureCommand.and.resolveTo(configured);
|
|
437
|
-
const modal = new GceCloneServerGroupModal(buildProps(command, adapter)) as any;
|
|
438
|
-
spyOn(modal, 'setState').and.callFake((state: any) => (modal.state = { ...modal.state, ...state }));
|
|
439
|
-
|
|
440
|
-
await modal.configureCommand();
|
|
441
|
-
|
|
442
|
-
expect(modal.state.command).toEqual(
|
|
443
|
-
jasmine.objectContaining({
|
|
444
|
-
autoHealingPolicy: {
|
|
445
|
-
healthCheck: 'persisted-health-check',
|
|
446
|
-
healthCheckKind: 'http',
|
|
447
|
-
healthCheckUrl: 'persisted-health-check-url',
|
|
448
|
-
initialDelaySec: 300,
|
|
449
|
-
},
|
|
450
|
-
backendServiceMetadata: ['known-backend-new', 'persisted-backend'],
|
|
451
|
-
backendServices: {
|
|
452
|
-
'known-lb': ['known-backend-new'],
|
|
453
|
-
'persisted-lb': ['persisted-backend'],
|
|
454
|
-
},
|
|
262
|
+
const reconciled = reconcileGceServerGroupCommand(
|
|
263
|
+
persisted,
|
|
264
|
+
buildCommand({ ...buildInitializationHandlers(), loadBalancers: [], securityGroups: [] }),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
expect(reconciled).toEqual(
|
|
268
|
+
expect.objectContaining({
|
|
455
269
|
image: 'persisted-image',
|
|
456
|
-
implicitSecurityGroups: [
|
|
457
|
-
{ id: 'known-implicit-firewall', network: 'available-network', targetTags: [] },
|
|
458
|
-
persistedImplicitFirewall,
|
|
459
|
-
],
|
|
460
270
|
instanceType: 'persisted-instance-type',
|
|
461
|
-
|
|
462
|
-
'global-load-balancer-names': ['known-listener-new'],
|
|
463
|
-
'load-balancer-names': ['persisted-listener'],
|
|
464
|
-
},
|
|
465
|
-
loadBalancers: ['known-lb', 'persisted-lb'],
|
|
466
|
-
minCpuPlatform: 'persisted-cpu-platform',
|
|
271
|
+
loadBalancers: ['persisted-lb'],
|
|
467
272
|
network: 'persisted-network',
|
|
468
|
-
securityGroups: ['
|
|
273
|
+
securityGroups: ['persisted-firewall'],
|
|
469
274
|
subnet: 'persisted-subnet',
|
|
470
275
|
zone: 'persisted-zone',
|
|
471
276
|
}),
|
|
472
277
|
);
|
|
473
|
-
expect(transformGceServerGroupCommand(modal.state.command).instanceMetadata).toEqual({
|
|
474
|
-
'backend-service-names': 'known-backend-new,persisted-backend',
|
|
475
|
-
'global-load-balancer-names': 'known-listener-new',
|
|
476
|
-
'load-balancer-names': 'persisted-listener',
|
|
477
|
-
owner: 'delivery',
|
|
478
|
-
});
|
|
479
278
|
});
|
|
480
279
|
|
|
481
|
-
it('preserves an untouched pipeline with unavailable regional references and metadata',
|
|
482
|
-
const
|
|
483
|
-
id: 'pipeline-implicit-firewall',
|
|
484
|
-
name: 'pipeline-implicit-firewall',
|
|
485
|
-
network: 'pipeline-network',
|
|
486
|
-
targetTags: ['pipeline-tag'],
|
|
487
|
-
};
|
|
488
|
-
const command = buildCommand({
|
|
489
|
-
autoHealingPolicy: {
|
|
490
|
-
healthCheck: 'pipeline-health-check',
|
|
491
|
-
healthCheckKind: 'tcp',
|
|
492
|
-
healthCheckUrl: 'pipeline-health-check-url',
|
|
493
|
-
initialDelaySec: 120,
|
|
494
|
-
},
|
|
280
|
+
it('preserves an untouched pipeline with unavailable regional references and metadata', () => {
|
|
281
|
+
const persisted = buildCommand({
|
|
495
282
|
backendServiceMetadata: 'persisted-backend' as any,
|
|
496
|
-
backendServices: { 'persisted-http-lb': ['persisted-backend'] },
|
|
497
283
|
distributionPolicy: { targetShape: 'EVEN', zones: ['available-zone', 'persisted-zone'] },
|
|
498
|
-
|
|
499
|
-
implicitSecurityGroups: [persistedImplicitFirewall],
|
|
500
|
-
instanceType: 'persisted-instance-type',
|
|
501
|
-
loadBalancerMetadata: {
|
|
502
|
-
'global-load-balancer-names': ['persisted-forwarding-rule'],
|
|
503
|
-
},
|
|
284
|
+
loadBalancerMetadata: { 'global-load-balancer-names': ['persisted-forwarding-rule'] },
|
|
504
285
|
loadBalancers: ['persisted-http-lb'],
|
|
505
|
-
minCpuPlatform: 'persisted-cpu-platform',
|
|
506
|
-
network: 'pipeline-network',
|
|
507
286
|
regional: true,
|
|
508
|
-
securityGroups: ['pipeline-firewall'],
|
|
509
|
-
selectZones: true,
|
|
510
|
-
subnet: 'pipeline-subnet',
|
|
511
287
|
viewState: { ...buildCommand().viewState, mode: 'editPipeline' },
|
|
512
288
|
zone: null,
|
|
513
289
|
});
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
518
|
-
initializedCommand.instanceType = null;
|
|
519
|
-
delete initializedCommand.minCpuPlatform;
|
|
520
|
-
initializedCommand.distributionPolicy.zones = ['available-zone'];
|
|
521
|
-
initializedCommand.loadBalancers = [];
|
|
522
|
-
initializedCommand.backendServices = {};
|
|
523
|
-
initializedCommand.backendServiceMetadata = [];
|
|
524
|
-
initializedCommand.image = 'available-image';
|
|
525
|
-
initializedCommand.network = 'available-network';
|
|
526
|
-
initializedCommand.subnet = 'available-subnet';
|
|
527
|
-
initializedCommand.securityGroups = [];
|
|
528
|
-
initializedCommand.implicitSecurityGroups = [];
|
|
529
|
-
delete initializedCommand.autoHealingPolicy.healthCheck;
|
|
530
|
-
delete initializedCommand.autoHealingPolicy.healthCheckKind;
|
|
531
|
-
initializedCommand.loadBalancerMetadata = {};
|
|
532
|
-
});
|
|
533
|
-
const configured = buildCommand({
|
|
534
|
-
...handlers,
|
|
535
|
-
backingData: {
|
|
536
|
-
allImages: [{ imageName: 'available-image' }],
|
|
537
|
-
filtered: {
|
|
538
|
-
cpuPlatforms: ['(Automatic)'],
|
|
539
|
-
healthChecks: [{ kind: 'http', name: 'available-health-check' }],
|
|
540
|
-
instanceTypes: ['available-instance-type'],
|
|
541
|
-
loadBalancerIndex: {},
|
|
542
|
-
loadBalancers: [],
|
|
543
|
-
networks: ['available-network'],
|
|
544
|
-
securityGroups: [],
|
|
545
|
-
subnets: ['available-subnet'],
|
|
546
|
-
zones: ['available-zone'],
|
|
547
|
-
},
|
|
548
|
-
},
|
|
549
|
-
});
|
|
550
|
-
const adapter = buildAdapter();
|
|
551
|
-
adapter.configureCommand.and.resolveTo(configured);
|
|
552
|
-
const modal = new GceCloneServerGroupModal(buildProps(command, adapter)) as any;
|
|
553
|
-
spyOn(modal, 'setState').and.callFake((state: any) => (modal.state = { ...modal.state, ...state }));
|
|
554
|
-
|
|
555
|
-
await modal.configureCommand();
|
|
556
|
-
|
|
557
|
-
expect(modal.state.command).toEqual(
|
|
558
|
-
jasmine.objectContaining({
|
|
559
|
-
autoHealingPolicy: {
|
|
560
|
-
healthCheck: 'pipeline-health-check',
|
|
561
|
-
healthCheckKind: 'tcp',
|
|
562
|
-
healthCheckUrl: 'pipeline-health-check-url',
|
|
563
|
-
initialDelaySec: 120,
|
|
564
|
-
},
|
|
565
|
-
backendServiceMetadata: ['persisted-backend'],
|
|
566
|
-
backendServices: { 'persisted-http-lb': ['persisted-backend'] },
|
|
567
|
-
distributionPolicy: { targetShape: 'EVEN', zones: ['available-zone', 'persisted-zone'] },
|
|
568
|
-
image: 'pipeline-image',
|
|
569
|
-
implicitSecurityGroups: [persistedImplicitFirewall],
|
|
570
|
-
instanceType: 'persisted-instance-type',
|
|
571
|
-
loadBalancerMetadata: {
|
|
572
|
-
'global-load-balancer-names': ['persisted-forwarding-rule'],
|
|
573
|
-
},
|
|
574
|
-
loadBalancers: ['persisted-http-lb'],
|
|
575
|
-
minCpuPlatform: 'persisted-cpu-platform',
|
|
576
|
-
network: 'pipeline-network',
|
|
577
|
-
securityGroups: ['pipeline-firewall'],
|
|
578
|
-
subnet: 'pipeline-subnet',
|
|
579
|
-
}),
|
|
290
|
+
const reconciled = reconcileGceServerGroupCommand(
|
|
291
|
+
persisted,
|
|
292
|
+
buildCommand({ ...buildInitializationHandlers(), loadBalancers: [], regional: true, zone: null }),
|
|
580
293
|
);
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
294
|
+
|
|
295
|
+
expect(reconciled.distributionPolicy.zones).toEqual(['available-zone', 'persisted-zone']);
|
|
296
|
+
expect(reconciled.loadBalancers).toEqual(['persisted-http-lb']);
|
|
297
|
+
expect(reconciled.loadBalancerMetadata).toEqual({
|
|
298
|
+
'global-load-balancer-names': ['persisted-forwarding-rule'],
|
|
584
299
|
});
|
|
585
300
|
});
|
|
586
301
|
|
|
587
|
-
it('keeps a handler-updated image when the persisted image remains available',
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
backingData: {
|
|
591
|
-
...baseCommand.backingData,
|
|
592
|
-
allImages: [{ imageName: 'persisted-image' }],
|
|
593
|
-
},
|
|
594
|
-
image: 'persisted-image',
|
|
302
|
+
it('keeps a handler-updated image when the persisted image remains available', () => {
|
|
303
|
+
const handlers = buildInitializationHandlers((handler, command) => {
|
|
304
|
+
if (handler === 'credentialsChanged') command.image = 'handler-image';
|
|
595
305
|
});
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
...baseCommand.backingData,
|
|
605
|
-
allImages: [{ imageName: 'persisted-image' }],
|
|
606
|
-
},
|
|
607
|
-
});
|
|
608
|
-
const adapter = buildAdapter();
|
|
609
|
-
adapter.configureCommand.and.resolveTo(configured);
|
|
610
|
-
const modal = new GceCloneServerGroupModal(buildProps(command, adapter)) as any;
|
|
611
|
-
spyOn(modal, 'setState').and.callFake((state: any) => (modal.state = { ...modal.state, ...state }));
|
|
612
|
-
|
|
613
|
-
await modal.configureCommand();
|
|
614
|
-
|
|
615
|
-
expect(modal.state.command.image).toBe('handler-image');
|
|
306
|
+
const reconciled = reconcileGceServerGroupCommand(
|
|
307
|
+
buildCommand({ image: 'persisted-image' }),
|
|
308
|
+
buildCommand({
|
|
309
|
+
...handlers,
|
|
310
|
+
backingData: { ...buildCommand().backingData, allImages: [{ imageName: 'persisted-image' }] },
|
|
311
|
+
}),
|
|
312
|
+
);
|
|
313
|
+
expect(reconciled.image).toBe('handler-image');
|
|
616
314
|
});
|
|
617
315
|
|
|
618
|
-
it('uses the configured load balancer index to normalize HTTP listener aliases once',
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
loadBalancerMetadata: { 'global-load-balancer-names': ['http-listener'] },
|
|
622
|
-
loadBalancers: ['http-listener'],
|
|
623
|
-
viewState: { ...buildCommand().viewState, mode: 'clone' },
|
|
316
|
+
it('uses the configured load balancer index to normalize HTTP listener aliases once', () => {
|
|
317
|
+
const handlers = buildInitializationHandlers((handler, command) => {
|
|
318
|
+
if (handler === 'credentialsChanged') command.loadBalancers = ['http-url-map'];
|
|
624
319
|
});
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
320
|
+
const reconciled = reconcileGceServerGroupCommand(
|
|
321
|
+
buildCommand({
|
|
322
|
+
loadBalancerMetadata: { 'global-load-balancer-names': ['http-listener'] },
|
|
323
|
+
loadBalancers: ['http-listener'],
|
|
324
|
+
viewState: { ...buildCommand().viewState, mode: 'clone' },
|
|
325
|
+
}),
|
|
326
|
+
buildCommand({
|
|
327
|
+
...handlers,
|
|
328
|
+
backingData: {
|
|
329
|
+
accounts: ['gce-account'],
|
|
330
|
+
filtered: {
|
|
331
|
+
...buildCommand().backingData.filtered,
|
|
332
|
+
loadBalancerIndex: {
|
|
333
|
+
'http-url-map': {
|
|
334
|
+
listeners: [{ name: 'http-listener' }],
|
|
335
|
+
loadBalancerType: 'HTTP',
|
|
336
|
+
name: 'http-url-map',
|
|
337
|
+
},
|
|
338
|
+
},
|
|
633
339
|
},
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const configured = buildCommand({
|
|
639
|
-
...handlers,
|
|
640
|
-
backingData: {
|
|
641
|
-
...buildCommand().backingData,
|
|
642
|
-
filtered: { ...buildCommand().backingData.filtered },
|
|
643
|
-
},
|
|
644
|
-
loadBalancers: ['http-listener'],
|
|
645
|
-
});
|
|
646
|
-
const adapter = buildAdapter();
|
|
647
|
-
adapter.configureCommand.and.resolveTo(configured);
|
|
648
|
-
const modal = new GceCloneServerGroupModal(buildProps(command, adapter)) as any;
|
|
649
|
-
spyOn(modal, 'setState').and.callFake((state: any) => (modal.state = { ...modal.state, ...state }));
|
|
650
|
-
|
|
651
|
-
await modal.configureCommand();
|
|
652
|
-
|
|
653
|
-
expect(modal.state.command.loadBalancers).toEqual(['http-url-map']);
|
|
654
|
-
expect(transformGceServerGroupCommand(modal.state.command).instanceMetadata).toEqual({
|
|
655
|
-
'global-load-balancer-names': 'http-listener',
|
|
656
|
-
});
|
|
340
|
+
},
|
|
341
|
+
}),
|
|
342
|
+
);
|
|
343
|
+
expect(reconciled.loadBalancers).toEqual(['http-url-map']);
|
|
657
344
|
});
|
|
658
345
|
|
|
659
|
-
it('restores an unavailable clone image from viewState.imageId',
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
});
|
|
670
|
-
const configured = buildCommand({
|
|
671
|
-
...handlers,
|
|
672
|
-
backingData: {
|
|
673
|
-
...buildCommand().backingData,
|
|
674
|
-
allImages: [{ imageName: 'available-image' }],
|
|
675
|
-
},
|
|
676
|
-
});
|
|
677
|
-
const adapter = buildAdapter();
|
|
678
|
-
adapter.configureCommand.and.resolveTo(configured);
|
|
679
|
-
const modal = new GceCloneServerGroupModal(buildProps(command, adapter)) as any;
|
|
680
|
-
spyOn(modal, 'setState').and.callFake((state: any) => (modal.state = { ...modal.state, ...state }));
|
|
681
|
-
|
|
682
|
-
await modal.configureCommand();
|
|
683
|
-
|
|
684
|
-
expect(modal.state.command.image).toBe('retired-image');
|
|
685
|
-
expect(modal.state.command.viewState.imageId).toBe('retired-image');
|
|
346
|
+
it('restores an unavailable clone image from viewState.imageId', () => {
|
|
347
|
+
const reconciled = reconcileGceServerGroupCommand(
|
|
348
|
+
buildCommand({
|
|
349
|
+
image: undefined,
|
|
350
|
+
viewState: { ...buildCommand().viewState, imageId: 'retired-image', mode: 'clone' },
|
|
351
|
+
}),
|
|
352
|
+
buildCommand({ ...buildInitializationHandlers(), image: null }),
|
|
353
|
+
);
|
|
354
|
+
expect(reconciled.image).toBe('retired-image');
|
|
355
|
+
expect(reconciled.viewState.imageId).toBe('retired-image');
|
|
686
356
|
});
|
|
687
357
|
|
|
688
|
-
it('preserves flat backend metadata for an unavailable load balancer without creating an empty mapping',
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
filtered: {
|
|
700
|
-
...buildCommand().backingData.filtered,
|
|
701
|
-
loadBalancerIndex: {},
|
|
702
|
-
loadBalancers: [],
|
|
703
|
-
},
|
|
704
|
-
},
|
|
705
|
-
});
|
|
706
|
-
const adapter = buildAdapter();
|
|
707
|
-
adapter.configureCommand.and.resolveTo(configured);
|
|
708
|
-
const modal = new GceCloneServerGroupModal(buildProps(command, adapter)) as any;
|
|
709
|
-
spyOn(modal, 'setState').and.callFake((state: any) => (modal.state = { ...modal.state, ...state }));
|
|
710
|
-
|
|
711
|
-
await modal.configureCommand();
|
|
712
|
-
|
|
713
|
-
expect(modal.state.command.backendServices).toBeUndefined();
|
|
714
|
-
expect(transformGceServerGroupCommand(modal.state.command).instanceMetadata['backend-service-names']).toBe(
|
|
358
|
+
it('preserves flat backend metadata for an unavailable load balancer without creating an empty mapping', () => {
|
|
359
|
+
const reconciled = reconcileGceServerGroupCommand(
|
|
360
|
+
buildCommand({
|
|
361
|
+
backendServiceMetadata: 'persisted-backend-a, persisted-backend-b' as any,
|
|
362
|
+
backendServices: undefined,
|
|
363
|
+
loadBalancers: ['unavailable-http-lb'],
|
|
364
|
+
}),
|
|
365
|
+
buildCommand({ ...buildInitializationHandlers(), loadBalancers: [] }),
|
|
366
|
+
);
|
|
367
|
+
expect(reconciled.backendServices).toBeUndefined();
|
|
368
|
+
expect(transformGceServerGroupCommand(reconciled).instanceMetadata['backend-service-names']).toBe(
|
|
715
369
|
'persisted-backend-a,persisted-backend-b',
|
|
716
370
|
);
|
|
717
371
|
});
|
|
718
372
|
|
|
719
|
-
it('preserves an unavailable clone account and region without running account-dependent handlers',
|
|
720
|
-
const command = buildCommand({
|
|
721
|
-
backingData: {
|
|
722
|
-
...buildCommand().backingData,
|
|
723
|
-
accounts: ['retired-account'],
|
|
724
|
-
filtered: { ...buildCommand().backingData.filtered, regions: ['retired-region'] },
|
|
725
|
-
},
|
|
726
|
-
credentials: 'retired-account',
|
|
727
|
-
region: 'retired-region',
|
|
728
|
-
viewState: { ...buildCommand().viewState, mode: 'clone' },
|
|
729
|
-
});
|
|
373
|
+
it('preserves an unavailable clone account and region without running account-dependent handlers', () => {
|
|
730
374
|
const handlers = buildInitializationHandlers();
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
},
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
spyOn(modal, 'setState').and.callFake((state: any) => (modal.state = { ...modal.state, ...state }));
|
|
743
|
-
|
|
744
|
-
await modal.configureCommand();
|
|
745
|
-
|
|
746
|
-
expect(modal.state.command.credentials).toBe('retired-account');
|
|
747
|
-
expect(modal.state.command.region).toBe('retired-region');
|
|
748
|
-
Object.keys(handlers).forEach((handler) => expect(handlers[handler]).not.toHaveBeenCalled());
|
|
375
|
+
const reconciled = reconcileGceServerGroupCommand(
|
|
376
|
+
buildCommand({
|
|
377
|
+
credentials: 'retired-account',
|
|
378
|
+
region: 'retired-region',
|
|
379
|
+
viewState: { ...buildCommand().viewState, mode: 'clone' },
|
|
380
|
+
}),
|
|
381
|
+
buildCommand({ ...handlers }),
|
|
382
|
+
);
|
|
383
|
+
expect(reconciled.credentials).toBe('retired-account');
|
|
384
|
+
expect(reconciled.region).toBe('retired-region');
|
|
385
|
+
Object.values(handlers).forEach((handler) => expect(handler).not.toHaveBeenCalled());
|
|
749
386
|
});
|
|
750
387
|
|
|
751
|
-
it('preserves an unavailable pipeline region after initializing the available account',
|
|
752
|
-
const command = buildCommand({
|
|
753
|
-
backingData: {
|
|
754
|
-
...buildCommand().backingData,
|
|
755
|
-
filtered: { ...buildCommand().backingData.filtered, regions: ['retired-region'] },
|
|
756
|
-
},
|
|
757
|
-
region: 'retired-region',
|
|
758
|
-
viewState: { ...buildCommand().viewState, mode: 'editPipeline' },
|
|
759
|
-
});
|
|
388
|
+
it('preserves an unavailable pipeline region after initializing the available account', () => {
|
|
760
389
|
const handlers = buildInitializationHandlers();
|
|
761
|
-
const
|
|
762
|
-
...
|
|
763
|
-
|
|
764
|
-
...buildCommand().backingData,
|
|
765
|
-
filtered: { ...buildCommand().backingData.filtered, regions: ['us-central1'] },
|
|
766
|
-
},
|
|
767
|
-
});
|
|
768
|
-
const adapter = buildAdapter();
|
|
769
|
-
adapter.configureCommand.and.resolveTo(configured);
|
|
770
|
-
const modal = new GceCloneServerGroupModal(buildProps(command, adapter)) as any;
|
|
771
|
-
spyOn(modal, 'setState').and.callFake((state: any) => (modal.state = { ...modal.state, ...state }));
|
|
772
|
-
|
|
773
|
-
await modal.configureCommand();
|
|
774
|
-
|
|
775
|
-
expect(modal.state.command.credentials).toBe('gce-account');
|
|
776
|
-
expect(modal.state.command.region).toBe('retired-region');
|
|
777
|
-
expect(handlers.credentialsChanged).toHaveBeenCalledOnceWith(modal.state.command);
|
|
778
|
-
['regionalChanged', 'regionChanged', 'networkChanged', 'zoneChanged', 'customInstanceChanged'].forEach((handler) =>
|
|
779
|
-
expect(handlers[handler]).not.toHaveBeenCalled(),
|
|
390
|
+
const reconciled = reconcileGceServerGroupCommand(
|
|
391
|
+
buildCommand({ region: 'retired-region', viewState: { ...buildCommand().viewState, mode: 'editPipeline' } }),
|
|
392
|
+
buildCommand({ ...handlers }),
|
|
780
393
|
);
|
|
394
|
+
expect(reconciled.region).toBe('retired-region');
|
|
395
|
+
expect(handlers.credentialsChanged).toHaveBeenCalledTimes(1);
|
|
781
396
|
});
|
|
782
397
|
|
|
783
|
-
it('hydrates regions before checking an available persisted region and runs all handlers in order',
|
|
784
|
-
const command = buildCommand();
|
|
398
|
+
it('hydrates regions before checking an available persisted region and runs all handlers in order', () => {
|
|
785
399
|
const calls: string[] = [];
|
|
786
|
-
const handlers = buildInitializationHandlers((handler,
|
|
400
|
+
const handlers = buildInitializationHandlers((handler, command) => {
|
|
787
401
|
calls.push(handler);
|
|
788
|
-
if (handler === 'credentialsChanged')
|
|
789
|
-
initializedCommand.backingData.filtered.regions = ['us-central1'];
|
|
790
|
-
}
|
|
402
|
+
if (handler === 'credentialsChanged') command.backingData.filtered.regions = ['us-central1'];
|
|
791
403
|
});
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
backingData: {
|
|
795
|
-
|
|
796
|
-
filtered: {},
|
|
797
|
-
},
|
|
798
|
-
});
|
|
799
|
-
const adapter = buildAdapter();
|
|
800
|
-
adapter.configureCommand.and.resolveTo(configured);
|
|
801
|
-
const modal = new GceCloneServerGroupModal(buildProps(command, adapter)) as any;
|
|
802
|
-
spyOn(modal, 'setState').and.callFake((state: any) => (modal.state = { ...modal.state, ...state }));
|
|
803
|
-
|
|
804
|
-
await modal.configureCommand();
|
|
805
|
-
|
|
404
|
+
reconcileGceServerGroupCommand(
|
|
405
|
+
buildCommand(),
|
|
406
|
+
buildCommand({ ...handlers, backingData: { accounts: ['gce-account'], filtered: {} } }),
|
|
407
|
+
);
|
|
806
408
|
expect(calls).toEqual([
|
|
807
409
|
'credentialsChanged',
|
|
808
410
|
'regionalChanged',
|
|
@@ -820,28 +422,15 @@ describe('GceCloneServerGroupModal', () => {
|
|
|
820
422
|
backingData: {
|
|
821
423
|
filtered: {
|
|
822
424
|
loadBalancerIndex: {
|
|
823
|
-
'current-http-lb': {
|
|
824
|
-
listeners: [{ name: 'current-forwarding-rule' }],
|
|
825
|
-
loadBalancerType: 'HTTP',
|
|
826
|
-
},
|
|
425
|
+
'current-http-lb': { listeners: [{ name: 'current-forwarding-rule' }], loadBalancerType: 'HTTP' },
|
|
827
426
|
},
|
|
828
427
|
},
|
|
829
428
|
},
|
|
830
|
-
instanceMetadata: {
|
|
831
|
-
'backend-service-names': 'stale-backend',
|
|
832
|
-
'global-load-balancer-names': 'stale-global-rule',
|
|
833
|
-
'load-balancer-names': 'stale-regional-rule',
|
|
834
|
-
owner: 'delivery',
|
|
835
|
-
},
|
|
836
|
-
loadBalancerMetadata: {
|
|
837
|
-
'global-load-balancer-names': ['persisted-global-rule'],
|
|
838
|
-
'load-balancer-names': ['persisted-regional-rule'],
|
|
839
|
-
},
|
|
429
|
+
instanceMetadata: { 'backend-service-names': 'stale-backend', owner: 'delivery' },
|
|
840
430
|
loadBalancers: ['current-http-lb'],
|
|
841
431
|
viewState: { ...buildCommand().viewState, mode: 'clone' },
|
|
842
432
|
}),
|
|
843
433
|
);
|
|
844
|
-
|
|
845
434
|
expect(transformed.instanceMetadata).toEqual({
|
|
846
435
|
'backend-service-names': 'current-backend',
|
|
847
436
|
'global-load-balancer-names': 'current-forwarding-rule',
|
|
@@ -853,23 +442,13 @@ describe('GceCloneServerGroupModal', () => {
|
|
|
853
442
|
const transformed = transformGceServerGroupCommand(
|
|
854
443
|
buildCommand({
|
|
855
444
|
backingData: {
|
|
856
|
-
filtered: {
|
|
857
|
-
loadBalancerIndex: {
|
|
858
|
-
'regional-lb': { loadBalancerType: 'NETWORK', name: 'regional-lb' },
|
|
859
|
-
},
|
|
860
|
-
},
|
|
861
|
-
},
|
|
862
|
-
backendServices: { 'regional-lb': ['backend-a', 'backend-b'], removed: ['removed-backend'] },
|
|
863
|
-
instanceMetadata: {
|
|
864
|
-
'backend-service-names': 'persisted-backend',
|
|
865
|
-
'global-load-balancer-names': 'persisted-global-rule',
|
|
445
|
+
filtered: { loadBalancerIndex: { 'regional-lb': { loadBalancerType: 'NETWORK', name: 'regional-lb' } } },
|
|
866
446
|
},
|
|
867
|
-
|
|
447
|
+
backendServices: { 'regional-lb': ['backend-a', 'backend-b'] },
|
|
868
448
|
loadBalancers: ['regional-lb'],
|
|
869
449
|
viewState: { ...buildCommand().viewState, mode: 'editPipeline' },
|
|
870
450
|
}),
|
|
871
451
|
);
|
|
872
|
-
|
|
873
452
|
expect(transformed.instanceMetadata).toEqual({
|
|
874
453
|
'backend-service-names': 'backend-a,backend-b',
|
|
875
454
|
'load-balancer-names': 'regional-lb',
|
|
@@ -877,21 +456,25 @@ describe('GceCloneServerGroupModal', () => {
|
|
|
877
456
|
});
|
|
878
457
|
|
|
879
458
|
['createPipeline', 'editPipeline'].forEach((mode) => {
|
|
880
|
-
it(`returns
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
459
|
+
it(`returns transformed WizardModal values without executing infrastructure in ${mode} mode`, async () => {
|
|
460
|
+
const writer = vi.fn();
|
|
461
|
+
const rendered = renderModal(
|
|
462
|
+
buildCommand({
|
|
463
|
+
capacity: { desired: '${ parameters.desired }' } as any,
|
|
464
|
+
minCpuPlatform: '(Automatic)',
|
|
465
|
+
tags: [{ value: 'web' }, 'api'],
|
|
466
|
+
viewState: { ...buildCommand().viewState, mode },
|
|
467
|
+
}),
|
|
468
|
+
buildAdapter(),
|
|
469
|
+
{ serverGroupWriter: { cloneServerGroup: writer } },
|
|
470
|
+
);
|
|
471
|
+
expect(await screen.findByRole('button', { name: 'Done' })).toBeEnabled();
|
|
890
472
|
|
|
891
|
-
|
|
473
|
+
fireEvent.click(screen.getByRole('button', { name: 'Done' }));
|
|
892
474
|
|
|
893
|
-
expect(props.closeModal).
|
|
894
|
-
|
|
475
|
+
await waitFor(() => expect(rendered.props.closeModal).toHaveBeenCalled());
|
|
476
|
+
expect(rendered.props.closeModal).toHaveBeenCalledWith(
|
|
477
|
+
expect.objectContaining({
|
|
895
478
|
capacity: {
|
|
896
479
|
desired: '${ parameters.desired }',
|
|
897
480
|
max: '${ parameters.desired }',
|
|
@@ -902,175 +485,269 @@ describe('GceCloneServerGroupModal', () => {
|
|
|
902
485
|
targetSize: '${ parameters.desired }',
|
|
903
486
|
}),
|
|
904
487
|
);
|
|
905
|
-
expect(
|
|
488
|
+
expect(writer).not.toHaveBeenCalled();
|
|
906
489
|
});
|
|
907
490
|
});
|
|
908
491
|
|
|
909
492
|
['create', 'clone'].forEach((mode) => {
|
|
910
|
-
it(`transforms and submits ${mode} commands through TaskMonitor`, () => {
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
],
|
|
922
|
-
securityGroups: ['firewall-id'],
|
|
923
|
-
viewState: { ...buildCommand().viewState, mode },
|
|
924
|
-
});
|
|
925
|
-
const props = buildProps(command);
|
|
926
|
-
const modal = new GceCloneServerGroupModal(props) as any;
|
|
927
|
-
const task = Promise.resolve({ id: 'task-id' });
|
|
928
|
-
const cloneServerGroup = jasmine.createSpy('cloneServerGroup').and.returnValue(task as any);
|
|
929
|
-
modal.context = { services: { serverGroupWriter: { cloneServerGroup } } };
|
|
930
|
-
const monitorSubmit = spyOn(
|
|
931
|
-
modal.state.taskMonitor,
|
|
932
|
-
'submit',
|
|
933
|
-
).and.callFake((submitMethod: () => PromiseLike<any>) => submitMethod());
|
|
934
|
-
|
|
935
|
-
modal.submit(command);
|
|
936
|
-
|
|
937
|
-
expect(monitorSubmit).toHaveBeenCalled();
|
|
938
|
-
expect(cloneServerGroup).toHaveBeenCalledWith(
|
|
939
|
-
jasmine.objectContaining({
|
|
940
|
-
autoscalingPolicy: { maxNumReplicas: 6, minNumReplicas: 2 },
|
|
941
|
-
capacity: { desired: 3, max: 6, min: 2 },
|
|
942
|
-
instanceMetadata: {
|
|
943
|
-
owner: 'delivery',
|
|
944
|
-
'global-load-balancer-names': 'global-forwarding-rule',
|
|
945
|
-
'load-balancer-names': 'regional-forwarding-rule',
|
|
493
|
+
it(`transforms and submits ${mode} commands through TaskMonitor`, async () => {
|
|
494
|
+
const writer = vi.fn().mockResolvedValue({ id: 'task-id' });
|
|
495
|
+
const monitorSubmit = vi.spyOn(TaskMonitor.prototype, 'startSubmit');
|
|
496
|
+
vi.spyOn(TaskReader, 'waitUntilTaskCompletes').mockReturnValue(new Promise(() => undefined));
|
|
497
|
+
const rendered = renderModal(
|
|
498
|
+
buildCommand({
|
|
499
|
+
autoscalingPolicy: {
|
|
500
|
+
maxNumReplicas: 6,
|
|
501
|
+
minNumReplicas: 2,
|
|
502
|
+
coolDownPeriodSec: 60,
|
|
503
|
+
cpuUtilization: { utilizationTarget: 0.5 },
|
|
946
504
|
},
|
|
947
|
-
|
|
505
|
+
instanceMetadata: { owner: 'delivery' },
|
|
506
|
+
loadBalancerMetadata: {
|
|
507
|
+
'global-load-balancer-names': ['stale-global-forwarding-rule'],
|
|
508
|
+
'load-balancer-names': ['stale-regional-forwarding-rule'],
|
|
509
|
+
},
|
|
510
|
+
loadBalancers: [
|
|
511
|
+
{ loadBalancerType: 'TCP', name: 'global-forwarding-rule' },
|
|
512
|
+
{ loadBalancerType: 'INTERNAL_MANAGED', listeners: [{ name: 'regional-forwarding-rule' }] },
|
|
513
|
+
],
|
|
514
|
+
securityGroups: ['firewall-id'],
|
|
515
|
+
viewState: { ...buildCommand().viewState, mode },
|
|
948
516
|
}),
|
|
949
|
-
|
|
517
|
+
buildAdapter(),
|
|
518
|
+
{ serverGroupWriter: { cloneServerGroup: writer } },
|
|
519
|
+
);
|
|
520
|
+
expect(await screen.findByRole('button', { name: 'Done' })).toBeEnabled();
|
|
521
|
+
|
|
522
|
+
fireEvent.change(screen.getByLabelText('Detail'), { target: { value: `${mode}-detail` } });
|
|
523
|
+
fireEvent.click(screen.getByRole('button', { name: 'Done' }));
|
|
524
|
+
|
|
525
|
+
await waitFor(() =>
|
|
526
|
+
expect(writer).toHaveBeenCalledWith(
|
|
527
|
+
expect.objectContaining({
|
|
528
|
+
autoscalingPolicy: expect.objectContaining({ maxNumReplicas: 6, minNumReplicas: 2 }),
|
|
529
|
+
capacity: { desired: 3, max: 6, min: 2 },
|
|
530
|
+
freeFormDetails: `${mode}-detail`,
|
|
531
|
+
instanceMetadata: {
|
|
532
|
+
owner: 'delivery',
|
|
533
|
+
'global-load-balancer-names': 'global-forwarding-rule',
|
|
534
|
+
'load-balancer-names': 'regional-forwarding-rule',
|
|
535
|
+
},
|
|
536
|
+
targetSize: 3,
|
|
537
|
+
}),
|
|
538
|
+
application,
|
|
539
|
+
),
|
|
950
540
|
);
|
|
951
|
-
|
|
541
|
+
expect(monitorSubmit).toHaveBeenCalled();
|
|
542
|
+
const submitted = writer.mock.lastCall[0];
|
|
952
543
|
expect(submitted.loadBalancerMetadata).toBeUndefined();
|
|
953
544
|
expect(submitted.securityGroups).toBeUndefined();
|
|
954
|
-
expect(props.closeModal).not.toHaveBeenCalled();
|
|
545
|
+
expect(rendered.props.closeModal).not.toHaveBeenCalled();
|
|
955
546
|
});
|
|
956
547
|
});
|
|
957
548
|
|
|
958
|
-
it('submits the immediate Formik command instead of stale component state', () => {
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
const wizard = wrapper.find(WizardModal);
|
|
966
|
-
wizard.prop('render')({ formik, nextIdx: () => 1, wizard: {} as any });
|
|
549
|
+
it('submits the immediate Formik command instead of stale component state', async () => {
|
|
550
|
+
const rendered = renderModal(
|
|
551
|
+
buildCommand({ stack: 'edited', viewState: { ...buildCommand().viewState, mode: 'editPipeline' } }),
|
|
552
|
+
buildAdapter(),
|
|
553
|
+
);
|
|
554
|
+
expect(await screen.findByRole('button', { name: 'Done' })).toBeEnabled();
|
|
555
|
+
fireEvent.change(screen.getByLabelText('Stack'), { target: { value: 'latest' } });
|
|
967
556
|
|
|
968
|
-
|
|
557
|
+
fireEvent.click(screen.getByRole('button', { name: 'Done' }));
|
|
969
558
|
|
|
970
|
-
expect(props.closeModal).
|
|
559
|
+
await waitFor(() => expect(rendered.props.closeModal).toHaveBeenCalled());
|
|
560
|
+
expect(rendered.props.closeModal).toHaveBeenCalledWith(expect.objectContaining({ stack: 'latest' }));
|
|
971
561
|
});
|
|
972
562
|
|
|
973
|
-
it('
|
|
974
|
-
const
|
|
975
|
-
const request = deferred<IGceServerGroupCommand>();
|
|
976
|
-
const adapter = buildAdapter();
|
|
977
|
-
adapter.configureCommand.and.returnValue(request.promise);
|
|
978
|
-
const wrapper = shallow(<GceCloneServerGroupModal {...buildProps(command, adapter)} />, {
|
|
979
|
-
disableLifecycleMethods: true,
|
|
980
|
-
} as any);
|
|
981
|
-
const modal = wrapper.instance() as any;
|
|
982
|
-
const formik: any = {
|
|
983
|
-
setValues: jasmine
|
|
984
|
-
.createSpy('setValues')
|
|
985
|
-
.and.callFake((values: IGceServerGroupCommand) => (formik.values = values)),
|
|
986
|
-
values: cloneDeep(command),
|
|
987
|
-
};
|
|
988
|
-
wrapper.find(WizardModal).prop('render')({ formik, nextIdx: () => 1, wizard: {} as any });
|
|
989
|
-
|
|
990
|
-
const configure = modal.configureCommand();
|
|
991
|
-
formik.values = { ...formik.values, stack: 'edited' };
|
|
992
|
-
const regionChanged = jasmine.createSpy('regionChanged');
|
|
993
|
-
request.resolve(
|
|
563
|
+
it('submits concrete regional external attachment names without changing other load balancer identities', () => {
|
|
564
|
+
const transformed = transformGceServerGroupCommand(
|
|
994
565
|
buildCommand({
|
|
995
|
-
backingData: {
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
566
|
+
backingData: {
|
|
567
|
+
filtered: {
|
|
568
|
+
loadBalancerIndex: {
|
|
569
|
+
'external-managed': {
|
|
570
|
+
listeners: [{ name: 'external-http' }, { name: 'external-https' }],
|
|
571
|
+
loadBalancerType: 'EXTERNAL_MANAGED',
|
|
572
|
+
name: 'external-managed',
|
|
573
|
+
},
|
|
574
|
+
'regional-network': {
|
|
575
|
+
loadBalancerType: 'REGIONAL_EXTERNAL_NETWORK',
|
|
576
|
+
name: 'regional-network',
|
|
577
|
+
},
|
|
578
|
+
'existing-tcp': {
|
|
579
|
+
loadBalancerType: 'TCP',
|
|
580
|
+
name: 'existing-tcp',
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
loadBalancers: ['external-managed', 'regional-network', 'existing-tcp'],
|
|
999
586
|
}),
|
|
1000
587
|
);
|
|
1001
|
-
await configure;
|
|
1002
588
|
|
|
1003
|
-
expect(
|
|
1004
|
-
expect(
|
|
1005
|
-
|
|
1006
|
-
|
|
589
|
+
expect(transformed.loadBalancers).toEqual(['external-http', 'external-https', 'regional-network', 'existing-tcp']);
|
|
590
|
+
expect(transformed.instanceMetadata).toEqual({
|
|
591
|
+
'global-load-balancer-names': 'existing-tcp',
|
|
592
|
+
'load-balancer-names': 'external-http,external-https,regional-network',
|
|
593
|
+
});
|
|
1007
594
|
});
|
|
1008
595
|
|
|
1009
|
-
it(
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
596
|
+
it.each([
|
|
597
|
+
[
|
|
598
|
+
'merges refreshed backing data into Formik edits made while configuration is loading',
|
|
599
|
+
async () => {
|
|
600
|
+
const request = deferred<IGceServerGroupCommand>();
|
|
601
|
+
const adapter = buildAdapter({ configureCommand: vi.fn().mockReturnValue(request.promise) });
|
|
602
|
+
const rendered = renderModal(
|
|
603
|
+
buildCommand({
|
|
604
|
+
unknownReference: 'keep-me',
|
|
605
|
+
viewState: { ...buildCommand().viewState, mode: 'editPipeline' },
|
|
606
|
+
}),
|
|
607
|
+
adapter,
|
|
608
|
+
);
|
|
609
|
+
fireEvent.change(await screen.findByLabelText('Stack'), { target: { value: 'edited' } });
|
|
610
|
+
const regionChanged = vi.fn();
|
|
611
|
+
|
|
612
|
+
await act(async () =>
|
|
613
|
+
request.resolve(
|
|
614
|
+
buildCommand({
|
|
615
|
+
backingData: { ...buildCommand().backingData, refreshed: true },
|
|
616
|
+
regionChanged,
|
|
617
|
+
stack: 'old',
|
|
618
|
+
unknownReference: 'keep-me',
|
|
619
|
+
viewState: { ...buildCommand().viewState, mode: 'editPipeline' },
|
|
620
|
+
}),
|
|
621
|
+
),
|
|
622
|
+
);
|
|
623
|
+
fireEvent.click(await screen.findByRole('button', { name: 'Done' }));
|
|
624
|
+
|
|
625
|
+
await waitFor(() => expect(rendered.props.closeModal).toHaveBeenCalled());
|
|
626
|
+
expect(rendered.props.closeModal).toHaveBeenCalledWith(
|
|
627
|
+
expect.objectContaining({
|
|
628
|
+
backingData: expect.objectContaining({ refreshed: true }),
|
|
629
|
+
stack: 'edited',
|
|
630
|
+
unknownReference: 'keep-me',
|
|
631
|
+
}),
|
|
632
|
+
);
|
|
633
|
+
expect(rendered.props.closeModal.mock.calls[0][0].regionChanged).toBe(regionChanged);
|
|
634
|
+
},
|
|
635
|
+
],
|
|
636
|
+
[
|
|
637
|
+
'keeps the latest rendered command when overlapping configuration requests finish out of order',
|
|
638
|
+
async () => {
|
|
639
|
+
const first = deferred<IGceServerGroupCommand>();
|
|
640
|
+
const second = deferred<IGceServerGroupCommand>();
|
|
641
|
+
const adapter = buildAdapter({
|
|
642
|
+
configureCommand: vi.fn().mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise),
|
|
643
|
+
});
|
|
644
|
+
const rendered = renderModal(
|
|
645
|
+
buildCommand({ stack: 'first-request', viewState: { ...buildCommand().viewState, mode: 'editPipeline' } }),
|
|
646
|
+
adapter,
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
rendered.rerenderCommand(
|
|
650
|
+
buildCommand({ stack: 'second-request', viewState: { ...buildCommand().viewState, mode: 'editPipeline' } }),
|
|
651
|
+
);
|
|
652
|
+
await waitFor(() => expect(adapter.configureCommand).toHaveBeenCalledTimes(2));
|
|
653
|
+
await act(async () =>
|
|
654
|
+
second.resolve(
|
|
655
|
+
buildCommand({
|
|
656
|
+
backingData: {
|
|
657
|
+
...buildCommand().backingData,
|
|
658
|
+
filtered: { ...buildCommand().backingData.filtered, regions: ['second-region'] },
|
|
659
|
+
},
|
|
660
|
+
}),
|
|
661
|
+
),
|
|
662
|
+
);
|
|
663
|
+
await act(async () =>
|
|
664
|
+
first.resolve(
|
|
665
|
+
buildCommand({
|
|
666
|
+
backingData: {
|
|
667
|
+
...buildCommand().backingData,
|
|
668
|
+
filtered: { ...buildCommand().backingData.filtered, regions: ['first-region'] },
|
|
669
|
+
},
|
|
670
|
+
}),
|
|
671
|
+
),
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
expect(screen.getByLabelText('Stack')).toHaveValue('second-request');
|
|
675
|
+
const regionOptions = Array.from(screen.getByLabelText('Region').querySelectorAll('option')).map(
|
|
676
|
+
(option) => option.value,
|
|
677
|
+
);
|
|
678
|
+
expect(regionOptions).toContain('second-region');
|
|
679
|
+
expect(regionOptions).not.toContain('first-region');
|
|
680
|
+
},
|
|
681
|
+
],
|
|
682
|
+
] as const)('%s', async (_description, verify) => verify());
|
|
683
|
+
|
|
684
|
+
it.each(['resolve', 'reject'] as const)(
|
|
685
|
+
'does not reconcile, update Formik, invoke callbacks, or warn after configuration %s following unmount',
|
|
686
|
+
async (outcome) => {
|
|
687
|
+
const request = deferred<IGceServerGroupCommand>();
|
|
688
|
+
const handlers = buildInitializationHandlers();
|
|
689
|
+
const adapter = buildAdapter({ configureCommand: vi.fn().mockReturnValue(request.promise) });
|
|
690
|
+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
691
|
+
const rendered = renderModal(buildCommand(), adapter);
|
|
692
|
+
await screen.findByLabelText('Stack');
|
|
693
|
+
|
|
694
|
+
rendered.unmount();
|
|
695
|
+
await act(async () => {
|
|
696
|
+
if (outcome === 'resolve') {
|
|
697
|
+
request.resolve(buildCommand({ ...handlers, backingData: { filtered: { regions: ['late'] } } }));
|
|
698
|
+
} else {
|
|
699
|
+
request.reject(new Error('late configuration failure'));
|
|
700
|
+
}
|
|
701
|
+
await request.promise.catch(() => undefined);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
Object.values(handlers).forEach((handler) => expect(handler).not.toHaveBeenCalled());
|
|
705
|
+
expect(rendered.props.closeModal).not.toHaveBeenCalled();
|
|
706
|
+
expect(rendered.props.dismissModal).not.toHaveBeenCalled();
|
|
707
|
+
expect(consoleError.mock.calls.flat().join(' ')).not.toMatch(/unmounted component/i);
|
|
708
|
+
consoleError.mockRestore();
|
|
709
|
+
},
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
it('renders actual TaskMonitor submission status inside WizardModal', async () => {
|
|
713
|
+
const task = deferred<any>();
|
|
714
|
+
renderModal(buildCommand(), buildAdapter(), { serverGroupWriter: { cloneServerGroup: () => task.promise } });
|
|
715
|
+
fireEvent.click(await screen.findByRole('button', { name: 'Done' }));
|
|
716
|
+
|
|
717
|
+
await waitFor(() => expect(screen.getAllByRole('heading', { name: 'Configure GCE server group' })).toHaveLength(2));
|
|
1025
718
|
});
|
|
1026
719
|
|
|
1027
|
-
it('
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
const configure = modal.configureCommand();
|
|
1037
|
-
modal.componentWillUnmount();
|
|
1038
|
-
request.resolve(buildCommand({ backingData: { filtered: { regions: ['late'] } } }));
|
|
1039
|
-
await configure;
|
|
1040
|
-
|
|
1041
|
-
expect(setState).not.toHaveBeenCalled();
|
|
1042
|
-
expect(formik.setValues).not.toHaveBeenCalled();
|
|
720
|
+
it('retains the modal command when infrastructure submission fails', async () => {
|
|
721
|
+
const rendered = renderModal(buildCommand(), buildAdapter(), {
|
|
722
|
+
serverGroupWriter: { cloneServerGroup: vi.fn().mockRejectedValue({ failureMessage: 'create failed' }) },
|
|
723
|
+
});
|
|
724
|
+
fireEvent.click(await screen.findByRole('button', { name: 'Done' }));
|
|
725
|
+
|
|
726
|
+
expect(await screen.findByText('create failed')).toBeInTheDocument();
|
|
727
|
+
expect(rendered.props.closeModal).not.toHaveBeenCalled();
|
|
728
|
+
expect(rendered.props.dismissModal).not.toHaveBeenCalled();
|
|
1043
729
|
});
|
|
1044
730
|
|
|
1045
|
-
it('refreshes and navigates to the created server group after
|
|
1046
|
-
const
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
go: jasmine.createSpy('go'),
|
|
1061
|
-
includes: jasmine.createSpy('includes').and.callFake((name: string) => name === '**.clusters'),
|
|
1062
|
-
};
|
|
1063
|
-
props.stateService = state;
|
|
1064
|
-
|
|
1065
|
-
modal.onTaskComplete();
|
|
1066
|
-
|
|
1067
|
-
expect(application.serverGroups.refresh).toHaveBeenCalled();
|
|
1068
|
-
expect(application.serverGroups.onNextRefresh).toHaveBeenCalledWith(modal.onApplicationRefresh);
|
|
1069
|
-
expect(application.serverGroups.onNextRefresh.calls.first().invocationOrder).toBeLessThan(
|
|
1070
|
-
application.serverGroups.refresh.calls.first().invocationOrder,
|
|
731
|
+
it('refreshes and navigates to the created server group after TaskMonitor completion', async () => {
|
|
732
|
+
const task = cloneTask();
|
|
733
|
+
const writer = vi.fn().mockResolvedValue(task);
|
|
734
|
+
vi.spyOn(TaskReader, 'waitUntilTaskCompletes').mockResolvedValue(task);
|
|
735
|
+
const refreshCallbacks: Array<() => void> = [];
|
|
736
|
+
application.serverGroups.onNextRefresh.mockImplementation((callback: () => void) => {
|
|
737
|
+
refreshCallbacks.push(callback);
|
|
738
|
+
return vi.fn();
|
|
739
|
+
});
|
|
740
|
+
const stateService = { go: vi.fn(), includes: vi.fn((state: string) => state === '**.clusters') };
|
|
741
|
+
renderModal(buildCommand(), buildAdapter(), { serverGroupWriter: { cloneServerGroup: writer } }, { stateService });
|
|
742
|
+
fireEvent.click(await screen.findByRole('button', { name: 'Done' }));
|
|
743
|
+
await waitFor(() => expect(application.serverGroups.refresh).toHaveBeenCalled());
|
|
744
|
+
expect(application.serverGroups.onNextRefresh.mock.invocationCallOrder[0]).toBeLessThan(
|
|
745
|
+
application.serverGroups.refresh.mock.invocationCallOrder[0],
|
|
1071
746
|
);
|
|
1072
|
-
|
|
1073
|
-
|
|
747
|
+
|
|
748
|
+
refreshCallbacks[0]();
|
|
749
|
+
|
|
750
|
+
expect(stateService.go).toHaveBeenCalledWith('.serverGroup', {
|
|
1074
751
|
accountId: 'gce-account',
|
|
1075
752
|
provider: 'gce',
|
|
1076
753
|
region: 'us-central1',
|
|
@@ -1078,68 +755,116 @@ describe('GceCloneServerGroupModal', () => {
|
|
|
1078
755
|
});
|
|
1079
756
|
});
|
|
1080
757
|
|
|
1081
|
-
it('unsubscribes from the application refresh after the callback runs', () => {
|
|
1082
|
-
const unsubscribe =
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
758
|
+
it('unsubscribes from the application refresh after the callback runs', async () => {
|
|
759
|
+
const unsubscribe = vi.fn();
|
|
760
|
+
let refreshCallback: (() => void) | undefined;
|
|
761
|
+
application.serverGroups.onNextRefresh.mockImplementation((callback: () => void) => {
|
|
762
|
+
refreshCallback = callback;
|
|
763
|
+
return unsubscribe;
|
|
764
|
+
});
|
|
765
|
+
const task = cloneTask();
|
|
766
|
+
vi.spyOn(TaskReader, 'waitUntilTaskCompletes').mockResolvedValue(task);
|
|
767
|
+
renderModal(buildCommand(), buildAdapter(), {
|
|
768
|
+
serverGroupWriter: { cloneServerGroup: vi.fn().mockResolvedValue(task) },
|
|
769
|
+
});
|
|
770
|
+
fireEvent.click(await screen.findByRole('button', { name: 'Done' }));
|
|
771
|
+
await waitFor(() => expect(application.serverGroups.refresh).toHaveBeenCalled());
|
|
1087
772
|
expect(unsubscribe).not.toHaveBeenCalled();
|
|
1088
773
|
|
|
1089
|
-
|
|
774
|
+
refreshCallback?.();
|
|
775
|
+
|
|
1090
776
|
expect(unsubscribe).toHaveBeenCalledTimes(1);
|
|
1091
777
|
});
|
|
1092
778
|
|
|
1093
|
-
it('ignores a late application refresh after unmount', () => {
|
|
1094
|
-
const unsubscribe =
|
|
779
|
+
it('ignores a late application refresh after unmount', async () => {
|
|
780
|
+
const unsubscribe = vi.fn();
|
|
1095
781
|
let refreshCallback: (() => void) | undefined;
|
|
1096
|
-
application.serverGroups.onNextRefresh.
|
|
782
|
+
application.serverGroups.onNextRefresh.mockImplementation((callback: () => void) => {
|
|
1097
783
|
refreshCallback = callback;
|
|
1098
784
|
return unsubscribe;
|
|
1099
785
|
});
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
};
|
|
786
|
+
const task = cloneTask();
|
|
787
|
+
vi.spyOn(TaskReader, 'waitUntilTaskCompletes').mockResolvedValue(task);
|
|
788
|
+
const stateService = { go: vi.fn(), includes: vi.fn() };
|
|
789
|
+
const rendered = renderModal(
|
|
790
|
+
buildCommand(),
|
|
791
|
+
buildAdapter(),
|
|
792
|
+
{ serverGroupWriter: { cloneServerGroup: vi.fn().mockResolvedValue(task) } },
|
|
793
|
+
{ stateService },
|
|
794
|
+
);
|
|
795
|
+
fireEvent.click(await screen.findByRole('button', { name: 'Done' }));
|
|
796
|
+
await waitFor(() => expect(application.serverGroups.refresh).toHaveBeenCalled());
|
|
1112
797
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
refreshCallback!();
|
|
798
|
+
rendered.unmount();
|
|
799
|
+
refreshCallback?.();
|
|
1116
800
|
|
|
1117
801
|
expect(unsubscribe).toHaveBeenCalledTimes(1);
|
|
1118
|
-
expect(
|
|
1119
|
-
expect(props.
|
|
1120
|
-
expect(props.
|
|
1121
|
-
expect(props.dismissModal).not.toHaveBeenCalled();
|
|
802
|
+
expect(stateService.go).not.toHaveBeenCalled();
|
|
803
|
+
expect(rendered.props.closeModal).not.toHaveBeenCalled();
|
|
804
|
+
expect(rendered.props.dismissModal).not.toHaveBeenCalled();
|
|
1122
805
|
});
|
|
1123
806
|
|
|
1124
|
-
it('unsubscribes from a pending application refresh before replacing it', () => {
|
|
1125
|
-
const firstUnsubscribe =
|
|
1126
|
-
const secondUnsubscribe =
|
|
1127
|
-
application.serverGroups.onNextRefresh.
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
807
|
+
it('unsubscribes from a pending application refresh before replacing it', async () => {
|
|
808
|
+
const firstUnsubscribe = vi.fn();
|
|
809
|
+
const secondUnsubscribe = vi.fn();
|
|
810
|
+
application.serverGroups.onNextRefresh.mockReturnValueOnce(firstUnsubscribe).mockReturnValueOnce(secondUnsubscribe);
|
|
811
|
+
const task = cloneTask();
|
|
812
|
+
vi.spyOn(TaskReader, 'waitUntilTaskCompletes').mockResolvedValue(task);
|
|
813
|
+
let monitor: TaskMonitor | undefined;
|
|
814
|
+
const handleTaskSuccess = TaskMonitor.prototype.handleTaskSuccess;
|
|
815
|
+
vi.spyOn(TaskMonitor.prototype, 'handleTaskSuccess').mockImplementation(function (
|
|
816
|
+
this: TaskMonitor,
|
|
817
|
+
...args: Parameters<TaskMonitor['handleTaskSuccess']>
|
|
818
|
+
) {
|
|
819
|
+
monitor = this;
|
|
820
|
+
return handleTaskSuccess.apply(this, args);
|
|
821
|
+
});
|
|
822
|
+
renderModal(buildCommand(), buildAdapter(), {
|
|
823
|
+
serverGroupWriter: { cloneServerGroup: vi.fn().mockResolvedValue(task) },
|
|
824
|
+
});
|
|
825
|
+
fireEvent.click(await screen.findByRole('button', { name: 'Done' }));
|
|
826
|
+
await waitFor(() => expect(application.serverGroups.onNextRefresh).toHaveBeenCalledTimes(1));
|
|
827
|
+
|
|
828
|
+
// A second task completion arrives before the first refresh callback has fired.
|
|
829
|
+
await act(async () => monitor!.handleTaskSuccess(task));
|
|
830
|
+
await waitFor(() => expect(application.serverGroups.onNextRefresh).toHaveBeenCalledTimes(2));
|
|
1132
831
|
|
|
1133
832
|
expect(firstUnsubscribe).toHaveBeenCalledTimes(1);
|
|
1134
833
|
expect(secondUnsubscribe).not.toHaveBeenCalled();
|
|
1135
834
|
});
|
|
1136
835
|
});
|
|
1137
836
|
|
|
837
|
+
function renderModal(
|
|
838
|
+
command: IGceServerGroupCommand,
|
|
839
|
+
adapter: IGceServerGroupWizardAdapter | undefined,
|
|
840
|
+
runtimeServices: Record<string, any> = { serverGroupWriter: { cloneServerGroup: vi.fn() } },
|
|
841
|
+
overrides: Record<string, any> = {},
|
|
842
|
+
) {
|
|
843
|
+
const props = { ...buildProps(command, adapter), ...overrides };
|
|
844
|
+
const router = new UIRouterReact();
|
|
845
|
+
const element = (nextProps = props) => (
|
|
846
|
+
<UIRouterContext.Provider value={router}>
|
|
847
|
+
<DeckRuntimeContext.Provider value={{ services: runtimeServices } as any}>
|
|
848
|
+
<GceCloneServerGroupModal {...nextProps} />
|
|
849
|
+
</DeckRuntimeContext.Provider>
|
|
850
|
+
</UIRouterContext.Provider>
|
|
851
|
+
);
|
|
852
|
+
const rendered = render(element());
|
|
853
|
+
return {
|
|
854
|
+
...rendered,
|
|
855
|
+
props,
|
|
856
|
+
rerenderCommand(nextCommand: IGceServerGroupCommand) {
|
|
857
|
+
rendered.rerender(element({ ...props, command: nextCommand }));
|
|
858
|
+
},
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
1138
862
|
function buildCommand(overrides: Partial<IGceServerGroupCommand> = {}): IGceServerGroupCommand {
|
|
1139
863
|
return {
|
|
1140
864
|
application: 'fnord',
|
|
1141
865
|
backingData: {
|
|
1142
866
|
accounts: ['gce-account'],
|
|
867
|
+
allImages: [{ imageName: 'ubuntu' }],
|
|
1143
868
|
filtered: {
|
|
1144
869
|
cpuPlatforms: ['(Automatic)'],
|
|
1145
870
|
images: ['ubuntu'],
|
|
@@ -1167,12 +892,7 @@ function buildCommand(overrides: Partial<IGceServerGroupCommand> = {}): IGceServ
|
|
|
1167
892
|
stack: 'main',
|
|
1168
893
|
subnet: 'default',
|
|
1169
894
|
tags: [],
|
|
1170
|
-
viewState: {
|
|
1171
|
-
dirty: {},
|
|
1172
|
-
disableImageSelection: false,
|
|
1173
|
-
mode: 'create',
|
|
1174
|
-
useSimpleCapacity: true,
|
|
1175
|
-
},
|
|
895
|
+
viewState: { dirty: {}, disableImageSelection: false, mode: 'create', useSimpleCapacity: true },
|
|
1176
896
|
zone: 'us-central1-a',
|
|
1177
897
|
...overrides,
|
|
1178
898
|
};
|
|
@@ -1182,29 +902,30 @@ function buildProps(command: IGceServerGroupCommand, adapter?: IGceServerGroupWi
|
|
|
1182
902
|
return {
|
|
1183
903
|
adapter,
|
|
1184
904
|
application,
|
|
1185
|
-
closeModal:
|
|
905
|
+
closeModal: vi.fn(),
|
|
1186
906
|
command,
|
|
1187
|
-
dismissModal:
|
|
907
|
+
dismissModal: vi.fn(),
|
|
1188
908
|
router: {},
|
|
1189
909
|
stateParams: {},
|
|
1190
|
-
stateService: { go:
|
|
910
|
+
stateService: { go: vi.fn(), includes: () => false },
|
|
1191
911
|
title: 'Configure GCE server group',
|
|
1192
912
|
};
|
|
1193
913
|
}
|
|
1194
914
|
|
|
1195
|
-
function buildAdapter(): any {
|
|
915
|
+
function buildAdapter(overrides: Record<string, any> = {}): any {
|
|
1196
916
|
return {
|
|
1197
|
-
applyCommandHandler:
|
|
1198
|
-
applyConfigurationRefresh:
|
|
1199
|
-
applyConfigurationUpdate:
|
|
1200
|
-
buildNewServerGroupCommand:
|
|
1201
|
-
configureCommand:
|
|
917
|
+
applyCommandHandler: vi.fn(),
|
|
918
|
+
applyConfigurationRefresh: vi.fn(),
|
|
919
|
+
applyConfigurationUpdate: vi.fn(),
|
|
920
|
+
buildNewServerGroupCommand: vi.fn(),
|
|
921
|
+
configureCommand: vi.fn().mockResolvedValue(buildCommand()),
|
|
922
|
+
...overrides,
|
|
1202
923
|
};
|
|
1203
924
|
}
|
|
1204
925
|
|
|
1205
926
|
function buildInitializationHandlers(
|
|
1206
927
|
onCall: (handler: string, command: IGceServerGroupCommand) => void = () => undefined,
|
|
1207
|
-
): Record<string,
|
|
928
|
+
): Record<string, ReturnType<typeof vi.fn>> {
|
|
1208
929
|
return [
|
|
1209
930
|
'credentialsChanged',
|
|
1210
931
|
'regionalChanged',
|
|
@@ -1213,16 +934,33 @@ function buildInitializationHandlers(
|
|
|
1213
934
|
'zoneChanged',
|
|
1214
935
|
'customInstanceChanged',
|
|
1215
936
|
].reduce((handlers, handler) => {
|
|
1216
|
-
handlers[handler] =
|
|
937
|
+
handlers[handler] = vi.fn().mockImplementation((command: IGceServerGroupCommand) => {
|
|
1217
938
|
onCall(handler, command);
|
|
1218
939
|
return { dirty: {} };
|
|
1219
940
|
});
|
|
1220
941
|
return handlers;
|
|
1221
|
-
}, {} as Record<string,
|
|
942
|
+
}, {} as Record<string, ReturnType<typeof vi.fn>>);
|
|
1222
943
|
}
|
|
1223
944
|
|
|
1224
|
-
function
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
945
|
+
function cloneTask(): any {
|
|
946
|
+
return {
|
|
947
|
+
execution: {
|
|
948
|
+
stages: [
|
|
949
|
+
{
|
|
950
|
+
context: { 'deploy.server.groups': { 'us-central1': 'fnord-main-api-v042' } },
|
|
951
|
+
type: 'cloneServerGroup',
|
|
952
|
+
},
|
|
953
|
+
],
|
|
954
|
+
},
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function deferred<T>() {
|
|
959
|
+
let resolve!: (value: T) => void;
|
|
960
|
+
let reject!: (reason: unknown) => void;
|
|
961
|
+
const promise = new Promise<T>((promiseResolve, promiseReject) => {
|
|
962
|
+
resolve = promiseResolve;
|
|
963
|
+
reject = promiseReject;
|
|
964
|
+
});
|
|
965
|
+
return { promise, reject, resolve };
|
|
1228
966
|
}
|