@spinnaker/azure 2026.2.2 → 2026.3.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 (215) hide show
  1. package/dist/azure.module.d.ts +3 -1
  2. package/dist/image/image.reader.d.ts +4 -2
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/instance/azureInstanceType.service.d.ts +6 -2
  6. package/dist/instance/details/AzureInstanceDetails.d.ts +67 -0
  7. package/dist/loadBalancer/configure/AzureLoadBalancerChoiceModal.d.ts +10 -5
  8. package/dist/loadBalancer/configure/AzureLoadBalancerModal.d.ts +102 -0
  9. package/dist/loadBalancer/details/azureLoadBalancerDetails.d.ts +26 -0
  10. package/dist/loadBalancer/loadBalancer.transformer.d.ts +45 -2
  11. package/dist/pipeline/stages/AzureAccountRegionClusterSelector.d.ts +32 -0
  12. package/dist/pipeline/stages/bake/azureBakeStage.d.ts +46 -2
  13. package/dist/pipeline/stages/destroyAsg/azureDestroyAsgStage.d.ts +14 -2
  14. package/dist/pipeline/stages/disableAsg/azureDisableAsgStage.d.ts +4 -2
  15. package/dist/pipeline/stages/enableAsg/azureEnableAsgStage.d.ts +4 -2
  16. package/dist/securityGroup/configure/AzureSecurityGroupModal.d.ts +70 -0
  17. package/dist/securityGroup/details/AzureSecurityGroupDetails.d.ts +51 -0
  18. package/dist/securityGroup/securityGroup.reader.d.ts +3 -2
  19. package/dist/securityGroup/securityGroup.transformer.d.ts +3 -2
  20. package/dist/securityGroup/securityGroup.write.service.d.ts +9 -2
  21. package/dist/serverGroup/configure/serverGroupCommandBuilder.service.d.ts +88 -2
  22. package/dist/serverGroup/configure/serverGroupConfiguration.service.d.ts +30 -2
  23. package/dist/serverGroup/configure/wizard/AzureCloneServerGroupModal.d.ts +44 -0
  24. package/dist/serverGroup/configure/wizard/pages/ServerGroupAdvancedSettings.d.ts +8 -0
  25. package/dist/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.d.ts +12 -0
  26. package/dist/serverGroup/configure/wizard/pages/ServerGroupCapacityAndZones.d.ts +11 -0
  27. package/dist/serverGroup/configure/wizard/pages/ServerGroupHealthSettings.d.ts +10 -0
  28. package/dist/serverGroup/configure/wizard/pages/ServerGroupImageSettings.d.ts +12 -0
  29. package/dist/serverGroup/configure/wizard/pages/ServerGroupInstanceType.d.ts +9 -0
  30. package/dist/serverGroup/configure/wizard/pages/ServerGroupLoadBalancers.d.ts +14 -0
  31. package/dist/serverGroup/configure/wizard/pages/ServerGroupNetworkSettings.d.ts +10 -0
  32. package/dist/serverGroup/configure/wizard/pages/ServerGroupSecurityGroups.d.ts +6 -0
  33. package/dist/serverGroup/configure/wizard/pages/ServerGroupTags.d.ts +9 -0
  34. package/dist/serverGroup/configure/wizard/pages/common.d.ts +14 -0
  35. package/dist/serverGroup/configure/wizard/pages/index.d.ts +10 -0
  36. package/dist/serverGroup/details/azureServerGroupDetails.d.ts +13 -0
  37. package/dist/serverGroup/details/rollback/RollbackServerGroupModal.d.ts +6 -4
  38. package/dist/serverGroup/serverGroup.transformer.d.ts +61 -2
  39. package/dist/validation/applicationName.validator.d.ts +6 -2
  40. package/package.json +5 -8
  41. package/src/azure.module.spec.ts +921 -0
  42. package/src/azure.module.ts +56 -68
  43. package/src/image/image.reader.js +4 -15
  44. package/src/image/image.reader.spec.js +4 -7
  45. package/src/instance/azureInstanceType.service.js +8 -16
  46. package/src/instance/azureInstanceType.service.spec.js +5 -13
  47. package/src/instance/details/AzureInstanceDetails.spec.tsx +364 -0
  48. package/src/instance/details/AzureInstanceDetails.tsx +533 -0
  49. package/src/loadBalancer/configure/AzureLoadBalancerChoiceModal.spec.tsx +125 -0
  50. package/src/loadBalancer/configure/AzureLoadBalancerChoiceModal.tsx +46 -26
  51. package/src/loadBalancer/configure/AzureLoadBalancerModal.spec.tsx +658 -0
  52. package/src/loadBalancer/configure/AzureLoadBalancerModal.tsx +960 -0
  53. package/src/loadBalancer/details/azureLoadBalancerDetails.spec.tsx +470 -0
  54. package/src/loadBalancer/details/azureLoadBalancerDetails.tsx +412 -0
  55. package/src/loadBalancer/loadBalancer.transformer.js +83 -93
  56. package/src/pipeline/stages/AzureAccountRegionClusterSelector.tsx +238 -0
  57. package/src/pipeline/stages/bake/azureBakeStage.tsx +723 -0
  58. package/src/pipeline/stages/destroyAsg/azureDestroyAsgStage.tsx +128 -0
  59. package/src/pipeline/stages/disableAsg/azureDisableAsgStage.tsx +92 -0
  60. package/src/pipeline/stages/enableAsg/azureEnableAsgStage.tsx +87 -0
  61. package/src/routerConsumers.spec.tsx +69 -0
  62. package/src/securityGroup/configure/AzureSecurityGroupModal.spec.tsx +558 -0
  63. package/src/securityGroup/configure/AzureSecurityGroupModal.tsx +782 -0
  64. package/src/securityGroup/details/AzureSecurityGroupDetails.spec.tsx +291 -0
  65. package/src/securityGroup/details/AzureSecurityGroupDetails.tsx +354 -0
  66. package/src/securityGroup/securityGroup.reader.js +3 -13
  67. package/src/securityGroup/securityGroup.transformer.js +3 -13
  68. package/src/securityGroup/securityGroup.write.service.spec.ts +105 -0
  69. package/src/securityGroup/securityGroup.write.service.ts +73 -0
  70. package/src/serverGroup/configure/serverGroupCommandBuilder.service.js +167 -166
  71. package/src/serverGroup/configure/serverGroupCommandBuilder.service.spec.js +63 -0
  72. package/src/serverGroup/configure/serverGroupConfiguration.service.js +325 -309
  73. package/src/serverGroup/configure/serverGroupConfiguration.service.spec.js +57 -53
  74. package/src/serverGroup/configure/wizard/AzureCloneServerGroupModal.spec.tsx +758 -0
  75. package/src/serverGroup/configure/wizard/AzureCloneServerGroupModal.tsx +349 -0
  76. package/src/serverGroup/configure/wizard/pages/ServerGroupAdvancedSettings.tsx +208 -0
  77. package/src/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.tsx +122 -0
  78. package/src/serverGroup/configure/wizard/pages/ServerGroupCapacityAndZones.tsx +80 -0
  79. package/src/serverGroup/configure/wizard/pages/ServerGroupHealthSettings.tsx +97 -0
  80. package/src/serverGroup/configure/wizard/pages/ServerGroupImageSettings.tsx +151 -0
  81. package/src/serverGroup/configure/wizard/pages/ServerGroupInstanceType.tsx +41 -0
  82. package/src/serverGroup/configure/wizard/pages/ServerGroupLoadBalancers.tsx +214 -0
  83. package/src/serverGroup/configure/wizard/pages/ServerGroupNetworkSettings.tsx +119 -0
  84. package/src/serverGroup/configure/wizard/pages/ServerGroupSecurityGroups.tsx +54 -0
  85. package/src/serverGroup/configure/wizard/pages/ServerGroupTags.tsx +40 -0
  86. package/src/serverGroup/configure/wizard/pages/common.tsx +72 -0
  87. package/src/serverGroup/configure/wizard/pages/index.ts +10 -0
  88. package/src/serverGroup/details/azureServerGroupDetails.spec.tsx +263 -0
  89. package/src/serverGroup/details/azureServerGroupDetails.tsx +318 -0
  90. package/src/serverGroup/details/rollback/RollbackServerGroupModal.tsx +24 -30
  91. package/src/serverGroup/serverGroup.transformer.js +11 -19
  92. package/src/serverGroup/serverGroup.transformer.spec.js +2 -6
  93. package/src/validation/applicationName.validator.js +14 -29
  94. package/dist/instance/details/instance.details.controller.d.ts +0 -2
  95. package/dist/loadBalancer/configure/createLoadBalancer.controller.d.ts +0 -2
  96. package/dist/loadBalancer/details/loadBalancerDetail.controller.d.ts +0 -2
  97. package/dist/pipeline/stages/bake/bakeExecutionDetails.controller.d.ts +0 -2
  98. package/dist/securityGroup/clone/cloneSecurityGroup.controller.d.ts +0 -2
  99. package/dist/securityGroup/configure/CreateSecurityGroupCtrl.d.ts +0 -2
  100. package/dist/securityGroup/configure/EditSecurityGroupCtrl.d.ts +0 -2
  101. package/dist/securityGroup/details/securityGroupDetail.controller.d.ts +0 -2
  102. package/dist/serverGroup/configure/serverGroup.configure.azure.module.d.ts +0 -2
  103. package/dist/serverGroup/configure/wizard/CloneServerGroup.azure.controller.d.ts +0 -2
  104. package/dist/serverGroup/configure/wizard/ServerGroupInstanceArchetype.controller.d.ts +0 -2
  105. package/dist/serverGroup/configure/wizard/ServerGroupInstanceType.controller.d.ts +0 -2
  106. package/dist/serverGroup/configure/wizard/advancedSettings/ServerGroupAdvancedSettings.controller.d.ts +0 -2
  107. package/dist/serverGroup/configure/wizard/advancedSettings/advancedSettingsSelector.directive.d.ts +0 -2
  108. package/dist/serverGroup/configure/wizard/basicSettings/ServerGroupBasicSettings.controller.d.ts +0 -2
  109. package/dist/serverGroup/configure/wizard/basicSettings/image.regional.filter.d.ts +0 -2
  110. package/dist/serverGroup/configure/wizard/capacity/capacitySelector.directive.d.ts +0 -2
  111. package/dist/serverGroup/configure/wizard/healthSettings/ServerGroupHealthSettings.controller.d.ts +0 -2
  112. package/dist/serverGroup/configure/wizard/healthSettings/healthSettingsSelector.directive.d.ts +0 -2
  113. package/dist/serverGroup/configure/wizard/image/ServerGroupImageSettings.controller.d.ts +0 -2
  114. package/dist/serverGroup/configure/wizard/image/imageSettingsSelector.directive.d.ts +0 -2
  115. package/dist/serverGroup/configure/wizard/loadBalancers/ServerGroupLoadBalancers.controller.d.ts +0 -2
  116. package/dist/serverGroup/configure/wizard/loadBalancers/serverGroupLoadBalancersSelector.directive.d.ts +0 -2
  117. package/dist/serverGroup/configure/wizard/networkSettings/ServerGroupNetworkSettings.controller.d.ts +0 -2
  118. package/dist/serverGroup/configure/wizard/networkSettings/ServerGroupNetworkSettingsSelector.directive.d.ts +0 -2
  119. package/dist/serverGroup/configure/wizard/securityGroup/ServerGroupSecurityGroups.controller.d.ts +0 -2
  120. package/dist/serverGroup/configure/wizard/securityGroup/serverGroupSecurityGroupsSelector.directive.d.ts +0 -2
  121. package/dist/serverGroup/configure/wizard/tags/tagsSelector.directive.d.ts +0 -2
  122. package/dist/serverGroup/configure/wizard/zones/zoneSelector.directive.d.ts +0 -2
  123. package/dist/serverGroup/details/serverGroup.details.module.d.ts +0 -2
  124. package/dist/serverGroup/details/serverGroupDetails.azure.controller.d.ts +0 -2
  125. package/src/instance/details/instance.details.controller.js +0 -371
  126. package/src/instance/details/instance.details.controller.spec.js +0 -129
  127. package/src/instance/details/instanceDetails.html +0 -43
  128. package/src/loadBalancer/configure/advancedSettings.html +0 -95
  129. package/src/loadBalancer/configure/createLoadBalancer.controller.js +0 -275
  130. package/src/loadBalancer/configure/createLoadBalancer.controller.spec.js +0 -39
  131. package/src/loadBalancer/configure/createLoadBalancer.html +0 -25
  132. package/src/loadBalancer/configure/createLoadBalancerProperties.html +0 -128
  133. package/src/loadBalancer/configure/editLoadBalancer.html +0 -26
  134. package/src/loadBalancer/configure/healthCheck.html +0 -74
  135. package/src/loadBalancer/configure/listeners.html +0 -50
  136. package/src/loadBalancer/details/loadBalancerDetail.controller.js +0 -181
  137. package/src/loadBalancer/details/loadBalancerDetail.controller.spec.js +0 -39
  138. package/src/loadBalancer/details/loadBalancerDetail.html +0 -128
  139. package/src/pipeline/stages/bake/azureBakeStage.js +0 -276
  140. package/src/pipeline/stages/bake/bakeExecutionDetails.controller.js +0 -40
  141. package/src/pipeline/stages/bake/bakeExecutionDetails.html +0 -50
  142. package/src/pipeline/stages/bake/bakeStage.html +0 -187
  143. package/src/pipeline/stages/destroyAsg/azureDestroyAsgStage.js +0 -73
  144. package/src/pipeline/stages/destroyAsg/destroyAsgStage.html +0 -9
  145. package/src/pipeline/stages/destroyAsg/destroyAsgStepLabel.html +0 -1
  146. package/src/pipeline/stages/disableAsg/azureDisableAsgStage.js +0 -65
  147. package/src/pipeline/stages/disableAsg/disableAsgStage.html +0 -11
  148. package/src/pipeline/stages/disableAsg/disableAsgStepLabel.html +0 -1
  149. package/src/pipeline/stages/enableAsg/azureEnableAsgStage.js +0 -70
  150. package/src/pipeline/stages/enableAsg/enableAsgStage.html +0 -11
  151. package/src/pipeline/stages/enableAsg/enableAsgStepLabel.html +0 -1
  152. package/src/securityGroup/clone/cloneSecurityGroup.controller.js +0 -162
  153. package/src/securityGroup/clone/cloneSecurityGroup.html +0 -8
  154. package/src/securityGroup/configure/CreateSecurityGroup.controller.spec.js +0 -78
  155. package/src/securityGroup/configure/CreateSecurityGroupCtrl.js +0 -265
  156. package/src/securityGroup/configure/EditSecurityGroupCtrl.js +0 -222
  157. package/src/securityGroup/configure/configSecurityGroup.mixin.controller.js +0 -262
  158. package/src/securityGroup/configure/createSecurityGroup.html +0 -20
  159. package/src/securityGroup/configure/createSecurityGroupIngress.html +0 -75
  160. package/src/securityGroup/configure/createSecurityGroupProperties.html +0 -88
  161. package/src/securityGroup/configure/editSecurityGroup.html +0 -19
  162. package/src/securityGroup/details/securityGroupDetail.controller.js +0 -142
  163. package/src/securityGroup/details/securityGroupDetail.html +0 -78
  164. package/src/securityGroup/securityGroup.write.service.js +0 -57
  165. package/src/serverGroup/configure/serverGroup.configure.azure.module.js +0 -47
  166. package/src/serverGroup/configure/wizard/CloneServerGroup.azure.controller.js +0 -247
  167. package/src/serverGroup/configure/wizard/ServerGroupInstanceArchetype.controller.js +0 -35
  168. package/src/serverGroup/configure/wizard/ServerGroupInstanceType.controller.js +0 -15
  169. package/src/serverGroup/configure/wizard/advancedSettings/ServerGroupAdvancedSettings.controller.js +0 -25
  170. package/src/serverGroup/configure/wizard/advancedSettings/advancedSettings.html +0 -5
  171. package/src/serverGroup/configure/wizard/advancedSettings/advancedSettingsSelector.directive.html +0 -138
  172. package/src/serverGroup/configure/wizard/advancedSettings/advancedSettingsSelector.directive.js +0 -43
  173. package/src/serverGroup/configure/wizard/basicSettings/ServerGroupBasicSettings.controller.js +0 -71
  174. package/src/serverGroup/configure/wizard/basicSettings/basicSettings.html +0 -131
  175. package/src/serverGroup/configure/wizard/basicSettings/image.regional.filter.js +0 -15
  176. package/src/serverGroup/configure/wizard/basicSettings/image.regional.filter.spec.js +0 -42
  177. package/src/serverGroup/configure/wizard/capacity/capacity.html +0 -5
  178. package/src/serverGroup/configure/wizard/capacity/capacitySelector.directive.html +0 -20
  179. package/src/serverGroup/configure/wizard/capacity/capacitySelector.directive.js +0 -21
  180. package/src/serverGroup/configure/wizard/capacity/zones.html +0 -5
  181. package/src/serverGroup/configure/wizard/healthSettings/ServerGroupHealthSettings.controller.js +0 -38
  182. package/src/serverGroup/configure/wizard/healthSettings/healthSettings.html +0 -5
  183. package/src/serverGroup/configure/wizard/healthSettings/healthSettingsSelector.directive.html +0 -32
  184. package/src/serverGroup/configure/wizard/healthSettings/healthSettingsSelector.directive.js +0 -22
  185. package/src/serverGroup/configure/wizard/image/ServerGroupImageSettings.controller.js +0 -34
  186. package/src/serverGroup/configure/wizard/image/imageSettings.html +0 -5
  187. package/src/serverGroup/configure/wizard/image/imageSettingsSelector.directive.html +0 -33
  188. package/src/serverGroup/configure/wizard/image/imageSettingsSelector.directive.js +0 -22
  189. package/src/serverGroup/configure/wizard/instanceArchetype.html +0 -33
  190. package/src/serverGroup/configure/wizard/instanceType/instanceType.html +0 -10
  191. package/src/serverGroup/configure/wizard/instanceType.html +0 -33
  192. package/src/serverGroup/configure/wizard/loadBalancers/ServerGroupLoadBalancers.controller.js +0 -137
  193. package/src/serverGroup/configure/wizard/loadBalancers/loadBalancers.html +0 -3
  194. package/src/serverGroup/configure/wizard/loadBalancers/serverGroupLoadBalancersSelector.directive.html +0 -41
  195. package/src/serverGroup/configure/wizard/loadBalancers/serverGroupLoadBalancersSelector.directive.js +0 -22
  196. package/src/serverGroup/configure/wizard/networkSettings/ServerGroupNetworkSettings.controller.js +0 -44
  197. package/src/serverGroup/configure/wizard/networkSettings/ServerGroupNetworkSettingsSelector.directive.html +0 -54
  198. package/src/serverGroup/configure/wizard/networkSettings/ServerGroupNetworkSettingsSelector.directive.js +0 -19
  199. package/src/serverGroup/configure/wizard/networkSettings/networkSettings.html +0 -3
  200. package/src/serverGroup/configure/wizard/securityGroup/ServerGroupSecurityGroups.controller.js +0 -28
  201. package/src/serverGroup/configure/wizard/securityGroup/securityGroups.html +0 -3
  202. package/src/serverGroup/configure/wizard/securityGroup/serverGroupSecurityGroupsSelector.directive.html +0 -40
  203. package/src/serverGroup/configure/wizard/securityGroup/serverGroupSecurityGroupsSelector.directive.js +0 -38
  204. package/src/serverGroup/configure/wizard/serverGroupWizard.html +0 -61
  205. package/src/serverGroup/configure/wizard/tags/tags.html +0 -5
  206. package/src/serverGroup/configure/wizard/tags/tagsSelector.directive.html +0 -16
  207. package/src/serverGroup/configure/wizard/tags/tagsSelector.directive.js +0 -30
  208. package/src/serverGroup/configure/wizard/templateSelection.html +0 -9
  209. package/src/serverGroup/configure/wizard/zones/zoneSelector.directive.html +0 -21
  210. package/src/serverGroup/configure/wizard/zones/zoneSelector.directive.js +0 -28
  211. package/src/serverGroup/details/serverGroup.details.module.js +0 -11
  212. package/src/serverGroup/details/serverGroupDetails.azure.controller.js +0 -260
  213. package/src/serverGroup/details/serverGroupDetails.html +0 -98
  214. package/src/subnet/subnetSelectField.directive.html +0 -28
  215. package/src/subnet/subnetSelectField.directive.js +0 -35
