@rancher/shell 3.0.7 → 3.0.8-rc.2

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 (123) hide show
  1. package/assets/images/vendor/githubapp.svg +13 -0
  2. package/assets/styles/base/_typography.scss +1 -1
  3. package/assets/styles/global/_layout.scss +21 -35
  4. package/assets/styles/themes/_modern.scss +5 -5
  5. package/assets/translations/en-us.yaml +102 -17
  6. package/assets/translations/zh-hans.yaml +0 -4
  7. package/components/EmberPage.vue +1 -1
  8. package/components/Inactivity.vue +222 -106
  9. package/components/InstallHelmCharts.vue +2 -2
  10. package/components/Resource/Detail/CopyToClipboard.vue +1 -1
  11. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +0 -2
  12. package/components/Resource/Detail/TitleBar/index.vue +10 -6
  13. package/components/ResourceDetail/index.vue +4 -1
  14. package/components/SortableTable/index.vue +18 -2
  15. package/components/{nav/WindowManager → Window}/ContainerLogs.vue +1 -1
  16. package/components/{nav/WindowManager → Window}/ContainerLogsActions.vue +1 -0
  17. package/components/{nav/WindowManager → Window}/__tests__/ContainerLogs.test.ts +1 -1
  18. package/components/{nav/WindowManager → Window}/__tests__/ContainerShell.test.ts +2 -2
  19. package/components/fleet/FleetConfigMapSelector.vue +117 -0
  20. package/components/fleet/FleetSecretSelector.vue +127 -0
  21. package/components/fleet/__tests__/FleetConfigMapSelector.test.ts +125 -0
  22. package/components/fleet/__tests__/FleetSecretSelector.test.ts +82 -0
  23. package/components/form/FileImageSelector.vue +13 -4
  24. package/components/form/FileSelector.vue +11 -2
  25. package/components/form/ResourceLabeledSelect.vue +1 -0
  26. package/components/form/__tests__/ResourceLabeledSelect.test.ts +90 -0
  27. package/components/nav/Header.vue +34 -13
  28. package/components/{DraggableZone.vue → nav/WindowManager/PinArea.vue} +47 -80
  29. package/components/nav/WindowManager/composables/useComponentsMount.ts +70 -0
  30. package/components/nav/WindowManager/composables/useDimensionsHandler.ts +105 -0
  31. package/components/nav/WindowManager/composables/useDragHandler.ts +99 -0
  32. package/components/nav/WindowManager/composables/usePanelHandler.ts +72 -0
  33. package/components/nav/WindowManager/composables/usePanelsHandler.ts +14 -0
  34. package/components/nav/WindowManager/composables/useResizeHandler.ts +167 -0
  35. package/components/nav/WindowManager/composables/useTabsHandler.ts +51 -0
  36. package/components/nav/WindowManager/constants.ts +23 -0
  37. package/components/nav/WindowManager/index.vue +61 -575
  38. package/components/nav/WindowManager/panels/HorizontalPanel.vue +265 -0
  39. package/components/nav/WindowManager/panels/TabBodyContainer.vue +39 -0
  40. package/components/nav/WindowManager/panels/VerticalPanel.vue +308 -0
  41. package/components/templates/default.vue +4 -40
  42. package/components/templates/home.vue +31 -5
  43. package/config/product/auth.js +1 -0
  44. package/config/query-params.js +1 -0
  45. package/config/settings.ts +8 -1
  46. package/config/store.js +4 -2
  47. package/config/types.js +2 -0
  48. package/detail/pod.vue +1 -0
  49. package/dialog/AddonConfigConfirmationDialog.vue +45 -1
  50. package/directives/ui-context.ts +97 -0
  51. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +52 -11
  52. package/edit/auth/AuthProviderWarningBanners.vue +14 -1
  53. package/edit/auth/github-app-steps.vue +97 -0
  54. package/edit/auth/github-steps.vue +75 -0
  55. package/edit/auth/github.vue +94 -65
  56. package/edit/fleet.cattle.io.helmop.vue +51 -2
  57. package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +15 -5
  58. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +11 -9
  59. package/edit/provisioning.cattle.io.cluster/rke2.vue +56 -9
  60. package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +28 -2
  61. package/initialize/install-directives.js +2 -0
  62. package/list/projectsecret.vue +1 -1
  63. package/machine-config/azure.vue +1 -1
  64. package/mixins/chart.js +1 -1
  65. package/models/__tests__/chart.test.ts +17 -9
  66. package/models/__tests__/compliance.cattle.io.clusterscanprofile.spec.js +30 -0
  67. package/models/catalog.cattle.io.app.js +1 -1
  68. package/models/chart.js +3 -1
  69. package/models/compliance.cattle.io.clusterscanprofile.js +1 -1
  70. package/models/management.cattle.io.authconfig.js +1 -0
  71. package/package.json +2 -2
  72. package/pages/auth/login.vue +5 -2
  73. package/pages/auth/verify.vue +1 -1
  74. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +3 -2
  75. package/pages/c/_cluster/apps/charts/chart.vue +2 -2
  76. package/pages/c/_cluster/explorer/EventsTable.vue +89 -3
  77. package/pages/c/_cluster/explorer/tools/index.vue +3 -3
  78. package/pages/c/_cluster/settings/performance.vue +12 -25
  79. package/pages/home.vue +313 -12
  80. package/plugins/axios.js +2 -1
  81. package/plugins/dashboard-store/actions.js +1 -1
  82. package/plugins/dashboard-store/resource-class.js +17 -2
  83. package/plugins/steve/steve-pagination-utils.ts +2 -2
  84. package/rancher-components/RcDropdown/RcDropdownItemSelect.vue +5 -1
  85. package/scripts/extension/publish +1 -1
  86. package/store/auth.js +8 -3
  87. package/store/aws.js +8 -6
  88. package/store/features.js +1 -0
  89. package/store/index.js +9 -3
  90. package/store/prefs.js +6 -0
  91. package/store/ui-context.ts +86 -0
  92. package/store/wm.ts +244 -0
  93. package/types/kube/kube-api.ts +2 -1
  94. package/types/rancher/index.d.ts +1 -0
  95. package/types/resources/settings.d.ts +29 -7
  96. package/types/shell/index.d.ts +59 -0
  97. package/types/window-manager.ts +22 -0
  98. package/utils/__tests__/cluster.test.ts +379 -1
  99. package/utils/cluster.js +157 -3
  100. package/utils/dynamic-content/__tests__/config.test.ts +187 -0
  101. package/utils/dynamic-content/__tests__/index.test.ts +390 -0
  102. package/utils/dynamic-content/__tests__/info.test.ts +263 -0
  103. package/utils/dynamic-content/__tests__/new-release.test.ts +216 -0
  104. package/utils/dynamic-content/__tests__/support-notice.test.ts +262 -0
  105. package/utils/dynamic-content/__tests__/util.test.ts +235 -0
  106. package/utils/dynamic-content/config.ts +55 -0
  107. package/utils/dynamic-content/index.ts +273 -0
  108. package/utils/dynamic-content/info.ts +219 -0
  109. package/utils/dynamic-content/new-release.ts +126 -0
  110. package/utils/dynamic-content/support-notice.ts +169 -0
  111. package/utils/dynamic-content/types.d.ts +101 -0
  112. package/utils/dynamic-content/util.ts +122 -0
  113. package/utils/dynamic-importer.js +2 -2
  114. package/utils/inactivity.ts +104 -0
  115. package/utils/pagination-utils.ts +19 -4
  116. package/utils/release-notes.ts +1 -1
  117. package/assets/images/icons/document.svg +0 -3
  118. package/store/wm.js +0 -95
  119. /package/components/{nav/WindowManager → Window}/ChartReadme.vue +0 -0
  120. /package/components/{nav/WindowManager → Window}/ContainerShell.vue +0 -0
  121. /package/components/{nav/WindowManager → Window}/KubectlShell.vue +0 -0
  122. /package/components/{nav/WindowManager → Window}/MachineSsh.vue +0 -0
  123. /package/components/{nav/WindowManager → Window}/Window.vue +0 -0
