@spinnaker/google 2026.3.2 → 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,9 +1,9 @@
1
1
  import React from 'react';
2
- import { mount, shallow } from 'enzyme';
2
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
3
+ import { UIRouterContext, UIRouterReact } from '@uirouter/react';
3
4
 
4
- import { LoadBalancerWriter, TaskMonitorWrapper } from '@spinnaker/core';
5
+ import { LoadBalancerWriter, TaskReader } from '@spinnaker/core';
5
6
 
6
- import { GceProxyLoadBalancerEditor } from './GceProxyLoadBalancerEditor';
7
7
  import {
8
8
  GceProxyLoadBalancerModal,
9
9
  normalizeGceProxyLoadBalancerCommand,
@@ -13,7 +13,8 @@ import {
13
13
 
14
14
  describe('GceProxyLoadBalancerModal', () => {
15
15
  const application = {
16
- loadBalancers: { onNextRefresh: jasmine.createSpy('onNextRefresh'), refresh: jasmine.createSpy('refresh') },
16
+ getDataSource: vi.fn(),
17
+ loadBalancers: { onNextRefresh: vi.fn(), refresh: vi.fn() },
17
18
  name: 'app',
18
19
  } as any;
19
20
 
@@ -50,14 +51,14 @@ describe('GceProxyLoadBalancerModal', () => {
50
51
 
51
52
  expect(command.mode).toBe('edit');
52
53
  expect(command.listeners[0]).toEqual(
53
- jasmine.objectContaining({
54
+ expect.objectContaining({
54
55
  address: { name: 'removed-address', selfLink: 'projects/test/global/addresses/removed-address' },
55
56
  certificate: { name: 'removed-cert', selfLink: 'projects/test/global/sslCertificates/removed-cert' },
56
57
  protocol: 'SSL',
57
58
  }),
58
59
  );
59
60
  expect(command.backendServices[0]).toEqual(
60
- jasmine.objectContaining({
61
+ expect.objectContaining({
61
62
  affinityCookieTtlSec: 120,
62
63
  connectionDrainingTimeoutSec: 30,
63
64
  healthCheck: {
@@ -71,7 +72,7 @@ describe('GceProxyLoadBalancerModal', () => {
71
72
  }),
72
73
  );
73
74
  expect(command.healthChecks[0]).toEqual(
74
- jasmine.objectContaining({ healthCheckType: 'TCP', name: 'removed-check', port: 443, timeoutSec: 5 }),
75
+ expect.objectContaining({ healthCheckType: 'TCP', name: 'removed-check', port: 443, timeoutSec: 5 }),
75
76
  );
76
77
  });
77
78
 
@@ -114,10 +115,10 @@ describe('GceProxyLoadBalancerModal', () => {
114
115
 
115
116
  expect(command.listeners[0].protocol).toBe('SSL');
116
117
  expect(payload).toEqual(
117
- jasmine.objectContaining({
118
- backendService: jasmine.objectContaining({
118
+ expect.objectContaining({
119
+ backendService: expect.objectContaining({
119
120
  connectionDrainingTimeoutSec: 30,
120
- healthCheck: jasmine.objectContaining({
121
+ healthCheck: expect.objectContaining({
121
122
  checkIntervalSec: 15,
122
123
  healthCheckType: 'HTTP',
123
124
  healthyThreshold: 2,
@@ -175,7 +176,7 @@ describe('GceProxyLoadBalancerModal', () => {
175
176
  const payload = serializeGceProxyLoadBalancerCommand(command);
176
177
 
177
178
  expect(payload).toEqual(
178
- jasmine.objectContaining({
179
+ expect.objectContaining({
179
180
  ipProtocol: 'UDP',
180
181
  network: 'network-a',
181
182
  ports: ['80', '443'],
@@ -187,22 +188,22 @@ describe('GceProxyLoadBalancerModal', () => {
187
188
  });
188
189
 
189
190
  it('returns the operation payload through the modal in pipeline mode without starting a task', () => {
190
- const closeModal = jasmine.createSpy('closeModal');
191
- const taskMonitor = { submit: jasmine.createSpy('submit') } as any;
191
+ const closeModal = vi.fn();
192
+ const taskMonitor = { submit: vi.fn() } as any;
192
193
  const command = validCommand('pipeline');
193
194
 
194
195
  submitGceProxyLoadBalancerCommand(command, { application, closeModal, taskMonitor });
195
196
 
196
- expect(closeModal).toHaveBeenCalledOnceWith(serializeGceProxyLoadBalancerCommand(command));
197
+ expect(closeModal).toHaveBeenCalledExactlyOnceWith(serializeGceProxyLoadBalancerCommand(command));
197
198
  expect(taskMonitor.submit).not.toHaveBeenCalled();
198
199
  });
199
200
 
200
201
  (['create', 'edit'] as const).forEach((mode) => {
201
202
  it(`submits the Clouddriver payload through LoadBalancerWriter in infrastructure ${mode} mode`, () => {
202
- const upsert = spyOn(LoadBalancerWriter, 'upsertLoadBalancer').and.returnValue(new Promise(() => {}));
203
+ const upsert = vi.spyOn(LoadBalancerWriter, 'upsertLoadBalancer').mockReturnValue(new Promise(() => {}));
203
204
  let operation: (() => PromiseLike<any>) | undefined;
204
205
  const taskMonitor = {
205
- submit: jasmine.createSpy('submit').and.callFake((submitOperation: () => PromiseLike<any>) => {
206
+ submit: vi.fn().mockImplementation((submitOperation: () => PromiseLike<any>) => {
206
207
  operation = submitOperation;
207
208
  }),
208
209
  } as any;
@@ -210,7 +211,7 @@ describe('GceProxyLoadBalancerModal', () => {
210
211
 
211
212
  submitGceProxyLoadBalancerCommand(command, {
212
213
  application,
213
- closeModal: jasmine.createSpy('closeModal'),
214
+ closeModal: vi.fn(),
214
215
  taskMonitor,
215
216
  });
216
217
  operation?.();
@@ -227,11 +228,11 @@ describe('GceProxyLoadBalancerModal', () => {
227
228
  it('exposes pipeline support and locks infrastructure identity fields in edit mode', () => {
228
229
  expect(GceProxyLoadBalancerModal.supportsPipelineConfig).toBe(true);
229
230
  expect(typeof GceProxyLoadBalancerModal.show).toBe('function');
230
- const wrapper = shallow(
231
+ const { container } = render(
231
232
  <GceProxyLoadBalancerModal
232
233
  app={application}
233
- closeModal={jasmine.createSpy('closeModal')}
234
- dismissModal={jasmine.createSpy('dismissModal')}
234
+ closeModal={vi.fn()}
235
+ dismissModal={vi.fn()}
235
236
  isNew={false}
236
237
  loadBalancer={
237
238
  {
@@ -243,56 +244,50 @@ describe('GceProxyLoadBalancerModal', () => {
243
244
  } as any
244
245
  }
245
246
  loadBalancerType="TCP"
247
+ readers={resolvedReaders()}
246
248
  />,
247
249
  );
248
250
 
249
- expect(wrapper.find(GceProxyLoadBalancerEditor).prop('command').mode).toBe('edit');
250
- expect(wrapper.find(GceProxyLoadBalancerEditor).prop('disabled')).toBe(true);
251
+ expect(screen.getByRole('heading', { name: 'Edit app-main' })).toBeInTheDocument();
252
+ ['name', 'credentials', 'region'].forEach((field) => expect(fieldControl(container, field)).toBeDisabled());
251
253
  });
252
254
 
253
- it('owns its refresh subscription across replacement and unmount', () => {
254
- const firstUnsubscribe = jasmine.createSpy('firstUnsubscribe');
255
- const secondUnsubscribe = jasmine.createSpy('secondUnsubscribe');
255
+ it('owns its refresh subscription through public submission and unmount', async () => {
256
+ const unsubscribe = vi.fn();
256
257
  const callbacks: Array<() => void> = [];
257
- const onNextRefresh = jasmine.createSpy('onNextRefresh').and.callFake((callback: () => void) => {
258
+ const onNextRefresh = vi.fn().mockImplementation((callback: () => void) => {
258
259
  callbacks.push(callback);
259
- return callbacks.length === 1 ? firstUnsubscribe : secondUnsubscribe;
260
+ return unsubscribe;
260
261
  });
261
- const refresh = jasmine.createSpy('refresh');
262
- const closeModal = jasmine.createSpy('closeModal');
263
- const pending = new Promise<any[]>(() => undefined);
264
- const readers = {
265
- accounts: () => pending,
266
- addresses: () => pending,
267
- backendServices: () => pending,
268
- certificates: () => pending,
269
- healthChecks: () => pending,
270
- networks: () => pending,
271
- regions: () => pending,
272
- subnets: () => pending,
273
- };
274
- const wrapper = mount(
275
- <GceProxyLoadBalancerModal
276
- application={{ loadBalancers: { onNextRefresh, refresh }, name: 'app' } as any}
277
- closeModal={closeModal}
278
- loadBalancerType="TCP"
279
- readers={readers}
280
- />,
262
+ const refresh = vi.fn();
263
+ const closeModal = vi.fn();
264
+ const task = { id: 'task' } as any;
265
+ vi.spyOn(LoadBalancerWriter, 'upsertLoadBalancer').mockResolvedValue(task);
266
+ vi.spyOn(TaskReader, 'waitUntilTaskCompletes').mockResolvedValue(task);
267
+ const router = new UIRouterReact();
268
+ const rendered = render(
269
+ <UIRouterContext.Provider value={router}>
270
+ <GceProxyLoadBalancerModal
271
+ application={{ getDataSource: vi.fn(), loadBalancers: { onNextRefresh, refresh }, name: 'app' } as any}
272
+ closeModal={closeModal}
273
+ isNew={false}
274
+ loadBalancer={proxyLoadBalancer()}
275
+ loadBalancerType="TCP"
276
+ readers={resolvedReaders()}
277
+ />
278
+ </UIRouterContext.Provider>,
281
279
  );
282
- const taskMonitor = wrapper.find(TaskMonitorWrapper).prop('monitor');
280
+ await waitFor(() => expect(screen.getByRole('button', { name: 'Update' })).toBeEnabled());
283
281
 
284
- taskMonitor.config.onTaskComplete();
282
+ fireEvent.click(screen.getByRole('button', { name: 'Update' }));
283
+ await waitFor(() => expect(refresh).toHaveBeenCalled());
285
284
 
286
- expect(onNextRefresh.calls.first().invocationOrder).toBeLessThan(refresh.calls.first().invocationOrder);
285
+ expect(onNextRefresh.mock.invocationCallOrder[0]).toBeLessThan(refresh.mock.invocationCallOrder[0]);
287
286
 
288
- taskMonitor.config.onTaskComplete();
287
+ rendered.unmount();
288
+ callbacks[0]();
289
289
 
290
- expect(firstUnsubscribe).toHaveBeenCalledTimes(1);
291
-
292
- wrapper.unmount();
293
- callbacks[1]();
294
-
295
- expect(secondUnsubscribe).toHaveBeenCalledTimes(1);
290
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
296
291
  expect(closeModal).not.toHaveBeenCalled();
297
292
  });
298
293
  });
@@ -311,3 +306,36 @@ function validCommand(mode: 'create' | 'edit' | 'pipeline'): any {
311
306
  'TCP',
312
307
  );
313
308
  }
309
+
310
+ function proxyLoadBalancer(): any {
311
+ return {
312
+ account: 'account-a',
313
+ backendService: { name: 'app-main', portName: 'tcp', sessionAffinity: 'NONE' },
314
+ healthChecks: [{ healthCheckType: 'TCP', name: 'check-a', port: 443 }],
315
+ loadBalancerType: 'TCP',
316
+ name: 'app-main',
317
+ portRange: '443',
318
+ region: 'global',
319
+ };
320
+ }
321
+
322
+ function fieldControl(container: HTMLElement, name: string): HTMLInputElement | HTMLSelectElement {
323
+ const control = container.querySelector(`[data-field="${name}"] input, [data-field="${name}"] select`);
324
+ if (!(control instanceof HTMLInputElement || control instanceof HTMLSelectElement)) {
325
+ throw new Error(`No control found for ${name}`);
326
+ }
327
+ return control;
328
+ }
329
+
330
+ function resolvedReaders(): any {
331
+ return {
332
+ accounts: () => Promise.resolve([]),
333
+ addresses: () => Promise.resolve([]),
334
+ backendServices: () => Promise.resolve([]),
335
+ certificates: () => Promise.resolve([]),
336
+ healthChecks: () => Promise.resolve([]),
337
+ networks: () => Promise.resolve([]),
338
+ regions: () => Promise.resolve([]),
339
+ subnets: () => Promise.resolve([]),
340
+ };
341
+ }
@@ -1,3 +1,4 @@
1
+ import type { Mocked } from 'vitest';
1
2
  import {
2
3
  GceLoadBalancerDataController,
3
4
  IGceLoadBalancerDataReaders,
@@ -39,7 +40,7 @@ describe('GCE load balancer data', () => {
39
40
  const first = deferred<unknown[]>();
40
41
  const second = deferred<unknown[]>();
41
42
  const readers = testReaders();
42
- readers.regions.and.callFake((account: string) => (account === 'first' ? first.promise : second.promise));
43
+ readers.regions.mockImplementation((account: string) => (account === 'first' ? first.promise : second.promise));
43
44
  const controller = new GceLoadBalancerDataController(readers);
44
45
 
45
46
  const firstLoad = controller.load('first');
@@ -56,7 +57,7 @@ describe('GCE load balancer data', () => {
56
57
  it('reports an error from the latest request', async () => {
57
58
  const readers = testReaders();
58
59
  const failure = new Error('regions failed');
59
- readers.regions.and.returnValue(Promise.reject(failure));
60
+ readers.regions.mockReturnValue(Promise.reject(failure));
60
61
  const controller = new GceLoadBalancerDataController(readers);
61
62
 
62
63
  await controller.load('test-account');
@@ -68,13 +69,13 @@ describe('GCE load balancer data', () => {
68
69
  it('does not publish responses after disposal', async () => {
69
70
  const pendingRegions = deferred<unknown[]>();
70
71
  const readers = testReaders();
71
- readers.regions.and.returnValue(pendingRegions.promise);
72
+ readers.regions.mockReturnValue(pendingRegions.promise);
72
73
  const controller = new GceLoadBalancerDataController(readers);
73
- const listener = jasmine.createSpy('listener');
74
+ const listener = vi.fn();
74
75
  controller.subscribe(listener);
75
76
 
76
77
  const loading = controller.load('test-account');
77
- listener.calls.reset();
78
+ listener.mockClear();
78
79
  controller.dispose();
79
80
  pendingRegions.resolve([{ name: 'late-region' }]);
80
81
  await loading;
@@ -97,17 +98,17 @@ describe('GCE load balancer data', () => {
97
98
  });
98
99
  });
99
100
 
100
- function testReaders(): jasmine.SpyObj<IGceLoadBalancerDataReaders> {
101
- return jasmine.createSpyObj<IGceLoadBalancerDataReaders>('readers', {
102
- accounts: Promise.resolve([{ name: 'test-account' }]),
103
- addresses: Promise.resolve([{ name: 'address' }]),
104
- backendServices: Promise.resolve([{ name: 'backend' }]),
105
- certificates: Promise.resolve([{ name: 'certificate' }]),
106
- healthChecks: Promise.resolve([{ name: 'check' }]),
107
- networks: Promise.resolve([{ name: 'network' }]),
108
- regions: Promise.resolve([{ name: 'europe-west1' }]),
109
- subnets: Promise.resolve([{ name: 'subnet' }]),
110
- });
101
+ function testReaders(): Mocked<IGceLoadBalancerDataReaders> {
102
+ return ({
103
+ accounts: vi.fn().mockReturnValue(Promise.resolve([{ name: 'test-account' }])),
104
+ addresses: vi.fn().mockReturnValue(Promise.resolve([{ name: 'address' }])),
105
+ backendServices: vi.fn().mockReturnValue(Promise.resolve([{ name: 'backend' }])),
106
+ certificates: vi.fn().mockReturnValue(Promise.resolve([{ name: 'certificate' }])),
107
+ healthChecks: vi.fn().mockReturnValue(Promise.resolve([{ name: 'check' }])),
108
+ networks: vi.fn().mockReturnValue(Promise.resolve([{ name: 'network' }])),
109
+ regions: vi.fn().mockReturnValue(Promise.resolve([{ name: 'europe-west1' }])),
110
+ subnets: vi.fn().mockReturnValue(Promise.resolve([{ name: 'subnet' }])),
111
+ } as unknown) as Mocked<IGceLoadBalancerDataReaders>;
111
112
  }
112
113
 
113
114
  function deferred<T>() {
@@ -19,14 +19,23 @@ describe('GCE load balancer models', () => {
19
19
  );
20
20
 
21
21
  expect(serializeGceLoadBalancerCommand(command)).toEqual(
22
- jasmine.objectContaining({
23
- healthChecks: [jasmine.objectContaining({ healthCheckType: 'GRPC', grpcServiceName: 'example.Health' })],
24
- listeners: [jasmine.objectContaining({ certificateMap: 'shared-map' })],
22
+ expect.objectContaining({
23
+ healthChecks: [expect.objectContaining({ healthCheckType: 'GRPC', grpcServiceName: 'example.Health' })],
24
+ listeners: [expect.objectContaining({ certificateMap: 'shared-map' })],
25
25
  }),
26
26
  );
27
27
  });
28
28
  it('defines capabilities for every supported load balancer type', () => {
29
- expect(GCE_LOAD_BALANCER_TYPES).toEqual(['NETWORK', 'INTERNAL', 'TCP', 'SSL', 'HTTP', 'INTERNAL_MANAGED']);
29
+ expect(GCE_LOAD_BALANCER_TYPES).toEqual([
30
+ 'NETWORK',
31
+ 'INTERNAL',
32
+ 'TCP',
33
+ 'SSL',
34
+ 'HTTP',
35
+ 'INTERNAL_MANAGED',
36
+ 'EXTERNAL_MANAGED',
37
+ 'REGIONAL_EXTERNAL_NETWORK',
38
+ ]);
30
39
  expect(Object.keys(GCE_LOAD_BALANCER_CAPABILITIES)).toEqual(GCE_LOAD_BALANCER_TYPES);
31
40
  expect(GCE_LOAD_BALANCER_CAPABILITIES.NETWORK).toEqual({
32
41
  address: true,
@@ -359,7 +368,7 @@ describe('GCE load balancer models', () => {
359
368
  );
360
369
 
361
370
  expect(command.healthChecks).toEqual([
362
- jasmine.objectContaining({
371
+ expect.objectContaining({
363
372
  checkIntervalSec: 15,
364
373
  healthCheckType: 'HTTP',
365
374
  healthyThreshold: 2,
@@ -387,6 +396,222 @@ describe('GCE load balancer models', () => {
387
396
 
388
397
  expect(command.healthChecks).toEqual([{ healthCheckType: 'HTTP', port: 53, requestPath: '/health' }]);
389
398
  });
399
+
400
+ it('normalizes and serializes EXTERNAL_MANAGED with regional scope, network, address, tier, and certificate', () => {
401
+ const regionalCertUrl =
402
+ '//certificatemanager.googleapis.com/projects/test/locations/europe-west1/certificates/regional-cert';
403
+ const command = normalizeGceLoadBalancerCommand(
404
+ {
405
+ account: 'account-a',
406
+ backendServices: [
407
+ {
408
+ healthCheck: 'projects/test/regions/europe-west1/healthChecks/check-a',
409
+ name: 'backend-a',
410
+ },
411
+ ],
412
+ defaultService: 'backend-a',
413
+ healthChecks: [{ healthCheckType: 'HTTP', name: 'check-a', port: 80, requestPath: '/health' }],
414
+ listeners: [
415
+ {
416
+ certificate: regionalCertUrl,
417
+ ipAddress: '203.0.113.10',
418
+ name: 'app-https',
419
+ networkTier: 'STANDARD',
420
+ port: 443,
421
+ protocol: 'HTTPS',
422
+ },
423
+ ],
424
+ loadBalancerType: 'EXTERNAL_MANAGED',
425
+ name: 'app-main',
426
+ network: 'projects/test/global/networks/network-a',
427
+ region: 'europe-west1',
428
+ urlMapName: 'app-main',
429
+ },
430
+ 'edit',
431
+ );
432
+
433
+ expect(command.loadBalancerType).toBe('EXTERNAL_MANAGED');
434
+ expect(command.region).toBe('europe-west1');
435
+ expect(command.network).toEqual({
436
+ name: 'network-a',
437
+ selfLink: 'projects/test/global/networks/network-a',
438
+ });
439
+ expect(command.listeners).toEqual([
440
+ {
441
+ address: { name: '203.0.113.10' },
442
+ certificate: { name: 'regional-cert', selfLink: regionalCertUrl },
443
+ name: 'app-https',
444
+ networkTier: 'STANDARD',
445
+ portRange: '443',
446
+ protocol: 'HTTPS',
447
+ },
448
+ ]);
449
+ expect(command.listeners[0].certificateMap).toBeUndefined();
450
+
451
+ const serialized = serializeGceLoadBalancerCommand(command);
452
+ expect(serialized).toEqual(
453
+ expect.objectContaining({
454
+ credentials: 'account-a',
455
+ loadBalancerType: 'EXTERNAL_MANAGED',
456
+ network: 'network-a',
457
+ region: 'europe-west1',
458
+ }),
459
+ );
460
+ expect(serialized.listeners).toEqual([
461
+ {
462
+ certificate: regionalCertUrl,
463
+ ipAddress: '203.0.113.10',
464
+ name: 'app-https',
465
+ networkTier: 'STANDARD',
466
+ portRange: '443',
467
+ protocol: 'HTTPS',
468
+ },
469
+ ]);
470
+ expect((serialized.listeners as any[])[0].certificateMap).toBeUndefined();
471
+ });
472
+
473
+ it('preserves EXTERNAL_MANAGED listener identity from a flat pipeline operation', () => {
474
+ const command = normalizeGceLoadBalancerCommand(
475
+ {
476
+ certificate:
477
+ '//certificatemanager.googleapis.com/projects/test/locations/europe-west1/certificates/regional-cert',
478
+ credentials: 'account-a',
479
+ ipAddress: '203.0.113.10',
480
+ ipProtocol: 'TCP',
481
+ loadBalancerName: 'app-https',
482
+ loadBalancerType: 'EXTERNAL_MANAGED',
483
+ name: 'app-https',
484
+ network: 'network-a',
485
+ networkTier: 'STANDARD',
486
+ portRange: '443',
487
+ region: 'europe-west1',
488
+ urlMapName: 'app-main',
489
+ },
490
+ 'pipeline',
491
+ );
492
+
493
+ expect(command.name).toBe('app-main');
494
+ expect(command.listeners).toEqual([
495
+ {
496
+ address: { name: '203.0.113.10' },
497
+ certificate: {
498
+ name: 'regional-cert',
499
+ selfLink:
500
+ '//certificatemanager.googleapis.com/projects/test/locations/europe-west1/certificates/regional-cert',
501
+ },
502
+ name: 'app-https',
503
+ networkTier: 'STANDARD',
504
+ portRange: '443',
505
+ protocol: 'HTTPS',
506
+ },
507
+ ]);
508
+ });
509
+
510
+ it('preserves REGIONAL_EXTERNAL_NETWORK port text for validation and source immutability', () => {
511
+ const command = normalizeGceLoadBalancerCommand(
512
+ {
513
+ account: 'account-a',
514
+ backendService: {
515
+ healthCheck: { healthCheckType: 'TCP', name: 'tcp-check', port: 80 },
516
+ name: 'app-main',
517
+ sessionAffinity: 'CLIENT_IP',
518
+ },
519
+ ipAddress: '35.1.2.3',
520
+ ipProtocol: 'TCP',
521
+ loadBalancerName: 'app-main',
522
+ loadBalancerType: 'REGIONAL_EXTERNAL_NETWORK',
523
+ networkTier: 'PREMIUM',
524
+ ports: ['80', ' 443 '],
525
+ region: 'europe-west1',
526
+ },
527
+ 'edit',
528
+ );
529
+
530
+ expect(command.loadBalancerType).toBe('REGIONAL_EXTERNAL_NETWORK');
531
+ expect(command.listeners).toEqual([
532
+ {
533
+ address: { name: '35.1.2.3' },
534
+ name: 'app-main',
535
+ portRange: '80, 443 ',
536
+ protocol: 'TCP',
537
+ },
538
+ ]);
539
+ expect(command.networkTier).toBe('PREMIUM');
540
+ expect(command.backendServices).toEqual([
541
+ {
542
+ healthCheck: { healthCheckType: 'TCP', name: 'tcp-check', port: 80 },
543
+ name: 'app-main',
544
+ sessionAffinity: 'CLIENT_IP',
545
+ },
546
+ ]);
547
+ expect(command.original).toEqual({
548
+ backendServices: command.backendServices,
549
+ healthChecks: command.healthChecks,
550
+ listeners: command.listeners,
551
+ });
552
+
553
+ command.listeners[0].protocol = 'UDP';
554
+ command.backendServices[0].sessionAffinity = 'CLIENT_IP_PROTO';
555
+
556
+ expect(command.original?.listeners[0].protocol).toBe('TCP');
557
+ expect(command.original?.backendServices[0].sessionAffinity).toBe('CLIENT_IP');
558
+
559
+ const serialized = serializeGceLoadBalancerCommand(command);
560
+ expect(serialized).toEqual({
561
+ backendService: {
562
+ healthCheck: { healthCheckType: 'TCP', name: 'tcp-check', port: 80 },
563
+ name: 'app-main',
564
+ sessionAffinity: 'CLIENT_IP_PROTO',
565
+ },
566
+ cloudProvider: 'gce',
567
+ credentials: 'account-a',
568
+ ipAddress: '35.1.2.3',
569
+ ipProtocol: 'UDP',
570
+ loadBalancerName: 'app-main',
571
+ loadBalancerType: 'REGIONAL_EXTERNAL_NETWORK',
572
+ name: 'app-main',
573
+ networkTier: 'PREMIUM',
574
+ ports: ['80', ' 443 '],
575
+ provider: 'gce',
576
+ region: 'europe-west1',
577
+ type: 'upsertLoadBalancer',
578
+ });
579
+ expect(serialized.portRange).toBeUndefined();
580
+ expect(serialized.backendServices).toBeUndefined();
581
+ });
582
+
583
+ (['HTTP', 'INTERNAL_MANAGED'] as const).forEach((loadBalancerType) => {
584
+ it(`normalizes and serializes ${loadBalancerType} without listener networkTier`, () => {
585
+ const command = normalizeGceLoadBalancerCommand(
586
+ {
587
+ account: 'account-a',
588
+ listeners: [
589
+ {
590
+ ipAddress: '203.0.113.10',
591
+ name: 'frontend',
592
+ networkTier: 'STANDARD',
593
+ port: 80,
594
+ protocol: 'HTTP',
595
+ },
596
+ ],
597
+ loadBalancerType,
598
+ name: 'web',
599
+ network: loadBalancerType === 'INTERNAL_MANAGED' ? 'network-a' : undefined,
600
+ networkTier: 'STANDARD',
601
+ region: loadBalancerType === 'INTERNAL_MANAGED' ? 'europe-west1' : 'global',
602
+ subnet: loadBalancerType === 'INTERNAL_MANAGED' ? 'subnet-a' : undefined,
603
+ },
604
+ 'create',
605
+ );
606
+
607
+ expect(command.listeners[0].networkTier).toBeUndefined();
608
+ expect(command.networkTier).toBeUndefined();
609
+
610
+ const serialized = serializeGceLoadBalancerCommand(command);
611
+ expect(serialized.networkTier).toBeUndefined();
612
+ expect((serialized.listeners as any[])[0].networkTier).toBeUndefined();
613
+ });
614
+ });
390
615
  });
391
616
 
392
617
  function readerHealthCheck() {