@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
@@ -0,0 +1,443 @@
1
+ import { fireEvent, render, within } from '@testing-library/react';
2
+ import React from 'react';
3
+
4
+ import {
5
+ buildGceRegionalExternalNetworkLoadBalancerOptions,
6
+ GceRegionalExternalNetworkLoadBalancerEditor,
7
+ validateGceRegionalExternalNetworkLoadBalancerCommand,
8
+ } from './GceRegionalExternalNetworkLoadBalancerEditor';
9
+ import { normalizeGceRegionalExternalNetworkLoadBalancerCommand } from './GceRegionalExternalNetworkLoadBalancerModal';
10
+ import type { IGceLoadBalancerData } from '../common';
11
+
12
+ describe('GceRegionalExternalNetworkLoadBalancerEditor', () => {
13
+ const emptyData = (): IGceLoadBalancerData => ({
14
+ accounts: [],
15
+ addresses: [],
16
+ backendServices: [],
17
+ certificates: [],
18
+ healthChecks: [],
19
+ networks: [],
20
+ regions: [],
21
+ subnets: [],
22
+ });
23
+
24
+ it('filters external addresses and stores selected IP and network tier', () => {
25
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
26
+ { account: 'account-a', loadBalancerName: 'app-main', region: 'europe-west1' },
27
+ 'create',
28
+ );
29
+ const options = buildGceRegionalExternalNetworkLoadBalancerOptions(command, {
30
+ ...emptyData(),
31
+ addresses: [
32
+ {
33
+ account: 'account-a',
34
+ address: '35.1.2.3',
35
+ addressType: 'EXTERNAL',
36
+ networkTier: 'PREMIUM',
37
+ region: 'europe-west1',
38
+ },
39
+ {
40
+ account: 'account-a',
41
+ address: '10.0.0.1',
42
+ addressType: 'INTERNAL',
43
+ networkTier: 'PREMIUM',
44
+ region: 'europe-west1',
45
+ },
46
+ { account: 'account-a', address: '198.51.100.1', addressType: 'EXTERNAL', region: 'us-central1' },
47
+ ],
48
+ } as any);
49
+
50
+ expect(options.addresses.map(({ address }) => address)).toEqual(['35.1.2.3']);
51
+
52
+ const onChange = vi.fn();
53
+ const { getByLabelText } = render(
54
+ <GceRegionalExternalNetworkLoadBalancerEditor
55
+ command={command}
56
+ data={
57
+ {
58
+ ...emptyData(),
59
+ addresses: [{ address: '35.1.2.3', addressType: 'EXTERNAL', networkTier: 'PREMIUM' }],
60
+ } as any
61
+ }
62
+ onChange={onChange}
63
+ />,
64
+ );
65
+
66
+ expect(optionValues(getByLabelText('IP address'))).toEqual(['', '35.1.2.3']);
67
+ fireEvent.change(getByLabelText('IP address'), { target: { value: '35.1.2.3' } });
68
+
69
+ expect(onChange).toHaveBeenCalledWith(
70
+ expect.objectContaining({
71
+ listeners: [
72
+ expect.objectContaining({ address: expect.objectContaining({ address: '35.1.2.3', name: '35.1.2.3' }) }),
73
+ ],
74
+ networkTier: 'PREMIUM',
75
+ }),
76
+ );
77
+ });
78
+
79
+ it('renders region, protocol, discrete ports, health check, and session affinity controls', () => {
80
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
81
+ {
82
+ account: 'account-a',
83
+ backendService: {
84
+ healthCheck: { healthCheckType: 'TCP', name: 'tcp-check', port: 80 },
85
+ name: 'app-main',
86
+ sessionAffinity: 'CLIENT_IP',
87
+ },
88
+ ipProtocol: 'TCP',
89
+ loadBalancerName: 'app-main',
90
+ ports: ['80', '443'],
91
+ region: 'europe-west1',
92
+ },
93
+ 'edit',
94
+ );
95
+ const { container, getByLabelText, getByRole } = render(
96
+ <GceRegionalExternalNetworkLoadBalancerEditor command={command} data={emptyData()} onChange={vi.fn()} />,
97
+ );
98
+
99
+ ['Account', 'Region', 'IP address', 'Network tier', 'Protocol', 'Ports', 'Session affinity'].forEach((label) =>
100
+ expect(getByLabelText(label)).toBeInTheDocument(),
101
+ );
102
+ expect(getByRole('heading', { name: 'Health Check' })).toBeInTheDocument();
103
+ expect(controls(container).healthCheckName).toHaveValue('tcp-check');
104
+ expect(optionValues(getByLabelText('Protocol'))).toEqual(['TCP', 'UDP']);
105
+ expect(optionValues(getByLabelText('Session affinity'))).toEqual([
106
+ 'NONE',
107
+ 'CLIENT_IP',
108
+ 'CLIENT_IP_PROTO',
109
+ 'CLIENT_IP_PORT_PROTO',
110
+ ]);
111
+ });
112
+
113
+ it('updates both health check references by name without mutating the command', () => {
114
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
115
+ {
116
+ account: 'account-a',
117
+ backendService: {
118
+ healthCheck: { healthCheckType: 'TCP', name: 'old-check', port: 80 },
119
+ name: 'app-main',
120
+ sessionAffinity: 'NONE',
121
+ },
122
+ loadBalancerName: 'app-main',
123
+ ports: ['80'],
124
+ region: 'europe-west1',
125
+ },
126
+ 'edit',
127
+ );
128
+ const onChange = vi.fn();
129
+ const { container } = render(
130
+ <GceRegionalExternalNetworkLoadBalancerEditor command={command} data={emptyData()} onChange={onChange} />,
131
+ );
132
+
133
+ fireEvent.change(controls(container).healthCheckName, { target: { value: 'new-check' } });
134
+
135
+ const nextCommand = onChange.mock.lastCall[0];
136
+ expect(nextCommand.backendServices[0].healthCheck).toBe(nextCommand.healthChecks[0]);
137
+ expect(nextCommand.backendServices[0].healthCheck.name).toBe('new-check');
138
+ expect(nextCommand.healthChecks[0].name).toBe('new-check');
139
+ expect((command.backendServices[0].healthCheck as any).name).toBe('old-check');
140
+ expect(command.healthChecks[0].name).toBe('old-check');
141
+ });
142
+
143
+ it('updates protocol, raw ports, and session affinity through editor changes', () => {
144
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
145
+ { account: 'account-a', loadBalancerName: 'app-main', region: 'europe-west1' },
146
+ 'create',
147
+ );
148
+ const onChange = vi.fn();
149
+ const { getByLabelText } = render(
150
+ <GceRegionalExternalNetworkLoadBalancerEditor command={command} data={emptyData()} onChange={onChange} />,
151
+ );
152
+
153
+ fireEvent.change(getByLabelText('Protocol'), { target: { value: 'UDP' } });
154
+ expect(onChange.mock.lastCall[0].listeners[0].protocol).toBe('UDP');
155
+
156
+ fireEvent.change(getByLabelText('Ports'), { target: { value: '80, 443 , 8080' } });
157
+ expect(onChange.mock.lastCall[0].ports).toEqual(['80', ' 443 ', ' 8080']);
158
+
159
+ fireEvent.change(getByLabelText('Session affinity'), { target: { value: 'CLIENT_IP_PORT_PROTO' } });
160
+ expect(onChange.mock.lastCall[0].backendServices[0].sessionAffinity).toBe('CLIENT_IP_PORT_PROTO');
161
+ });
162
+
163
+ it('locks identity and forwarding-rule fields while allowing backend edits', () => {
164
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
165
+ {
166
+ account: 'account-a',
167
+ backendService: {
168
+ healthCheck: { healthCheckType: 'TCP', name: 'tcp-check', port: 80 },
169
+ name: 'app-main',
170
+ sessionAffinity: 'CLIENT_IP',
171
+ },
172
+ ipAddress: '35.1.2.3',
173
+ ipProtocol: 'TCP',
174
+ loadBalancerName: 'app-main',
175
+ networkTier: 'PREMIUM',
176
+ ports: ['80'],
177
+ region: 'europe-west1',
178
+ },
179
+ 'edit',
180
+ );
181
+ const { container } = render(
182
+ <GceRegionalExternalNetworkLoadBalancerEditor command={command} data={emptyData()} onChange={vi.fn()} />,
183
+ );
184
+ const fields = controls(container);
185
+
186
+ (['name', 'credentials', 'region', 'address', 'networkTier', 'protocol', 'ports'] as const).forEach((field) =>
187
+ expect(fields[field], field).toBeDisabled(),
188
+ );
189
+ (['sessionAffinity', 'healthCheckName'] as const).forEach((field) => expect(fields[field], field).toBeEnabled());
190
+ });
191
+
192
+ it('drops the selected address when the account or region changes', () => {
193
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
194
+ { account: 'account-a', ipAddress: '35.1.2.3', loadBalancerName: 'app-main', region: 'europe-west1' },
195
+ 'create',
196
+ );
197
+ const onChange = vi.fn();
198
+ const { getByLabelText } = render(
199
+ <GceRegionalExternalNetworkLoadBalancerEditor
200
+ command={command}
201
+ data={{
202
+ ...emptyData(),
203
+ accounts: [{ name: 'account-a' }, { name: 'account-b' }],
204
+ regions: [{ name: 'europe-west1' }, { name: 'us-central1' }],
205
+ }}
206
+ onChange={onChange}
207
+ />,
208
+ );
209
+
210
+ fireEvent.change(getByLabelText('Account'), { target: { value: 'account-b' } });
211
+ expect(onChange.mock.lastCall[0].credentials).toBe('account-b');
212
+ expect(onChange.mock.lastCall[0].listeners[0].address).toBeUndefined();
213
+
214
+ fireEvent.change(getByLabelText('Region'), { target: { value: 'us-central1' } });
215
+ expect(onChange.mock.lastCall[0].region).toBe('us-central1');
216
+ expect(onChange.mock.lastCall[0].listeners[0].address).toBeUndefined();
217
+ });
218
+
219
+ it('lets an ephemeral address choose its network tier and derives it from a reserved address', () => {
220
+ const onChange = vi.fn();
221
+ const ephemeral = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
222
+ { account: 'account-a', loadBalancerName: 'app-main', region: 'europe-west1' },
223
+ 'create',
224
+ );
225
+ const ephemeralRender = render(
226
+ <GceRegionalExternalNetworkLoadBalancerEditor command={ephemeral} data={emptyData()} onChange={onChange} />,
227
+ );
228
+ const ephemeralTier = ephemeralRender.getByLabelText('Network tier');
229
+
230
+ expect(ephemeralTier).toBeEnabled();
231
+ expect(optionValues(ephemeralTier)).toEqual(['PREMIUM', 'STANDARD']);
232
+ fireEvent.change(ephemeralTier, { target: { value: 'STANDARD' } });
233
+ expect(onChange.mock.lastCall[0].networkTier).toBe('STANDARD');
234
+ ephemeralRender.unmount();
235
+
236
+ const reserved = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
237
+ { account: 'account-a', ipAddress: '35.1.2.3', loadBalancerName: 'app-main', region: 'europe-west1' },
238
+ 'create',
239
+ );
240
+ const { getByLabelText } = render(
241
+ <GceRegionalExternalNetworkLoadBalancerEditor command={reserved} data={emptyData()} onChange={onChange} />,
242
+ );
243
+ expect(getByLabelText('Network tier')).toBeDisabled();
244
+ });
245
+
246
+ it('associates every field label with its control', () => {
247
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
248
+ { account: 'account-a', loadBalancerName: 'app-main', region: 'europe-west1' },
249
+ 'create',
250
+ );
251
+ const { container } = render(
252
+ <GceRegionalExternalNetworkLoadBalancerEditor command={command} data={emptyData()} onChange={vi.fn()} />,
253
+ );
254
+
255
+ const labels = Array.from(container.querySelectorAll('label'));
256
+ expect(labels.length).toBeGreaterThan(0);
257
+ labels.forEach((label) => {
258
+ expect(label.htmlFor).toBeTruthy();
259
+ expect(container.querySelectorAll(`#${label.htmlFor}`)).toHaveLength(1);
260
+ expect(label.control, label.textContent).not.toBeNull();
261
+ });
262
+ });
263
+
264
+ it('validates required discrete ports, protocol, health check, and supported session affinity', () => {
265
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
266
+ {
267
+ account: '',
268
+ backendService: { name: '', sessionAffinity: 'GENERATED_COOKIE' },
269
+ loadBalancerName: '',
270
+ ports: ['70000', 'abc'],
271
+ region: '',
272
+ },
273
+ 'create',
274
+ );
275
+
276
+ expect(validateGceRegionalExternalNetworkLoadBalancerCommand(command)).toEqual([
277
+ 'Name is required.',
278
+ 'Account is required.',
279
+ 'Region is required.',
280
+ 'Ports must be between 1 and 65535.',
281
+ 'Backend service name is required.',
282
+ 'Each backend service requires a health check.',
283
+ 'Health check name is required.',
284
+ 'Health check port must be between 1 and 65535.',
285
+ 'Session affinity must be NONE, CLIENT_IP, CLIENT_IP_PROTO, or CLIENT_IP_PORT_PROTO.',
286
+ ]);
287
+ });
288
+
289
+ it('accepts every supported passthrough session affinity', () => {
290
+ (['NONE', 'CLIENT_IP', 'CLIENT_IP_PROTO', 'CLIENT_IP_PORT_PROTO'] as const).forEach((sessionAffinity) => {
291
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
292
+ {
293
+ account: 'account-a',
294
+ backendService: {
295
+ healthCheck: { healthCheckType: 'TCP', name: 'tcp-check', port: 80 },
296
+ name: 'app-main',
297
+ sessionAffinity,
298
+ },
299
+ loadBalancerName: 'app-main',
300
+ ports: ['80'],
301
+ region: 'europe-west1',
302
+ },
303
+ 'create',
304
+ );
305
+
306
+ expect(
307
+ validateGceRegionalExternalNetworkLoadBalancerCommand(command).some((error) =>
308
+ error.startsWith('Session affinity'),
309
+ ),
310
+ ).toBe(false);
311
+ });
312
+ });
313
+
314
+ (['edit', 'pipeline'] as const).forEach((mode) => {
315
+ it(`rejects an explicitly null ${mode} health check without throwing`, () => {
316
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
317
+ {
318
+ account: 'account-a',
319
+ backendService: {
320
+ healthCheck: null,
321
+ name: 'app-main',
322
+ sessionAffinity: 'NONE',
323
+ },
324
+ loadBalancerName: 'app-main',
325
+ ports: ['80'],
326
+ region: 'europe-west1',
327
+ },
328
+ mode,
329
+ );
330
+
331
+ expect(validateGceRegionalExternalNetworkLoadBalancerCommand(command)).toEqual([
332
+ 'Each backend service requires a health check.',
333
+ 'Health check name is required.',
334
+ 'Health check port must be between 1 and 65535.',
335
+ ]);
336
+ });
337
+ });
338
+
339
+ it('rejects a health check whose name is omitted', () => {
340
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
341
+ {
342
+ account: 'account-a',
343
+ backendService: {
344
+ healthCheck: { healthCheckType: 'TCP', name: 'tcp-check', port: 80 },
345
+ name: 'app-main',
346
+ sessionAffinity: 'NONE',
347
+ },
348
+ loadBalancerName: 'app-main',
349
+ ports: ['80'],
350
+ region: 'europe-west1',
351
+ },
352
+ 'create',
353
+ );
354
+ command.backendServices[0].healthCheck = { healthCheckType: 'TCP', name: '', port: 80 };
355
+ command.healthChecks = [{ healthCheckType: 'TCP', name: '', port: 80 }];
356
+
357
+ expect(validateGceRegionalExternalNetworkLoadBalancerCommand(command)).toContain('Health check name is required.');
358
+ });
359
+
360
+ it('rejects more than five discrete ports', () => {
361
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
362
+ {
363
+ account: 'account-a',
364
+ backendService: {
365
+ healthCheck: { healthCheckType: 'TCP', name: 'tcp-check', port: 80 },
366
+ name: 'app-main',
367
+ sessionAffinity: 'NONE',
368
+ },
369
+ loadBalancerName: 'app-main',
370
+ ports: ['1', '2', '3', '4', '5', '6'],
371
+ region: 'europe-west1',
372
+ },
373
+ 'create',
374
+ );
375
+
376
+ expect(validateGceRegionalExternalNetworkLoadBalancerCommand(command)).toContain(
377
+ 'REGIONAL_EXTERNAL_NETWORK load balancers accept between one and five ports.',
378
+ );
379
+ });
380
+
381
+ it('rejects non-lexical ports, unsupported protocols, and destructive edit changes', () => {
382
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
383
+ {
384
+ account: 'account-a',
385
+ backendService: {
386
+ healthCheck: { healthCheckType: 'TCP', name: 'tcp-check', port: 80 },
387
+ name: 'app-main',
388
+ sessionAffinity: 'NONE',
389
+ },
390
+ ipProtocol: 'TCP',
391
+ loadBalancerName: 'app-main',
392
+ ports: ['80'],
393
+ region: 'europe-west1',
394
+ },
395
+ 'edit',
396
+ );
397
+
398
+ ['0', '65536', '1.5', '1e2', ' 80', '80 ', '', 'abc'].forEach((port) => {
399
+ command.ports = [port];
400
+ expect(validateGceRegionalExternalNetworkLoadBalancerCommand(command)).toContain(
401
+ 'Ports must be between 1 and 65535.',
402
+ );
403
+ });
404
+
405
+ command.ports = ['80'];
406
+ command.listeners[0].protocol = 'HTTP';
407
+ expect(validateGceRegionalExternalNetworkLoadBalancerCommand(command)).toEqual(
408
+ expect.arrayContaining([
409
+ 'Protocol must be TCP or UDP.',
410
+ 'Protocol and ports cannot be changed while editing a REGIONAL_EXTERNAL_NETWORK load balancer.',
411
+ ]),
412
+ );
413
+
414
+ const healthCheck = { ...command.healthChecks[0], port: '1e2' } as any;
415
+ command.healthChecks = [healthCheck];
416
+ command.backendServices[0].healthCheck = healthCheck;
417
+ expect(validateGceRegionalExternalNetworkLoadBalancerCommand(command)).toContain(
418
+ 'Health check port must be between 1 and 65535.',
419
+ );
420
+ });
421
+ });
422
+
423
+ function optionValues(select: HTMLElement): string[] {
424
+ return within(select)
425
+ .getAllByRole('option')
426
+ .map((option) => (option as HTMLOptionElement).value);
427
+ }
428
+
429
+ function controls(container: HTMLElement) {
430
+ const { getAllByLabelText, getByLabelText } = within(container);
431
+ const [name, healthCheckName] = getAllByLabelText('Name');
432
+ return {
433
+ address: getByLabelText('IP address'),
434
+ credentials: getByLabelText('Account'),
435
+ healthCheckName,
436
+ name,
437
+ networkTier: getByLabelText('Network tier'),
438
+ ports: getByLabelText('Ports'),
439
+ protocol: getByLabelText('Protocol'),
440
+ region: getByLabelText('Region'),
441
+ sessionAffinity: getByLabelText('Session affinity'),
442
+ };
443
+ }