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

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 (89) hide show
  1. package/apis/intf/resources-api/resource-base.ts +42 -2
  2. package/apis/intf/resources-api/resource-instance.ts +101 -0
  3. package/apis/intf/resources-api/resources-api.ts +177 -23
  4. package/apis/intf/resources.ts +8 -6
  5. package/apis/resources/__tests__/resources-api-class.test.ts +309 -28
  6. package/apis/resources/resources-api-class.ts +232 -43
  7. package/assets/images/providers/traefik.png +0 -0
  8. package/assets/translations/en-us.yaml +10 -4
  9. package/chart/__tests__/rancher-monitoring-dashboards-index.test.ts +269 -0
  10. package/chart/rancher-monitoring-dashboards/index.vue +155 -0
  11. package/cloud-credential/__tests__/generic.test.ts +132 -0
  12. package/cloud-credential/generic.vue +33 -3
  13. package/components/EtcdInfoBanner.vue +2 -12
  14. package/components/GrafanaDashboard.vue +2 -8
  15. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +68 -14
  16. package/components/Resource/Detail/Masthead/index.vue +8 -3
  17. package/components/form/Security.vue +1 -1
  18. package/components/formatter/Translate.vue +22 -1
  19. package/components/nav/Group.vue +5 -10
  20. package/components/nav/TopLevelMenu.vue +1 -30
  21. package/components/nav/__tests__/Group.test.ts +61 -0
  22. package/config/labels-annotations.js +1 -0
  23. package/config/table-headers.js +5 -2
  24. package/core/__tests__/plugin-products-apply.test.ts +787 -0
  25. package/core/__tests__/plugin-products-extend.test.ts +189 -0
  26. package/core/__tests__/plugin-products-helpers.test.ts +81 -78
  27. package/core/__tests__/plugin-products-new.test.ts +389 -0
  28. package/core/__tests__/plugin-products-scenarios.test.ts +980 -0
  29. package/core/__tests__/plugin-products-side-menu.test.ts +1935 -0
  30. package/core/plugin-products-base.ts +201 -93
  31. package/core/plugin-products-extending.ts +1 -1
  32. package/core/plugin-products-external.ts +380 -0
  33. package/core/plugin-products-helpers.ts +28 -24
  34. package/core/plugin-products-internal.ts +207 -0
  35. package/core/plugin-products-top-level.ts +10 -5
  36. package/core/plugin-products-type-guards.ts +17 -6
  37. package/core/plugin-products.ts +5 -7
  38. package/core/plugin-types.ts +1 -389
  39. package/core/plugin.ts +10 -11
  40. package/core/types.ts +6 -202
  41. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +52 -0
  42. package/detail/pod.vue +1 -1
  43. package/detail/provisioning.cattle.io.cluster.vue +0 -2
  44. package/edit/auth/AuthProviderWarningBanners.vue +12 -0
  45. package/edit/auth/__tests__/AuthProviderWarningBanners.test.ts +10 -1
  46. package/edit/auth/__tests__/azuread.test.ts +1 -0
  47. package/edit/auth/__tests__/github.test.ts +1 -0
  48. package/edit/auth/__tests__/oidc.test.ts +1 -0
  49. package/edit/auth/__tests__/saml.test.ts +1 -0
  50. package/edit/auth/ldap/__tests__/index.test.ts +1 -0
  51. package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
  52. package/edit/provisioning.cattle.io.cluster/rke2.vue +1 -1
  53. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +2 -2
  54. package/list/management.cattle.io.user.vue +24 -5
  55. package/list/utils/management.cattle.io.cluster.utils.ts +1 -1
  56. package/machine-config/__tests__/generic.test.ts +163 -0
  57. package/machine-config/components/EC2Networking.vue +31 -7
  58. package/machine-config/components/__tests__/EC2Networking.test.ts +38 -15
  59. package/machine-config/generic.vue +32 -5
  60. package/mixins/resource-fetch.js +1 -1
  61. package/models/__tests__/rke-machine.cattle.io.vmwarevspheremachinetemplate.test.ts +22 -0
  62. package/models/pod.js +12 -10
  63. package/models/rke-machine.cattle.io.vmwarevspheremachinetemplate.js +9 -0
  64. package/package.json +1 -1
  65. package/pages/auth/login.vue +1 -1
  66. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +110 -0
  67. package/pages/c/_cluster/apps/charts/install.vue +29 -0
  68. package/pages/c/_cluster/auth/config/index.vue +11 -4
  69. package/pages/c/_cluster/monitoring/index.vue +6 -1
  70. package/plugins/dashboard-store/resource-class.js +10 -3
  71. package/plugins/steve/__tests__/steve-class-resource-api.test.ts +147 -0
  72. package/plugins/steve/steve-class.js +43 -0
  73. package/plugins/steve/subscribe.js +11 -0
  74. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +115 -0
  75. package/rancher-components/Form/LabeledInput/LabeledInput.vue +83 -3
  76. package/store/__tests__/auth.test.ts +18 -0
  77. package/store/__tests__/notifications.test.ts +466 -1
  78. package/store/__tests__/ui-context.test.ts +255 -0
  79. package/store/auth.js +11 -1
  80. package/store/features.js +1 -0
  81. package/store/plugins.js +6 -0
  82. package/types/rancher/steve.api.ts +2 -2
  83. package/types/shell/index.d.ts +58 -0
  84. package/types/store/dashboard-store.types.ts +2 -2
  85. package/types/store/type-map.ts +262 -1
  86. package/utils/async.ts +2 -0
  87. package/utils/grafana.js +2 -17
  88. package/utils/monitoring.js +38 -1
  89. package/core/__tests__/plugin-products.test.ts +0 -4694
