@rancher/shell 3.0.12-rc.4 → 3.0.12-rc.5

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 (81) hide show
  1. package/assets/styles/global/_button.scss +1 -1
  2. package/assets/translations/en-us.yaml +39 -10
  3. package/components/ActionDropdownShell.vue +5 -3
  4. package/components/ButtonGroup.vue +26 -1
  5. package/components/CruResource.vue +51 -2
  6. package/components/PromptRestore.vue +93 -32
  7. package/components/Questions/index.vue +1 -0
  8. package/components/ResourceTable.vue +1 -0
  9. package/components/SortableTable/index.vue +4 -3
  10. package/components/Wizard.vue +14 -1
  11. package/components/__tests__/ButtonGroup.test.ts +56 -0
  12. package/components/__tests__/PromptRestore.test.ts +169 -19
  13. package/components/fleet/GitRepoAdvancedTab.vue +1 -0
  14. package/components/fleet/GitRepoMetadataTab.vue +5 -0
  15. package/components/fleet/HelmOpAppCoConfigTab.vue +4 -0
  16. package/components/fleet/HelmOpMetadataTab.vue +5 -0
  17. package/components/form/FileSelector.vue +39 -1
  18. package/components/form/PrivateRegistry.constants.ts +7 -0
  19. package/components/form/PrivateRegistry.vue +253 -18
  20. package/components/form/SelectOrCreateAuthSecret.vue +140 -17
  21. package/components/form/__tests__/FileSelector.test.ts +23 -0
  22. package/components/form/__tests__/PrivateRegistry.test.ts +463 -73
  23. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +122 -0
  24. package/components/formatter/EtcdSnapshotName.vue +73 -0
  25. package/components/nav/Header.vue +8 -1
  26. package/components/templates/default.vue +7 -0
  27. package/config/features.js +1 -0
  28. package/config/labels-annotations.js +2 -0
  29. package/config/product/manager.js +6 -0
  30. package/config/secret.ts +10 -0
  31. package/config/settings.ts +6 -2
  32. package/config/types.js +7 -0
  33. package/detail/provisioning.cattle.io.cluster.vue +79 -3
  34. package/dialog/RotateEncryptionKeyDialog.vue +33 -9
  35. package/dialog/__tests__/RotateEncryptionKeyDialog.test.ts +78 -0
  36. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +92 -0
  37. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +101 -0
  38. package/edit/__tests__/management.cattle.io.setting.test.ts +2 -1
  39. package/edit/compliance.cattle.io.clusterscanprofile.vue +39 -41
  40. package/edit/fleet.cattle.io.gitrepo.vue +70 -16
  41. package/edit/fleet.cattle.io.helmop.vue +51 -5
  42. package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
  43. package/edit/{management.cattle.io.setting.vue → management.cattle.io.setting/index.vue} +32 -9
  44. package/edit/management.cattle.io.setting/system-default-registry-pull-secrets.vue +81 -0
  45. package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +3 -12
  46. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +18 -0
  47. package/edit/provisioning.cattle.io.cluster/rke2.vue +5 -1
  48. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +0 -1
  49. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +14 -55
  50. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +156 -0
  51. package/models/__tests__/secret.test.ts +68 -1
  52. package/models/management.cattle.io.cluster.js +21 -3
  53. package/models/pod.js +13 -2
  54. package/models/provisioning.cattle.io.cluster.js +59 -9
  55. package/models/rke.cattle.io.etcdsnapshot.js +17 -9
  56. package/models/secret.js +19 -0
  57. package/models/workload.js +12 -7
  58. package/package.json +1 -1
  59. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +485 -107
  60. package/pages/c/_cluster/apps/charts/install.vue +114 -28
  61. package/pkg/require-asset.lib.js +25 -0
  62. package/pkg/vue.config.js +7 -0
  63. package/plugins/dashboard-store/__tests__/resource-class.test.ts +84 -0
  64. package/plugins/dashboard-store/getters.js +0 -1
  65. package/plugins/dashboard-store/resource-class.js +52 -12
  66. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +30 -0
  67. package/rancher-components/Form/TextArea/__tests__/TextAreaAutoGrow.test.ts +95 -0
  68. package/rancher-components/RcButton/index.ts +1 -1
  69. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +6 -1
  70. package/store/__tests__/features.test.ts +131 -0
  71. package/store/__tests__/growl.test.ts +374 -0
  72. package/store/__tests__/modal.test.ts +131 -0
  73. package/store/__tests__/slideInPanel.test.ts +88 -0
  74. package/store/__tests__/type-map.utils.test.ts +433 -0
  75. package/store/features.js +4 -0
  76. package/types/shell/index.d.ts +62 -0
  77. package/utils/__tests__/operation-cr.test.ts +34 -0
  78. package/utils/operation-cr.js +19 -0
  79. package/utils/require-asset.ts +7 -0
  80. package/utils/validators/__tests__/private-registry.test.ts +27 -15
  81. package/utils/validators/private-registry.ts +15 -4