@@ -0,0 +1,187 @@
1
+ import { getConfig } from '../config';
2
+ import { SETTING } from '@shell/config/settings';
3
+ import * as version from '@shell/config/version';
4
+
5
+ const DEFAULT_ENDPOINT = 'https://updates.rancher.io/rancher/$dist/updates';
6
+
7
+ // Mock dependencies
8
+ jest.mock('@shell/config/version', () => ({ getVersionData: jest.fn(), isRancherPrime: jest.fn() }));
9
+
10
+ describe('getConfig', () => {
11
+ let mockGetters: any;
12
+
13
+ beforeEach(() => {
14
+ // Reset mocks before each test
15
+ (version.getVersionData as jest.Mock).mockClear();
16
+ (version.isRancherPrime as jest.Mock).mockClear();
17
+
18
+ // Default mock for getters
19
+ mockGetters = { 'management/byId': jest.fn() };
20
+ });
21
+
22
+ describe('community distribution', () => {
23
+ beforeEach(() => {
24
+ (version.getVersionData as jest.Mock).mockReturnValue({ RancherPrime: 'false' });
25
+ (version.isRancherPrime as jest.Mock).mockReturnValue(false);
26
+ });
27
+
28
+ it('should return default community config when no settings are present', () => {
29
+ mockGetters['management/byId'].mockReturnValue(null);
30
+ const config = getConfig(mockGetters);
31
+
32
+ expect(config.prime).toBe(false);
33
+ expect(config.distribution).toBe('community');
34
+ expect(config.endpoint).toBe(DEFAULT_ENDPOINT);
35
+ expect(config.enabled).toBe(true);
36
+ expect(config.debug).toBe(false);
37
+ expect(config.log).toBe(false);
38
+ });
39
+
40
+ it('should enable debug and log modes when enabled setting is "debug"', () => {
41
+ mockGetters['management/byId'].mockImplementation((type: string, id: string) => {
42
+ if (id === SETTING.DYNAMIC_CONTENT_ENABLED) {
43
+ return { value: 'debug' };
44
+ }
45
+ if (id === SETTING.DYNAMIC_CONTENT_ENDPOINT) {
46
+ return { value: 'https://test.endpoint' };
47
+ }
48
+
49
+ return null;
50
+ });
51
+
52
+ const config = getConfig(mockGetters);
53
+
54
+ expect(config.enabled).toBe(true);
55
+ expect(config.debug).toBe(true);
56
+ expect(config.log).toBe(true);
57
+ });
58
+
59
+ it('should enable log mode when enabled setting is "log"', () => {
60
+ mockGetters['management/byId'].mockImplementation((type: string, id: string) => {
61
+ if (id === SETTING.DYNAMIC_CONTENT_ENABLED) {
62
+ return { value: 'log' };
63
+ }
64
+ if (id === SETTING.DYNAMIC_CONTENT_ENDPOINT) {
65
+ return { value: 'https://test.endpoint' };
66
+ }
67
+
68
+ return null;
69
+ });
70
+
71
+ const config = getConfig(mockGetters);
72
+
73
+ expect(config.enabled).toBe(true);
74
+ expect(config.debug).toBe(false);
75
+ expect(config.log).toBe(true);
76
+ });
77
+
78
+ it('should not use the endpoint from settings when community', () => {
79
+ mockGetters['management/byId'].mockImplementation((type: string, id: string) => {
80
+ if (id === SETTING.DYNAMIC_CONTENT_ENDPOINT) {
81
+ return { value: 'https://custom.endpoint/rancher/$dist' };
82
+ }
83
+
84
+ return null;
85
+ });
86
+
87
+ const config = getConfig(mockGetters);
88
+
89
+ expect(config.endpoint).toBe(DEFAULT_ENDPOINT);
90
+ expect(config.enabled).toBe(true);
91
+ });
92
+ });
93
+
94
+ describe('prime distribution', () => {
95
+ beforeEach(() => {
96
+ (version.getVersionData as jest.Mock).mockReturnValue({ RancherPrime: 'true' });
97
+ (version.isRancherPrime as jest.Mock).mockReturnValue(true);
98
+ });
99
+
100
+ it('should return default prime config when no settings are present', () => {
101
+ mockGetters['management/byId'].mockReturnValue(null);
102
+ const config = getConfig(mockGetters);
103
+
104
+ expect(config.prime).toBe(true);
105
+ expect(config.distribution).toBe('prime');
106
+ expect(config.endpoint).toBe(DEFAULT_ENDPOINT);
107
+ expect(config.enabled).toBe(true);
108
+ });
109
+
110
+ it('should be disabled when the setting is "false"', () => {
111
+ mockGetters['management/byId'].mockImplementation((type: string, id: string) => {
112
+ if (id === SETTING.DYNAMIC_CONTENT_ENABLED) {
113
+ return { value: 'false' };
114
+ }
115
+ if (id === SETTING.DYNAMIC_CONTENT_ENDPOINT) {
116
+ return { value: 'https://test.endpoint' };
117
+ }
118
+
119
+ return null;
120
+ });
121
+
122
+ const config = getConfig(mockGetters);
123
+
124
+ expect(config.enabled).toBe(false);
125
+ });
126
+
127
+ it('should be enabled when the setting is "true"', () => {
128
+ mockGetters['management/byId'].mockImplementation((type: string, id: string) => {
129
+ if (id === SETTING.DYNAMIC_CONTENT_ENABLED) {
130
+ return { value: 'true' };
131
+ }
132
+ if (id === SETTING.DYNAMIC_CONTENT_ENDPOINT) {
133
+ return { value: 'https://test.endpoint' };
134
+ }
135
+
136
+ return null;
137
+ });
138
+
139
+ const config = getConfig(mockGetters);
140
+
141
+ expect(config.enabled).toBe(true);
142
+ });
143
+
144
+ it('should NOT use the endpoint from settings if it is not a valid HTTPS URL', () => {
145
+ mockGetters['management/byId'].mockImplementation((type: string, id: string) => {
146
+ if (id === SETTING.DYNAMIC_CONTENT_ENDPOINT) {
147
+ return { value: 'http://insecure.endpoint/rancher/$dist' };
148
+ }
149
+
150
+ return null;
151
+ });
152
+
153
+ const config = getConfig(mockGetters);
154
+
155
+ expect(config.endpoint).toBe(DEFAULT_ENDPOINT); // Falls back to default
156
+ expect(config.enabled).toBe(true);
157
+ });
158
+ });
159
+
160
+ describe('general', () => {
161
+ it('should handle exceptions when reading settings and return defaults', () => {
162
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
163
+ const testError = new Error('Something went wrong');
164
+
165
+ (version.getVersionData as jest.Mock).mockReturnValue({ RancherPrime: 'false' });
166
+ (version.isRancherPrime as jest.Mock).mockReturnValue(false);
167
+ mockGetters['management/byId'].mockImplementation(() => {
168
+ throw testError;
169
+ });
170
+
171
+ const config = getConfig(mockGetters);
172
+
173
+ // It should fall back to the default configuration
174
+ expect(config.prime).toBe(false);
175
+ expect(config.distribution).toBe('community');
176
+ expect(config.endpoint).toBe(DEFAULT_ENDPOINT);
177
+ expect(config.enabled).toBe(true);
178
+ expect(config.debug).toBe(false);
179
+ expect(config.log).toBe(false);
180
+
181
+ // And it should log the error
182
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Error reading dynamic content settings', testError);
183
+
184
+ consoleErrorSpy.mockRestore();
185
+ });
186
+ });
187
+ });
@@ -0,0 +1,390 @@
1
+ import { fetchAndProcessDynamicContent, fetchDynamicContent, UPDATE_DATE_FORMAT } from '../index';
2
+ import * as config from '../config';
3
+ import * as util from '../util';
4
+ import * as newRelease from '../new-release';
5
+ import * as supportNotice from '../support-notice';
6
+ import * as info from '../info';
7
+ import * as typeMap from '@shell/store/type-map';
8
+ import * as version from '@shell/config/version';
9
+ import * as jsyaml from 'js-yaml';
10
+ import dayjs from 'dayjs';
11
+ import { Context } from '../types';
12
+
13
+ // Mock dependencies
14
+ jest.mock('../config');
15
+ jest.mock('../util');
16
+ jest.mock('../new-release');
17
+ jest.mock('../support-notice');
18
+ jest.mock('../info');
19
+ jest.mock('@shell/store/type-map');
20
+ jest.mock('@shell/config/version');
21
+ jest.mock('js-yaml');
22
+
23
+ // Mock dayjs to control time
24
+ const mockDayInstance = {
25
+ format: jest.fn(() => '2023-01-01'),
26
+ add: jest.fn((amount, unit) => {
27
+ return dayjs('2023-01-01').add(amount, unit);
28
+ }),
29
+ diff: jest.fn(() => 100), // a value > 30 for concurrent check
30
+ toString: jest.fn(() => new Date('2023-01-01T12:00:00Z').toString()),
31
+ isAfter: jest.fn(),
32
+ isValid: jest.fn(() => true),
33
+ };
34
+
35
+ const mockGetConfig = config.getConfig as jest.Mock;
36
+ const mockCreateLogger = util.createLogger as jest.Mock;
37
+ const mockProcessReleaseVersion = newRelease.processReleaseVersion as jest.Mock;
38
+ const mockProcessSupportNotices = supportNotice.processSupportNotices as jest.Mock;
39
+ const mockIsAdminUser = typeMap.isAdminUser as jest.Mock;
40
+ const mockGetVersionData = version.getVersionData as jest.Mock;
41
+ const mockYamlLoad = jsyaml.load as jest.Mock;
42
+ const mockSystemInfoProvider = info.SystemInfoProvider as jest.Mock;
43
+
44
+ describe('dynamic content', () => {
45
+ let mockDispatch: jest.Mock;
46
+ let mockGetters: any;
47
+ let mockAxios: jest.Mock;
48
+ let mockLogger: any;
49
+ let mockConfig: any;
50
+ let localStorageMock: any;
51
+ let consoleLogSpy: jest.SpyInstance;
52
+
53
+ beforeEach(() => {
54
+ mockDispatch = jest.fn();
55
+ mockGetters = { isSingleProduct: false };
56
+ mockAxios = jest.fn();
57
+ mockLogger = {
58
+ debug: jest.fn(),
59
+ info: jest.fn(),
60
+ error: jest.fn()
61
+ };
62
+ mockConfig = {
63
+ enabled: true,
64
+ debug: false,
65
+ log: false,
66
+ prime: false,
67
+ endpoint: '$dist'
68
+ };
69
+
70
+ mockGetConfig.mockReturnValue(mockConfig);
71
+ mockCreateLogger.mockReturnValue(mockLogger);
72
+ mockIsAdminUser.mockReturnValue(true);
73
+ mockGetVersionData.mockReturnValue({ Version: '2.9.0', RancherPrime: 'false' });
74
+ mockSystemInfoProvider.mockImplementation(() => ({ buildQueryString: () => 'qs=1' }));
75
+
76
+ // Mock localStorage
77
+ let store: { [key: string]: string } = {};
78
+
79
+ localStorageMock = {
80
+ getItem: (key: string) => store[key] || null,
81
+ setItem: (key: string, value: string) => {
82
+ store[key] = value.toString();
83
+ },
84
+ removeItem: (key: string) => {
85
+ delete store[key];
86
+ },
87
+ clear: () => {
88
+ store = {};
89
+ }
90
+ };
91
+ Object.defineProperty(window, 'localStorage', { value: localStorageMock });
92
+
93
+ jest.useFakeTimers();
94
+
95
+ // Mock console
96
+ consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
97
+ });
98
+
99
+ afterEach(() => {
100
+ jest.clearAllMocks();
101
+ jest.useRealTimers();
102
+ localStorageMock.clear();
103
+ });
104
+
105
+ describe('fetchAndProcessDynamicContent', () => {
106
+ it('should exit early if in single product mode', async() => {
107
+ mockGetters.isSingleProduct = true;
108
+
109
+ const ret = fetchAndProcessDynamicContent(mockDispatch, mockGetters, mockAxios);
110
+
111
+ jest.runAllTimers();
112
+
113
+ await ret;
114
+ expect(mockGetConfig).not.toHaveBeenCalled();
115
+ });
116
+
117
+ it('should exit early if dynamic content is disabled', async() => {
118
+ mockConfig.enabled = false;
119
+ await fetchAndProcessDynamicContent(mockDispatch, mockGetters, mockAxios);
120
+
121
+ expect(mockLogger.debug).not.toHaveBeenCalled();
122
+ expect(consoleLogSpy).toHaveBeenCalledWith('Dynamic content disabled through configuration'); // eslint-disable-line no-console
123
+ });
124
+
125
+ it('should exit early if version is invalid', async() => {
126
+ mockGetVersionData.mockReturnValue({ Version: 'invalid' });
127
+ const ret = fetchAndProcessDynamicContent(mockDispatch, mockGetters, mockAxios);
128
+
129
+ jest.runAllTimers();
130
+
131
+ await ret;
132
+ expect(mockProcessReleaseVersion).not.toHaveBeenCalled();
133
+ });
134
+
135
+ it('should process content for community users', async() => {
136
+ const content = { releases: [], support: {} };
137
+
138
+ mockAxios.mockResolvedValue({ data: 'yaml: data' });
139
+ mockYamlLoad.mockReturnValue(content);
140
+
141
+ const ret = fetchAndProcessDynamicContent(mockDispatch, mockGetters, mockAxios);
142
+
143
+ jest.runAllTimers();
144
+ await ret;
145
+
146
+ expect(mockProcessReleaseVersion).toHaveBeenCalledTimes(1);
147
+ expect(mockProcessSupportNotices).toHaveBeenCalledTimes(1);
148
+ });
149
+
150
+ it('should process content for prime admin users', async() => {
151
+ mockConfig.prime = true;
152
+ mockIsAdminUser.mockReturnValue(true);
153
+ const content = { releases: [], support: {} };
154
+
155
+ mockAxios.mockResolvedValue({ data: 'yaml: data' });
156
+ mockYamlLoad.mockReturnValue(content);
157
+
158
+ const ret = fetchAndProcessDynamicContent(mockDispatch, mockGetters, mockAxios);
159
+
160
+ jest.runAllTimers();
161
+ await ret;
162
+
163
+ expect(mockProcessReleaseVersion).toHaveBeenCalledTimes(1);
164
+ expect(mockProcessSupportNotices).toHaveBeenCalledTimes(1);
165
+ });
166
+
167
+ it('should NOT process content for prime non-admin users', async() => {
168
+ mockConfig.prime = true;
169
+ mockIsAdminUser.mockReturnValue(false);
170
+ const content = { releases: [], support: {} };
171
+
172
+ mockAxios.mockResolvedValue({ data: 'yaml: data' });
173
+ mockYamlLoad.mockReturnValue(content);
174
+
175
+ const ret = fetchAndProcessDynamicContent(mockDispatch, mockGetters, mockAxios);
176
+
177
+ jest.runAllTimers();
178
+ await ret;
179
+
180
+ expect(mockProcessReleaseVersion).not.toHaveBeenCalled();
181
+ expect(mockProcessSupportNotices).not.toHaveBeenCalled();
182
+ });
183
+
184
+ it('should override version if debugVersion is set', async() => {
185
+ const content = { settings: { debugVersion: '2.10.0' } };
186
+
187
+ mockAxios.mockResolvedValue({ data: content });
188
+ mockYamlLoad.mockReturnValue(content);
189
+
190
+ const ret = fetchAndProcessDynamicContent(mockDispatch, mockGetters, mockAxios);
191
+
192
+ jest.runAllTimers();
193
+ await ret;
194
+
195
+ expect(mockLogger.debug).toHaveBeenCalledWith('Read configuration', {
196
+ debug: false, enabled: true, endpoint: '$dist', log: false, prime: false
197
+ });
198
+ expect(mockLogger.debug).toHaveBeenCalledWith('Overriding version number to 2.10.0');
199
+ expect(mockProcessReleaseVersion).toHaveBeenCalledWith(
200
+ expect.any(Object),
201
+ undefined,
202
+ expect.objectContaining({ version: expect.objectContaining({ version: '2.10.0' }) })
203
+ );
204
+ });
205
+
206
+ it('should clear logs if logging is disabled', async() => {
207
+ mockConfig.log = false;
208
+ localStorageMock.setItem('rancher-updates-debug-log', '[]');
209
+ mockAxios.mockResolvedValue({ data: '' });
210
+
211
+ const ret = fetchAndProcessDynamicContent(mockDispatch, mockGetters, mockAxios);
212
+
213
+ jest.runAllTimers();
214
+ await ret;
215
+
216
+ expect(localStorageMock.getItem('rancher-updates-debug-log')).toBeNull();
217
+ });
218
+
219
+ it('should handle errors during processing', async() => {
220
+ const error = new Error('Processing failed');
221
+
222
+ mockProcessReleaseVersion.mockImplementation(() => {
223
+ throw error;
224
+ });
225
+ mockAxios.mockResolvedValue({ data: '' });
226
+
227
+ const ret = fetchAndProcessDynamicContent(mockDispatch, mockGetters, mockAxios);
228
+
229
+ jest.runAllTimers();
230
+ await ret;
231
+
232
+ expect(mockLogger.error).toHaveBeenCalledWith('Error reading or processing dynamic content', error);
233
+ });
234
+ });
235
+
236
+ describe('fetchDynamicContent', () => {
237
+ let context: Context;
238
+
239
+ beforeEach(() => {
240
+ context = {
241
+ dispatch: mockDispatch,
242
+ getters: mockGetters,
243
+ axios: mockAxios,
244
+ logger: mockLogger,
245
+ config: mockConfig,
246
+ isAdmin: true,
247
+ settings: { releaseNotesUrl: '', suseExtensions: [] },
248
+ };
249
+ });
250
+
251
+ it('should fetch and parse content when needed', async() => {
252
+ const content = { version: '1.0' };
253
+
254
+ mockAxios.mockResolvedValue({ data: 'yaml: data' });
255
+ mockYamlLoad.mockReturnValue(content);
256
+
257
+ const fetchPromise = fetchDynamicContent(context);
258
+
259
+ jest.runAllTimers();
260
+ const result = await fetchPromise;
261
+
262
+ expect(mockAxios).toHaveBeenCalledTimes(1);
263
+ expect(mockYamlLoad).toHaveBeenCalledWith('yaml: data');
264
+ expect(result).toStrictEqual(content);
265
+
266
+ const tomorrow = dayjs().add(1, 'day').format(UPDATE_DATE_FORMAT);
267
+
268
+ expect(localStorageMock.getItem('rancher-updates-last-content')).toBe(JSON.stringify(content));
269
+ // Check updateFetchInfo(false) was called
270
+ expect(localStorageMock.getItem('rancher-updates-fetch-next')).toBe(tomorrow);
271
+ expect(localStorageMock.getItem('rancher-updates-fetch-errors')).toBeNull();
272
+ });
273
+
274
+ it('should skip fetch if not due', async() => {
275
+ const today = dayjs().format(UPDATE_DATE_FORMAT);
276
+ const tomorrow = dayjs().add(1, 'day').format(UPDATE_DATE_FORMAT);
277
+
278
+ localStorageMock.setItem('rancher-updates-fetch-next', tomorrow);
279
+ const fetchPromise = fetchDynamicContent(context);
280
+
281
+ jest.runAllTimers();
282
+ await fetchPromise;
283
+
284
+ expect(mockAxios).not.toHaveBeenCalled();
285
+ expect(mockLogger.info).toHaveBeenCalledWith(`Skipping update check for dynamic content - next check due on ${ tomorrow } (today is ${ today })`);
286
+ });
287
+
288
+ it('should skip fetch if another is in progress', async() => {
289
+ const today = dayjs().format(UPDATE_DATE_FORMAT);
290
+ const later = dayjs().subtract(10, 'second').toString();
291
+
292
+ localStorageMock.setItem('rancher-updates-fetch-next', today);
293
+ localStorageMock.setItem('rancher-updates-fetching', later);
294
+
295
+ const fetchPromise = fetchDynamicContent(context);
296
+
297
+ jest.runAllTimers();
298
+ await fetchPromise;
299
+
300
+ expect(mockAxios).not.toHaveBeenCalled();
301
+ expect(mockLogger.debug).toHaveBeenCalledWith('Already fetching dynamic content in another tab (or previous tab closed while fetching) - skipping');
302
+ });
303
+
304
+ it('should proceed with fetch if a stale fetch was in progress', async() => {
305
+ (mockDayInstance.diff as jest.Mock).mockReturnValue(100); // more than 30s
306
+ localStorageMock.setItem('rancher-updates-fetching', new Date('2023-01-01T11:00:00Z').toString());
307
+ mockAxios.mockResolvedValue({ data: '' });
308
+
309
+ const fetchPromise = fetchDynamicContent(context);
310
+
311
+ jest.runAllTimers();
312
+ await fetchPromise;
313
+
314
+ expect(mockAxios).toHaveBeenCalledTimes(1);
315
+ });
316
+
317
+ it('should handle axios fetch error and backoff', async() => {
318
+ const error = new Error('Network Error');
319
+
320
+ mockAxios.mockRejectedValue(error);
321
+
322
+ const fetchPromise = fetchDynamicContent(context);
323
+
324
+ jest.runAllTimers();
325
+ await fetchPromise;
326
+
327
+ const tomorrow = dayjs().add(1, 'day').format(UPDATE_DATE_FORMAT);
328
+
329
+ expect(mockLogger.error).toHaveBeenCalledWith('Error occurred reading dynamic content', error);
330
+ // Check updateFetchInfo(true) was called
331
+ expect(localStorageMock.getItem('rancher-updates-fetch-errors')).toBe('1');
332
+ expect(localStorageMock.getItem('rancher-updates-fetch-next')).toBe(tomorrow); // 1 day backoff
333
+ });
334
+
335
+ it('should handle YAML parsing error', async() => {
336
+ const error = new Error('YAML Error');
337
+
338
+ mockAxios.mockResolvedValue({ data: 'invalid yaml' });
339
+ mockYamlLoad.mockImplementation(() => {
340
+ throw error;
341
+ });
342
+
343
+ const fetchPromise = fetchDynamicContent(context);
344
+
345
+ jest.runAllTimers();
346
+ await fetchPromise;
347
+
348
+ expect(mockLogger.error).toHaveBeenCalledWith('Failed to parse YAML/JSON from dynamic content package', error);
349
+ });
350
+
351
+ it('should handle axios fetch error with unexpected data', async() => {
352
+ mockAxios.mockResolvedValue({ data: null });
353
+
354
+ const fetchPromise = fetchDynamicContent(context);
355
+
356
+ jest.runAllTimers();
357
+ await fetchPromise;
358
+
359
+ expect(mockLogger.error).toHaveBeenCalledWith('Error fetching dynamic content package (unexpected data)');
360
+ });
361
+
362
+ it('should increment backoff on multiple consecutive errors', async() => {
363
+ const error = new Error('Network Error');
364
+
365
+ mockAxios.mockRejectedValue(error);
366
+ localStorageMock.setItem('rancher-updates-fetch-errors', '2'); // 2 previous errors
367
+
368
+ const fetchPromise = fetchDynamicContent(context);
369
+
370
+ jest.runAllTimers();
371
+ await fetchPromise;
372
+
373
+ expect(localStorageMock.getItem('rancher-updates-fetch-errors')).toBe('3');
374
+ });
375
+
376
+ it('should cap backoff at max value', async() => {
377
+ const error = new Error('Network Error');
378
+
379
+ mockAxios.mockRejectedValue(error);
380
+ localStorageMock.setItem('rancher-updates-fetch-errors', '10');
381
+
382
+ const fetchPromise = fetchDynamicContent(context);
383
+
384
+ jest.runAllTimers();
385
+ await fetchPromise;
386
+
387
+ expect(localStorageMock.getItem('rancher-updates-fetch-errors')).toBe('6');
388
+ });
389
+ });
390
+ });