@@ -10,6 +10,7 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
10
10
  let mockDispatch: jest.Mock;
11
11
  let mockSchemaFor: jest.Mock;
12
12
  let mockPaginationEnabled: jest.Mock;
13
+ let mockUrlFor: jest.Mock;
13
14
  let consoleErrorSpy: any;
14
15
 
15
16
  beforeEach(() => {
@@ -18,12 +19,23 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
18
19
  mockDispatch = jest.fn() as any;
19
20
  mockSchemaFor = jest.fn() as any;
20
21
  mockPaginationEnabled = jest.fn() as any;
22
+ mockUrlFor = jest.fn() as any;
23
+ mockUrlFor.mockImplementation((type: string, id: string) => {
24
+ const schema = mockSchemaFor(type);
25
+
26
+ if (!schema) {
27
+ throw new Error(`No schema found for type "${ type }"`);
28
+ }
29
+
30
+ return id ? `${ schema.linkFor() }/${ id }` : schema.linkFor();
31
+ });
21
32
 
22
33
  mockStore = {
23
34
  dispatch: mockDispatch,
24
35
  getters: {
25
36
  [`${ storeType }/schemaFor`]: mockSchemaFor,
26
- [`${ storeType }/paginationEnabled`]: mockPaginationEnabled
37
+ [`${ storeType }/paginationEnabled`]: mockPaginationEnabled,
38
+ [`${ storeType }/urlFor`]: mockUrlFor
27
39
  }
28
40
  } as any;
29
41
 
@@ -49,7 +61,7 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
49
61
  const result = await resourcesApi.find('pod', 'default/test-pod', { watch: true });
50
62
 
51
63
  // Assert
52
- expect(result).toStrictEqual(mockResource);
64
+ expect(result).toMatchObject(mockResource);
53
65
  expect(mockDispatch).toHaveBeenCalledWith(`${ storeType }/find`, {
54
66
  type: 'pod',
55
67
  id: 'default/test-pod',
@@ -72,7 +84,7 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
72
84
  const result = await resourcesApi.find('node', 'worker-1');
73
85
 
74
86
  // Assert
75
- expect(result).toStrictEqual(mockResource);
87
+ expect(result).toMatchObject(mockResource);
76
88
  expect(mockDispatch).toHaveBeenCalledWith(`${ storeType }/find`, {
77
89
  type: 'node',
78
90
  id: 'worker-1',
@@ -89,7 +101,8 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
89
101
  `Resource API error - ${ storeType } - Resource "pod" is namespaced. The resourceId must be in "namespace/name" format, but received "test-pod"`
90
102
  );
91
103
  expect(consoleErrorSpy).toHaveBeenCalledWith(
92
- `Resource API error - ${ storeType } - Resource "pod" is namespaced. The resourceId must be in "namespace/name" format, but received "test-pod"`
104
+ `Resource API error - ${ storeType } - Resource "pod" is namespaced. The resourceId must be in "namespace/name" format, but received "test-pod"`,
105
+ undefined
93
106
  );
94
107
  expect(mockDispatch).not.toHaveBeenCalled();
95
108
  });
@@ -105,7 +118,7 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
105
118
  const result = await resourcesApi.find('custom.crd', 'my-resource');
106
119
 
107
120
  // Assert
108
- expect(result).toStrictEqual(mockResource);
121
+ expect(result).toMatchObject(mockResource);
109
122
  expect(mockDispatch).toHaveBeenCalledWith(`${ storeType }/find`, {
110
123
  type: 'custom.crd',
111
124
  id: 'my-resource',
@@ -125,22 +138,11 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
125
138
  `Resource API error - ${ storeType } - Failed to find resource pod/default/test-pod: Network error`
126
139
  );
127
140
  expect(consoleErrorSpy).toHaveBeenCalledWith(
128
- `Resource API error - ${ storeType } - Failed to find resource pod/default/test-pod: Network error`
141
+ `Resource API error - ${ storeType } - Failed to find resource pod/default/test-pod: Network error`,
142
+ expect.any(Error)
129
143
  );
130
144
  });
131
145
 
132
- it('should return null when dispatch returns undefined', async() => {
133
- // Arrange
134
- mockSchemaFor.mockReturnValue({ attributes: { namespaced: true } });
135
- mockDispatch.mockResolvedValue(undefined);
136
-
137
- // Act
138
- const result = await resourcesApi.find('pod', 'default/nonexistent-pod');
139
-
140
- // Assert
141
- expect(result).toBeNull();
142
- });
143
-
144
146
  it('should handle resources with different types correctly', async() => {
145
147
  // Arrange
146
148
  const mockDeployment = {
@@ -155,7 +157,7 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
155
157
  const result = await resourcesApi.find('apps.deployment', 'kube-system/test-deployment');
156
158
 
157
159
  // Assert
158
- expect(result).toStrictEqual(mockDeployment);
160
+ expect(result).toMatchObject(mockDeployment);
159
161
  expect(mockDispatch).toHaveBeenCalledWith(`${ storeType }/find`, {
160
162
  type: 'apps.deployment',
161
163
  id: 'kube-system/test-deployment',
@@ -179,7 +181,9 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
179
181
  const result = await resourcesApi.findFiltered('pod', { labelSelector });
180
182
 
181
183
  // Assert
182
- expect(result).toStrictEqual(mockResources);
184
+ expect(result).toHaveLength(2);
185
+ expect((result as any[])[0]).toMatchObject(mockResources[0]);
186
+ expect((result as any[])[1]).toMatchObject(mockResources[1]);
183
187
  expect(mockDispatch).toHaveBeenCalledWith(`${ storeType }/findLabelSelector`, {
184
188
  type: 'pod',
185
189
  matching: {
@@ -271,7 +275,8 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
271
275
  `Resource API error - ${ storeType } - Failed to find filtered resources pod: Network error`
272
276
  );
273
277
  expect(consoleErrorSpy).toHaveBeenCalledWith(
274
- `Resource API error - ${ storeType } - Failed to find filtered resources pod: Network error`
278
+ `Resource API error - ${ storeType } - Failed to find filtered resources pod: Network error`,
279
+ expect.any(Error)
275
280
  );
276
281
  });
277
282
 
@@ -303,8 +308,9 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
303
308
  } as any);
304
309
 
305
310
  // Assert
306
- expect(result).toStrictEqual(transientResponse);
307
311
  expect(result).toHaveProperty('data');
312
+ expect((result as any).data).toHaveLength(1);
313
+ expect((result as any).data[0]).toMatchObject({ metadata: { name: 'pod-1' } });
308
314
  });
309
315
 
310
316
  it('should find resources with pagination options when enabled', async() => {
@@ -328,7 +334,9 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
328
334
  const result = await resourcesApi.findFiltered('pod', paginationOptions as any);
329
335
 
330
336
  // Assert
331
- expect(result).toStrictEqual(mockResources);
337
+ expect(result).toHaveLength(2);
338
+ expect((result as any[])[0]).toMatchObject(mockResources[0]);
339
+ expect((result as any[])[1]).toMatchObject(mockResources[1]);
332
340
  expect(mockDispatch).toHaveBeenCalledWith(`${ storeType }/findPage`, {
333
341
  type: 'pod',
334
342
  opt: paginationOptions
@@ -361,9 +369,10 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
361
369
  const result = await resourcesApi.findFiltered('pod', paginationOptions as any);
362
370
 
363
371
  // Assert
364
- expect(result).toStrictEqual(transientResponse);
365
372
  expect(result).toHaveProperty('data');
366
373
  expect(result).toHaveProperty('pagination');
374
+ expect((result as any).data).toHaveLength(1);
375
+ expect((result as any).data[0]).toMatchObject({ metadata: { name: 'pod-1' } });
367
376
  });
368
377
 
369
378
  it('should throw error when pagination is not enabled', async() => {
@@ -382,7 +391,8 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
382
391
  `Resource API error - ${ storeType } - findFiltered requests with FindFilteredPageOptions are only supported when ui-sql-cache is enabled`
383
392
  );
384
393
  expect(consoleErrorSpy).toHaveBeenCalledWith(
385
- `Resource API error - ${ storeType } - findFiltered requests with FindFilteredPageOptions are only supported when ui-sql-cache is enabled`
394
+ `Resource API error - ${ storeType } - findFiltered requests with FindFilteredPageOptions are only supported when ui-sql-cache is enabled`,
395
+ undefined
386
396
  );
387
397
  expect(mockDispatch).not.toHaveBeenCalled();
388
398
  });
@@ -396,7 +406,8 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
396
406
  /findFiltered request was made with unknown options/
397
407
  );
398
408
  expect(consoleErrorSpy).toHaveBeenCalledWith(
399
- expect.stringContaining('findFiltered request was made with unknown options')
409
+ expect.stringContaining('findFiltered request was made with unknown options'),
410
+ undefined
400
411
  );
401
412
  });
402
413
 
@@ -442,7 +453,10 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
442
453
  const result = await resourcesApi.findAll('pod');
443
454
 
444
455
  // Assert
445
- expect(result).toStrictEqual(mockResources);
456
+ expect(result).toHaveLength(3);
457
+ expect((result as any[])[0]).toMatchObject(mockResources[0]);
458
+ expect((result as any[])[1]).toMatchObject(mockResources[1]);
459
+ expect((result as any[])[2]).toMatchObject(mockResources[2]);
446
460
  expect(mockDispatch).toHaveBeenCalledWith(`${ storeType }/findAll`, {
447
461
  type: 'pod',
448
462
  opt: {}
@@ -531,7 +545,8 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
531
545
  `Resource API error - ${ storeType } - Failed to find all resources pod: Network error`
532
546
  );
533
547
  expect(consoleErrorSpy).toHaveBeenCalledWith(
534
- `Resource API error - ${ storeType } - Failed to find all resources pod: Network error`
548
+ `Resource API error - ${ storeType } - Failed to find all resources pod: Network error`,
549
+ expect.any(Error)
535
550
  );
536
551
  });