@@ -3,78 +3,95 @@ import { mount } from '@vue/test-utils';
3
3
  import Install from '@shell/pages/c/_cluster/apps/charts/install.vue';
4
4
  import { CATALOG as CATALOG_ANNOTATIONS } from '@shell/config/labels-annotations';
5
5
 
6
- describe('page: Install', () => {
7
- it('should use version annotations for target namespace and name', async() => {
8
- const mockStore = {
9
- dispatch: jest.fn((action) => {
10
- if (action === 'cluster/create') {
11
- return Promise.resolve({ metadata: { namespace: '', name: '' } });
12
- }
13
-
14
- return Promise.resolve();
15
- }),
16
- getters: {
17
- 'catalog/inStore': 'cluster',
18
- 'catalog/repo': () => ({ metadata: { name: 'test-repo' } }),
19
- 'features/get': () => false,
20
- defaultNamespace: 'default',
21
- 'i18n/withFallback': (key: string) => key,
22
- 'type-map/hasCustomChart': () => false,
23
- 'cluster/all': () => [],
24
- 'cluster/byId': () => null,
25
- 'management/all': () => [],
26
- 'prefs/get': () => {},
27
- 'catalog/charts': [],
28
- 'wm/byId': () => null,
29
- 'i18n/t': (key: string) => key,
6
+ const defaultStubs = {
7
+ Loading: true,
8
+ Wizard: true,
9
+ Banner: true,
10
+ Checkbox: true,
11
+ LabeledInput: true,
12
+ LabeledSelect: true,
13
+ NameNsDescription: true,
14
+ Tabbed: true,
15
+ Questions: true,
16
+ YamlEditor: true,
17
+ ResourceCancelModal: true,
18
+ UnitInput: true,
19
+ TypeDescription: true,
20
+ LazyImage: true,
21
+ ChartReadme: true,
22
+ ButtonGroup: true,
23
+ PrivateRegistry: true,
24
+ };
25
+
26
+ const defaultGetters = {
27
+ 'catalog/inStore': 'cluster',
28
+ 'catalog/repo': () => ({ metadata: { name: 'test-repo' } }),
29
+ 'features/get': () => false,
30
+ defaultNamespace: 'default',
31
+ 'i18n/withFallback': (key: string) => key,
32
+ 'type-map/hasCustomChart': () => false,
33
+ 'cluster/all': () => [],
34
+ 'cluster/byId': () => null,
35
+ 'management/all': () => [],
36
+ 'prefs/get': () => {},
37
+ 'catalog/charts': [],
38
+ 'wm/byId': () => null,
39
+ 'i18n/t': (key: string) => key,
40
+ };
41
+
42
+ const mountInstall = (options: {
43
+ data?: () => Record<string, any>;
44
+ getters?: Record<string, any>;
45
+ mocks?: Record<string, any>;
46
+ } = {}) => {
47
+ const mockStore = {
48
+ dispatch: jest.fn((action) => {
49
+ if (action === 'cluster/create') {
50
+ return Promise.resolve({ metadata: { namespace: '', name: '' } });
30
51
  }
31
- };
32
52
 
33
- const wrapper = mount(Install, {
34
- global: {
35
- mocks: {
36
- $store: mockStore,
37
- $route: { query: {} },
38
- $fetchState: { pending: false },
39
- t: (key: string) => key,
40
- },
41
- stubs: {
42
- Loading: true,
43
- Wizard: true,
44
- Banner: true,
45
- Checkbox: true,
46
- LabeledInput: true,
47
- LabeledSelect: true,
48
- NameNsDescription: true,
49
- Tabbed: true,
50
- Questions: true,
51
- YamlEditor: true,
52
- ResourceCancelModal: true,
53
- UnitInput: true,
54
- TypeDescription: true,
55
- LazyImage: true,
56
- ChartReadme: true,
57
- ButtonGroup: true,
58
- }
53
+ return Promise.resolve();
54
+ }),
55
+ getters: {
56
+ ...defaultGetters,
57
+ ...options.getters
58
+ }
59
+ };
60
+
61
+ return mount(Install, {
62
+ global: {
63
+ mocks: {
64
+ $store: mockStore,
65
+ $route: { query: {} },
66
+ $fetchState: { pending: false },
67
+ t: (key: string) => key,
68
+ ...options.mocks
59
69
  },
60
- data() {
61
- return {
62
- version: {
63
- annotations: {
64
- [CATALOG_ANNOTATIONS.NAMESPACE]: 'custom-ns',
65
- [CATALOG_ANNOTATIONS.RELEASE_NAME]: 'custom-name',
66
- }
67
- },
68
- chart: {
69
- targetNamespace: 'wrong-ns',
70
- targetName: 'wrong-name',
71
- versions: []
72
- },
73
- query: { versionName: '1.0.0' },
74
- chartValues: { global: { imagePullSecrets: [] } },
75
- repo: { spec: { clientSecret: { name: 'test-secret' } } }
76
- };
77
- }
70
+ stubs: defaultStubs
71
+ },
72
+ data: options.data
73
+ });
74
+ };
75
+
76
+ describe('page: Install', () => {
77
+ it('should use version annotations for target namespace and name', async() => {
78
+ const wrapper = mountInstall({
79
+ data: () => ({
80
+ version: {
81
+ annotations: {
82
+ [CATALOG_ANNOTATIONS.NAMESPACE]: 'custom-ns',
83
+ [CATALOG_ANNOTATIONS.RELEASE_NAME]: 'custom-name',
84
+ }
85
+ },
86
+ chart: {
87
+ targetNamespace: 'wrong-ns',
88
+ targetName: 'wrong-name',
89
+ versions: []
90
+ },
91
+ query: { versionName: '1.0.0' },
92
+ chartValues: { global: { imagePullSecrets: [] } },
93
+ repo: { spec: { clientSecret: { name: 'test-secret' } } }
94
+ })
78
95
  });
79
96
 
80
97
  // Mock methods from mixins
@@ -97,44 +114,12 @@ describe('page: Install', () => {
97
114
  const mockReplace = jest.fn();
98
115
  const expectedLocation = { name: 'app-location' };
99
116
 
100
- const wrapper = mount(Install, {
101
- global: {
102
- mocks: {
103
- $store: {
104
- getters: {
105
- 'i18n/t': (key: string) => key,
106
- 'cluster/id': 'cluster-id',
107
- }
108
- },
109
- $route: { query: {} },
110
- $router: { replace: mockReplace },
111
- $fetchState: { pending: false },
112
- },
113
- stubs: {
114
- Loading: true,
115
- Wizard: true,
116
- Banner: true,
117
- Checkbox: true,
118
- LabeledInput: true,
119
- LabeledSelect: true,
120
- NameNsDescription: true,
121
- Tabbed: true,
122
- Questions: true,
123
- YamlEditor: true,
124
- ResourceCancelModal: true,
125
- UnitInput: true,
126
- TypeDescription: true,
127
- LazyImage: true,
128
- ChartReadme: true,
129
- ButtonGroup: true,
130
- }
131
- },
132
- data() {
133
- return {
134
- existing: false,
135
- chart: null,
136
- };
137
- }
117
+ const wrapper = mountInstall({
118
+ data: () => ({
119
+ existing: false,
120
+ chart: null,
121
+ }),
122
+ mocks: { $router: { replace: mockReplace } }
138
123
  });
139
124
 
140
125
  jest.spyOn((wrapper.vm as any), 'appLocation').mockReturnValue(expectedLocation);
@@ -144,4 +129,397 @@ describe('page: Install', () => {
144
129
  expect(mockReplace).toHaveBeenCalledWith(expectedLocation);
145
130
  });
146
131
  });
132
+
133
+ describe('showRegistryPullSecrets', () => {
134
+ it('should return true when repo has defaultImagePullSecrets', () => {
135
+ const wrapper = mountInstall({ data: () => ({ repo: { spec: { defaultImagePullSecrets: [{ name: 'my-secret' }] } } }) });
136
+
137
+ expect(wrapper.vm.showRegistryPullSecrets).toBe(true);
138
+ });
139
+
140
+ it('should return false when repo has no defaultImagePullSecrets', () => {
141
+ const wrapper = mountInstall({ data: () => ({ repo: { spec: {} } }) });
142
+
143
+ expect(wrapper.vm.showRegistryPullSecrets).toBe(false);
144
+ });
145
+
146
+ it('should return false when defaultImagePullSecrets is empty', () => {
147
+ const wrapper = mountInstall({ data: () => ({ repo: { spec: { defaultImagePullSecrets: [] } } }) });
148
+
149
+ expect(wrapper.vm.showRegistryPullSecrets).toBe(false);
150
+ });
151
+ });
152
+
153
+ describe('repoDefaultPullSecretNames', () => {
154
+ it('should map defaultImagePullSecrets to names', () => {
155
+ const wrapper = mountInstall({
156
+ data: () => ({
157
+ repo: {
158
+ spec: {
159
+ defaultImagePullSecrets: [
160
+ { name: 'secret-a' },
161
+ { name: 'secret-b' }
162
+ ]
163
+ }
164
+ }
165
+ })
166
+ });
167
+
168
+ expect(wrapper.vm.repoDefaultPullSecretNames).toStrictEqual(['secret-a', 'secret-b']);
169
+ });
170
+
171
+ it('should filter out entries without a name', () => {
172
+ const wrapper = mountInstall({
173
+ data: () => ({
174
+ repo: {
175
+ spec: {
176
+ defaultImagePullSecrets: [
177
+ { name: 'valid' },
178
+ { name: '' },
179
+ {}
180
+ ]
181
+ }
182
+ }
183
+ })
184
+ });
185
+
186
+ expect(wrapper.vm.repoDefaultPullSecretNames).toStrictEqual(['valid']);
187
+ });
188
+
189
+ it('should return empty array when no defaultImagePullSecrets', () => {
190
+ const wrapper = mountInstall({ data: () => ({ repo: { spec: {} } }) });
191
+
192
+ expect(wrapper.vm.repoDefaultPullSecretNames).toStrictEqual([]);
193
+ });
194
+ });
195
+
196
+ describe('existingValuesPullSecrets', () => {
197
+ it('should return empty array on fresh install', () => {
198
+ const wrapper = mountInstall({
199
+ data: () => ({
200
+ existing: false,
201
+ chartValues: { global: { imagePullSecrets: ['secret-1', 'secret-2'] } }
202
+ })
203
+ });
204
+
205
+ expect(wrapper.vm.existingValuesPullSecrets).toStrictEqual([]);
206
+ });
207
+
208
+ it('should return imagePullSecrets from chart values on upgrade', () => {
209
+ const wrapper = mountInstall({
210
+ data: () => ({
211
+ existing: { metadata: { name: 'existing-release' } },
212
+ chartValues: { global: { imagePullSecrets: ['secret-1', 'secret-2'] } }
213
+ })
214
+ });
215
+
216
+ expect(wrapper.vm.existingValuesPullSecrets).toStrictEqual(['secret-1', 'secret-2']);
217
+ });
218
+
219
+ it('should filter out falsy entries', () => {
220
+ const wrapper = mountInstall({
221
+ data: () => ({
222
+ existing: { metadata: { name: 'existing-release' } },
223
+ chartValues: { global: { imagePullSecrets: ['secret-1', null, '', 'secret-2'] } }
224
+ })
225
+ });
226
+
227
+ expect(wrapper.vm.existingValuesPullSecrets).toStrictEqual(['secret-1', 'secret-2']);
228
+ });
229
+
230
+ it('should return empty array when imagePullSecrets is not an array', () => {
231
+ const wrapper = mountInstall({
232
+ data: () => ({
233
+ existing: { metadata: { name: 'existing-release' } },
234
+ chartValues: { global: { imagePullSecrets: 'not-an-array' } }
235
+ })
236
+ });
237
+
238
+ expect(wrapper.vm.existingValuesPullSecrets).toStrictEqual([]);
239
+ });
240
+
241
+ it('should return empty array when global has no imagePullSecrets', () => {
242
+ const wrapper = mountInstall({
243
+ data: () => ({
244
+ existing: { metadata: { name: 'existing-release' } },
245
+ chartValues: { global: {} }
246
+ })
247
+ });
248
+
249
+ expect(wrapper.vm.existingValuesPullSecrets).toStrictEqual([]);
250
+ });
251
+ });
252
+
253
+ describe('addGlobalValuesTo', () => {
254
+ it('should set imagePullSecrets when registryPullSecret is selected', () => {
255
+ const wrapper = mountInstall({
256
+ data: () => ({
257
+ repo: { spec: { defaultImagePullSecrets: [{ name: 'default-secret' }] } },
258
+ registryPullSecret: 'my-selected-secret',
259
+ customRegistrySetting: null,
260
+ currentCluster: null,
261
+ serverUrlSetting: { value: '' },
262
+ })
263
+ });
264
+
265
+ const values = { global: {} };
266
+
267
+ wrapper.vm.addGlobalValuesTo(values);
268
+
269
+ expect(values.global.imagePullSecrets).toStrictEqual(['my-selected-secret']);
270
+ });
271
+
272
+ it('should delete imagePullSecrets when showRegistryPullSecrets is true but no pullSecret selected', () => {
273
+ const wrapper = mountInstall({
274
+ data: () => ({
275
+ repo: { spec: { defaultImagePullSecrets: [{ name: 'default-secret' }] } },
276
+ registryPullSecret: null,
277
+ customRegistrySetting: null,
278
+ currentCluster: null,
279
+ serverUrlSetting: { value: '' },
280
+ })
281
+ });
282
+
283
+ const values = { global: { imagePullSecrets: ['old-secret'] } };
284
+
285
+ wrapper.vm.addGlobalValuesTo(values);
286
+
287
+ expect(values.global.imagePullSecrets).toBeUndefined();
288
+ });
289
+
290
+ it('should not modify imagePullSecrets when showRegistryPullSecrets is false', () => {
291
+ const wrapper = mountInstall({
292
+ data: () => ({
293
+ repo: { spec: {} },
294
+ registryPullSecret: null,
295
+ customRegistrySetting: null,
296
+ currentCluster: null,
297
+ serverUrlSetting: { value: '' },
298
+ })
299
+ });
300
+
301
+ const values = { global: { imagePullSecrets: ['existing-secret'] } };
302
+
303
+ wrapper.vm.addGlobalValuesTo(values);
304
+
305
+ expect(values.global.imagePullSecrets).toStrictEqual(['existing-secret']);
306
+ });
307
+ });
308
+
309
+ describe('finish()', () => {
310
+ const createFinishWrapper = (overrides: Record<string, any> = {}) => {
311
+ const mockDoAction = jest.fn().mockResolvedValue({
312
+ operationNamespace: 'ns',
313
+ operationName: 'op'
314
+ });
315
+ const mockActionLinkFor = jest.fn().mockReturnValue('https://rancher/v1/catalog.cattle.io.clusterrepos/rancher-charts?action=install');
316
+
317
+ const wrapper = mountInstall({
318
+ data: () => ({
319
+ repo: {
320
+ spec: { defaultImagePullSecrets: overrides.defaultImagePullSecrets || [] },
321
+ doAction: mockDoAction,
322
+ actionLinkFor: mockActionLinkFor,
323
+ waitForOperation: jest.fn().mockResolvedValue(undefined),
324
+ },
325
+ existing: overrides.existing || null,
326
+ skipPullSecrets: overrides.skipPullSecrets ?? false,
327
+ registryPullSecret: overrides.registryPullSecret ?? null,
328
+ customRegistrySetting: null,
329
+ currentCluster: null,
330
+ serverUrlSetting: { value: '' },
331
+ errors: [],
332
+ chart: { versions: [] },
333
+ version: { annotations: {} },
334
+ chartValues: overrides.chartValues || { global: {} },
335
+ value: { metadata: { namespace: 'default', name: 'test' } },
336
+ ...overrides
337
+ })
338
+ });
339
+
340
+ // Mock mixin methods
341
+ jest.spyOn(wrapper.vm as any, 'createNamespaceIfNeeded').mockResolvedValue(undefined);
342
+ jest.spyOn(wrapper.vm as any, 'applyHooks').mockResolvedValue(overrides.hookResults || {});
343
+ jest.spyOn(wrapper.vm as any, 'actionInput').mockReturnValue({ errors: [], input: {} });
344
+ jest.spyOn(wrapper.vm as any, 'done').mockImplementation();
345
+
346
+ // Mock store dispatch for operation find
347
+ const store = (wrapper.vm as any).$store;
348
+
349
+ store.dispatch = jest.fn().mockResolvedValue({
350
+ waitForLink: jest.fn().mockResolvedValue(undefined),
351
+ openLogs: jest.fn(),
352
+ });
353
+
354
+ return {
355
+ wrapper, mockDoAction, mockActionLinkFor
356
+ };
357
+ };
358
+
359
+ it('should read created secret name from hookResults when registryPullSecret is null', async() => {
360
+ const { wrapper } = createFinishWrapper({
361
+ defaultImagePullSecrets: [{ name: 'repo-default' }],
362
+ hookResults: { registerAuthSecret: { metadata: { name: 'hook-created-secret' } } },
363
+ });
364
+ const btnCb = jest.fn();
365
+
366
+ await wrapper.vm.finish(btnCb);
367
+
368
+ expect(wrapper.vm.registryPullSecret).toBe('hook-created-secret');
369
+ });
370
+
371
+ it('should not overwrite registryPullSecret from hookResults when already set', async() => {
372
+ const { wrapper } = createFinishWrapper({
373
+ defaultImagePullSecrets: [{ name: 'repo-default' }],
374
+ registryPullSecret: 'already-selected',
375
+ hookResults: { registerAuthSecret: { metadata: { name: 'hook-created-secret' } } },
376
+ });
377
+ const btnCb = jest.fn();
378
+
379
+ await wrapper.vm.finish(btnCb);
380
+
381
+ expect(wrapper.vm.registryPullSecret).toBe('already-selected');
382
+ });
383
+
384
+ it('should not read hookResults when skipPullSecrets is true', async() => {
385
+ const { wrapper } = createFinishWrapper({
386
+ defaultImagePullSecrets: [{ name: 'repo-default' }],
387
+ skipPullSecrets: true,
388
+ hookResults: { registerAuthSecret: { metadata: { name: 'hook-created-secret' } } },
389
+ });
390
+ const btnCb = jest.fn();
391
+
392
+ await wrapper.vm.finish(btnCb);
393
+
394
+ expect(wrapper.vm.registryPullSecret).toBeNull();
395
+ });
396
+
397
+ it('should add skipPullSecrets query param when skipPullSecrets is true', async() => {
398
+ const { wrapper, mockDoAction } = createFinishWrapper({ skipPullSecrets: true });
399
+ const btnCb = jest.fn();
400
+
401
+ await wrapper.vm.finish(btnCb);
402
+
403
+ expect(mockDoAction).toHaveBeenCalledWith(
404
+ 'install',
405
+ expect.anything(),
406
+ expect.objectContaining({ url: expect.stringContaining('skipPullSecrets=true') })
407
+ );
408
+ });
409
+
410
+ it('should not add skipPullSecrets query param when skipPullSecrets is false', async() => {
411
+ const { wrapper, mockDoAction } = createFinishWrapper({ skipPullSecrets: false });
412
+ const btnCb = jest.fn();
413
+
414
+ await wrapper.vm.finish(btnCb);
415
+
416
+ expect(mockDoAction).toHaveBeenCalledWith(
417
+ 'install',
418
+ expect.anything(),
419
+ {}
420
+ );
421
+ });
422
+
423
+ it('should use upgrade action name when existing release is present', async() => {
424
+ const { wrapper, mockDoAction } = createFinishWrapper({
425
+ existing: { metadata: { name: 'existing-release' } },
426
+ skipPullSecrets: true,
427
+ });
428
+ const btnCb = jest.fn();
429
+
430
+ await wrapper.vm.finish(btnCb);
431
+
432
+ expect(mockDoAction).toHaveBeenCalledWith(
433
+ 'upgrade',
434
+ expect.anything(),
435
+ expect.objectContaining({ url: expect.stringContaining('skipPullSecrets=true') })
436
+ );
437
+ });
438
+ });
439
+
440
+ describe('fetch() pull secret pre-selection', () => {
441
+ const createFetchWrapper = (overrides: Record<string, any> = {}) => {
442
+ const existingMock = overrides.existing ? {
443
+ metadata: overrides.existing.metadata || { namespace: 'default', name: 'release' },
444
+ fetchValues: jest.fn().mockResolvedValue(undefined),
445
+ deployedAsLegacy: jest.fn().mockResolvedValue(false),
446
+ deployedAsMultiCluster: jest.fn().mockResolvedValue(false),
447
+ values: overrides.chartValues?.global ? overrides.chartValues : {},
448
+ } : null;
449
+
450
+ const wrapper = mountInstall({
451
+ data: () => ({
452
+ repo: {
453
+ spec: {
454
+ defaultImagePullSecrets: overrides.defaultImagePullSecrets || [],
455
+ clientSecret: { name: 'test-secret' }
456
+ }
457
+ },
458
+ existing: existingMock,
459
+ chartValues: overrides.chartValues || { global: {} },
460
+ chart: { versions: [] },
461
+ version: { annotations: {} },
462
+ query: { versionName: '1.0.0' },
463
+ })
464
+ });
465
+
466
+ // Mock mixin methods used during fetch
467
+ jest.spyOn(wrapper.vm as any, 'fetchChart').mockResolvedValue(undefined);
468
+ jest.spyOn(wrapper.vm as any, 'fetchAutoInstallInfo').mockResolvedValue(undefined);
469
+ jest.spyOn(wrapper.vm as any, 'getClusterRegistry').mockResolvedValue(undefined);
470
+ jest.spyOn(wrapper.vm as any, 'getGlobalRegistry').mockResolvedValue(undefined);
471
+ jest.spyOn(wrapper.vm as any, 'loadValuesComponent').mockResolvedValue(undefined);
472
+ jest.spyOn(wrapper.vm as any, 'updateStepOneReady').mockImplementation();
473
+
474
+ return wrapper;
475
+ };
476
+
477
+ it('should pre-select single existing pull secret on upgrade', async() => {
478
+ const wrapper = createFetchWrapper({
479
+ existing: { metadata: { namespace: 'default', name: 'release' } },
480
+ defaultImagePullSecrets: [{ name: 'repo-default' }],
481
+ chartValues: { global: { imagePullSecrets: ['existing-secret'] } },
482
+ });
483
+
484
+ await Install.fetch.call(wrapper.vm);
485
+
486
+ expect(wrapper.vm.registryPullSecret).toBe('existing-secret');
487
+ });
488
+
489
+ it('should not pre-select when multiple existing pull secrets exist', async() => {
490
+ const wrapper = createFetchWrapper({
491
+ existing: { metadata: { namespace: 'default', name: 'release' } },
492
+ defaultImagePullSecrets: [{ name: 'repo-default' }],
493
+ chartValues: { global: { imagePullSecrets: ['secret-1', 'secret-2'] } },
494
+ });
495
+
496
+ await Install.fetch.call(wrapper.vm);
497
+
498
+ expect(wrapper.vm.registryPullSecret).toBeNull();
499
+ });
500
+
501
+ it('should not pre-select on fresh install', async() => {
502
+ const wrapper = createFetchWrapper({
503
+ existing: null,
504
+ defaultImagePullSecrets: [{ name: 'repo-default' }],
505
+ chartValues: { global: { imagePullSecrets: ['existing-secret'] } },
506
+ });
507
+
508
+ await Install.fetch.call(wrapper.vm);
509
+
510
+ expect(wrapper.vm.registryPullSecret).toBeNull();
511
+ });
512
+
513
+ it('should not pre-select when no imagePullSecrets in chart values', async() => {
514
+ const wrapper = createFetchWrapper({
515
+ existing: { metadata: { namespace: 'default', name: 'release' } },
516
+ defaultImagePullSecrets: [{ name: 'repo-default' }],
517
+ chartValues: { global: {} },
518
+ });
519
+
520
+ await Install.fetch.call(wrapper.vm);
521
+
522
+ expect(wrapper.vm.registryPullSecret).toBeNull();
523
+ });
524
+ });
147
525
  });