@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.
Files changed (134) hide show
  1. package/assets/images/providers/entraid-black.svg +4 -0
  2. package/assets/images/providers/entraid.svg +9 -0
  3. package/assets/images/vendor/entraid.svg +9 -0
  4. package/assets/styles/app.scss +0 -1
  5. package/assets/translations/en-us.yaml +19 -17
  6. package/assets/translations/zh-hans.yaml +4 -8
  7. package/chart/__tests__/S3.test.ts +10 -3
  8. package/components/CountBox.vue +20 -0
  9. package/components/CreateDriver.vue +0 -12
  10. package/components/DetailText.vue +12 -3
  11. package/components/SelectIconGrid.vue +5 -0
  12. package/components/__tests__/CountBox.test.ts +72 -0
  13. package/components/__tests__/DetailText.test.ts +113 -0
  14. package/components/fleet/FleetClusterTargets/index.vue +18 -1
  15. package/components/form/InputWithSelect.vue +18 -10
  16. package/components/form/KeyValue.vue +17 -1
  17. package/components/form/LabeledSelect.vue +82 -24
  18. package/components/form/Select.vue +73 -56
  19. package/components/form/ServiceNameSelect.vue +13 -11
  20. package/components/form/__tests__/KeyValue.test.ts +66 -0
  21. package/components/form/__tests__/NodeScheduling.test.ts +9 -0
  22. package/components/form/labeled-select-utils/useLabeledSelectPagination.ts +138 -0
  23. package/components/nav/Group.vue +7 -6
  24. package/components/nav/Header.vue +24 -3
  25. package/components/nav/NotificationCenter/Notification.vue +4 -1
  26. package/components/nav/NotificationCenter/NotificationHeader.vue +20 -8
  27. package/components/nav/NotificationCenter/__tests__/NotificationHeader.test.ts +80 -0
  28. package/components/nav/Type.vue +8 -7
  29. package/components/nav/WindowManager/index.vue +2 -1
  30. package/components/nav/WorkspaceSwitcher.vue +13 -0
  31. package/components/nav/__tests__/Group.test.ts +67 -0
  32. package/components/nav/__tests__/Header.test.ts +235 -0
  33. package/components/nav/__tests__/Type.test.ts +20 -3
  34. package/components/templates/default.vue +34 -4
  35. package/components/templates/home.vue +12 -25
  36. package/components/templates/plain.vue +13 -26
  37. package/composables/useLabeledFormElement.ts +10 -2
  38. package/composables/useLabeledSelect.ts +60 -0
  39. package/composables/useUserRetentionValidation.ts +1 -49
  40. package/config/cookies.js +0 -1
  41. package/config/labels-annotations.js +1 -0
  42. package/config/query-params.js +1 -0
  43. package/config/router/routes.js +0 -8
  44. package/core/__tests__/plugin-products.test.ts +616 -25
  45. package/core/plugin-products-base.ts +31 -14
  46. package/core/plugin-products-helpers.ts +5 -4
  47. package/core/plugin-types.ts +18 -3
  48. package/core/types.ts +3 -1
  49. package/detail/__tests__/management.cattle.io.fleetworkspace.test.ts +128 -0
  50. package/detail/management.cattle.io.fleetworkspace.vue +49 -0
  51. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +9 -0
  52. package/edit/__tests__/kontainerDriver.test.ts +0 -13
  53. package/edit/__tests__/nodeDriver.test.ts +5 -11
  54. package/edit/__tests__/resources.cattle.io.restore.test.ts +9 -0
  55. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
  56. package/edit/auth/__tests__/oidc.test.ts +54 -0
  57. package/edit/auth/azuread.vue +1 -1
  58. package/edit/auth/oidc.vue +8 -0
  59. package/edit/kontainerDriver.vue +1 -2
  60. package/edit/nodeDriver.vue +0 -2
  61. package/edit/provisioning.cattle.io.cluster/AgentEnv.vue +1 -0
  62. package/edit/provisioning.cattle.io.cluster/__tests__/AgentEnv.test.ts +25 -0
  63. package/edit/provisioning.cattle.io.cluster/index.vue +70 -99
  64. package/initialize/App.vue +29 -2
  65. package/initialize/install-plugins.js +0 -2
  66. package/list/__tests__/management.cattle.io.feature.test.ts +105 -0
  67. package/list/catalog.cattle.io.app.vue +25 -5
  68. package/list/management.cattle.io.feature.vue +1 -1
  69. package/list/management.cattle.io.fleetworkspace.vue +8 -0
  70. package/machine-config/amazonec2.vue +1 -0
  71. package/mixins/chart.js +40 -9
  72. package/models/__tests__/catalog.cattle.io.app.test.ts +15 -1
  73. package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +84 -0
  74. package/models/__tests__/chart.test.ts +99 -6
  75. package/models/__tests__/management.cattle.io.feature.test.ts +131 -0
  76. package/models/__tests__/monitoring.coreos.com.alertmanagerconfig.test.ts +98 -0
  77. package/models/catalog.cattle.io.app.js +21 -17
  78. package/models/catalog.cattle.io.clusterrepo.js +39 -11
  79. package/models/chart.js +33 -19
  80. package/models/fleet-application.js +1 -1
  81. package/models/fleet.cattle.io.bundle.js +1 -1
  82. package/models/kontainerdriver.js +11 -0
  83. package/models/management.cattle.io.authconfig.js +5 -1
  84. package/models/management.cattle.io.cluster.js +0 -53
  85. package/models/management.cattle.io.feature.js +3 -3
  86. package/models/management.cattle.io.kontainerdriver.js +1 -26
  87. package/models/monitoring.coreos.com.alertmanagerconfig.js +31 -17
  88. package/models/nodedriver.js +7 -0
  89. package/package.json +13 -12
  90. package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +189 -0
  91. package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +55 -0
  92. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +53 -0
  93. package/pages/c/_cluster/apps/charts/chart.vue +217 -33
  94. package/pages/c/_cluster/apps/charts/index.vue +2 -2
  95. package/pages/c/_cluster/apps/charts/install.vue +8 -3
  96. package/pages/c/_cluster/auth/user.retention/index.vue +55 -22
  97. package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +5 -7
  98. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +39 -2
  99. package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +61 -0
  100. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +15 -10
  101. package/pages/c/_cluster/uiplugins/index.vue +23 -25
  102. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +205 -1
  103. package/rancher-components/Form/LabeledInput/LabeledInput.vue +82 -4
  104. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +1 -1
  105. package/scripts/test-plugins-build.sh +5 -2
  106. package/server/server-middleware.js +2 -2
  107. package/static/humans.txt +1 -0
  108. package/static/robots.txt +34 -0
  109. package/static/welcome-cow.svg +18 -0
  110. package/store/__tests__/catalog.test.ts +161 -11
  111. package/store/auth.js +0 -3
  112. package/store/catalog.js +60 -8
  113. package/types/shell/index.d.ts +26 -22
  114. package/utils/__tests__/git.test.ts +270 -0
  115. package/utils/__tests__/inactivity.test.ts +316 -0
  116. package/utils/__tests__/object.test.ts +77 -0
  117. package/utils/__tests__/time.test.ts +14 -1
  118. package/utils/__tests__/url.test.ts +246 -0
  119. package/utils/object.js +33 -2
  120. package/utils/time.ts +5 -0
  121. package/vue.config.js +0 -9
  122. package/assets/images/providers/azuread-black.svg +0 -22
  123. package/assets/images/providers/azuread.svg +0 -25
  124. package/assets/images/vendor/azuread.svg +0 -18
  125. package/assets/styles/fonts/_dots.scss +0 -18
  126. package/components/EmberPage.vue +0 -622
  127. package/components/EmberPageView.vue +0 -39
  128. package/components/form/labeled-select-utils/labeled-select-pagination.ts +0 -116
  129. package/mixins/labeled-form-element.ts +0 -225
  130. package/pages/c/_cluster/explorer/tools/pages/_page.vue +0 -28
  131. package/pages/c/_cluster/manager/pages/_page.vue +0 -22
  132. package/pages/c/_cluster/mcapps/pages/_page.vue +0 -22
  133. package/plugins/ember-cookie.js +0 -17
  134. package/utils/ember-page.js +0 -30
