@rancher/shell 3.0.12-rc.1 → 3.0.12-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.
- package/assets/images/providers/entraid-black.svg +4 -0
- package/assets/images/providers/entraid.svg +9 -0
- package/assets/images/vendor/entraid.svg +9 -0
- package/assets/styles/app.scss +0 -1
- package/assets/translations/en-us.yaml +19 -17
- package/assets/translations/zh-hans.yaml +4 -8
- package/chart/__tests__/S3.test.ts +10 -3
- package/components/CountBox.vue +20 -0
- package/components/CreateDriver.vue +0 -12
- package/components/DetailText.vue +12 -3
- package/components/SelectIconGrid.vue +5 -0
- package/components/__tests__/CountBox.test.ts +72 -0
- package/components/__tests__/DetailText.test.ts +113 -0
- package/components/fleet/FleetClusterTargets/index.vue +18 -1
- package/components/form/InputWithSelect.vue +18 -10
- package/components/form/KeyValue.vue +17 -1
- package/components/form/LabeledSelect.vue +82 -24
- package/components/form/Select.vue +73 -56
- package/components/form/ServiceNameSelect.vue +13 -11
- package/components/form/__tests__/KeyValue.test.ts +66 -0
- package/components/form/__tests__/NodeScheduling.test.ts +9 -0
- package/components/form/labeled-select-utils/useLabeledSelectPagination.ts +138 -0
- package/components/nav/Group.vue +7 -6
- package/components/nav/Header.vue +24 -3
- package/components/nav/NotificationCenter/Notification.vue +4 -1
- package/components/nav/NotificationCenter/NotificationHeader.vue +20 -8
- package/components/nav/NotificationCenter/__tests__/NotificationHeader.test.ts +80 -0
- package/components/nav/Type.vue +8 -7
- package/components/nav/WindowManager/index.vue +2 -1
- package/components/nav/WorkspaceSwitcher.vue +13 -0
- package/components/nav/__tests__/Group.test.ts +67 -0
- package/components/nav/__tests__/Header.test.ts +235 -0
- package/components/nav/__tests__/Type.test.ts +20 -3
- package/components/templates/default.vue +34 -4
- package/components/templates/home.vue +12 -25
- package/components/templates/plain.vue +13 -26
- package/composables/useLabeledFormElement.ts +10 -2
- package/composables/useLabeledSelect.ts +60 -0
- package/composables/useUserRetentionValidation.ts +1 -49
- package/config/cookies.js +0 -1
- package/config/labels-annotations.js +1 -0
- package/config/query-params.js +1 -0
- package/config/router/routes.js +0 -8
- package/core/__tests__/plugin-products.test.ts +616 -25
- package/core/plugin-products-base.ts +31 -14
- package/core/plugin-products-helpers.ts +5 -4
- package/core/plugin-types.ts +18 -3
- package/core/types.ts +3 -1
- package/detail/__tests__/management.cattle.io.fleetworkspace.test.ts +128 -0
- package/detail/management.cattle.io.fleetworkspace.vue +49 -0
- package/edit/__tests__/fleet.cattle.io.helmop.test.ts +9 -0
- package/edit/__tests__/kontainerDriver.test.ts +0 -13
- package/edit/__tests__/nodeDriver.test.ts +5 -11
- package/edit/__tests__/resources.cattle.io.restore.test.ts +9 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
- package/edit/auth/__tests__/oidc.test.ts +54 -0
- package/edit/auth/azuread.vue +1 -1
- package/edit/auth/oidc.vue +8 -0
- package/edit/kontainerDriver.vue +1 -2
- package/edit/nodeDriver.vue +0 -2
- package/edit/provisioning.cattle.io.cluster/AgentEnv.vue +1 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/AgentEnv.test.ts +25 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +70 -99
- package/initialize/App.vue +29 -2
- package/initialize/install-plugins.js +0 -2
- package/list/__tests__/management.cattle.io.feature.test.ts +105 -0
- package/list/catalog.cattle.io.app.vue +25 -5
- package/list/management.cattle.io.feature.vue +1 -1
- package/list/management.cattle.io.fleetworkspace.vue +8 -0
- package/machine-config/amazonec2.vue +1 -0
- package/mixins/chart.js +40 -9
- package/models/__tests__/catalog.cattle.io.app.test.ts +15 -1
- package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +84 -0
- package/models/__tests__/chart.test.ts +99 -6
- package/models/__tests__/management.cattle.io.feature.test.ts +131 -0
- package/models/__tests__/monitoring.coreos.com.alertmanagerconfig.test.ts +98 -0
- package/models/catalog.cattle.io.app.js +21 -17
- package/models/catalog.cattle.io.clusterrepo.js +39 -11
- package/models/chart.js +33 -19
- package/models/fleet-application.js +1 -1
- package/models/fleet.cattle.io.bundle.js +1 -1
- package/models/kontainerdriver.js +11 -0
- package/models/management.cattle.io.authconfig.js +5 -1
- package/models/management.cattle.io.cluster.js +0 -53
- package/models/management.cattle.io.feature.js +3 -3
- package/models/management.cattle.io.kontainerdriver.js +1 -26
- package/models/monitoring.coreos.com.alertmanagerconfig.js +31 -17
- package/models/nodedriver.js +7 -0
- package/package.json +13 -12
- package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +189 -0
- package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +55 -0
- package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +53 -0
- package/pages/c/_cluster/apps/charts/chart.vue +217 -33
- package/pages/c/_cluster/apps/charts/index.vue +2 -2
- package/pages/c/_cluster/apps/charts/install.vue +8 -3
- package/pages/c/_cluster/auth/user.retention/index.vue +55 -22
- package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +5 -7
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +39 -2
- package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +61 -0
- package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +15 -10
- package/pages/c/_cluster/uiplugins/index.vue +23 -25
- package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +205 -1
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +82 -4
- package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +1 -1
- package/scripts/test-plugins-build.sh +5 -2
- package/server/server-middleware.js +2 -2
- package/static/humans.txt +1 -0
- package/static/robots.txt +34 -0
- package/static/welcome-cow.svg +18 -0
- package/store/__tests__/catalog.test.ts +161 -11
- package/store/auth.js +0 -3
- package/store/catalog.js +60 -8
- package/types/shell/index.d.ts +26 -22
- package/utils/__tests__/git.test.ts +270 -0
- package/utils/__tests__/inactivity.test.ts +316 -0
- package/utils/__tests__/object.test.ts +77 -0
- package/utils/__tests__/time.test.ts +14 -1
- package/utils/__tests__/url.test.ts +246 -0
- package/utils/object.js +33 -2
- package/utils/time.ts +5 -0
- package/vue.config.js +0 -9
- package/assets/images/providers/azuread-black.svg +0 -22
- package/assets/images/providers/azuread.svg +0 -25
- package/assets/images/vendor/azuread.svg +0 -18
- package/assets/styles/fonts/_dots.scss +0 -18
- package/components/EmberPage.vue +0 -622
- package/components/EmberPageView.vue +0 -39
- package/components/form/labeled-select-utils/labeled-select-pagination.ts +0 -116
- package/mixins/labeled-form-element.ts +0 -225
- package/pages/c/_cluster/explorer/tools/pages/_page.vue +0 -28
- package/pages/c/_cluster/manager/pages/_page.vue +0 -22
- package/pages/c/_cluster/mcapps/pages/_page.vue +0 -22
- package/plugins/ember-cookie.js +0 -17
- package/utils/ember-page.js +0 -30
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { Inactivity } from '@shell/utils/inactivity';
|
|
2
|
+
|
|
3
|
+
describe('inactivity', () => {
|
|
4
|
+
describe('inactivity class', () => {
|
|
5
|
+
let inactivity: Inactivity;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
inactivity = new Inactivity();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('getSessionTokenName', () => {
|
|
12
|
+
it('returns undefined by default', () => {
|
|
13
|
+
expect(inactivity.getSessionTokenName()).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('returns the token name after setSessionTokenName', () => {
|
|
17
|
+
inactivity.setSessionTokenName('my-token');
|
|
18
|
+
expect(inactivity.getSessionTokenName()).toStrictEqual('my-token');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('setSessionTokenName', () => {
|
|
23
|
+
it('sets the session token name', () => {
|
|
24
|
+
inactivity.setSessionTokenName('token-abc');
|
|
25
|
+
expect(inactivity.getSessionTokenName()).toStrictEqual('token-abc');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('overwrites a previously set token name', () => {
|
|
29
|
+
inactivity.setSessionTokenName('first');
|
|
30
|
+
inactivity.setSessionTokenName('second');
|
|
31
|
+
expect(inactivity.getSessionTokenName()).toStrictEqual('second');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('getUserActivity', () => {
|
|
36
|
+
it('dispatches management/find with correct parameters', async() => {
|
|
37
|
+
const mockResult = { status: { expiresAt: '2026-01-01T00:00:00Z' } };
|
|
38
|
+
const mockStore = { dispatch: jest.fn().mockResolvedValue(mockResult) };
|
|
39
|
+
|
|
40
|
+
const result = await inactivity.getUserActivity(mockStore, 'my-token');
|
|
41
|
+
|
|
42
|
+
expect(mockStore.dispatch).toHaveBeenCalledWith('management/find', {
|
|
43
|
+
type: 'ext.cattle.io.useractivity',
|
|
44
|
+
id: 'my-token',
|
|
45
|
+
opt: {
|
|
46
|
+
force: true, watch: false, logoutOnError: false
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
expect(result).toStrictEqual(mockResult);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('passes force=false when specified', async() => {
|
|
53
|
+
const mockResult = { status: { expiresAt: '2026-01-01T00:00:00Z' } };
|
|
54
|
+
const mockStore = { dispatch: jest.fn().mockResolvedValue(mockResult) };
|
|
55
|
+
|
|
56
|
+
await inactivity.getUserActivity(mockStore, 'my-token', false);
|
|
57
|
+
|
|
58
|
+
expect(mockStore.dispatch).toHaveBeenCalledWith('management/find', {
|
|
59
|
+
type: 'ext.cattle.io.useractivity',
|
|
60
|
+
id: 'my-token',
|
|
61
|
+
opt: {
|
|
62
|
+
force: false, watch: false, logoutOnError: false
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('dispatches auth/logout with sessionIdle=true on 401 error', async() => {
|
|
68
|
+
const mockStore = {
|
|
69
|
+
dispatch: jest.fn()
|
|
70
|
+
.mockRejectedValueOnce({ _status: 401 })
|
|
71
|
+
.mockResolvedValue('logged out')
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const result = await inactivity.getUserActivity(mockStore, 'my-token');
|
|
75
|
+
|
|
76
|
+
expect(mockStore.dispatch).toHaveBeenCalledWith('auth/logout', { sessionIdle: true });
|
|
77
|
+
expect(result).toStrictEqual('logged out');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('re-throws non-401 errors', async() => {
|
|
81
|
+
const mockStore = { dispatch: jest.fn().mockRejectedValue({ _status: 500, message: 'Server Error' }) };
|
|
82
|
+
|
|
83
|
+
await expect(inactivity.getUserActivity(mockStore, 'my-token')).rejects.toThrow(Error);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('updateUserActivity', () => {
|
|
88
|
+
it('sets spec with tokenId and seenAt and calls save', async() => {
|
|
89
|
+
const mockSaved = { status: { expiresAt: '2026-01-01T00:00:00Z' } };
|
|
90
|
+
const mockResource: any = { save: jest.fn().mockResolvedValue(mockSaved) };
|
|
91
|
+
|
|
92
|
+
const result = await inactivity.updateUserActivity(mockResource, 'my-token', '2026-01-01T00:00:00Z');
|
|
93
|
+
|
|
94
|
+
expect(mockResource.spec).toStrictEqual({
|
|
95
|
+
tokenId: 'my-token',
|
|
96
|
+
seenAt: '2026-01-01T00:00:00Z'
|
|
97
|
+
});
|
|
98
|
+
expect(mockResource.save).toHaveBeenCalledWith({ force: true });
|
|
99
|
+
expect(result).toStrictEqual(mockSaved);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('omits seenAt from spec when empty string', async() => {
|
|
103
|
+
const mockSaved = { status: { expiresAt: '2026-01-01T00:00:00Z' } };
|
|
104
|
+
const mockResource: any = { save: jest.fn().mockResolvedValue(mockSaved) };
|
|
105
|
+
|
|
106
|
+
await inactivity.updateUserActivity(mockResource, 'my-token', '');
|
|
107
|
+
|
|
108
|
+
expect(mockResource.spec).toStrictEqual({ tokenId: 'my-token' });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('re-throws errors from save', async() => {
|
|
112
|
+
const mockResource: any = { save: jest.fn().mockRejectedValue(new Error('save failed')) };
|
|
113
|
+
|
|
114
|
+
await expect(inactivity.updateUserActivity(mockResource, 'my-token', '2026-01-01T00:00:00Z')).rejects.toThrow(Error);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('parseTTLData', () => {
|
|
119
|
+
afterEach(() => {
|
|
120
|
+
jest.restoreAllMocks();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('returns expiresAt from userActivityData.status', () => {
|
|
124
|
+
const expiresAt = '2030-01-01T01:00:00.000Z';
|
|
125
|
+
const now = new Date('2030-01-01T00:00:00.000Z').getTime();
|
|
126
|
+
|
|
127
|
+
jest.spyOn(Date, 'now').mockReturnValue(now);
|
|
128
|
+
|
|
129
|
+
const data: any = {
|
|
130
|
+
status: { expiresAt },
|
|
131
|
+
metadata: { name: 'token-1' }
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const result = inactivity.parseTTLData(data);
|
|
135
|
+
|
|
136
|
+
expect(result.expiresAt).toStrictEqual(expiresAt);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('returns sessionTokenName from userActivityData.metadata.name', () => {
|
|
140
|
+
const expiresAt = '2030-01-01T01:00:00.000Z';
|
|
141
|
+
const now = new Date('2030-01-01T00:00:00.000Z').getTime();
|
|
142
|
+
|
|
143
|
+
jest.spyOn(Date, 'now').mockReturnValue(now);
|
|
144
|
+
|
|
145
|
+
const data: any = {
|
|
146
|
+
status: { expiresAt },
|
|
147
|
+
metadata: { name: 'token-abc' }
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const result = inactivity.parseTTLData(data);
|
|
151
|
+
|
|
152
|
+
expect(result.sessionTokenName).toStrictEqual('token-abc');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('calculates courtesyTimer as 20% of threshold, capped at 300s', () => {
|
|
156
|
+
// 1 hour = 3600 seconds; threshold = 3600 - 3 = 3597s
|
|
157
|
+
// courtesyTimerVal = floor(3597 * 0.2) = floor(719.4) = 719
|
|
158
|
+
// courtesyTimer = min(719, 300) = 300
|
|
159
|
+
const now = new Date('2030-01-01T00:00:00.000Z').getTime();
|
|
160
|
+
const expiresAt = new Date(now + 3600 * 1000).toISOString(); // 1 hour later
|
|
161
|
+
|
|
162
|
+
jest.spyOn(Date, 'now').mockReturnValue(now);
|
|
163
|
+
|
|
164
|
+
const data: any = {
|
|
165
|
+
status: { expiresAt },
|
|
166
|
+
metadata: { name: 'token-1' }
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const result = inactivity.parseTTLData(data);
|
|
170
|
+
|
|
171
|
+
expect(result.courtesyTimer).toStrictEqual(300);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('calculates courtesyTimer as 20% when under 300s cap', () => {
|
|
175
|
+
// 100 seconds until expiry; threshold = 100 - 3 = 97s
|
|
176
|
+
// courtesyTimerVal = floor(97 * 0.2) = floor(19.4) = 19
|
|
177
|
+
// courtesyTimer = min(19, 300) = 19
|
|
178
|
+
const now = new Date('2030-01-01T00:00:00.000Z').getTime();
|
|
179
|
+
const expiresAt = new Date(now + 100 * 1000).toISOString();
|
|
180
|
+
|
|
181
|
+
jest.spyOn(Date, 'now').mockReturnValue(now);
|
|
182
|
+
|
|
183
|
+
const data: any = {
|
|
184
|
+
status: { expiresAt },
|
|
185
|
+
metadata: { name: 'token-1' }
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const result = inactivity.parseTTLData(data);
|
|
189
|
+
|
|
190
|
+
expect(result.courtesyTimer).toStrictEqual(19);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('courtesyCountdown equals courtesyTimer', () => {
|
|
194
|
+
const now = new Date('2030-01-01T00:00:00.000Z').getTime();
|
|
195
|
+
const expiresAt = new Date(now + 100 * 1000).toISOString();
|
|
196
|
+
|
|
197
|
+
jest.spyOn(Date, 'now').mockReturnValue(now);
|
|
198
|
+
|
|
199
|
+
const data: any = {
|
|
200
|
+
status: { expiresAt },
|
|
201
|
+
metadata: { name: 'token-1' }
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const result = inactivity.parseTTLData(data);
|
|
205
|
+
|
|
206
|
+
expect(result.courtesyCountdown).toStrictEqual(result.courtesyTimer);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('calculates showModalAfter = thresholdSeconds - courtesyTimer', () => {
|
|
210
|
+
// 100s; threshold = 97s; courtesyTimer = 19s; showModalAfter = 97 - 19 = 78
|
|
211
|
+
const now = new Date('2030-01-01T00:00:00.000Z').getTime();
|
|
212
|
+
const expiresAt = new Date(now + 100 * 1000).toISOString();
|
|
213
|
+
|
|
214
|
+
jest.spyOn(Date, 'now').mockReturnValue(now);
|
|
215
|
+
|
|
216
|
+
const data: any = {
|
|
217
|
+
status: { expiresAt },
|
|
218
|
+
metadata: { name: 'token-1' }
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const result = inactivity.parseTTLData(data);
|
|
222
|
+
|
|
223
|
+
expect(result.showModalAfter).toStrictEqual(78);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('calculates showModalAfter correctly with large TTL (1 hour)', () => {
|
|
227
|
+
// 3600s; threshold = 3597s; courtesyTimer = 300s (capped); showModalAfter = 3597 - 300 = 3297
|
|
228
|
+
const now = new Date('2030-01-01T00:00:00.000Z').getTime();
|
|
229
|
+
const expiresAt = new Date(now + 3600 * 1000).toISOString();
|
|
230
|
+
|
|
231
|
+
jest.spyOn(Date, 'now').mockReturnValue(now);
|
|
232
|
+
|
|
233
|
+
const data: any = {
|
|
234
|
+
status: { expiresAt },
|
|
235
|
+
metadata: { name: 'token-1' }
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const result = inactivity.parseTTLData(data);
|
|
239
|
+
|
|
240
|
+
expect(result.showModalAfter).toStrictEqual(3297);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('handles expired session (negative thresholdSeconds)', () => {
|
|
244
|
+
// Already expired by 10 seconds; threshold = -10 - 3 = -13s
|
|
245
|
+
// courtesyTimerVal = floor(-13 * 0.2) = floor(-2.6) = -3
|
|
246
|
+
// courtesyTimer = min(-3, 300) = -3
|
|
247
|
+
// showModalAfter = -13 - (-3) = -10
|
|
248
|
+
const now = new Date('2030-01-01T00:00:10.000Z').getTime();
|
|
249
|
+
const expiresAt = new Date('2030-01-01T00:00:00.000Z').toISOString();
|
|
250
|
+
|
|
251
|
+
jest.spyOn(Date, 'now').mockReturnValue(now);
|
|
252
|
+
|
|
253
|
+
const data: any = {
|
|
254
|
+
status: { expiresAt },
|
|
255
|
+
metadata: { name: 'token-1' }
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const result = inactivity.parseTTLData(data);
|
|
259
|
+
|
|
260
|
+
expect(result.courtesyTimer).toStrictEqual(-3);
|
|
261
|
+
expect(result.showModalAfter).toStrictEqual(-10);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('returns undefined for expiresAt when status.expiresAt is absent', () => {
|
|
265
|
+
const now = new Date('2030-01-01T00:00:00.000Z').getTime();
|
|
266
|
+
|
|
267
|
+
jest.spyOn(Date, 'now').mockReturnValue(now);
|
|
268
|
+
|
|
269
|
+
const data: any = {
|
|
270
|
+
status: {},
|
|
271
|
+
metadata: { name: 'token-1' }
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const result = inactivity.parseTTLData(data);
|
|
275
|
+
|
|
276
|
+
expect(result.expiresAt).toBeUndefined();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('returns undefined for sessionTokenName when metadata.name is absent', () => {
|
|
280
|
+
const now = new Date('2030-01-01T00:00:00.000Z').getTime();
|
|
281
|
+
const expiresAt = new Date(now + 100 * 1000).toISOString();
|
|
282
|
+
|
|
283
|
+
jest.spyOn(Date, 'now').mockReturnValue(now);
|
|
284
|
+
|
|
285
|
+
const data: any = {
|
|
286
|
+
status: { expiresAt },
|
|
287
|
+
metadata: {}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const result = inactivity.parseTTLData(data);
|
|
291
|
+
|
|
292
|
+
expect(result.sessionTokenName).toBeUndefined();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('handles exactly 5 minute threshold (boundary: courtesyTimer not capped)', () => {
|
|
296
|
+
// 300s; threshold = 300 - 3 = 297s
|
|
297
|
+
// courtesyTimerVal = floor(297 * 0.2) = floor(59.4) = 59
|
|
298
|
+
// courtesyTimer = min(59, 300) = 59
|
|
299
|
+
const now = new Date('2030-01-01T00:00:00.000Z').getTime();
|
|
300
|
+
const expiresAt = new Date(now + 300 * 1000).toISOString();
|
|
301
|
+
|
|
302
|
+
jest.spyOn(Date, 'now').mockReturnValue(now);
|
|
303
|
+
|
|
304
|
+
const data: any = {
|
|
305
|
+
status: { expiresAt },
|
|
306
|
+
metadata: { name: 'token-1' }
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const result = inactivity.parseTTLData(data);
|
|
310
|
+
|
|
311
|
+
expect(result.courtesyTimer).toStrictEqual(59);
|
|
312
|
+
expect(result.showModalAfter).toStrictEqual(238);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|
|
@@ -210,6 +210,83 @@ describe('fx: diff', () => {
|
|
|
210
210
|
|
|
211
211
|
expect(result).toStrictEqual(expected);
|
|
212
212
|
});
|
|
213
|
+
it('should preserve nested object values when transitioning from empty object to populated object', () => {
|
|
214
|
+
const from = { parent: { child: { config: {} } } };
|
|
215
|
+
const to = {
|
|
216
|
+
parent: {
|
|
217
|
+
child: {
|
|
218
|
+
config: {
|
|
219
|
+
annotations: { hello: 'world' },
|
|
220
|
+
labels: { key: 'value' }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const result = diff(from, to);
|
|
227
|
+
const expected = {
|
|
228
|
+
parent: {
|
|
229
|
+
child: {
|
|
230
|
+
config: {
|
|
231
|
+
annotations: { hello: 'world' },
|
|
232
|
+
labels: { key: 'value' }
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
expect(result).toStrictEqual(expected);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should preserve explicit empty object when clearing nested object values', () => {
|
|
242
|
+
const from = { parent: { child: { config: { annotations: { hello: 'world' } } } } };
|
|
243
|
+
const to = { parent: { child: { config: {} } } };
|
|
244
|
+
|
|
245
|
+
const result = diff(from, to);
|
|
246
|
+
const expected = { parent: { child: { config: {} } } };
|
|
247
|
+
|
|
248
|
+
expect(result).toStrictEqual(expected);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should not emit nested empty objects when there are no differences', () => {
|
|
252
|
+
const from = { parent: { child: { config: { annotations: { hello: 'world' } } } } };
|
|
253
|
+
const to = { parent: { child: { config: { annotations: { hello: 'world' } } } } };
|
|
254
|
+
|
|
255
|
+
const result = diff(from, to);
|
|
256
|
+
const expected = {};
|
|
257
|
+
|
|
258
|
+
expect(result).toStrictEqual(expected);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should correctly nullify nested keys when removed', () => {
|
|
262
|
+
const from = {
|
|
263
|
+
parent: {
|
|
264
|
+
child: {
|
|
265
|
+
config: {
|
|
266
|
+
annotations: { hello: 'world' },
|
|
267
|
+
labels: { key: 'value' }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const to = { parent: { child: { config: { annotations: { hello: 'world' } } } } };
|
|
273
|
+
|
|
274
|
+
const result = diff(from, to);
|
|
275
|
+
const expected = { parent: { child: { config: { labels: { key: null } } } } };
|
|
276
|
+
|
|
277
|
+
expect(result).toStrictEqual(expected);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should not nullify child keys when parent object is updated', () => {
|
|
281
|
+
const from = { parent: { child: { config: {} } } };
|
|
282
|
+
const to = { parent: { child: { config: { annotations: { hello: 'world' } } } } };
|
|
283
|
+
|
|
284
|
+
const result = diff(from, to);
|
|
285
|
+
const expected = { parent: { child: { config: { annotations: { hello: 'world' } } } } };
|
|
286
|
+
|
|
287
|
+
expect(result).toStrictEqual(expected);
|
|
288
|
+
expect(result.parent.child.config).not.toHaveProperty('annotations', null);
|
|
289
|
+
});
|
|
213
290
|
});
|
|
214
291
|
|
|
215
292
|
describe('fx: definedKeys', () => {
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { DATE_FORMAT, TIME_FORMAT } from '@shell/store/prefs';
|
|
2
|
-
import { dateTimeFormat } from '@shell/utils/time';
|
|
2
|
+
import { dateTimeFormat, isMissingDate } from '@shell/utils/time';
|
|
3
3
|
import { type Store } from 'vuex';
|
|
4
|
+
import { ZERO_TIME } from '@shell/config/types';
|
|
5
|
+
|
|
6
|
+
describe('function: isMissingDate', () => {
|
|
7
|
+
it.each([
|
|
8
|
+
[undefined, true],
|
|
9
|
+
[null, true],
|
|
10
|
+
['', true],
|
|
11
|
+
[ZERO_TIME, true],
|
|
12
|
+
['2010-10-21T04:29:00Z', false],
|
|
13
|
+
])('given %p, returns %p', (date, expected) => {
|
|
14
|
+
expect(isMissingDate(date)).toBe(expected);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
4
17
|
|
|
5
18
|
describe('function: dateTimeFormat', () => {
|
|
6
19
|
jest.useFakeTimers()
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addParam, addParams, removeParam, parseLinkHeader, isMaybeSecure, portMatch, parse, stringify
|
|
3
|
+
} from '@shell/utils/url';
|
|
4
|
+
|
|
5
|
+
describe('fx: addParam', () => {
|
|
6
|
+
it('should add a query parameter to a URL without existing params', () => {
|
|
7
|
+
expect(addParam('https://example.com/path', 'foo', 'bar')).toStrictEqual('https://example.com/path?foo=bar');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should append a query parameter to a URL with existing params', () => {
|
|
11
|
+
expect(addParam('https://example.com/path?a=1', 'b', '2')).toStrictEqual('https://example.com/path?a=1&b=2');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should encode special characters in key and value', () => {
|
|
15
|
+
expect(addParam('https://example.com', 'my key', 'hello world')).toStrictEqual('https://example.com?my%20key=hello%20world');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should add multiple values from an array', () => {
|
|
19
|
+
expect(addParam('https://example.com', 'tag', ['a', 'b'])).toStrictEqual('https://example.com?tag=a&tag=b');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should add a key-only param when value is null', () => {
|
|
23
|
+
expect(addParam('https://example.com', 'flag', null)).toStrictEqual('https://example.com?flag');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should handle an array with a null value', () => {
|
|
27
|
+
expect(addParam('https://example.com', 'flag', [null])).toStrictEqual('https://example.com?flag');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should add a param with an empty string value', () => {
|
|
31
|
+
expect(addParam('https://example.com', 'key', '')).toStrictEqual('https://example.com?key=');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should add a duplicate key as an additional param', () => {
|
|
35
|
+
expect(addParam('https://example.com?a=1', 'a', '2')).toStrictEqual('https://example.com?a=1&a=2');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('fx: addParams', () => {
|
|
40
|
+
it('should add multiple parameters to a URL', () => {
|
|
41
|
+
expect(addParams('https://example.com', { a: '1', b: '2' })).toStrictEqual('https://example.com?a=1&b=2');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should return the URL unchanged if params is empty', () => {
|
|
45
|
+
expect(addParams('https://example.com', {})).toStrictEqual('https://example.com');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return the URL unchanged if params is null', () => {
|
|
49
|
+
expect(addParams('https://example.com', null)).toStrictEqual('https://example.com');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return the URL unchanged if params is a non-object value', () => {
|
|
53
|
+
expect(addParams('https://example.com', 'not-an-object')).toStrictEqual('https://example.com');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('fx: removeParam', () => {
|
|
58
|
+
it('should remove a query parameter from a URL', () => {
|
|
59
|
+
expect(removeParam('https://example.com?foo=bar&baz=qux', 'foo')).toStrictEqual('https://example.com/?baz=qux');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should return a normalized URL if the param does not exist', () => {
|
|
63
|
+
expect(removeParam('https://example.com?a=1', 'nonexistent')).toStrictEqual('https://example.com/?a=1');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should remove the only query parameter', () => {
|
|
67
|
+
expect(removeParam('https://example.com?only=param', 'only')).toStrictEqual('https://example.com/');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should normalize a key-only query parameter to key= (parser treats it as empty value)', () => {
|
|
71
|
+
expect(removeParam('https://example.com?flag', 'flag')).toStrictEqual('https://example.com/?flag=');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('fx: parseLinkHeader', () => {
|
|
76
|
+
it('should parse a single link header entry', () => {
|
|
77
|
+
expect(parseLinkHeader('<https://example.com/page2>; rel="next"')).toStrictEqual({ next: 'https://example.com/page2' });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should parse multiple link header entries', () => {
|
|
81
|
+
const header = '<https://example.com/page2>; rel="next", <https://example.com/page1>; rel="prev"';
|
|
82
|
+
|
|
83
|
+
expect(parseLinkHeader(header)).toStrictEqual({
|
|
84
|
+
next: 'https://example.com/page2',
|
|
85
|
+
prev: 'https://example.com/page1',
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should return an empty object for an empty string', () => {
|
|
90
|
+
expect(parseLinkHeader('')).toStrictEqual({});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should return an empty object for a malformed header', () => {
|
|
94
|
+
expect(parseLinkHeader('not a valid link header')).toStrictEqual({});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should lowercase the rel value', () => {
|
|
98
|
+
expect(parseLinkHeader('<https://example.com>; rel="Next"')).toStrictEqual({ next: 'https://example.com' });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('fx: portMatch', () => {
|
|
103
|
+
it.each([
|
|
104
|
+
{
|
|
105
|
+
ports: [443], equals: [443, 8443], endsWith: [], expected: true, desc: 'port is in the equals list'
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
ports: [8080], equals: [443, 8443], endsWith: ['443'], expected: false, desc: 'port is not in equals or endsWith lists'
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
ports: [8443], equals: [], endsWith: ['443'], expected: true, desc: 'port string ends with the given suffix'
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
ports: [443], equals: [], endsWith: ['443'], expected: false, desc: 'port equals the suffix exactly (endsWith excludes exact match)'
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
ports: [], equals: [443], endsWith: ['443'], expected: false, desc: 'ports array is empty'
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
ports: [80, 443], equals: [443], endsWith: [], expected: true, desc: 'any port in the array matches equals'
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
ports: [18443], equals: [], endsWith: ['443'], expected: true, desc: 'multi-digit port ending with suffix'
|
|
124
|
+
},
|
|
125
|
+
])('should return $expected when $desc', ({
|
|
126
|
+
ports, equals, endsWith, expected
|
|
127
|
+
}) => {
|
|
128
|
+
expect(portMatch(ports, equals, endsWith)).toBe(expected);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('fx: isMaybeSecure', () => {
|
|
133
|
+
it.each([
|
|
134
|
+
{
|
|
135
|
+
port: 80, proto: 'https', expected: true, desc: 'https protocol'
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
port: 80, proto: 'HTTPS', expected: true, desc: 'HTTPS protocol (case-insensitive)'
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
port: 443, proto: 'http', expected: true, desc: 'port 443'
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
port: 8443, proto: 'http', expected: true, desc: 'port 8443'
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
port: 18443, proto: 'http', expected: true, desc: 'port 18443 (endsWith 443)'
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
port: 80, proto: 'http', expected: false, desc: 'http on non-secure port'
|
|
151
|
+
},
|
|
152
|
+
])('should return $expected for $desc', ({ port, proto, expected }) => {
|
|
153
|
+
expect(isMaybeSecure(port, proto)).toBe(expected);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('fx: parse', () => {
|
|
158
|
+
it('should parse a simple URL', () => {
|
|
159
|
+
const result = parse('https://example.com/path?foo=bar');
|
|
160
|
+
|
|
161
|
+
expect(result.protocol).toStrictEqual('https');
|
|
162
|
+
expect(result.host).toStrictEqual('example.com');
|
|
163
|
+
expect(result.path).toStrictEqual('/path');
|
|
164
|
+
expect(result.query).toStrictEqual({ foo: 'bar' });
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should parse a URL with port', () => {
|
|
168
|
+
const result = parse('https://example.com:8080/');
|
|
169
|
+
|
|
170
|
+
expect(result.host).toStrictEqual('example.com');
|
|
171
|
+
expect(result.port).toStrictEqual('8080');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should parse a URL with user credentials', () => {
|
|
175
|
+
const result = parse('https://user:pass@example.com/');
|
|
176
|
+
|
|
177
|
+
expect(result.user).toStrictEqual('user');
|
|
178
|
+
expect(result.password).toStrictEqual('pass');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should parse a URL with anchor', () => {
|
|
182
|
+
const result = parse('https://example.com/page#section1');
|
|
183
|
+
|
|
184
|
+
expect(result.anchor).toStrictEqual('section1');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should parse a URL with multiple query params', () => {
|
|
188
|
+
expect(parse('https://example.com?a=1&b=2').query).toStrictEqual({ a: '1', b: '2' });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should parse a URL with user only (no password)', () => {
|
|
192
|
+
const result = parse('https://admin@example.com/');
|
|
193
|
+
|
|
194
|
+
expect(result.user).toStrictEqual('admin');
|
|
195
|
+
expect(result.password).toStrictEqual('');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should set empty strings for missing optional fields', () => {
|
|
199
|
+
const result = parse('https://example.com/path');
|
|
200
|
+
|
|
201
|
+
expect(result.port).toStrictEqual('');
|
|
202
|
+
expect(result.anchor).toStrictEqual('');
|
|
203
|
+
expect(result.user).toStrictEqual('');
|
|
204
|
+
expect(result.password).toStrictEqual('');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('fx: stringify', () => {
|
|
209
|
+
it('should reconstruct a simple URL', () => {
|
|
210
|
+
expect(stringify(parse('https://example.com/path'))).toStrictEqual('https://example.com/path');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should include user and password when both present', () => {
|
|
214
|
+
expect(stringify(parse('https://user:pass@example.com/'))).toStrictEqual('https://user:pass@example.com/');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should include user only when password is absent', () => {
|
|
218
|
+
expect(stringify(parse('https://admin@example.com/'))).toStrictEqual('https://admin@example.com/');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should include port when present', () => {
|
|
222
|
+
expect(stringify(parse('https://example.com:9090/'))).toStrictEqual('https://example.com:9090/');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should include anchor when present', () => {
|
|
226
|
+
expect(stringify(parse('https://example.com/page#section'))).toStrictEqual('https://example.com/page#section');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should default path to / when path is empty', () => {
|
|
230
|
+
const parsed = parse('https://example.com');
|
|
231
|
+
|
|
232
|
+
parsed.path = '';
|
|
233
|
+
|
|
234
|
+
expect(stringify(parsed)).toStrictEqual('https://example.com/');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should include query parameters', () => {
|
|
238
|
+
expect(stringify(parse('https://example.com/path?a=1&b=2'))).toStrictEqual('https://example.com/path?a=1&b=2');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should round-trip a complex URL', () => {
|
|
242
|
+
const url = 'https://user:pass@example.com:8080/some/path?key=value&other=test#anchor';
|
|
243
|
+
|
|
244
|
+
expect(stringify(parse(url))).toStrictEqual(url);
|
|
245
|
+
});
|
|
246
|
+
});
|