537
552
 
@@ -547,4 +562,270 @@ describe.each(['cluster', 'management'] as const)('resourcesApiClassImpl with st
547
562
  expect(mockDispatch).toHaveBeenCalledTimes(1);
548
563
  });
549
564
  });
565
+
566
+ describe('create', () => {
567
+ it('should send a POST request with the resource data', async() => {
568
+ // Arrange
569
+ const inputData = {
570
+ type: 'configmap',
571
+ metadata: { name: 'my-config', namespace: 'default' },
572
+ };
573
+ const mockResponse = {
574
+ type: 'configmap',
575
+ metadata: { name: 'my-config', namespace: 'default' },
576
+ };
577
+
578
+ mockSchemaFor.mockReturnValue({
579
+ attributes: { namespaced: true },
580
+ linkFor: () => 'https://rancher/v1/configmaps'
581
+ });
582
+ mockDispatch.mockResolvedValue(mockResponse);
583
+
584
+ // Act
585
+ const result = await resourcesApi.create(inputData);
586
+
587
+ // Assert
588
+ expect(mockDispatch).toHaveBeenCalledWith(`${ storeType }/request`, {
589
+ opt: {
590
+ url: 'https://rancher/v1/configmaps',
591
+ method: 'POST',
592
+ headers: { 'content-type': 'application/json' },
593
+ data: inputData,
594
+ }
595
+ });
596
+ expect(result).toStrictEqual(mockResponse);
597
+ });
598
+
599
+ it('should throw when data.type is missing', async() => {
600
+ // Act & Assert
601
+ await expect(resourcesApi.create({} as any)).rejects.toThrow(
602
+ `Resource API error - ${ storeType } - Resource data must include a "type" property`
603
+ );
604
+ expect(mockDispatch).not.toHaveBeenCalled();
605
+ });
606
+
607
+ it('should throw error and log when request fails', async() => {
608
+ // Arrange
609
+ mockSchemaFor.mockReturnValue({
610
+ attributes: { namespaced: false },
611
+ linkFor: () => 'https://rancher/v1/configmaps'
612
+ });
613
+ mockDispatch.mockRejectedValue(new Error('Request failed'));
614
+
615
+ // Act & Assert
616
+ await expect(resourcesApi.create({ type: 'configmap' })).rejects.toThrow(
617
+ `Resource API error - ${ storeType } - Failed to create resource of type "configmap": Request failed`
618
+ );
619
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
620
+ `Resource API error - ${ storeType } - Failed to create resource of type "configmap": Request failed`,
621
+ expect.any(Error)
622
+ );
623
+ });
624
+ });
625
+
626
+ describe('update', () => {
627
+ it('should send a PATCH request with strategic-merge-patch content type', async() => {
628
+ // Arrange
629
+ const mockResponse = { metadata: { name: 'my-config' }, data: { key: 'patched' } };
630
+
631
+ mockSchemaFor.mockReturnValue({
632
+ attributes: { namespaced: true },
633
+ linkFor: () => 'https://rancher/v1/configmaps'
634
+ });
635
+ mockDispatch.mockResolvedValue(mockResponse);
636
+
637
+ // Act
638
+ const result = await resourcesApi.update('configmap', 'default/my-config', { data: { key: 'patched' } });
639
+
640
+ // Assert
641
+ expect(result).toStrictEqual(mockResponse);
642
+ expect(mockDispatch).toHaveBeenCalledWith(`${ storeType }/request`, {
643
+ opt: {
644
+ url: 'https://rancher/v1/configmaps/default/my-config',
645
+ method: 'patch',
646
+ headers: { 'content-type': 'application/strategic-merge-patch+json' },
647
+ data: { data: { key: 'patched' } },
648
+ }
649
+ });
650
+ });
651
+
652
+ it('should throw error for namespaced resource without namespace in id', async() => {
653
+ // Arrange
654
+ mockSchemaFor.mockReturnValue({ attributes: { namespaced: true } });
655
+
656
+ // Act & Assert
657
+ await expect(resourcesApi.update('configmap', 'my-config', {})).rejects.toThrow(
658
+ `Resource API error - ${ storeType } - Resource "configmap" is namespaced. The resourceId must be in "namespace/name" format, but received "my-config"`
659
+ );
660
+ });
661
+
662
+ it('should throw error when no schema is found', async() => {
663
+ // Arrange
664
+ mockSchemaFor.mockReturnValue(null);
665
+
666
+ // Act & Assert
667
+ await expect(resourcesApi.update('configmap', 'default/my-config', {})).rejects.toThrow(
668
+ 'No schema found for type "configmap"'
669
+ );
670
+ });
671
+
672
+ it('should throw error and log when request fails', async() => {
673
+ // Arrange
674
+ mockSchemaFor.mockReturnValue({
675
+ attributes: { namespaced: false },
676
+ linkFor: () => 'https://rancher/v1/nodes'
677
+ });
678
+ mockDispatch.mockRejectedValue(new Error('Network error'));
679
+
680
+ // Act & Assert
681
+ await expect(resourcesApi.update('node', 'worker-1', {})).rejects.toThrow(
682
+ `Resource API error - ${ storeType } - Failed to update resource node/worker-1: Network error`
683
+ );
684
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
685
+ `Resource API error - ${ storeType } - Failed to update resource node/worker-1: Network error`,
686
+ expect.any(Error)
687
+ );
688
+ });
689
+ });
690
+
691
+ describe('replace', () => {
692
+ it('should send a PUT request with cleanForSave applied', async() => {
693
+ // Arrange
694
+ const inputData = {
695
+ type: 'configmap',
696
+ metadata: { name: 'my-config', namespace: 'default' },
697
+ data: { key: 'value' },
698
+ };
699
+ const cleanedData = { ...inputData };
700
+ const mockModel = { cleanForSave: jest.fn<any, any[]>().mockReturnValue(cleanedData) };
701
+ const mockResponse = { ...inputData, metadata: { ...inputData.metadata, resourceVersion: '2' } };
702
+
703
+ mockSchemaFor.mockReturnValue({
704
+ attributes: { namespaced: true },
705
+ linkFor: () => 'https://rancher/v1/configmaps'
706
+ });
707
+ // First dispatch: create (returns model), second dispatch: request (returns response)
708
+ mockDispatch
709
+ .mockResolvedValueOnce(mockModel)
710
+ .mockResolvedValueOnce(mockResponse);
711
+
712
+ // Act
713
+ const result = await resourcesApi.replace('configmap', 'default/my-config', inputData);
714
+
715
+ // Assert
716
+ expect(result).toStrictEqual(mockResponse);
717
+ expect(mockDispatch).toHaveBeenCalledWith(`${ storeType }/create`, inputData);
718
+ expect(mockModel.cleanForSave).toHaveBeenCalledWith({ ...inputData });
719
+ expect(mockDispatch).toHaveBeenCalledWith(`${ storeType }/request`, {
720
+ opt: {
721
+ url: 'https://rancher/v1/configmaps/default/my-config',
722
+ method: 'put',
723
+ headers: { 'content-type': 'application/json' },
724
+ data: cleanedData,
725
+ }
726
+ });
727
+ });
728
+
729
+ it('should throw error for namespaced resource without namespace in id', async() => {
730
+ // Arrange
731
+ mockSchemaFor.mockReturnValue({ attributes: { namespaced: true } });
732
+
733
+ // Act & Assert
734
+ await expect(resourcesApi.replace('configmap', 'my-config', {})).rejects.toThrow(
735
+ `Resource API error - ${ storeType } - Resource "configmap" is namespaced. The resourceId must be in "namespace/name" format, but received "my-config"`
736
+ );
737
+ });
738
+
739
+ it('should throw error and log when request fails', async() => {
740
+ // Arrange
741
+ const mockModel = { cleanForSave: jest.fn<any, any[]>().mockReturnValue({}) };
742
+
743
+ mockSchemaFor.mockReturnValue({
744
+ attributes: { namespaced: false },
745
+ linkFor: () => 'https://rancher/v1/nodes'
746
+ });
747
+ mockDispatch
748
+ .mockResolvedValueOnce(mockModel)
749
+ .mockRejectedValueOnce(new Error('Conflict'));
750
+
751
+ // Act & Assert
752
+ await expect(resourcesApi.replace('node', 'worker-1', {})).rejects.toThrow(
753
+ `Resource API error - ${ storeType } - Failed to update resource node/worker-1: Conflict`
754
+ );
755
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
756
+ `Resource API error - ${ storeType } - Failed to update resource node/worker-1: Conflict`,
757
+ expect.any(Error)
758
+ );
759
+ });
760
+ });
761
+
762
+ describe('delete', () => {
763
+ it('should send a DELETE request with the correct URL', async() => {
764
+ // Arrange
765
+ mockSchemaFor.mockReturnValue({
766
+ attributes: { namespaced: true },
767
+ linkFor: () => 'https://rancher/v1/configmaps'
768
+ });
769
+ mockDispatch.mockResolvedValue(undefined);
770
+
771
+ // Act
772
+ await resourcesApi.delete('configmap', 'default/my-config');
773
+
774
+ // Assert
775
+ expect(mockDispatch).toHaveBeenCalledWith(`${ storeType }/request`, {
776
+ opt: {
777
+ url: 'https://rancher/v1/configmaps/default/my-config',
778
+ method: 'delete',
779
+ }
780
+ });
781
+ });
782
+
783
+ it('should work with cluster-scoped resources', async() => {
784
+ // Arrange
785
+ mockSchemaFor.mockReturnValue({
786
+ attributes: { namespaced: false },
787
+ linkFor: () => 'https://rancher/v1/nodes'
788
+ });
789
+ mockDispatch.mockResolvedValue(undefined);
790
+
791
+ // Act
792
+ await resourcesApi.delete('node', 'worker-1');
793
+
794
+ // Assert
795
+ expect(mockDispatch).toHaveBeenCalledWith(`${ storeType }/request`, {
796
+ opt: {
797
+ url: 'https://rancher/v1/nodes/worker-1',
798
+ method: 'delete',
799
+ }
800
+ });
801
+ });
802
+
803
+ it('should throw error for namespaced resource without namespace in id', async() => {
804
+ // Arrange
805
+ mockSchemaFor.mockReturnValue({ attributes: { namespaced: true } });
806
+
807
+ // Act & Assert
808
+ await expect(resourcesApi.delete('configmap', 'my-config')).rejects.toThrow(
809
+ `Resource API error - ${ storeType } - Resource "configmap" is namespaced. The resourceId must be in "namespace/name" format, but received "my-config"`
810
+ );
811
+ });
812
+
813
+ it('should throw error and log when request fails', async() => {
814
+ // Arrange
815
+ mockSchemaFor.mockReturnValue({
816
+ attributes: { namespaced: false },
817
+ linkFor: () => 'https://rancher/v1/nodes'
818
+ });
819
+ mockDispatch.mockRejectedValue(new Error('Not Found'));
820
+
821
+ // Act & Assert
822
+ await expect(resourcesApi.delete('node', 'worker-1')).rejects.toThrow(
823
+ `Resource API error - ${ storeType } - Failed to delete resource node/worker-1: Not Found`
824
+ );
825
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
826
+ `Resource API error - ${ storeType } - Failed to delete resource node/worker-1: Not Found`,
827
+ expect.any(Error)
828
+ );
829
+ });
830
+ });
550
831
  });