@@ -0,0 +1,658 @@
1
+ import { LoadBalancerWriter, nativePromiseService } from '@spinnaker/core';
2
+
3
+ import { AzureLoadBalancerTypes } from '../../utility';
4
+ import { AzureLoadBalancerTransformer } from '../loadBalancer.transformer';
5
+ import {
6
+ applyAzureLoadBalancerTypeDefaults,
7
+ AzureLoadBalancerModalComponent as AzureLoadBalancerModal,
8
+ findAzureVnetBySelectValue,
9
+ formatInputValue,
10
+ getAzureLoadBalancerTypeChoice,
11
+ getAzureVnetSelectLabel,
12
+ getAzureVnetSelectValue,
13
+ listenerProtocolOptions,
14
+ liftAzureLoadBalancerSessionPersistence,
15
+ normalizeAzureLoadBalancerForSubmit,
16
+ parseOptionalNumber,
17
+ probeProtocolOptions,
18
+ shouldRenderCreateFields,
19
+ shouldRenderNetworkFields,
20
+ shouldRenderSkuField,
21
+ validateAzureLoadBalancerForSubmit,
22
+ validSkus,
23
+ } from './AzureLoadBalancerModal';
24
+
25
+ describe('AzureLoadBalancerModal', () => {
26
+ function buildModal(props: any, state: any = {}): any {
27
+ const modal = Object.create(AzureLoadBalancerModal.prototype);
28
+ modal.props = props;
29
+ modal.state = { existingLoadBalancerNames: [], loadBalancer: null, ...state };
30
+ modal.transformer = new AzureLoadBalancerTransformer(nativePromiseService);
31
+ return modal;
32
+ }
33
+
34
+ it('owns its refresh subscription and ignores a late refresh after unmount', () => {
35
+ const firstUnsubscribe = jasmine.createSpy('firstUnsubscribe');
36
+ const secondUnsubscribe = jasmine.createSpy('secondUnsubscribe');
37
+ const callbacks: Array<() => void> = [];
38
+ const loadBalancers = {
39
+ onNextRefresh: jasmine.createSpy('onNextRefresh').and.callFake((callback: () => void) => {
40
+ callbacks.push(callback);
41
+ return callbacks.length === 1 ? firstUnsubscribe : secondUnsubscribe;
42
+ }),
43
+ refresh: jasmine.createSpy('refresh'),
44
+ };
45
+ const closeModal = jasmine.createSpy('closeModal');
46
+ const dismissModal = jasmine.createSpy('dismissModal');
47
+ const stateService = {
48
+ go: jasmine.createSpy('go'),
49
+ includes: jasmine.createSpy('includes'),
50
+ };
51
+ const application = {
52
+ defaultCredentials: { azure: 'test' },
53
+ defaultRegions: { azure: 'westus' },
54
+ getDataSource: () => null,
55
+ loadBalancers,
56
+ name: 'fnord',
57
+ };
58
+ const modal = new AzureLoadBalancerModal({
59
+ app: application,
60
+ application,
61
+ closeModal,
62
+ dismissModal,
63
+ forPipelineConfig: false,
64
+ isNew: false,
65
+ loadBalancer: null,
66
+ loadBalancerType: AzureLoadBalancerTypes[0],
67
+ stateService,
68
+ } as any);
69
+ modal.componentDidMount();
70
+ const setState = spyOn(modal, 'setState');
71
+ const stateBeforeRefresh = modal.state;
72
+
73
+ expect((modal as any).mounted).toBe(true);
74
+
75
+ (modal as any).onTaskComplete();
76
+
77
+ expect(loadBalancers.refresh).toHaveBeenCalledTimes(1);
78
+ expect(loadBalancers.onNextRefresh).toHaveBeenCalledOnceWith((modal as any).onApplicationRefresh);
79
+ expect(loadBalancers.onNextRefresh.calls.first().invocationOrder).toBeLessThan(
80
+ loadBalancers.refresh.calls.first().invocationOrder,
81
+ );
82
+
83
+ (modal as any).onTaskComplete();
84
+ expect(firstUnsubscribe).toHaveBeenCalledTimes(1);
85
+
86
+ modal.componentWillUnmount();
87
+ callbacks[1]();
88
+
89
+ expect(secondUnsubscribe).toHaveBeenCalledTimes(1);
90
+ expect((modal as any).applicationRefreshUnsubscribe).toBeUndefined();
91
+ expect(closeModal).not.toHaveBeenCalled();
92
+ expect(dismissModal).not.toHaveBeenCalled();
93
+ expect(stateService.go).not.toHaveBeenCalled();
94
+ expect(setState).not.toHaveBeenCalled();
95
+ expect(modal.state).toBe(stateBeforeRefresh);
96
+ });
97
+
98
+ describe('normalizeAzureLoadBalancerForSubmit', () => {
99
+ it('does not copy selected vnet and subnet for Azure Load Balancer', () => {
100
+ const loadBalancer = normalizeAzureLoadBalancerForSubmit(
101
+ {
102
+ name: 'fnord-frontend',
103
+ clusterName: 'fnord-frontend',
104
+ probes: [{ probeProtocol: 'TCP', probePath: '/health' }],
105
+ loadBalancingRules: [{ protocol: 'TCP' }],
106
+ selectedVnet: { name: 'vnet-a', resourceGroup: 'rg-a' },
107
+ selectedSubnet: { name: 'subnet-a' },
108
+ securityGroups: ['sg-1'],
109
+ } as any,
110
+ 'Azure Load Balancer',
111
+ );
112
+
113
+ expect(loadBalancer.vnet).toBeUndefined();
114
+ expect(loadBalancer.vnetResourceGroup).toBeUndefined();
115
+ expect(loadBalancer.subnet).toBeUndefined();
116
+ expect(loadBalancer.securityGroups).toBeNull();
117
+ });
118
+
119
+ it('preserves existing Azure Load Balancer vnet and subnet fields', () => {
120
+ const loadBalancer = normalizeAzureLoadBalancerForSubmit(
121
+ {
122
+ name: 'fnord-frontend',
123
+ probes: [{ probeProtocol: 'TCP' }],
124
+ loadBalancingRules: [{ protocol: 'TCP' }],
125
+ selectedVnet: { name: 'selected-vnet', resourceGroup: 'selected-rg' },
126
+ selectedSubnet: { name: 'selected-subnet' },
127
+ vnet: 'existing-vnet',
128
+ vnetResourceGroup: 'existing-rg',
129
+ subnet: 'existing-subnet',
130
+ } as any,
131
+ 'Azure Load Balancer',
132
+ );
133
+
134
+ expect(loadBalancer.vnet).toBe('existing-vnet');
135
+ expect(loadBalancer.vnetResourceGroup).toBe('existing-rg');
136
+ expect(loadBalancer.subnet).toBe('existing-subnet');
137
+ });
138
+
139
+ it('copies selected vnet and subnet into the Application Gateway submit payload', () => {
140
+ const loadBalancer = normalizeAzureLoadBalancerForSubmit(
141
+ {
142
+ name: 'fnord-frontend',
143
+ clusterName: 'fnord-frontend',
144
+ probes: [{ probeProtocol: 'HTTP', probePath: '/' }],
145
+ loadBalancingRules: [{ protocol: 'HTTP' }],
146
+ selectedVnet: { name: 'vnet-a', resourceGroup: 'rg-a' },
147
+ selectedSubnet: { name: 'subnet-a' },
148
+ securityGroups: ['sg-1'],
149
+ } as any,
150
+ 'Azure Application Gateway',
151
+ );
152
+
153
+ expect(loadBalancer.vnet).toBe('vnet-a');
154
+ expect(loadBalancer.vnetResourceGroup).toBe('rg-a');
155
+ expect(loadBalancer.subnet).toBe('subnet-a');
156
+ expect(loadBalancer.type).toBe('upsertLoadBalancer');
157
+ expect(loadBalancer.loadBalancerType).toBe('Azure Application Gateway');
158
+ expect(loadBalancer.probes[0].probeName).toBe('fnord-frontend-probe');
159
+ expect(loadBalancer.loadBalancingRules[0].ruleName).toBe('fnord-frontend-rule0');
160
+ expect(loadBalancer.loadBalancingRules[0].probeName).toBe('fnord-frontend-probe');
161
+ expect(loadBalancer.securityGroups).toEqual(['sg-1']);
162
+ });
163
+
164
+ it('clears tcp probe paths and nulls security groups without vnet or subnet type', () => {
165
+ const loadBalancer = normalizeAzureLoadBalancerForSubmit(
166
+ {
167
+ name: 'fnord-frontend',
168
+ probes: [{ probeProtocol: 'TCP', probePath: '/health' }],
169
+ loadBalancingRules: [{ protocol: 'TCP' }, { protocol: 'TCP' }],
170
+ securityGroups: ['sg-1'],
171
+ } as any,
172
+ 'Azure Load Balancer',
173
+ );
174
+
175
+ expect(loadBalancer.securityGroups).toBeNull();
176
+ expect(loadBalancer.probes[0].probePath).toBeUndefined();
177
+ expect(loadBalancer.probes[0].probeName).toBe('fnord-frontend-probe');
178
+ expect(loadBalancer.loadBalancingRules[0].ruleName).toBe('fnord-frontend-rule0');
179
+ expect(loadBalancer.loadBalancingRules[1].ruleName).toBe('fnord-frontend-rule1');
180
+ });
181
+
182
+ it('keeps empty numeric listener fields empty so validation can reject them', () => {
183
+ const loadBalancer = normalizeAzureLoadBalancerForSubmit(
184
+ {
185
+ name: 'fnord-frontend',
186
+ credentials: 'test',
187
+ region: 'westus',
188
+ probes: [{ probeProtocol: 'TCP', probePort: 80 }],
189
+ loadBalancingRules: [{ protocol: 'TCP', externalPort: '', backendPort: '' }],
190
+ } as any,
191
+ 'Azure Load Balancer',
192
+ );
193
+
194
+ expect(loadBalancer.loadBalancingRules[0].externalPort).toBe('');
195
+ expect(loadBalancer.loadBalancingRules[0].backendPort).toBe('');
196
+ expect(validateAzureLoadBalancerForSubmit(loadBalancer, 'Azure Load Balancer', { isNew: true })).toContain(
197
+ 'Listener 1 external port is required.',
198
+ );
199
+ expect(validateAzureLoadBalancerForSubmit(loadBalancer, 'Azure Load Balancer', { isNew: true })).toContain(
200
+ 'Listener 1 backend port is required.',
201
+ );
202
+ });
203
+
204
+ it('strips stale rule-level persistence and keeps top-level session persistence', () => {
205
+ const loadBalancer = normalizeAzureLoadBalancerForSubmit(
206
+ {
207
+ name: 'fnord-frontend',
208
+ sessionPersistence: 'Client IP',
209
+ probes: [{ probeProtocol: 'TCP', probePort: 80 }],
210
+ loadBalancingRules: [
211
+ { protocol: 'TCP', externalPort: 80, backendPort: 80, persistence: 'None', sessionPersistence: 'None' },
212
+ ],
213
+ } as any,
214
+ 'Azure Load Balancer',
215
+ );
216
+
217
+ expect(loadBalancer.sessionPersistence).toBe('Client IP');
218
+ expect(loadBalancer.loadBalancingRules[0].persistence).toBeUndefined();
219
+ expect(loadBalancer.loadBalancingRules[0].sessionPersistence).toBeUndefined();
220
+ });
221
+ });
222
+
223
+ describe('load balancer type behavior', () => {
224
+ it('infers the load balancer type from an existing command when the type prop is missing', () => {
225
+ expect(getAzureLoadBalancerTypeChoice({ loadBalancerType: 'Azure Application Gateway' }).type).toBe(
226
+ 'Azure Application Gateway',
227
+ );
228
+ expect(getAzureLoadBalancerTypeChoice({ loadBalancerType: 'APPLICATION_GATEWAY' }).type).toBe(
229
+ 'Azure Application Gateway',
230
+ );
231
+ });
232
+
233
+ it('uses Azure Load Balancer protocol options and defaults', () => {
234
+ const command = applyAzureLoadBalancerTypeDefaults(
235
+ { probes: [{}], loadBalancingRules: [{}] },
236
+ 'Azure Load Balancer',
237
+ );
238
+
239
+ expect(listenerProtocolOptions('Azure Load Balancer')).toEqual(['TCP', 'UDP']);
240
+ expect(probeProtocolOptions('Azure Load Balancer')).toEqual(['TCP', 'HTTP']);
241
+ expect(command.loadBalancingRules[0].protocol).toBe('TCP');
242
+ expect(command.probes[0].probeProtocol).toBe('TCP');
243
+ expect(command.sessionPersistence).toBe('None');
244
+ expect(command.loadBalancingRules[0].sessionPersistence).toBeUndefined();
245
+ });
246
+
247
+ it('preserves top-level Azure Load Balancer session persistence in defaults and submit payload', () => {
248
+ const command = applyAzureLoadBalancerTypeDefaults(
249
+ {
250
+ sessionPersistence: 'Client IP',
251
+ probes: [{ probeProtocol: 'TCP' }],
252
+ loadBalancingRules: [{ protocol: 'TCP' }],
253
+ },
254
+ 'Azure Load Balancer',
255
+ );
256
+ const payload = normalizeAzureLoadBalancerForSubmit(command, 'Azure Load Balancer');
257
+
258
+ expect(command.sessionPersistence).toBe('Client IP');
259
+ expect(command.loadBalancingRules[0].sessionPersistence).toBeUndefined();
260
+ expect(payload.sessionPersistence).toBe('Client IP');
261
+ });
262
+
263
+ it('lifts edit-mode Azure Load Balancer persistence to top-level session persistence', () => {
264
+ const command = liftAzureLoadBalancerSessionPersistence(
265
+ {
266
+ elb: { loadBalancingRules: [{ persistence: 'Client IP and protocol' }] },
267
+ loadBalancingRules: [{ protocol: 'TCP' }],
268
+ },
269
+ 'Azure Load Balancer',
270
+ );
271
+
272
+ expect(command.sessionPersistence).toBe('Client IP and protocol');
273
+ });
274
+
275
+ it('prefers edit-mode top-level elb session persistence over rule persistence', () => {
276
+ const command = liftAzureLoadBalancerSessionPersistence(
277
+ {
278
+ elb: { sessionPersistence: 'Client IP', loadBalancingRules: [{ persistence: 'Client IP and protocol' }] },
279
+ loadBalancingRules: [{ protocol: 'TCP' }],
280
+ },
281
+ 'Azure Load Balancer',
282
+ );
283
+
284
+ expect(command.sessionPersistence).toBe('Client IP');
285
+ });
286
+
287
+ it('initializes edit-mode top-level elb session persistence through the modal path', () => {
288
+ const modal = buildModal({
289
+ app: { name: 'fnord' },
290
+ application: { name: 'fnord' },
291
+ isNew: false,
292
+ loadBalancerType: AzureLoadBalancerTypes[0],
293
+ loadBalancer: {
294
+ account: 'test',
295
+ detail: 'frontend',
296
+ elb: {
297
+ loadBalancingRules: [{ persistence: 'Client IP and protocol', protocol: 'TCP' }],
298
+ probes: [{ probePort: 80, probeProtocol: 'TCP' }],
299
+ sessionPersistence: 'Client IP',
300
+ },
301
+ name: 'fnord-frontend',
302
+ region: 'westus',
303
+ stack: 'frontend',
304
+ },
305
+ });
306
+
307
+ expect(modal.initializeLoadBalancer().sessionPersistence).toBe('Client IP');
308
+ });
309
+
310
+ it('initializes saved pipeline commands without converting from live load balancer shape', () => {
311
+ const pipelineCommand = {
312
+ credentials: 'test',
313
+ detail: 'frontend',
314
+ loadBalancerType: 'Azure Application Gateway',
315
+ loadBalancingRules: [{ backendPort: 80, externalPort: 80, protocol: 'HTTP' }],
316
+ name: 'fnord-frontend',
317
+ probes: [{ probePath: '/', probePort: 80, probeProtocol: 'HTTP' }],
318
+ region: 'westus',
319
+ stack: 'frontend',
320
+ subnet: 'subnet-a',
321
+ type: 'upsertLoadBalancer',
322
+ vnet: 'vnet-a',
323
+ vnetResourceGroup: 'rg-a',
324
+ };
325
+ const modal = buildModal({
326
+ app: { name: 'fnord' },
327
+ application: { name: 'fnord' },
328
+ forPipelineConfig: true,
329
+ isNew: false,
330
+ loadBalancer: pipelineCommand,
331
+ });
332
+
333
+ const command = modal.initializeLoadBalancer();
334
+
335
+ expect(command.credentials).toBe('test');
336
+ expect(command.region).toBe('westus');
337
+ expect(command.probes[0].probePort).toBe(80);
338
+ expect(command.loadBalancingRules[0].backendPort).toBe(80);
339
+ expect(command.vnet).toBe('vnet-a');
340
+ expect(command.vnetResourceGroup).toBe('rg-a');
341
+ expect(command.subnet).toBe('subnet-a');
342
+ });
343
+
344
+ it('uses Application Gateway protocol options and defaults', () => {
345
+ const command = applyAzureLoadBalancerTypeDefaults(
346
+ { probes: [{}], loadBalancingRules: [{}] },
347
+ 'Azure Application Gateway',
348
+ );
349
+
350
+ expect(listenerProtocolOptions('Azure Application Gateway')).toEqual(['HTTP']);
351
+ expect(probeProtocolOptions('Azure Application Gateway')).toEqual(['HTTP']);
352
+ expect(command.loadBalancingRules[0].protocol).toBe('HTTP');
353
+ expect(command.probes[0].probeProtocol).toBe('HTTP');
354
+ });
355
+
356
+ it('preserves advanced settings when applying protocol defaults', () => {
357
+ const command = applyAzureLoadBalancerTypeDefaults(
358
+ {
359
+ probes: [{ probeProtocol: 'TCP', probeInterval: 15, unhealthyThreshold: 3, timeout: 60 }],
360
+ loadBalancingRules: [{ protocol: 'TCP', idleTimeout: 12, sessionPersistence: 'Client IP' }],
361
+ },
362
+ 'Azure Load Balancer',
363
+ );
364
+
365
+ expect(command.probes[0].probeInterval).toBe(15);
366
+ expect(command.probes[0].unhealthyThreshold).toBe(3);
367
+ expect(command.probes[0].timeout).toBe(60);
368
+ expect(command.loadBalancingRules[0].idleTimeout).toBe(12);
369
+ expect(command.sessionPersistence).toBe('None');
370
+ });
371
+ });
372
+
373
+ describe('validation', () => {
374
+ it('rejects missing stack and detail in create mode', () => {
375
+ const errors = validateAzureLoadBalancerForSubmit(
376
+ {
377
+ name: 'fnord',
378
+ credentials: 'test',
379
+ region: 'westus',
380
+ stack: '',
381
+ detail: '',
382
+ probes: [{ probeProtocol: 'TCP', probePort: 80 }],
383
+ loadBalancingRules: [{ protocol: 'TCP', externalPort: 80, backendPort: 80 }],
384
+ },
385
+ 'Azure Load Balancer',
386
+ { isNew: true },
387
+ );
388
+
389
+ expect(errors).toContain('Stack is required.');
390
+ expect(errors).toContain('Detail is required.');
391
+ });
392
+
393
+ it('matches legacy stack and detail patterns', () => {
394
+ const errors = validateAzureLoadBalancerForSubmit(
395
+ {
396
+ name: 'fnord-front-end',
397
+ credentials: 'test',
398
+ region: 'westus',
399
+ stack: 'front-end',
400
+ detail: 'front_end',
401
+ probes: [{ probeProtocol: 'TCP', probePort: 80 }],
402
+ loadBalancingRules: [{ protocol: 'TCP', externalPort: 80, backendPort: 80 }],
403
+ },
404
+ 'Azure Load Balancer',
405
+ { isNew: true },
406
+ );
407
+
408
+ expect(errors).toContain('Stack may only contain letters and numbers.');
409
+ expect(errors).toContain('Detail may only contain letters, numbers, and hyphens.');
410
+ });
411
+
412
+ it('rejects duplicate names and missing Application Gateway network fields', () => {
413
+ const errors = validateAzureLoadBalancerForSubmit(
414
+ {
415
+ name: 'fnord-frontend',
416
+ credentials: 'test',
417
+ region: 'westus',
418
+ stack: 'frontend',
419
+ detail: '',
420
+ probes: [{ probeProtocol: 'HTTP', probePort: 80, probePath: '/' }],
421
+ loadBalancingRules: [{ protocol: 'HTTP', externalPort: 80, backendPort: 80 }],
422
+ },
423
+ 'Azure Application Gateway',
424
+ { isNew: true, existingLoadBalancerNames: ['fnord-frontend'] },
425
+ );
426
+
427
+ expect(errors).toContain('A load balancer named fnord-frontend already exists in this account and region.');
428
+ expect(errors).toContain('VNet is required.');
429
+ expect(errors).toContain('Subnet is required.');
430
+ });
431
+
432
+ it('rejects duplicate names case-insensitively', () => {
433
+ const errors = validateAzureLoadBalancerForSubmit(
434
+ {
435
+ name: 'fnord-frontend',
436
+ credentials: 'test',
437
+ region: 'westus',
438
+ stack: 'frontend',
439
+ detail: 'blue',
440
+ probes: [{ probeProtocol: 'TCP', probePort: 80 }],
441
+ loadBalancingRules: [{ protocol: 'TCP', externalPort: 80, backendPort: 80 }],
442
+ },
443
+ 'Azure Load Balancer',
444
+ { isNew: true, existingLoadBalancerNames: ['FNORD-FRONTEND'] },
445
+ );
446
+
447
+ expect(errors).toContain('A load balancer named fnord-frontend already exists in this account and region.');
448
+ });
449
+
450
+ it('rejects overlong names and HTTP probes without paths', () => {
451
+ const errors = validateAzureLoadBalancerForSubmit(
452
+ {
453
+ name: 'a'.repeat(33),
454
+ credentials: 'test',
455
+ region: 'westus',
456
+ probes: [{ probeProtocol: 'HTTP', probePort: 80 }],
457
+ loadBalancingRules: [{ protocol: 'TCP', externalPort: 80, backendPort: 80 }],
458
+ },
459
+ 'Azure Load Balancer',
460
+ { isNew: false },
461
+ );
462
+
463
+ expect(errors).toContain('Name must be 32 characters or fewer.');
464
+ expect(errors).toContain('Health check path is required.');
465
+ });
466
+
467
+ it('rejects negative numeric values', () => {
468
+ const errors = validateAzureLoadBalancerForSubmit(
469
+ {
470
+ name: 'fnord-frontend',
471
+ credentials: 'test',
472
+ region: 'westus',
473
+ probes: [{ probeProtocol: 'TCP', probePort: -1, probeInterval: -1, unhealthyThreshold: -1 }],
474
+ loadBalancingRules: [{ protocol: 'TCP', externalPort: -1, backendPort: -1, idleTimeout: -1 }],
475
+ },
476
+ 'Azure Load Balancer',
477
+ { isNew: false },
478
+ );
479
+ const applicationGatewayErrors = validateAzureLoadBalancerForSubmit(
480
+ {
481
+ name: 'fnord-frontend',
482
+ credentials: 'test',
483
+ region: 'westus',
484
+ probes: [{ probeProtocol: 'HTTP', probePort: 80, probePath: '/', timeout: -1 }],
485
+ loadBalancingRules: [{ protocol: 'HTTP', externalPort: 80, backendPort: 80 }],
486
+ },
487
+ 'Azure Application Gateway',
488
+ { isNew: false },
489
+ );
490
+
491
+ expect(errors).toContain('Listener 1 external port must be 0 or greater.');
492
+ expect(errors).toContain('Listener 1 backend port must be 0 or greater.');
493
+ expect(errors).toContain('Listener 1 idle timeout must be 0 or greater.');
494
+ expect(errors).toContain('Health check port must be 0 or greater.');
495
+ expect(errors).toContain('Health check interval must be 0 or greater.');
496
+ expect(errors).toContain('Unhealthy threshold must be 0 or greater.');
497
+ expect(applicationGatewayErrors).toContain('Health check timeout must be 0 or greater.');
498
+ });
499
+
500
+ it('parses empty optional numbers as undefined', () => {
501
+ expect(parseOptionalNumber('')).toBeUndefined();
502
+ expect(parseOptionalNumber('8080')).toBe(8080);
503
+ });
504
+ });
505
+
506
+ describe('submit', () => {
507
+ it('returns the normalized command without submitting a task for pipeline configuration', () => {
508
+ const closeModal = jasmine.createSpy('closeModal');
509
+ const upsertLoadBalancer = spyOn(LoadBalancerWriter, 'upsertLoadBalancer').and.callFake(
510
+ () => Promise.reject({}) as any,
511
+ );
512
+ const application = {
513
+ defaultCredentials: { azure: 'test' },
514
+ defaultRegions: { azure: 'westus' },
515
+ getDataSource: () => null,
516
+ name: 'fnord',
517
+ };
518
+ const modal = new AzureLoadBalancerModal({
519
+ app: application,
520
+ application,
521
+ closeModal,
522
+ forPipelineConfig: true,
523
+ isNew: true,
524
+ loadBalancer: null,
525
+ loadBalancerType: AzureLoadBalancerTypes[0],
526
+ } as any);
527
+
528
+ modal.state = {
529
+ ...modal.state,
530
+ accountsLoaded: true,
531
+ loadBalancer: {
532
+ credentials: 'test',
533
+ detail: 'frontend',
534
+ loadBalancingRules: [{ backendPort: 80, externalPort: 80, protocol: 'TCP' }],
535
+ name: 'fnord-frontend',
536
+ probes: [{ probePort: 80, probeProtocol: 'TCP' }],
537
+ region: 'westus',
538
+ stack: 'frontend',
539
+ },
540
+ };
541
+
542
+ (modal as any).submit();
543
+
544
+ expect(upsertLoadBalancer).not.toHaveBeenCalled();
545
+ expect(closeModal).toHaveBeenCalledWith(
546
+ jasmine.objectContaining({
547
+ loadBalancerType: 'Azure Load Balancer',
548
+ name: 'fnord-frontend',
549
+ type: 'upsertLoadBalancer',
550
+ }),
551
+ );
552
+ });
553
+
554
+ it('submits the normalized command through TaskMonitor outside pipeline configuration', () => {
555
+ const upsertLoadBalancer = spyOn(LoadBalancerWriter, 'upsertLoadBalancer').and.returnValue(
556
+ Promise.resolve({} as any),
557
+ );
558
+ const application = {
559
+ defaultCredentials: { azure: 'test' },
560
+ defaultRegions: { azure: 'westus' },
561
+ getDataSource: () => null,
562
+ loadBalancers: {
563
+ onNextRefresh: jasmine.createSpy('onNextRefresh'),
564
+ refresh: jasmine.createSpy('refresh'),
565
+ },
566
+ name: 'fnord',
567
+ };
568
+ const modal = new AzureLoadBalancerModal({
569
+ app: application,
570
+ application,
571
+ closeModal: jasmine.createSpy('closeModal'),
572
+ forPipelineConfig: false,
573
+ isNew: true,
574
+ loadBalancer: null,
575
+ loadBalancerType: AzureLoadBalancerTypes[0],
576
+ } as any);
577
+ spyOn(modal as any, 'setState').and.callFake((state: any) => {
578
+ modal.state = { ...modal.state, ...state };
579
+ });
580
+ const submitTask = spyOn(modal.state.taskMonitor, 'submit').and.callFake((submitMethod: any) => submitMethod());
581
+
582
+ modal.state = {
583
+ ...modal.state,
584
+ accountsLoaded: true,
585
+ loadBalancer: {
586
+ clusterName: 'fnord-frontend',
587
+ credentials: 'test',
588
+ detail: 'frontend',
589
+ loadBalancingRules: [{ backendPort: 80, externalPort: 80, protocol: 'TCP' }],
590
+ name: 'fnord-frontend',
591
+ probes: [{ probePort: 80, probeProtocol: 'TCP' }],
592
+ region: 'westus',
593
+ stack: 'frontend',
594
+ },
595
+ };
596
+
597
+ (modal as any).submit();
598
+
599
+ expect(submitTask).toHaveBeenCalled();
600
+ expect(upsertLoadBalancer).toHaveBeenCalledWith(
601
+ jasmine.objectContaining({
602
+ loadBalancerType: 'Azure Load Balancer',
603
+ name: 'fnord-frontend',
604
+ type: 'upsertLoadBalancer',
605
+ }),
606
+ application,
607
+ 'Create',
608
+ {
609
+ appName: 'fnord',
610
+ cloudProvider: 'azure',
611
+ clusterName: 'fnord-frontend',
612
+ loadBalancerName: 'fnord-frontend',
613
+ resourceGroupName: 'fnord-frontend',
614
+ },
615
+ );
616
+ });
617
+ });
618
+
619
+ describe('rendering helpers', () => {
620
+ it('preserves zero input values for rendering', () => {
621
+ expect(formatInputValue(0)).toBe(0);
622
+ expect(formatInputValue(null)).toBe('');
623
+ expect(formatInputValue(undefined)).toBe('');
624
+ });
625
+
626
+ it('distinguishes vnets with the same name in different resource groups', () => {
627
+ const vnets = [
628
+ { name: 'shared', resourceGroup: 'rg-a' },
629
+ { name: 'shared', resourceGroup: 'rg-b' },
630
+ ] as any[];
631
+
632
+ expect(findAzureVnetBySelectValue(vnets, getAzureVnetSelectValue(vnets[1]))).toBe(vnets[1]);
633
+ expect(getAzureVnetSelectLabel(vnets[0])).toBe('shared (rg-a)');
634
+ expect(getAzureVnetSelectLabel(vnets[1])).toBe('shared (rg-b)');
635
+ });
636
+
637
+ it('does not render create-only location fields in edit mode', () => {
638
+ expect(shouldRenderCreateFields(false)).toBe(false);
639
+ expect(shouldRenderCreateFields(true)).toBe(true);
640
+ });
641
+
642
+ it('only renders network fields for Application Gateway create', () => {
643
+ expect(shouldRenderNetworkFields(false, 'Azure Application Gateway')).toBe(false);
644
+ expect(shouldRenderNetworkFields(true, 'Azure Load Balancer')).toBe(false);
645
+ expect(shouldRenderNetworkFields(true, 'Azure Application Gateway')).toBe(true);
646
+ });
647
+
648
+ it('only renders sku for Application Gateway create', () => {
649
+ expect(shouldRenderSkuField(false, 'Azure Application Gateway')).toBe(false);
650
+ expect(shouldRenderSkuField(true, 'Azure Load Balancer')).toBe(false);
651
+ expect(shouldRenderSkuField(true, 'Azure Application Gateway')).toBe(true);
652
+ });
653
+
654
+ it('constrains Application Gateway sku values to legacy options', () => {
655
+ expect(validSkus).toEqual(['Standard_v2', 'Standard_Small']);
656
+ });
657
+ });
658
+ });