package/store/auth.js CHANGED
@@ -3,7 +3,6 @@ import { MANAGEMENT, EXT } from '@shell/config/types';
3
3
  import { addObjects, findBy, joinStringList } from '@shell/utils/array';
4
4
  import { openAuthPopup, returnTo } from '@shell/utils/auth';
5
5
  import { base64Encode } from '@shell/utils/crypto';
6
- import { removeEmberPage } from '@shell/utils/ember-page';
7
6
  import { randomStr } from '@shell/utils/string';
8
7
  import { addParams, parse as parseUrl, removeParam } from '@shell/utils/url';
9
8
 
@@ -429,8 +428,6 @@ export const actions = {
429
428
  },
430
429
 
431
430
  uiLogout({ commit, dispatch }, options = {}) {
432
- removeEmberPage();
433
-
434
431
  commit('loggedOut');
435
432
  dispatch('onLogout', options, { root: true });
436
433
 
package/store/catalog.js CHANGED
@@ -325,8 +325,12 @@ export const actions = {
325
325
  * force: Always refresh catalog's helm repo by re-fetching index.yaml
326
326
  *
327
327
  * reset: clear existing charts and version cache
328
+ *
329
+ * repoKeys: Optional array of specific repo keys (IDs) to refresh. When provided, only these specific
330
+ * repos will be fetched, and only their existing charts will be cleared from the cache to avoid
331
+ * duplicate chart entries or wiping out unrelated chart data.
328
332
  */
329
- async load(ctx, { force, reset } = {}) {
333
+ async load(ctx, { force, reset, repoKeys = [] } = {}) {
330
334
  const {
331
335
  state, getters, rootGetters, commit, dispatch
332
336
  } = ctx;
@@ -360,14 +364,34 @@ export const actions = {
360
364
  promises = {};
361
365
 
362
366
  for ( const repo of repos ) {
363
- if ( (force === true || !getters.isLoaded(repo)) && repo.canLoad ) {
367
+ let shouldLoad = false;
368
+
369
+ if (repoKeys.length) {
370
+ // If repoKeys are explicitly provided (e.g. refreshing a single repo from the UI),
371
+ // we ONLY want to load the repos in that array. We intentionally ignore `!getters.isLoaded(repo)`
372
+ // here so we don't accidentally fetch other unrelated repos just because they haven't loaded yet.
373
+ shouldLoad = repoKeys.includes(repo._key);
374
+ } else {
375
+ // Default behavior: load if explicitly forced, OR if the repo hasn't been loaded into state yet.
376
+ shouldLoad = force === true || !getters.isLoaded(repo);
377
+ }
378
+
379
+ if ( shouldLoad && repo.canLoad ) {
364
380
  console.info('Loading index for repo', repo.name, `(${ repo._key })`); // eslint-disable-line no-console
365
381
  promises[repo._key] = repo.followLink('index');
366
382
  }
367
383
  }
368
384
 
369
385
  const res = await allHashSettled(promises);
370
- const charts = reset ? {} : state.charts;
386
+ const charts = reset ? {} : { ...state.charts };
387
+ let versionInfos = null;
388
+
389
+ if (reset) {
390
+ versionInfos = {};
391
+ } else if (repoKeys.length) {
392
+ versionInfos = { ...state.versionInfos };
393
+ }
394
+
371
395
  const errors = [];
372
396
 
373
397
  for ( const key of Object.keys(res) ) {
@@ -379,6 +403,28 @@ export const actions = {
379
403
  continue;
380
404
  }
381
405
 
406
+ // We are targeting specific repos. To prevent duplicate chart versions from appearing,
407
+ // we must remove the old charts for this specific repo before appending the newly fetched ones,
408
+ // but ONLY if the fetch was successful.
409
+ if (repoKeys.length && repoKeys.includes(key)) {
410
+ for (const chartKey in charts) {
411
+ if (charts[chartKey].repoKey === key) {
412
+ delete charts[chartKey];
413
+ }
414
+ }
415
+
416
+ // Also clear out cached version info for this repo so we don't display stale READMEs/values
417
+ const repoType = repo.type === CATALOG.CLUSTER_REPO ? 'cluster' : 'namespace';
418
+ const repoName = repo.metadata.name;
419
+ const versionPrefix = `${ repoType }/${ repoName }/`;
420
+
421
+ for (const versionKey in versionInfos) {
422
+ if (versionKey.startsWith(versionPrefix)) {
423
+ delete versionInfos[versionKey];
424
+ }
425
+ }
426
+ }
427
+
382
428
  for ( const k in obj.value.entries ) {
383
429
  for ( const entry of obj.value.entries[k] ) {
384
430
  addChart(ctx, charts, entry, repo);
@@ -394,13 +440,17 @@ export const actions = {
394
440
  loaded,
395
441
  });
396
442
 
397
- if (reset) {
398
- commit('setVersions', {});
443
+ if (versionInfos) {
444
+ commit('setVersions', versionInfos);
399
445
  }
400
446
  },
401
447
 
448
+ /**
449
+ * Globally refreshes all loaded repositories by triggering their refresh actions concurrently,
450
+ * bypassing individual catalog loads, and then performs a single, global catalog/load.
451
+ */
402
452
  async refresh({ getters, commit, dispatch }) {
403
- const promises = getters.repos.map((x) => x.refresh());
453
+ const promises = getters.repos.map((x) => x.refresh(false));
404
454
 
405
455
  // @TODO wait for repo state to indicate they're done once the API has that
406
456
 
@@ -488,7 +538,9 @@ function addChart(ctx, map, chart, repo) {
488
538
  certified = CATALOG_ANNOTATIONS._OTHER;
489
539
  }
490
540
 
491
- if ( chart.deprecated ) {
541
+ const isDeprecated = !!chart.deprecated || chart.annotations?.[CATALOG_ANNOTATIONS.DEPRECATED] === 'true';
542
+
543
+ if ( isDeprecated ) {
492
544
  sideLabel = DEPRECATED;
493
545
  } else if ( chart.annotations?.[CATALOG_ANNOTATIONS.EXPERIMENTAL] ) {
494
546
  sideLabel = EXPERIMENTAL;
@@ -546,7 +598,7 @@ function addChart(ctx, map, chart, repo) {
546
598
  versions: [],
547
599
  keywords: chart.keywords || [],
548
600
  categories: filterCategories(chart.keywords),
549
- deprecated: !!chart.deprecated,
601
+ deprecated: isDeprecated,
550
602
  primeOnly,
551
603
  experimental,
552
604
  hidden: !!chart.annotations?.[CATALOG_ANNOTATIONS.HIDDEN],
@@ -81,6 +81,7 @@ export namespace CATALOG {
81
81
  let _OTHER: string;
82
82
  let PRIME_ONLY: string;
83
83
  let EXPERIMENTAL: string;
84
+ let DEPRECATED: string;
84
85
  let NAMESPACE: string;
85
86
  let RELEASE_NAME: string;
86
87
  let FEATURED: string;
@@ -247,6 +248,7 @@ export namespace CATALOG {
247
248
  let _OTHER: string;
248
249
  let PRIME_ONLY: string;
249
250
  let EXPERIMENTAL: string;
251
+ let DEPRECATED: string;
250
252
  let NAMESPACE: string;
251
253
  let RELEASE_NAME: string;
252
254
  let FEATURED: string;
@@ -650,6 +652,7 @@ export const DEPRECATED: "deprecated";
650
652
  export const HIDDEN: "hidden";
651
653
  export const FROM_TOOLS: "tools";
652
654
  export const FROM_CLUSTER: "cluster";
655
+ export const NEW_APP_INSTANCE: "new-instance";
653
656
  export const HIDE_SIDE_NAV: "hide-side-nav";
654
657
  export const PROVIDER: "provider";
655
658
  export const CLOUD_CREDENTIAL: "cloud";
@@ -725,6 +728,7 @@ export const DEPRECATED: "deprecated";
725
728
  export const HIDDEN: "hidden";
726
729
  export const FROM_TOOLS: "tools";
727
730
  export const FROM_CLUSTER: "cluster";
731
+ export const NEW_APP_INSTANCE: "new-instance";
728
732
  export const HIDE_SIDE_NAV: "hide-side-nav";
729
733
  export const PROVIDER: "provider";
730
734
  export const CLOUD_CREDENTIAL: "cloud";
@@ -5745,7 +5749,17 @@ export default class ClusterRepo {
5745
5749
  };
5746
5750
  get _isClusterRepoDisabled(): boolean;
5747
5751
  get _availableActions(): any;
5748
- refresh(): Promise<void>;
5752
+ /**
5753
+ * Refreshes the repository by updating its forceUpdate annotation and waiting for it to become active.
5754
+ * @param {boolean} dispatchLoad - Whether to dispatch the catalog/load action after refreshing. Defaults to true.
5755
+ */
5756
+ refresh(dispatchLoad?: boolean): Promise<void>;
5757
+ /**
5758
+ * Performs a bulk refresh on multiple repositories concurrently, bypassing individual
5759
+ * catalog loads, and dispatches a single catalog/load for all repositories once they are active.
5760
+ * @param {ClusterRepo[]} items - Array of repository instances to refresh.
5761
+ */
5762
+ refreshBulk(items: ClusterRepo[]): Promise<void>;
5749
5763
  disableClusterRepo(): Promise<void>;
5750
5764
  enableClusterRepo(): Promise<void>;
5751
5765
  get isGit(): boolean;
@@ -5793,7 +5807,17 @@ export default class ClusterRepo {
5793
5807
  };
5794
5808
  get _isClusterRepoDisabled(): boolean;
5795
5809
  get _availableActions(): any;
5796
- refresh(): Promise<void>;
5810
+ /**
5811
+ * Refreshes the repository by updating its forceUpdate annotation and waiting for it to become active.
5812
+ * @param {boolean} dispatchLoad - Whether to dispatch the catalog/load action after refreshing. Defaults to true.
5813
+ */
5814
+ refresh(dispatchLoad?: boolean): Promise<void>;
5815
+ /**
5816
+ * Performs a bulk refresh on multiple repositories concurrently, bypassing individual
5817
+ * catalog loads, and dispatches a single catalog/load for all repositories once they are active.
5818
+ * @param {ClusterRepo[]} items - Array of repository instances to refresh.
5819
+ */
5820
+ refreshBulk(items: ClusterRepo[]): Promise<void>;
5797
5821
  disableClusterRepo(): Promise<void>;
5798
5822
  enableClusterRepo(): Promise<void>;
5799
5823
  get isGit(): boolean;
@@ -9213,26 +9237,6 @@ export function resolveMachineConfigComponent(key: any): string;
9213
9237
  export function resolveCloudCredentialComponent(key: any): string;
9214
9238
  }
9215
9239
 
9216
- // @shell/utils/ember-page
9217
-
9218
- declare module '@shell/utils/ember-page' {
9219
- export function findEmberPage(): HTMLElement;
9220
- export function clearEmberInactiveTimer(): void;
9221
- export function startEmberInactiveTimer(): void;
9222
- export function removeEmberPage(): void;
9223
- export const EMBER_FRAME: "ember-iframe";
9224
- }
9225
-
9226
- // @shell/utils/ember-page.js
9227
-
9228
- declare module '@shell/utils/ember-page.js' {
9229
- export function findEmberPage(): HTMLElement;
9230
- export function clearEmberInactiveTimer(): void;
9231
- export function startEmberInactiveTimer(): void;
9232
- export function removeEmberPage(): void;
9233
- export const EMBER_FRAME: "ember-iframe";
9234
- }
9235
-
9236
9240
  // @shell/utils/error
9237
9241
 
9238
9242
  declare module '@shell/utils/error' {
@@ -0,0 +1,270 @@
1
+ import { GitUtils, Commit } from '@shell/utils/git';
2
+
3
+ describe('git utils', () => {
4
+ describe('gitUtils.github.normalize.repo', () => {
5
+ it('maps owner fields from GitHub API response', () => {
6
+ const data = {
7
+ owner: {
8
+ login: 'octocat',
9
+ html_url: 'https://github.com/octocat',
10
+ avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4'
11
+ },
12
+ description: 'Hello World',
13
+ created_at: '2021-01-01T00:00:00Z',
14
+ updated_at: '2021-06-01T00:00:00Z',
15
+ html_url: 'https://github.com/octocat/hello-world',
16
+ name: 'hello-world'
17
+ };
18
+
19
+ const result = GitUtils.github.normalize.repo(data);
20
+
21
+ expect(result.owner).toStrictEqual({
22
+ name: 'octocat',
23
+ htmlUrl: 'https://github.com/octocat',
24
+ avatarUrl: 'https://avatars.githubusercontent.com/u/1?v=4'
25
+ });
26
+ });
27
+
28
+ it('maps repo-level fields from GitHub API response', () => {
29
+ const data = {
30
+ owner: {
31
+ login: 'octocat', html_url: '', avatar_url: ''
32
+ },
33
+ description: 'A repository description',
34
+ created_at: '2021-01-01T00:00:00Z',
35
+ updated_at: '2022-03-15T12:00:00Z',
36
+ html_url: 'https://github.com/octocat/repo',
37
+ name: 'my-repo'
38
+ };
39
+
40
+ const result = GitUtils.github.normalize.repo(data);
41
+
42
+ expect(result.description).toStrictEqual('A repository description');
43
+ expect(result.created_at).toStrictEqual('2021-01-01T00:00:00Z');
44
+ expect(result.updated_at).toStrictEqual('2022-03-15T12:00:00Z');
45
+ expect(result.htmlUrl).toStrictEqual('https://github.com/octocat/repo');
46
+ expect(result.name).toStrictEqual('my-repo');
47
+ });
48
+
49
+ it('handles missing owner gracefully', () => {
50
+ const data = {
51
+ owner: undefined,
52
+ description: 'No owner',
53
+ created_at: '2021-01-01T00:00:00Z',
54
+ updated_at: '2021-01-01T00:00:00Z',
55
+ html_url: 'https://github.com/no/owner',
56
+ name: 'no-owner'
57
+ };
58
+
59
+ const result = GitUtils.github.normalize.repo(data);
60
+
61
+ expect(result.owner).toStrictEqual({
62
+ name: undefined,
63
+ htmlUrl: undefined,
64
+ avatarUrl: undefined
65
+ });
66
+ });
67
+ });
68
+
69
+ describe('gitUtils.github.normalize.commit', () => {
70
+ it('maps commit fields from GitHub API response', () => {
71
+ const data = {
72
+ commit: {
73
+ message: 'fix: resolve issue',
74
+ committer: { date: '2022-01-15T10:30:00Z' }
75
+ },
76
+ html_url: 'https://github.com/octocat/repo/commit/abc1234567890',
77
+ sha: 'abc1234567890abcdef',
78
+ author: {
79
+ login: 'octocat',
80
+ avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4',
81
+ htmlUrl: 'https://github.com/octocat'
82
+ }
83
+ };
84
+
85
+ const result = GitUtils.github.normalize.commit(data);
86
+
87
+ expect(result.message).toStrictEqual('fix: resolve issue');
88
+ expect(result.htmlUrl).toStrictEqual('https://github.com/octocat/repo/commit/abc1234567890');
89
+ expect(result.sha).toStrictEqual('abc1234');
90
+ expect(result.commitId).toStrictEqual('abc1234567890abcdef');
91
+ expect(result.date).toStrictEqual('2022-01-15T10:30:00Z');
92
+ expect(result.isChecked).toStrictEqual(false);
93
+ });
94
+
95
+ it('truncates sha to 7 characters', () => {
96
+ const data = {
97
+ commit: { message: 'chore: update deps', committer: { date: '2022-01-01T00:00:00Z' } },
98
+ html_url: 'https://github.com/octocat/repo/commit/1234567890',
99
+ sha: '1234567890abcdef1234',
100
+ author: {
101
+ login: 'user', avatar_url: '', htmlUrl: ''
102
+ }
103
+ };
104
+
105
+ const result = GitUtils.github.normalize.commit(data);
106
+
107
+ expect(result.sha).toStrictEqual('1234567');
108
+ });
109
+
110
+ it('returns undefined sha when sha is empty string', () => {
111
+ const data = {
112
+ commit: { message: 'fix: bug', committer: { date: '2022-01-01T00:00:00Z' } },
113
+ html_url: 'https://github.com/octocat/repo/commit/x',
114
+ sha: '',
115
+ author: {
116
+ login: 'user', avatar_url: '', htmlUrl: ''
117
+ }
118
+ };
119
+
120
+ const result: Commit = GitUtils.github.normalize.commit(data);
121
+
122
+ expect(result.sha).toBeUndefined();
123
+ });
124
+
125
+ it('maps author fields from GitHub API response', () => {
126
+ const data = {
127
+ commit: { message: 'feat: new feature', committer: { date: '2022-01-01T00:00:00Z' } },
128
+ html_url: 'https://github.com/octocat/repo/commit/abc',
129
+ sha: 'abcdef1234567890',
130
+ author: {
131
+ login: 'contributor',
132
+ avatar_url: 'https://avatars.githubusercontent.com/u/2?v=4',
133
+ htmlUrl: 'https://github.com/contributor'
134
+ }
135
+ };
136
+
137
+ const result = GitUtils.github.normalize.commit(data);
138
+
139
+ expect(result.author).toStrictEqual({
140
+ name: 'contributor',
141
+ avatarUrl: 'https://avatars.githubusercontent.com/u/2?v=4',
142
+ htmlUrl: 'https://github.com/contributor'
143
+ });
144
+ });
145
+ });
146
+
147
+ describe('gitUtils.gitlab.normalize.repo', () => {
148
+ it('maps owner fields from GitLab API response', () => {
149
+ const data = {
150
+ namespace: {
151
+ name: 'my-group',
152
+ web_url: 'https://gitlab.com/my-group',
153
+ avatar_url: 'https://gitlab.com/uploads/group.png'
154
+ },
155
+ description: 'GitLab repo description',
156
+ created_at: '2021-02-01T00:00:00Z',
157
+ last_activity_at: '2022-07-20T00:00:00Z',
158
+ web_url: 'https://gitlab.com/my-group/project',
159
+ name: 'project'
160
+ };
161
+
162
+ const result = GitUtils.gitlab.normalize.repo(data);
163
+
164
+ expect(result.owner).toStrictEqual({
165
+ name: 'my-group',
166
+ htmlUrl: 'https://gitlab.com/my-group',
167
+ avatarUrl: 'https://gitlab.com/uploads/group.png'
168
+ });
169
+ });
170
+
171
+ it('maps repo-level fields from GitLab API response', () => {
172
+ const data = {
173
+ namespace: {
174
+ name: 'ns', web_url: '', avatar_url: ''
175
+ },
176
+ description: 'My GitLab project',
177
+ created_at: '2020-05-01T00:00:00Z',
178
+ last_activity_at: '2023-01-10T00:00:00Z',
179
+ web_url: 'https://gitlab.com/ns/myproject',
180
+ name: 'myproject'
181
+ };
182
+
183
+ const result = GitUtils.gitlab.normalize.repo(data);
184
+
185
+ expect(result.description).toStrictEqual('My GitLab project');
186
+ expect(result.created_at).toStrictEqual('2020-05-01T00:00:00Z');
187
+ expect(result.updated_at).toStrictEqual('2023-01-10T00:00:00Z');
188
+ expect(result.htmlUrl).toStrictEqual('https://gitlab.com/ns/myproject');
189
+ expect(result.name).toStrictEqual('myproject');
190
+ });
191
+
192
+ it('maps updated_at from last_activity_at (not updated_at)', () => {
193
+ const data = {
194
+ namespace: {
195
+ name: 'ns', web_url: '', avatar_url: ''
196
+ },
197
+ description: '',
198
+ created_at: '2020-01-01T00:00:00Z',
199
+ last_activity_at: '2023-06-15T08:00:00Z',
200
+ updated_at: 'should-be-ignored',
201
+ web_url: 'https://gitlab.com/ns/proj',
202
+ name: 'proj'
203
+ };
204
+
205
+ const result = GitUtils.gitlab.normalize.repo(data);
206
+
207
+ expect(result.updated_at).toStrictEqual('2023-06-15T08:00:00Z');
208
+ });
209
+ });
210
+
211
+ describe('gitUtils.gitlab.normalize.commit', () => {
212
+ it('maps commit fields from GitLab API response', () => {
213
+ const data = {
214
+ message: 'refactor: clean up code',
215
+ web_url: 'https://gitlab.com/ns/proj/-/commit/abc1234',
216
+ short_id: 'abc1234',
217
+ id: 'abc1234567890abcdef',
218
+ author_name: 'Jane Doe',
219
+ avatar_url: 'https://gitlab.com/uploads/jane.png',
220
+ committed_date: '2022-03-10T14:00:00Z'
221
+ };
222
+
223
+ const result = GitUtils.gitlab.normalize.commit(data);
224
+
225
+ expect(result.message).toStrictEqual('refactor: clean up code');
226
+ expect(result.htmlUrl).toStrictEqual('https://gitlab.com/ns/proj/-/commit/abc1234');
227
+ expect(result.sha).toStrictEqual('abc1234');
228
+ expect(result.commitId).toStrictEqual('abc1234567890abcdef');
229
+ expect(result.date).toStrictEqual('2022-03-10T14:00:00Z');
230
+ expect(result.isChecked).toStrictEqual(false);
231
+ });
232
+
233
+ it('maps author fields from GitLab API response', () => {
234
+ const data = {
235
+ message: 'feat: add feature',
236
+ web_url: 'https://gitlab.com/ns/proj/-/commit/xyz',
237
+ short_id: 'xyz',
238
+ id: 'xyzabcdef',
239
+ author_name: 'John Smith',
240
+ avatar_url: 'https://gitlab.com/uploads/john.png',
241
+ committed_date: '2022-04-01T00:00:00Z'
242
+ };
243
+
244
+ const result = GitUtils.gitlab.normalize.commit(data);
245
+
246
+ expect(result.author).toStrictEqual({
247
+ name: 'John Smith',
248
+ avatarUrl: 'https://gitlab.com/uploads/john.png',
249
+ htmlUrl: 'https://gitlab.com/ns/proj/-/commit/xyz'
250
+ });
251
+ });
252
+
253
+ it('uses web_url for both htmlUrl and author.htmlUrl', () => {
254
+ const data = {
255
+ message: 'fix: something',
256
+ web_url: 'https://gitlab.com/ns/proj/-/commit/def456',
257
+ short_id: 'def456',
258
+ id: 'def456abc',
259
+ author_name: 'Dev',
260
+ avatar_url: '',
261
+ committed_date: '2022-01-01T00:00:00Z'
262
+ };
263
+
264
+ const result = GitUtils.gitlab.normalize.commit(data);
265
+
266
+ expect(result.htmlUrl).toStrictEqual('https://gitlab.com/ns/proj/-/commit/def456');
267
+ expect((result.author as any).htmlUrl).toStrictEqual('https://gitlab.com/ns/proj/-/commit/def456');
268
+ });
269
+ });
270
+ });