@spinnaker/google 2026.3.1 → 2026.4.0

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.
Files changed (108) hide show
  1. package/dist/autoscalingPolicy/GceAutoScalingFieldLayout.d.ts +2 -2
  2. package/dist/domain/loadBalancer.d.ts +6 -3
  3. package/dist/image/ImageSelect.d.ts +1 -1
  4. package/dist/index.js +159 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/loadBalancer/configure/common/gceLoadBalancerModels.d.ts +13 -2
  7. package/dist/loadBalancer/configure/http/GceHttpLoadBalancerEditor.d.ts +3 -0
  8. package/dist/loadBalancer/configure/http/GceHttpLoadBalancerModal.d.ts +1 -1
  9. package/dist/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.d.ts +30 -0
  10. package/dist/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.d.ts +53 -0
  11. package/dist/loadBalancer/httpLoadBalancerUtils.service.d.ts +3 -1
  12. package/dist/serverGroup/configure/wizard/GceCloneServerGroupModal.d.ts +3 -0
  13. package/dist/serverGroup/configure/wizard/GceServerGroupWizard.types.d.ts +1 -0
  14. package/dist/serverGroup/configure/wizard/customInstance/CustomInstanceConfigurer.d.ts +1 -1
  15. package/dist/serverGroup/configure/wizard/customInstance/GceCustomInstanceBuilder.d.ts +2 -2
  16. package/dist/serverGroup/configure/wizard/pages/AdvancedSettings.d.ts +3 -0
  17. package/dist/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.d.ts +1 -0
  18. package/dist/serverGroup/configure/wizard/pages/Policies.d.ts +21 -3
  19. package/dist/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.d.ts +1 -0
  20. package/dist/serverGroup/configure/wizard/pages/ServerGroupCapacity.d.ts +1 -0
  21. package/dist/serverGroup/configure/wizard/pages/ServerGroupImageSettings.d.ts +1 -0
  22. package/dist/serverGroup/configure/wizard/pages/ServerGroupInstanceType.d.ts +1 -0
  23. package/dist/serverGroup/configure/wizard/zones/GceInstanceFlexibilityConfigurer.d.ts +2 -2
  24. package/dist/serverGroup/details/GceInstanceFlexibilityPolicyDetails.d.ts +2 -2
  25. package/dist/serverGroup/details/stateful/MarkDiskStatefulButton.d.ts +2 -2
  26. package/dist/serverGroup/details/stateful/UpdateBootImageButton.d.ts +2 -2
  27. package/package.json +5 -5
  28. package/src/autoHealingPolicy/GceAutoHealingPolicyEditor.spec.tsx +68 -52
  29. package/src/autoscalingPolicy/GceAutoscalingPolicyEditor.spec.tsx +43 -50
  30. package/src/autoscalingPolicy/GceAutoscalingPolicyEditor.tsx +1 -1
  31. package/src/autoscalingPolicy/GceAutoscalingPolicyWriter.spec.ts +1 -1
  32. package/src/domain/loadBalancer.ts +6 -3
  33. package/src/gce.module.spec.ts +132 -153
  34. package/src/instance/details/GceInstanceDetails.spec.tsx +86 -83
  35. package/src/loadBalancer/configure/choice/GceLoadBalancerChoiceModal.spec.tsx +162 -121
  36. package/src/loadBalancer/configure/choice/GceLoadBalancerChoiceModal.tsx +13 -1
  37. package/src/loadBalancer/configure/common/GceProxyLoadBalancerEditor.spec.tsx +47 -30
  38. package/src/loadBalancer/configure/common/GceProxyLoadBalancerEditor.tsx +2 -1
  39. package/src/loadBalancer/configure/common/GceProxyLoadBalancerModal.spec.tsx +85 -57
  40. package/src/loadBalancer/configure/common/gceLoadBalancerData.spec.ts +17 -16
  41. package/src/loadBalancer/configure/common/gceLoadBalancerModels.spec.ts +230 -5
  42. package/src/loadBalancer/configure/common/gceLoadBalancerModels.ts +186 -22
  43. package/src/loadBalancer/configure/common/gceLoadBalancerSubmission.spec.ts +298 -2
  44. package/src/loadBalancer/configure/common/gceLoadBalancerSubmission.ts +35 -5
  45. package/src/loadBalancer/configure/common/useGceLoadBalancerData.spec.tsx +37 -46
  46. package/src/loadBalancer/configure/http/GceHttpLoadBalancerEditor.spec.tsx +399 -58
  47. package/src/loadBalancer/configure/http/GceHttpLoadBalancerEditor.tsx +148 -41
  48. package/src/loadBalancer/configure/http/GceHttpLoadBalancerListenerEditor.spec.tsx +169 -36
  49. package/src/loadBalancer/configure/http/GceHttpLoadBalancerListenerEditor.tsx +80 -12
  50. package/src/loadBalancer/configure/http/GceHttpLoadBalancerModal.spec.tsx +224 -124
  51. package/src/loadBalancer/configure/http/GceHttpLoadBalancerModal.tsx +8 -2
  52. package/src/loadBalancer/configure/http/GceHttpLoadBalancerResourceEditors.spec.tsx +57 -22
  53. package/src/loadBalancer/configure/http/GceHttpLoadBalancerResourceEditors.tsx +21 -0
  54. package/src/loadBalancer/configure/http/GceHttpLoadBalancerRoutingEditor.spec.tsx +24 -20
  55. package/src/loadBalancer/configure/network/GceNetworkLoadBalancerEditor.spec.tsx +41 -32
  56. package/src/loadBalancer/configure/network/GceNetworkLoadBalancerModal.spec.tsx +35 -30
  57. package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.spec.tsx +443 -0
  58. package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.tsx +401 -0
  59. package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.spec.tsx +330 -0
  60. package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.tsx +348 -0
  61. package/src/loadBalancer/details/gceLoadBalancerDetails.spec.tsx +439 -35
  62. package/src/loadBalancer/details/gceLoadBalancerDetails.tsx +104 -21
  63. package/src/loadBalancer/httpLoadBalancerUtils.service.spec.ts +126 -0
  64. package/src/loadBalancer/httpLoadBalancerUtils.service.ts +27 -11
  65. package/src/loadBalancer/loadBalancer.setTransformer.spec.ts +165 -0
  66. package/src/loadBalancer/loadBalancer.setTransformer.ts +15 -2
  67. package/src/pipeline/stages/googleStageConfig.spec.js +22 -16
  68. package/src/securityGroup/configure/GceSecurityGroupModal.spec.tsx +177 -193
  69. package/src/securityGroup/configure/GceSecurityGroupModal.tsx +14 -2
  70. package/src/securityGroup/details/GceSecurityGroupDetails.spec.tsx +69 -78
  71. package/src/serverGroup/configure/serverGroupCommandBuilder.service.spec.js +35 -15
  72. package/src/serverGroup/configure/serverGroupConfiguration.service.js +7 -1
  73. package/src/serverGroup/configure/serverGroupConfiguration.service.spec.js +74 -4
  74. package/src/serverGroup/configure/wizard/GceCloneServerGroupModal.spec.tsx +593 -855
  75. package/src/serverGroup/configure/wizard/GceCloneServerGroupModal.tsx +120 -38
  76. package/src/serverGroup/configure/wizard/GceServerGroupWizard.types.ts +1 -0
  77. package/src/serverGroup/configure/wizard/GceServerGroupWizardPage.spec.tsx +53 -50
  78. package/src/serverGroup/configure/wizard/customInstance/GceCustomInstanceBuilder.spec.tsx +65 -49
  79. package/src/serverGroup/configure/wizard/hiddenMetadataKeys.value.js +1 -0
  80. package/src/serverGroup/configure/wizard/pages/AdvancedSettings.spec.tsx +101 -108
  81. package/src/serverGroup/configure/wizard/pages/AdvancedSettings.tsx +32 -28
  82. package/src/serverGroup/configure/wizard/pages/GceServerGroupFirewalls.spec.tsx +60 -59
  83. package/src/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.spec.tsx +240 -114
  84. package/src/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.tsx +152 -63
  85. package/src/serverGroup/configure/wizard/pages/Policies.spec.tsx +93 -48
  86. package/src/serverGroup/configure/wizard/pages/Policies.tsx +80 -55
  87. package/src/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.spec.tsx +123 -94
  88. package/src/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.tsx +24 -18
  89. package/src/serverGroup/configure/wizard/pages/ServerGroupCapacity.spec.tsx +152 -133
  90. package/src/serverGroup/configure/wizard/pages/ServerGroupCapacity.tsx +40 -37
  91. package/src/serverGroup/configure/wizard/pages/ServerGroupImageSettings.spec.tsx +66 -60
  92. package/src/serverGroup/configure/wizard/pages/ServerGroupImageSettings.tsx +8 -2
  93. package/src/serverGroup/configure/wizard/pages/ServerGroupInstanceType.spec.tsx +131 -101
  94. package/src/serverGroup/configure/wizard/pages/ServerGroupInstanceType.tsx +29 -28
  95. package/src/serverGroup/configure/wizard/zones/GceInstanceFlexibilityConfigurer.spec.tsx +104 -81
  96. package/src/serverGroup/details/GceInstanceFlexibilityPolicyDetails.spec.tsx +10 -11
  97. package/src/serverGroup/details/autoHealingPolicy/GceAutoHealingPolicyDetails.spec.tsx +51 -59
  98. package/src/serverGroup/details/autoHealingPolicy/modal/GceUpsertAutoHealingPolicyModal.spec.tsx +3 -3
  99. package/src/serverGroup/details/autoscalingPolicy/GceAutoscalingPolicyDetails.spec.tsx +49 -58
  100. package/src/serverGroup/details/autoscalingPolicy/modal/GceUpsertAutoscalingPolicyModal.spec.tsx +3 -3
  101. package/src/serverGroup/details/gceServerGroupDetails.spec.tsx +162 -141
  102. package/src/serverGroup/details/resize/GceResizeServerGroupModal.spec.tsx +102 -86
  103. package/src/serverGroup/details/resize/GceResizeServerGroupModal.tsx +4 -0
  104. package/src/serverGroup/details/rollback/GceRollbackServerGroupModal.spec.tsx +40 -31
  105. package/src/serverGroup/serverGroup.transformer.js +1 -0
  106. package/src/serverGroup/serverGroup.transformer.spec.js +48 -0
  107. package/src/validation/ApplicationNameValidator.spec.ts +1 -1
  108. /package/dist/{gce.logo2ae85d8704b1d6b8.png → gce.logoChI3w_A5.png} +0 -0
@@ -1,10 +1,25 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
1
2
  import React from 'react';
2
- import { shallow } from 'enzyme';
3
3
 
4
- import { CloudProviderRegistry, ManagedMenuItem } from '@spinnaker/core';
4
+ import {
5
+ CloudProviderRegistry,
6
+ ConfirmationModalService,
7
+ HelpContentsRegistry,
8
+ HelpTextExpandedContext,
9
+ InfrastructureCaches,
10
+ TaskExecutor,
11
+ } from '@spinnaker/core';
12
+
13
+ import { mockHttpClient } from '../../../../core/src/api/mock/mockHttpSupport';
5
14
 
6
15
  import { GceLoadBalancerChoiceModal } from '../configure/choice/GceLoadBalancerChoiceModal';
7
- import { GceLoadBalancerActions } from './gceLoadBalancerDetails';
16
+ import {
17
+ GceLoadBalancerActions,
18
+ GceLoadBalancerBackendServicesSection,
19
+ GceLoadBalancerInformationSection,
20
+ GceLoadBalancerListenersSection,
21
+ loadGceLoadBalancerDetails,
22
+ } from './gceLoadBalancerDetails';
8
23
 
9
24
  describe('GceLoadBalancerActions', () => {
10
25
  const app = { name: 'fnord' } as any;
@@ -15,51 +30,440 @@ describe('GceLoadBalancerActions', () => {
15
30
  name: 'fnord-main',
16
31
  region: 'europe-west1',
17
32
  } as any;
33
+ const managedLoadBalancer = {
34
+ ...loadBalancer,
35
+ isManaged: true,
36
+ managedResourceSummary: { id: 'resource-a', isPaused: false, locations: { account: 'account-a', regions: [] } },
37
+ };
18
38
 
19
39
  it('hides write actions when the Google provider is disabled', () => {
20
- spyOn(CloudProviderRegistry, 'isDisabled').and.returnValue(true);
40
+ vi.spyOn(CloudProviderRegistry, 'isDisabled').mockReturnValue(true);
21
41
 
22
- const wrapper = shallow(<GceLoadBalancerActions app={app} loadBalancer={loadBalancer} />);
42
+ const { container } = render(<GceLoadBalancerActions app={app} loadBalancer={loadBalancer} />);
23
43
 
24
- expect(wrapper.isEmptyRender()).toBe(true);
44
+ expect(container).toBeEmptyDOMElement();
25
45
  });
26
46
 
27
- it('opens the current load balancer in edit mode through managed-resource gating', () => {
28
- spyOn(CloudProviderRegistry, 'isDisabled').and.returnValue(false);
29
- const show = spyOn(GceLoadBalancerChoiceModal, 'show').and.returnValue(Promise.resolve() as any);
47
+ it('opens the current load balancer in edit mode through managed-resource gating', async () => {
48
+ vi.spyOn(CloudProviderRegistry, 'isDisabled').mockReturnValue(false);
49
+ const show = vi.spyOn(GceLoadBalancerChoiceModal, 'show').mockReturnValue(Promise.resolve() as any);
50
+ const confirm = vi.spyOn(ConfirmationModalService, 'confirm').mockReturnValue(Promise.reject() as any);
30
51
 
31
- const wrapper = shallow(<GceLoadBalancerActions app={app} loadBalancer={loadBalancer} />);
32
- const edit = wrapper.find(ManagedMenuItem).filterWhere((item) => item.prop('children') === 'Edit Load Balancer');
52
+ const managed = render(<GceLoadBalancerActions app={app} loadBalancer={managedLoadBalancer} />);
53
+ fireEvent.click(screen.getByText('Edit Load Balancer'));
54
+ await vi.waitFor(() =>
55
+ expect(confirm).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ header: 'Pause Management?' })),
56
+ );
57
+ expect(show).not.toHaveBeenCalled();
58
+ managed.unmount();
33
59
 
34
- expect(edit.prop('application')).toBe(app);
35
- expect(edit.prop('resource')).toBe(loadBalancer);
36
- edit.prop('onClick')();
37
- expect(show).toHaveBeenCalledOnceWith({
38
- app,
39
- application: app,
40
- forPipelineConfig: false,
41
- isNew: false,
42
- loadBalancer,
43
- mode: 'edit',
44
- } as any);
60
+ render(<GceLoadBalancerActions app={app} loadBalancer={loadBalancer} />);
61
+
62
+ fireEvent.click(screen.getByText('Edit Load Balancer'));
63
+ await vi.waitFor(() =>
64
+ expect(show).toHaveBeenCalledExactlyOnceWith({
65
+ app,
66
+ application: app,
67
+ forPipelineConfig: false,
68
+ isNew: false,
69
+ loadBalancer,
70
+ mode: 'edit',
71
+ } as any),
72
+ );
45
73
  });
46
74
 
47
- it('keeps delete behind managed-resource gating and disables it while instances are attached', () => {
48
- spyOn(CloudProviderRegistry, 'isDisabled').and.returnValue(false);
75
+ it('keeps delete behind managed-resource gating and disables it while instances are attached', async () => {
76
+ vi.spyOn(CloudProviderRegistry, 'isDisabled').mockReturnValue(false);
77
+ const confirm = vi.spyOn(ConfirmationModalService, 'confirm').mockReturnValue(Promise.reject() as any);
49
78
 
50
- const editable = shallow(<GceLoadBalancerActions app={app} loadBalancer={loadBalancer} />);
51
- const managedDelete = editable
52
- .find(ManagedMenuItem)
53
- .filterWhere((item) => item.prop('children') === 'Delete Load Balancer');
54
- expect(managedDelete.prop('application')).toBe(app);
55
- expect(managedDelete.prop('resource')).toBe(loadBalancer);
79
+ const editable = render(<GceLoadBalancerActions app={app} loadBalancer={managedLoadBalancer} />);
80
+ fireEvent.click(editable.getByText('Delete Load Balancer'));
81
+ await vi.waitFor(() =>
82
+ expect(confirm).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ header: 'Pause Management?' })),
83
+ );
84
+ editable.unmount();
85
+ confirm.mockClear();
56
86
 
57
- const attached = shallow(
87
+ render(
58
88
  <GceLoadBalancerActions app={app} loadBalancer={{ ...loadBalancer, instances: [{ name: 'instance-a' }] }} />,
59
89
  );
60
- expect(
61
- attached.find(ManagedMenuItem).filterWhere((item) => item.prop('children') === 'Delete Load Balancer'),
62
- ).toHaveSize(0);
63
- expect(attached.find('li.disabled').text()).toContain('Delete Load Balancer');
90
+ const attachedDelete = screen.getByText('Delete Load Balancer');
91
+ expect(attachedDelete.closest('li')).toHaveClass('disabled');
92
+ fireEvent.click(attachedDelete);
93
+ await Promise.resolve();
94
+ expect(confirm).not.toHaveBeenCalled();
95
+ });
96
+ });
97
+
98
+ describe('loadGceLoadBalancerDetails', () => {
99
+ it('matches scoped regional HTTP load balancers by raw urlMapName', async () => {
100
+ const normalizedLoadBalancer = {
101
+ account: 'test',
102
+ defaultService: { healthCheck: { name: 'hc-default' }, name: 'backend-default' },
103
+ hostRules: [],
104
+ listeners: [{ name: 'regional-listener' }],
105
+ loadBalancerType: 'EXTERNAL_MANAGED',
106
+ name: 'regional-url-map (test/us-central1/EXTERNAL_MANAGED)',
107
+ provider: 'gce',
108
+ region: 'us-central1',
109
+ urlMapName: 'regional-url-map',
110
+ };
111
+ const loadBalancerReader = {
112
+ getLoadBalancerDetails: vi.fn().mockReturnValue(
113
+ Promise.resolve([
114
+ {
115
+ dnsname: '1.2.3.4',
116
+ listenerDescriptions: [{ listener: { loadBalancerPort: '443' } }],
117
+ },
118
+ ]),
119
+ ),
120
+ };
121
+ const accountService = {
122
+ getAccountDetails: vi.fn().mockReturnValue(Promise.resolve({ project: 'gce-project' })),
123
+ };
124
+ const autoClose = vi.fn();
125
+
126
+ const loadBalancer = await loadGceLoadBalancerDetails({
127
+ app: { loadBalancers: { data: [normalizedLoadBalancer] } } as any,
128
+ autoClose,
129
+ loadBalancerParams: {
130
+ accountId: 'test',
131
+ name: 'regional-url-map',
132
+ provider: 'gce',
133
+ region: 'us-central1',
134
+ vpcId: null,
135
+ },
136
+ loadBalancerReader: loadBalancerReader as any,
137
+ accountService: accountService as any,
138
+ });
139
+
140
+ expect(autoClose).not.toHaveBeenCalled();
141
+ expect(loadBalancerReader.getLoadBalancerDetails).toHaveBeenCalledWith(
142
+ 'gce',
143
+ 'test',
144
+ 'us-central1',
145
+ 'regional-listener',
146
+ );
147
+ expect(loadBalancer).toBe(normalizedLoadBalancer as any);
148
+ expect((loadBalancer as any).logsLink).toContain('regional-url-map');
149
+ expect((loadBalancer as any).logsLink).not.toContain('(test/us-central1/EXTERNAL_MANAGED)');
150
+ });
151
+
152
+ it('prefers an exact name over a regional URL-map alias', async () => {
153
+ const autoClose = vi.fn();
154
+ const loadBalancerReader = {
155
+ getLoadBalancerDetails: vi.fn().mockReturnValue(Promise.resolve([])),
156
+ };
157
+ const accountService = {
158
+ getAccountDetails: vi.fn().mockReturnValue(Promise.resolve({})),
159
+ };
160
+ const internalManaged = {
161
+ account: 'test',
162
+ listeners: [{ name: 'internal-listener' }],
163
+ loadBalancerType: 'INTERNAL_MANAGED',
164
+ name: 'shared-map',
165
+ provider: 'gce',
166
+ region: 'us-central1',
167
+ urlMapName: 'shared-map',
168
+ };
169
+
170
+ const loadBalancer = await loadGceLoadBalancerDetails({
171
+ app: {
172
+ loadBalancers: {
173
+ data: [
174
+ {
175
+ account: 'test',
176
+ listeners: [{ name: 'external-listener' }],
177
+ loadBalancerType: 'EXTERNAL_MANAGED',
178
+ name: 'shared-map (test/us-central1/EXTERNAL_MANAGED)',
179
+ provider: 'gce',
180
+ region: 'us-central1',
181
+ urlMapName: 'shared-map',
182
+ },
183
+ internalManaged,
184
+ ],
185
+ },
186
+ } as any,
187
+ autoClose,
188
+ loadBalancerParams: {
189
+ accountId: 'test',
190
+ name: 'shared-map',
191
+ provider: 'gce',
192
+ region: 'us-central1',
193
+ vpcId: null,
194
+ },
195
+ loadBalancerReader: loadBalancerReader as any,
196
+ accountService: accountService as any,
197
+ });
198
+
199
+ expect(autoClose).not.toHaveBeenCalled();
200
+ expect(loadBalancer).toBe(internalManaged as any);
201
+ expect(loadBalancerReader.getLoadBalancerDetails).toHaveBeenCalledWith(
202
+ 'gce',
203
+ 'test',
204
+ 'us-central1',
205
+ 'internal-listener',
206
+ );
207
+ });
208
+
209
+ it('renders REGIONAL_EXTERNAL_NETWORK addresses without an HTTP URL scheme', async () => {
210
+ const loadBalancer = {
211
+ account: 'test',
212
+ loadBalancerType: 'REGIONAL_EXTERNAL_NETWORK',
213
+ name: 'passthrough-lb',
214
+ provider: 'gce',
215
+ region: 'us-central1',
216
+ };
217
+ await loadGceLoadBalancerDetails({
218
+ accountService: { getAccountDetails: () => Promise.resolve({}) } as any,
219
+ app: { loadBalancers: { data: [loadBalancer] } } as any,
220
+ autoClose: vi.fn(),
221
+ loadBalancerParams: {
222
+ accountId: 'test',
223
+ name: 'passthrough-lb',
224
+ provider: 'gce',
225
+ region: 'us-central1',
226
+ vpcId: null,
227
+ },
228
+ loadBalancerReader: {
229
+ getLoadBalancerDetails: () => Promise.resolve([{ dnsname: '203.0.113.10' }]),
230
+ } as any,
231
+ });
232
+
233
+ // AccountTag lazily loads credentials; answer that request so the fail-closed test client allows it.
234
+ const http = mockHttpClient();
235
+ http.expectGET('/credentials').respond(200, []);
236
+ const { container } = render(<GceLoadBalancerInformationSection app={{}} loadBalancer={loadBalancer} />);
237
+ expect(screen.getByText('203.0.113.10')).toBeInTheDocument();
238
+ expect(container).not.toHaveTextContent('http://');
239
+ expect(container).not.toHaveTextContent('https://');
240
+ await http.flush();
241
+ });
242
+
243
+ it('reads back the REGIONAL_EXTERNAL_NETWORK backend service and health check', async () => {
244
+ const loadBalancer = {
245
+ account: 'test',
246
+ backendService: { name: 'passthrough-backend', healthCheck: { name: 'tcp-hc' } },
247
+ loadBalancerType: 'REGIONAL_EXTERNAL_NETWORK',
248
+ name: 'passthrough-lb',
249
+ provider: 'gce',
250
+ region: 'us-central1',
251
+ };
252
+ await loadGceLoadBalancerDetails({
253
+ accountService: { getAccountDetails: () => Promise.resolve({}) } as any,
254
+ app: { loadBalancers: { data: [loadBalancer] } } as any,
255
+ autoClose: vi.fn(),
256
+ loadBalancerParams: {
257
+ accountId: 'test',
258
+ name: 'passthrough-lb',
259
+ provider: 'gce',
260
+ region: 'us-central1',
261
+ vpcId: null,
262
+ },
263
+ loadBalancerReader: {
264
+ getLoadBalancerDetails: () => Promise.resolve([{ dnsname: '203.0.113.10' }]),
265
+ } as any,
266
+ });
267
+
268
+ render(<GceLoadBalancerBackendServicesSection app={{}} loadBalancer={loadBalancer} />);
269
+ expect(screen.getByText('passthrough-backend')).toBeInTheDocument();
270
+ expect(screen.getByText('tcp-hc')).toBeInTheDocument();
271
+ });
272
+ });
273
+
274
+ describe('GceLoadBalancerActions delete behavior', () => {
275
+ const app = { name: 'fnord' } as any;
276
+
277
+ beforeEach(() => {
278
+ vi.spyOn(CloudProviderRegistry, 'isDisabled').mockReturnValue(false);
279
+ });
280
+
281
+ it('deletes EXTERNAL_MANAGED load balancers using raw listener names and regional scope', async () => {
282
+ const confirmSpy = vi.spyOn(ConfirmationModalService, 'confirm').mockReturnValue(Promise.resolve({}) as any);
283
+ vi.spyOn(InfrastructureCaches, 'clearCache').mockReturnValue(undefined);
284
+ const executeTaskSpy = vi.spyOn(TaskExecutor, 'executeTask').mockReturnValue(Promise.resolve({}) as any);
285
+ const loadBalancer = {
286
+ account: 'test-account',
287
+ instances: [],
288
+ listeners: [{ name: 'regional-listener-443' }],
289
+ loadBalancerType: 'EXTERNAL_MANAGED',
290
+ name: 'regional-url-map (test-account/us-central1/EXTERNAL_MANAGED)',
291
+ provider: 'gce',
292
+ region: 'us-central1',
293
+ urlMapName: 'regional-url-map',
294
+ };
295
+
296
+ render(<GceLoadBalancerActions app={app} loadBalancer={loadBalancer} />);
297
+ fireEvent.click(screen.getByText('Delete Load Balancer'));
298
+ await vi.waitFor(() => expect(confirmSpy).toHaveBeenCalledTimes(1));
299
+ const modalParams = confirmSpy.mock.lastCall[0] as any;
300
+ await modalParams.submitMethod({ deleteHealthChecks: true });
301
+
302
+ expect(executeTaskSpy).toHaveBeenCalledWith({
303
+ application: app,
304
+ description: 'Delete load balancer: regional-url-map in test-account:us-central1',
305
+ job: [
306
+ expect.objectContaining({
307
+ cloudProvider: 'gce',
308
+ credentials: 'test-account',
309
+ deleteHealthChecks: true,
310
+ loadBalancerName: 'regional-listener-443',
311
+ loadBalancerType: 'EXTERNAL_MANAGED',
312
+ region: 'us-central1',
313
+ regions: ['us-central1'],
314
+ type: 'deleteLoadBalancer',
315
+ }),
316
+ ],
317
+ });
318
+ });
319
+
320
+ it('shows delete-health-check controls for EXTERNAL_MANAGED and REGIONAL_EXTERNAL_NETWORK load balancers', async () => {
321
+ const confirmSpy = vi.spyOn(ConfirmationModalService, 'confirm').mockReturnValue(Promise.resolve({}) as any);
322
+ const deleteBodyHasHealthCheckOption = async (loadBalancer: any): Promise<boolean> => {
323
+ const callCount = confirmSpy.mock.calls.length;
324
+ const actions = render(<GceLoadBalancerActions app={app} loadBalancer={loadBalancer} />);
325
+ fireEvent.click(actions.getByText('Delete Load Balancer'));
326
+ await vi.waitFor(() => expect(confirmSpy).toHaveBeenCalledTimes(callCount + 1));
327
+ actions.unmount();
328
+
329
+ const body = render(<>{(confirmSpy.mock.lastCall[0] as any).bodyContent}</>);
330
+ const checkbox = body.queryByRole('checkbox', { name: 'Delete associated health checks' });
331
+ body.unmount();
332
+ return checkbox !== null;
333
+ };
334
+
335
+ const externalManaged = {
336
+ account: 'test-account',
337
+ instances: [],
338
+ listeners: [{ name: 'regional-listener' }],
339
+ loadBalancerType: 'EXTERNAL_MANAGED',
340
+ name: 'regional-url-map (test-account/us-central1/EXTERNAL_MANAGED)',
341
+ provider: 'gce',
342
+ region: 'us-central1',
343
+ urlMapName: 'regional-url-map',
344
+ };
345
+ expect(await deleteBodyHasHealthCheckOption(externalManaged)).toBe(true);
346
+
347
+ const regionalExternalNetwork = {
348
+ account: 'test-account',
349
+ backendService: { healthCheck: { name: 'network-hc' } },
350
+ instances: [],
351
+ loadBalancerType: 'REGIONAL_EXTERNAL_NETWORK',
352
+ name: 'regional-network-lb',
353
+ provider: 'gce',
354
+ region: 'us-central1',
355
+ };
356
+ expect(await deleteBodyHasHealthCheckOption(regionalExternalNetwork)).toBe(true);
357
+
358
+ // INTERNAL deletes always remove unused health checks, so no option is offered.
359
+ expect(await deleteBodyHasHealthCheckOption({ ...regionalExternalNetwork, loadBalancerType: 'INTERNAL' })).toBe(
360
+ false,
361
+ );
362
+ });
363
+ });
364
+
365
+ describe('GceLoadBalancerListenersSection', () => {
366
+ const app = { name: 'fnord' } as any;
367
+
368
+ beforeEach(() => {
369
+ vi.spyOn(HelpContentsRegistry, 'getHelpField').mockImplementation((id: string) => `help:${id}`);
370
+ });
371
+
372
+ // Expanded help renders the registered help contents inline, which makes the HelpField id observable.
373
+ function renderListeners(loadBalancer: any) {
374
+ return render(
375
+ <HelpTextExpandedContext.Provider value={true}>
376
+ <GceLoadBalancerListenersSection app={app} loadBalancer={loadBalancer} />
377
+ </HelpTextExpandedContext.Provider>,
378
+ );
379
+ }
380
+
381
+ function definitions(container: HTMLElement): string[] {
382
+ return Array.from(container.querySelectorAll('dd')).map((node) => node.textContent);
383
+ }
384
+
385
+ it('keeps the historical listener list for existing load balancer families', () => {
386
+ const network = renderListeners({
387
+ loadBalancerType: 'NETWORK',
388
+ listeners: [{ port: '8080' }],
389
+ elb: {
390
+ listenerDescriptions: [
391
+ { listener: { instancePort: '8080', instanceProtocol: 'TCP', loadBalancerPort: '8080', protocol: 'TCP' } },
392
+ ],
393
+ },
394
+ });
395
+
396
+ expect(network.getAllByRole('listitem').map((node) => node.textContent)).toEqual(['8080']);
397
+ expect(definitions(network.container)).toEqual([]);
398
+ network.unmount();
399
+
400
+ const http = renderListeners({
401
+ loadBalancerType: 'HTTP',
402
+ listeners: [{ port: '80' }, { port: '443', protocol: 'HTTPS' }],
403
+ provider: 'gce',
404
+ });
405
+
406
+ expect(http.getAllByRole('listitem').map((node) => node.textContent)).toEqual(['80', 'HTTPS:443']);
407
+ expect(http.queryByText(/^help:/)).not.toBeInTheDocument();
408
+ });
409
+
410
+ it('renders REGIONAL_EXTERNAL_NETWORK listenerDescriptions without named-port help', () => {
411
+ const { container, queryByText } = renderListeners({
412
+ loadBalancerType: 'REGIONAL_EXTERNAL_NETWORK',
413
+ elb: {
414
+ listenerDescriptions: [
415
+ { listener: { instancePort: '8080', instanceProtocol: 'TCP', loadBalancerPort: '8080', protocol: 'TCP' } },
416
+ ],
417
+ },
418
+ });
419
+
420
+ expect(definitions(container)).toEqual(['TCP:8080 → TCP:8080']);
421
+ expect(queryByText(/^help:/)).not.toBeInTheDocument();
422
+ });
423
+
424
+ it('prefers EXTERNAL_MANAGED elb.listenerDescriptions over normalized listeners', () => {
425
+ const { container, getByText } = renderListeners({
426
+ loadBalancerType: 'EXTERNAL_MANAGED',
427
+ listeners: [{ port: '80' }, { port: '443' }],
428
+ provider: 'gce',
429
+ elb: {
430
+ listenerDescriptions: [
431
+ {
432
+ listener: {
433
+ instancePort: '8080',
434
+ instanceProtocol: 'HTTP',
435
+ loadBalancerPort: '443',
436
+ protocol: 'HTTPS',
437
+ },
438
+ },
439
+ ],
440
+ },
441
+ });
442
+
443
+ expect(definitions(container)).toEqual(['HTTPS:443 → HTTP:8080']);
444
+ expect(getByText('help:gce.httpLoadBalancer.namedPort')).toBeInTheDocument();
445
+ });
446
+
447
+ it('falls back to normalized EXTERNAL_MANAGED listeners only when elb.listenerDescriptions are absent', () => {
448
+ const { container } = renderListeners({
449
+ loadBalancerType: 'EXTERNAL_MANAGED',
450
+ listeners: [
451
+ { port: '80', name: 'frontend-80' },
452
+ { port: '443', certificate: 'projects/p/certificates/cert', name: 'frontend-443' },
453
+ ],
454
+ provider: 'gce',
455
+ });
456
+
457
+ expect(definitions(container)).toEqual(['HTTP:80', 'HTTPS:443']);
458
+ });
459
+
460
+ it('shows no listeners configured when a regional external network load balancer has no descriptions', () => {
461
+ const { container, getByText } = renderListeners({
462
+ loadBalancerType: 'REGIONAL_EXTERNAL_NETWORK',
463
+ listeners: [{ port: '8080' }],
464
+ });
465
+
466
+ expect(getByText('No listeners configured')).toBeInTheDocument();
467
+ expect(definitions(container)).toEqual([]);
64
468
  });
65
469
  });