Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-08-05 06:09:25 +00:00
parent 24fca38040
commit 962fbcfb94
62 changed files with 695 additions and 372 deletions

View File

@ -57,7 +57,11 @@ tasks:
# Waiting for GitLab ...
gp await-port 3000
printf "Waiting for GitLab at $(gp url 3000) ..."
until $(curl -sNL $(gp url 3000) | grep -q "GitLab"); do printf '.'; sleep 5; done && echo ""
# Check /-/readiness which returns JSON, but we're only interested in the exit code
#
# We use http://localhost:3000 instead of the public hostname because
# it's no longer possible to access as specific cookies are required
until curl --silent --no-buffer --fail http://localhost:3000/-/readiness > /dev/null 2>&1; do printf '.'; sleep 5; done && echo ""
# Give Gitpod a few more seconds to set up everything ...
sleep 5
printf "$(date) GitLab is up (took ~%.1f minutes)\n" "$((10*$SECONDS/60))e-1" | tee -a /workspace/startup.log

View File

@ -103,6 +103,7 @@ export default {
<tr
class="gl-h-11 gl-border-0 gl-border-solid gl-border-t-1 gl-border-gray-100 gl-h-11"
data-qa-selector="project_import_row"
:data-qa-source-project="repo.importSource.fullName"
>
<td class="gl-p-4">
<gl-link :href="repo.importSource.providerLink" target="_blank" data-testid="providerLink"
@ -155,7 +156,7 @@ export default {
</template>
<template v-else-if="repo.importedProject">{{ displayFullPath }}</template>
</td>
<td class="gl-p-4">
<td class="gl-p-4" data-qa-selector="import_status_indicator">
<import-status :status="importStatus" />
</td>
<td data-testid="actions">

View File

@ -1,5 +1,5 @@
<script>
import { GlIcon, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui';
import { GlIcon, GlSprintf, GlBadge } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
@ -8,7 +8,7 @@ import { PACKAGE_TYPE_NUGET } from '~/packages_and_registries/package_registry/c
import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
name: 'PackageTitle',
@ -19,11 +19,8 @@ export default {
PackageTags,
MetadataItem,
GlBadge,
TimeAgoTooltip,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
i18n: {
packageInfo: __('v%{version} published %{timeAgo}'),
},
@ -77,17 +74,21 @@ export default {
<title-area :title="packageEntity.name" :avatar="packageIcon" data-qa-selector="package_title">
<template #sub-header>
<gl-icon name="eye" class="gl-mr-3" />
<gl-sprintf :message="$options.i18n.packageInfo">
<template #version>
{{ packageEntity.version }}
</template>
<span data-testid="sub-header">
<gl-sprintf :message="$options.i18n.packageInfo">
<template #version>
{{ packageEntity.version }}
</template>
<template #timeAgo>
<span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
&nbsp;{{ timeFormatted(packageEntity.created_at) }}
</span>
</template>
</gl-sprintf>
<template #timeAgo>
<time-ago-tooltip
v-if="packageEntity.createdAt"
class="gl-ml-2"
:time="packageEntity.createdAt"
/>
</template>
</gl-sprintf>
</span>
</template>
<template v-if="packageTypeDisplay" #metadata-type>

View File

@ -46,38 +46,44 @@ export const fetchProjects = ({ commit, state }, search) => {
}
};
export const loadFrequentGroups = async ({ commit }) => {
const data = loadDataFromLS(GROUPS_LOCAL_STORAGE_KEY);
commit(types.LOAD_FREQUENT_ITEMS, { key: GROUPS_LOCAL_STORAGE_KEY, data });
export const preloadStoredFrequentItems = ({ commit }) => {
const storedGroups = loadDataFromLS(GROUPS_LOCAL_STORAGE_KEY);
commit(types.LOAD_FREQUENT_ITEMS, { key: GROUPS_LOCAL_STORAGE_KEY, data: storedGroups });
const promises = data.map((d) => Api.group(d.id));
const storedProjects = loadDataFromLS(PROJECTS_LOCAL_STORAGE_KEY);
commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: storedProjects });
};
export const loadFrequentGroups = async ({ commit, state }) => {
const storedData = state.frequentItems[GROUPS_LOCAL_STORAGE_KEY];
const promises = storedData.map((d) => Api.group(d.id));
try {
const inflatedData = mergeById(await Promise.all(promises), data);
const inflatedData = mergeById(await Promise.all(promises), storedData);
commit(types.LOAD_FREQUENT_ITEMS, { key: GROUPS_LOCAL_STORAGE_KEY, data: inflatedData });
} catch {
createFlash({ message: __('There was a problem fetching recent groups.') });
}
};
export const loadFrequentProjects = async ({ commit }) => {
const data = loadDataFromLS(PROJECTS_LOCAL_STORAGE_KEY);
commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data });
const promises = data.map((d) => Api.project(d.id).then((res) => res.data));
export const loadFrequentProjects = async ({ commit, state }) => {
const storedData = state.frequentItems[PROJECTS_LOCAL_STORAGE_KEY];
const promises = storedData.map((d) => Api.project(d.id).then((res) => res.data));
try {
const inflatedData = mergeById(await Promise.all(promises), data);
const inflatedData = mergeById(await Promise.all(promises), storedData);
commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: inflatedData });
} catch {
createFlash({ message: __('There was a problem fetching recent projects.') });
}
};
export const setFrequentGroup = ({ state }, item) => {
setFrequentItemToLS(GROUPS_LOCAL_STORAGE_KEY, state.frequentItems, item);
export const setFrequentGroup = ({ state, commit }, item) => {
const frequentItems = setFrequentItemToLS(GROUPS_LOCAL_STORAGE_KEY, state.frequentItems, item);
commit(types.LOAD_FREQUENT_ITEMS, { key: GROUPS_LOCAL_STORAGE_KEY, data: frequentItems });
};
export const setFrequentProject = ({ state }, item) => {
setFrequentItemToLS(PROJECTS_LOCAL_STORAGE_KEY, state.frequentItems, item);
export const setFrequentProject = ({ state, commit }, item) => {
const frequentItems = setFrequentItemToLS(PROJECTS_LOCAL_STORAGE_KEY, state.frequentItems, item);
commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: frequentItems });
};
export const setQuery = ({ commit }, { key, value }) => {

View File

@ -21,7 +21,7 @@ export const loadDataFromLS = (key) => {
export const setFrequentItemToLS = (key, data, itemData) => {
if (!AccessorUtilities.isLocalStorageAccessSafe()) {
return;
return [];
}
const keyList = [
@ -66,9 +66,11 @@ export const setFrequentItemToLS = (key, data, itemData) => {
// Note we do not need to commit a mutation here as immediately after this we refresh the page to
// update the search results.
localStorage.setItem(key, JSON.stringify(frequentItems));
return frequentItems;
} catch {
// The LS got in a bad state, let's wipe it
localStorage.removeItem(key);
return [];
}
};

View File

@ -39,8 +39,11 @@ export default {
return !this.query.snippets || this.query.snippets === 'false';
},
},
created() {
this.preloadStoredFrequentItems();
},
methods: {
...mapActions(['applyQuery', 'setQuery']),
...mapActions(['applyQuery', 'setQuery', 'preloadStoredFrequentItems']),
},
};
</script>

View File

@ -18,12 +18,18 @@ export default {
},
},
computed: {
...mapState(['groups', 'fetchingGroups']),
...mapState(['query', 'groups', 'fetchingGroups']),
...mapGetters(['frequentGroups']),
selectedGroup() {
return isEmpty(this.initialData) ? ANY_OPTION : this.initialData;
},
},
created() {
// This tracks groups searched via the top nav search bar
if (this.query.nav_source === 'navbar' && this.initialData?.id) {
this.setFrequentGroup(this.initialData);
}
},
methods: {
...mapActions(['fetchGroups', 'setFrequentGroup', 'loadFrequentGroups']),
handleGroupChange(group) {
@ -33,7 +39,11 @@ export default {
}
visitUrl(
setUrlParams({ [GROUP_DATA.queryParam]: group.id, [PROJECT_DATA.queryParam]: null }),
setUrlParams({
[GROUP_DATA.queryParam]: group.id,
[PROJECT_DATA.queryParam]: null,
nav_source: null,
}),
);
},
},

View File

@ -17,12 +17,18 @@ export default {
},
},
computed: {
...mapState(['projects', 'fetchingProjects']),
...mapState(['query', 'projects', 'fetchingProjects']),
...mapGetters(['frequentProjects']),
selectedProject() {
return this.initialData ? this.initialData : ANY_OPTION;
},
},
created() {
// This tracks projects searched via the top nav search bar
if (this.query.nav_source === 'navbar' && this.initialData?.id) {
this.setFrequentProject(this.initialData);
}
},
methods: {
...mapActions(['fetchProjects', 'setFrequentProject', 'loadFrequentProjects']),
handleProjectChange(project) {
@ -35,6 +41,7 @@ export default {
const queryParams = {
...(project.namespace?.id && { [GROUP_DATA.queryParam]: project.namespace.id }),
[PROJECT_DATA.queryParam]: project.id,
nav_source: null,
};
visitUrl(setUrlParams(queryParams));

View File

@ -284,8 +284,8 @@ export class SearchAutocomplete {
if (projectId) {
const projectOptions = gl.projectOptions[getProjectSlug()];
const url = groupId
? `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}&group_id=${groupId}`
: `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}`;
? `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}&group_id=${groupId}&nav_source=navbar`
: `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}&nav_source=navbar`;
options.push({
icon,
@ -313,7 +313,7 @@ export class SearchAutocomplete {
},
false,
),
url: `${gon.relative_url_root}/search?search=${term}&group_id=${groupId}`,
url: `${gon.relative_url_root}/search?search=${term}&group_id=${groupId}&nav_source=navbar`,
});
}
@ -321,7 +321,7 @@ export class SearchAutocomplete {
icon,
text: term,
template: s__('SearchAutocomplete|in all GitLab'),
url: `${gon.relative_url_root}/search?search=${term}`,
url: `${gon.relative_url_root}/search?search=${term}&nav_source=navbar`,
});
return options;

View File

@ -143,7 +143,7 @@ module GroupsHelper
end
def remove_group_message(group)
_("You are going to remove %{group_name}, this will also delete all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
_("You are going to remove %{group_name}. This will also delete all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
{ group_name: group.name }
end

View File

@ -101,6 +101,8 @@ module Projects
@project.track_project_repository
@project.create_project_setting unless @project.project_setting
yield if block_given?
event_service.create_project(@project, current_user)
system_hook_service.execute_hooks_for(@project, :create)

View File

@ -28,3 +28,4 @@
= render 'groups/settings/transfer', group: @group
= render 'groups/settings/remove', group: @group
= render_if_exists 'groups/settings/restore', group: @group
= render_if_exists 'groups/settings/immediately_remove', group: @group

View File

@ -5,6 +5,7 @@
= hidden_field_tag :group_id, params[:group_id]
- if params[:project_id].present?
= hidden_field_tag :project_id, params[:project_id]
- group_attributes = @group&.attributes&.slice('id', 'name')&.merge(full_name: @group&.full_name)
- project_attributes = @project&.attributes&.slice('id', 'namespace_id', 'name')&.merge(name_with_namespace: @project&.name_with_namespace)
- if @search_results
@ -16,7 +17,7 @@
= render_if_exists 'search/form_elasticsearch', attrs: { class: 'mb-2 mb-sm-0 align-self-center' }
.gl-mt-3
#js-search-topbar{ data: { "group-initial-data": @group.to_json, "project-initial-data": project_attributes.to_json } }
#js-search-topbar{ data: { "group-initial-data": group_attributes.to_json, "project-initial-data": project_attributes.to_json } }
- if @search_term
= render 'search/category'
= render 'search/results'

View File

@ -1,8 +0,0 @@
---
name: allow_archive_as_web_access_format
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64471
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334944
milestone: '14.1'
type: development
group: group::release
default_enabled: false

View File

@ -3,15 +3,17 @@ data_category: Optional
key_path: counts_monthly.successful_deployments
description: Total successful deployments
product_section: ops
product_stage:
product_stage: release
product_group: group::release
product_category:
product_category: continuous_delivery
value_type: number
status: data_available
time_frame: 28d
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -3,15 +3,17 @@ data_category: Optional
key_path: counts_monthly.failed_deployments
description: Total failed deployments
product_section: ops
product_stage:
product_stage: release
product_group: group::release
product_category:
product_category: continuous_delivery
value_type: number
status: data_available
time_frame: 28d
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -3,15 +3,16 @@ data_category: Optional
key_path: usage_activity_by_stage_monthly.release.deployments
description: Unique users triggering deployments
product_section: ops
product_stage:
product_stage: release
product_group: group::release
product_category:
product_category: continuous_delivery
value_type: number
status: data_available
time_frame: 28d
data_source:
data_source: database
distribution:
- ce
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -3,15 +3,17 @@ data_category: Optional
key_path: usage_activity_by_stage_monthly.release.failed_deployments
description: Total failed deployments
product_section: ops
product_stage:
product_stage: release
product_group: group::release
product_category:
product_category: continuous_delivery
value_type: number
status: data_available
time_frame: 28d
data_source:
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -3,15 +3,17 @@ data_category: Optional
key_path: usage_activity_by_stage_monthly.release.releases
description: Unique users creating release tags
product_section: ops
product_stage:
product_stage: release
product_group: group::release
product_category:
product_category: continuous_delivery
value_type: number
status: data_available
time_frame: 28d
data_source:
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -3,15 +3,17 @@ data_category: Optional
key_path: usage_activity_by_stage_monthly.release.successful_deployments
description: Total successful deployments
product_section: ops
product_stage:
product_stage: release
product_group: group::release
product_category:
product_category: continuous_delivery
value_type: number
status: data_available
time_frame: 28d
data_source:
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -4,7 +4,7 @@ key_path: counts.deployments
description: Total deployments count
product_section: ops
product_stage: release
product_group: group::ops release
product_group: group::release
value_type: number
status: data_available
milestone: "8.12"

View File

@ -4,14 +4,16 @@ key_path: counts.feature_flags
description: Number of feature flag toggles
product_section: ops
product_stage: release
product_group: group::progressive delivery
product_category:
product_group: group::release
product_category: feature_flags
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,17 +1,19 @@
---
data_category: Optional
key_path: counts.deploy_keys
description:
description: Count of deploy keys
product_section: ops
product_stage:
product_stage: release
product_group: group::release
product_category:
product_category: continuous_delivery
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -3,15 +3,17 @@ data_category: Optional
key_path: counts.successful_deployments
description: Total successful deployments
product_section: ops
product_stage:
product_stage: release
product_group: group::release
product_category:
product_category: continuous_delivery
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -3,15 +3,17 @@ data_category: Optional
key_path: counts.failed_deployments
description: Total failed deployments
product_section: ops
product_stage:
product_stage: release
product_group: group::release
product_category:
product_category: continuous_delivery
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -3,15 +3,17 @@ data_category: Optional
key_path: counts.environments
description: Total available and stopped environments
product_section: ops
product_stage:
product_stage: release
product_group: group::release
product_category:
product_category: continuous_delivery
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,17 +1,19 @@
---
data_category: Optional
key_path: counts.in_review_folder
description:
description: A number of environments with name review/*
product_section: ops
product_stage:
product_stage: release
product_group: group::release
product_category:
product_category: continuous_delivery
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,17 +1,19 @@
---
data_category: Optional
key_path: counts.releases
description: Unique release tags
description: Count of releases
product_section: ops
product_stage:
product_stage: releases
product_group: group::release
product_category:
product_category: release_orchestration
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -3,15 +3,17 @@ data_category: Optional
key_path: usage_activity_by_stage.release.deployments
description: Unique users triggering deployments
product_section: ops
product_stage:
product_stage: release
product_group: group::release
product_category:
product_category: continuous_delivery
value_type: number
status: data_available
time_frame: all
data_source:
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -3,15 +3,17 @@ data_category: Optional
key_path: usage_activity_by_stage.release.failed_deployments
description: Total failed deployments
product_section: ops
product_stage:
product_stage: release
product_group: group::release
product_category:
product_category: continuous_delivery
value_type: number
status: data_available
time_frame: all
data_source:
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -3,15 +3,17 @@ data_category: Optional
key_path: usage_activity_by_stage.release.releases
description: Unique users creating release tags
product_section: ops
product_stage:
product_stage: release
product_group: group::release
product_category:
product_category: release_orchestration
value_type: number
status: data_available
time_frame: all
data_source:
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -3,15 +3,17 @@ data_category: Optional
key_path: usage_activity_by_stage.release.successful_deployments
description: Total successful deployments
product_section: ops
product_stage:
product_stage: release
product_group: group::release
product_category:
product_category: continuous_delivery
value_type: number
status: data_available
time_frame: all
data_source:
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -4,14 +4,16 @@ key_path: counts.pages_domains
description: Total GitLab Pages domains
product_section: ops
product_stage: release
product_group: group::release management
product_category:
product_group: group::release
product_category: pages
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -2,16 +2,18 @@
data_category: Optional
key_path: gitlab_pages.version
description: The version number of GitLab Pages
product_section: growth
product_stage: growth
product_group: group::product intelligence
product_category: collection
product_section: ops
product_stage: release
product_group: group::release
product_category: pages
value_type: string
status: data_available
time_frame: none
data_source: system
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -2,9 +2,9 @@
data_category: Optional
key_path: gitlab_pages.enabled
description: Whether GitLab Pages is enabled
product_section: growth
product_stage: growth
product_group: group::product intelligence
product_section: ops
product_stage: release
product_group: group::release
product_category: collection
value_type: boolean
status: data_available
@ -12,6 +12,8 @@ time_frame: none
data_source: system
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -425,6 +425,10 @@ it('does something', () => {
### Mocking the current location in Jest
NOTE:
The value of `window.location.href` is reset before every test to avoid earlier
tests affecting later ones.
If your tests require `window.location.href` to take a particular value, use
the `setWindowLocation` helper:
@ -442,7 +446,6 @@ it('passes', () => {
});
```
NOTE:
To modify only the hash, use either the `setWindowLocation` helper, or assign
directly to `window.location.hash`, e.g.:

View File

@ -1156,7 +1156,7 @@ Tiers: `ultimate`
### `counts.deploy_keys`
Missing description
Count of deploy keys
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216181908_deploy_keys.yml)
@ -1166,7 +1166,7 @@ Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.deployments`
@ -1174,7 +1174,7 @@ Total deployments count
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210201124934_deployments.yml)
Group: `group::ops release`
Group: `group::release`
Data Category: `Optional`
@ -1224,6 +1224,20 @@ Status: `data_available`
Tiers: `free`, `premium`, `ultimate`
### `counts.diff_searches`
Total count of merge request diff searches
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210723075525_diff_searches.yml)
Group: `group::code review`
Data Category: `Optional`
Status: `implemented`
Tiers: `free`, `premium`, `ultimate`
### `counts.environments`
Total available and stopped environments
@ -1236,7 +1250,7 @@ Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.epic_issues`
@ -1292,7 +1306,7 @@ Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.feature_flags`
@ -1300,13 +1314,13 @@ Number of feature flag toggles
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216181249_feature_flags.yml)
Group: `group::progressive delivery`
Group: `group::release`
Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.geo_event_log_max_id`
@ -2864,7 +2878,7 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.in_review_folder`
Missing description
A number of environments with name review/*
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216181916_in_review_folder.yml)
@ -2874,7 +2888,7 @@ Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.incident_issues`
@ -4988,7 +5002,7 @@ Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.personal_snippets`
@ -6728,7 +6742,7 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.releases`
Unique release tags
Count of releases
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216181918_releases.yml)
@ -6738,7 +6752,7 @@ Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.remote_mirrors`
@ -7046,7 +7060,7 @@ Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.suggestions`
@ -7706,20 +7720,6 @@ Status: `data_available`
Tiers: `free`, `premium`, `ultimate`
### `counts.user_searches_diffs`
Count of users who search merge request diffs
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210723075525_user_searches_diffs.yml)
Group: `group::code review`
Data Category: `Optional`
Status: `implemented`
Tiers: `free`, `premium`, `ultimate`
### `counts.web_hooks`
Missing description
@ -8026,7 +8026,7 @@ Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts_monthly.packages`
@ -8124,7 +8124,7 @@ Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts_weekly.aggregated_metrics.code_review_category_monthly_active_users`
@ -8424,29 +8424,29 @@ Tiers: `free`
Whether GitLab Pages is enabled
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/settings/20210204124934_enabled.yml)
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/settings/20210204124934_pages_enabled.yml)
Group: `group::product intelligence`
Group: `group::release`
Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `gitlab_pages.version`
The version number of GitLab Pages
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/license/20210204124936_version.yml)
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/license/20210204124936_pages_version.yml)
Group: `group::product intelligence`
Group: `group::release`
Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `gitlab_shared_runners_enabled`
@ -10716,20 +10716,6 @@ Status: `data_available`
Tiers: `free`, `premium`, `ultimate`
### `redis_hll_counters.code_review.i_code_review_searches_in_diff_monthly`
Count of searches in merge request diffs
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210722132444_i_code_review_searches_in_diff_monthly.yml)
Group: `group::code review`
Data Category: `Optional`
Status: `implemented`
Tiers: `free`, `premium`, `ultimate`
### `redis_hll_counters.code_review.i_code_review_user_add_suggestion_monthly`
Count of unique users per month who added a suggestion
@ -11612,6 +11598,20 @@ Status: `implemented`
Tiers: `free`, `premium`, `ultimate`
### `redis_hll_counters.code_review.i_code_review_user_searches_diff_weekly`
Count of users who search merge request diffs
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_7d/20210720144005_i_code_review_user_searches_diff_weekly.yml)
Group: `group::code review`
Data Category: `Optional`
Status: `implemented`
Tiers: `free`, `premium`, `ultimate`
### `redis_hll_counters.code_review.i_code_review_user_single_file_diffs_monthly`
Count of unique users per month with diffs viewed file by file
@ -20422,7 +20422,7 @@ Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `usage_activity_by_stage.release.failed_deployments`
@ -20436,7 +20436,7 @@ Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `usage_activity_by_stage.release.projects_mirrored_with_pipelines_enabled`
@ -20464,7 +20464,7 @@ Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `usage_activity_by_stage.release.successful_deployments`
@ -20478,7 +20478,7 @@ Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `usage_activity_by_stage.secure.api_fuzzing_scans`
@ -22830,7 +22830,7 @@ Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `usage_activity_by_stage_monthly.release.failed_deployments`
@ -22844,7 +22844,7 @@ Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `usage_activity_by_stage_monthly.release.projects_mirrored_with_pipelines_enabled`
@ -22872,7 +22872,7 @@ Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `usage_activity_by_stage_monthly.release.successful_deployments`
@ -22886,7 +22886,7 @@ Data Category: `Optional`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `usage_activity_by_stage_monthly.secure.api_fuzzing_pipeline`

View File

@ -430,6 +430,28 @@ Specifically:
- In [GitLab 13.6 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/39504), if the user who sets up the deletion is removed from the group before the
deletion happens, the job is cancelled, and the group is no longer scheduled for deletion.
## Remove a group immediately **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/336985) in GitLab 14.2.
If you don't want to wait, you can remove a group immediately.
Prerequisites:
- You must have at least the Owner role for a group.
- You have [marked the group for deletion](#remove-a-group).
To immediately remove a group marked for deletion:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Settings > General**.
1. Expand **Advanced**.
1. In the "Permanently remove group" section, select **Remove group**.
1. Confirm the action when asked to.
Your group, its subgroups, projects, and all related resources, including issues and merge requests,
are deleted.
## Restore a group **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33257) in GitLab 12.8.

View File

@ -298,7 +298,7 @@ module Gitlab
when :api
api_request?
when :archive
archive_request? if Feature.enabled?(:allow_archive_as_web_access_format, default_enabled: :yaml)
archive_request?
end
end

View File

@ -41,7 +41,9 @@ module Gitlab
end
def tunnel_url
URI.join(external_url, K8S_PROXY_PATH).to_s
uri = URI.join(external_url, K8S_PROXY_PATH)
uri.scheme = uri.scheme.in?(%w(grpcs wss)) ? 'https' : 'http'
uri.to_s
end
# Return GitLab KAS internal_url

View File

@ -4317,6 +4317,9 @@ msgstr ""
msgid "Are you ABSOLUTELY SURE you wish to delete this project?"
msgstr ""
msgid "Are you ABSOLUTELY SURE you wish to remove this group?"
msgstr ""
msgid "Are you sure that you want to archive this project?"
msgstr ""
@ -24072,6 +24075,9 @@ msgstr ""
msgid "Permanently delete project"
msgstr ""
msgid "Permanently remove group"
msgstr ""
msgid "Permissions"
msgstr ""
@ -33688,6 +33694,9 @@ msgstr ""
msgid "This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}on %{date}%{strongClose}, including its repositories and all related resources, including issues and merge requests."
msgstr ""
msgid "This action will %{strongOpen}permanently remove%{strongClose} %{codeOpen}%{group}%{codeClose} %{strongOpen}immediately%{strongClose}."
msgstr ""
msgid "This also resolves all related threads"
msgstr ""
@ -37633,7 +37642,7 @@ msgstr ""
msgid "You are going to delete %{project_full_name}. Deleted projects CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr ""
msgid "You are going to remove %{group_name}, this will also delete all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgid "You are going to remove %{group_name}. This will also delete all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr ""
msgid "You are going to remove the fork relationship from %{project_full_name}. Are you ABSOLUTELY sure?"

View File

@ -21,7 +21,7 @@ module QA
end
def to_s
<<~MSG.strip % { page: @page_class }
format(<<~MSG.strip, page: @page_class)
%{page} has no required elements.
See https://docs.gitlab.com/ee/development/testing_guide/end_to_end/dynamic_element_validation.html#required-elements
MSG
@ -232,7 +232,7 @@ module QA
visible = kwargs.delete(:visible)
visible = visible.nil? && true
try_find_element = ->(wait) do
try_find_element = lambda do |wait|
if disabled.nil?
has_css?(element_selector_css(name, kwargs), text: text, wait: wait, class: klass, visible: visible)
else
@ -322,13 +322,13 @@ module QA
# It would be ideal if we could detect when the animation is complete
# but in some cases there's nothing we can easily access via capybara
# so instead we wait for the element, and then we wait a little longer
raise ElementNotFound, %Q(Couldn't find element named "#{name}") unless has_element?(name)
raise ElementNotFound, %(Couldn't find element named "#{name}") unless has_element?(name)
sleep 1
end
def within_element(name, **kwargs)
wait_for_requests
wait_for_requests(skip_finished_loading_check: kwargs.delete(:skip_finished_loading_check))
text = kwargs.delete(:text)
page.within(element_selector_css(name, kwargs), text: text) do
@ -386,9 +386,7 @@ module QA
end
def self.errors
if views.empty?
return ["Page class does not have views / elements defined!"]
end
return ["Page class does not have views / elements defined!"] if views.empty?
views.flat_map(&:errors)
end

View File

@ -18,12 +18,17 @@ module QA
element :import_button
element :project_path_content
element :go_to_project_button
element :import_status_indicator
end
view "app/assets/javascripts/import_entities/components/group_dropdown.vue" do
element :target_namespace_selector_dropdown
end
# Add personal access token
#
# @param [String] personal_access_token
# @return [void]
def add_personal_access_token(personal_access_token)
# If for some reasons this process is retried, user cannot re-enter github token in the same group
# In this case skip this step and proceed to import project row
@ -34,71 +39,50 @@ module QA
finished_loading?
end
def import!(full_path, name)
return if already_imported(full_path)
choose_test_namespace(full_path)
set_path(full_path, name)
import_project(full_path)
wait_for_success
end
# TODO: refactor to use 'go to project' button instead of generic main menu
def go_to_project(name)
Page::Main::Menu.perform(&:go_to_projects)
Page::Dashboard::Projects.perform do |dashboard|
dashboard.go_to_project(name)
end
end
private
def within_repo_path(full_path, &block)
project_import_row = find_element(:project_import_row, text: full_path)
within(project_import_row, &block)
end
def choose_test_namespace(full_path)
within_repo_path(full_path) do
within_element(:target_namespace_selector_dropdown) { click_button(class: 'dropdown-toggle') }
click_element(:target_group_dropdown_item, group_name: Runtime::Namespace.path)
end
end
def set_path(full_path, name)
within_repo_path(full_path) do
fill_element(:project_path_field, name)
end
end
def import_project(full_path)
within_repo_path(full_path) do
# Import project
#
# @param [String] source_project_name
# @param [String] target_group_path
# @return [void]
def import!(gh_project_name, target_group_path, project_name)
within_element(:project_import_row, source_project: gh_project_name) do
click_element(:target_namespace_selector_dropdown)
click_element(:target_group_dropdown_item, group_name: target_group_path)
fill_element(:project_path_field, project_name)
click_element(:import_button)
end
end
def wait_for_success
# TODO: set reload:false and remove skip_finished_loading_check_on_refresh when
# https://gitlab.com/gitlab-org/gitlab/-/issues/292861 is fixed
wait_until(
max_duration: 90,
sleep_interval: 5.0,
reload: true,
skip_finished_loading_check_on_refresh: true
) do
# TODO: Refactor to explicitly wait for specific project import successful status
# This check can create false positive if main importing message appears with delay and check exits early
page.has_no_content?('Importing 1 repository', wait: 3)
# Check Go to project button present
#
# @param [String] gh_project_name
# @return [Boolean]
def has_go_to_project_button?(gh_project_name)
within_element(:project_import_row, source_project: gh_project_name) do
has_element?(:go_to_project_button)
end
end
def already_imported(full_path)
within_repo_path(full_path) do
has_element?(:project_path_content) && has_element?(:go_to_project_button)
# Check if import page has a successfully imported project
#
# @param [String] source_project_name
# @param [Integer] wait
# @return [Boolean]
def has_imported_project?(gh_project_name, wait: QA::Support::WaitForRequests::DEFAULT_MAX_WAIT_TIME)
within_element(:project_import_row, source_project: gh_project_name, skip_finished_loading_check: true) do
# TODO: remove retrier with reload:true once https://gitlab.com/gitlab-org/gitlab/-/issues/292861 is fixed
wait_until(
max_duration: wait,
sleep_interval: 5,
reload: true,
skip_finished_loading_check_on_refresh: true
) do
has_element?(:import_status_indicator, text: "Complete")
end
end
end
alias_method :wait_for_success, :has_imported_project?
end
end
end

View File

@ -18,9 +18,12 @@ module QA
Page::Project::Import::Github.perform do |import_page|
import_page.add_personal_access_token(github_personal_access_token)
import_page.import!(github_repository_path, name)
import_page.go_to_project(name)
import_page.import!(github_repository_path, group.full_path, name)
import_page.wait_for_success(github_repository_path)
end
reload!
visit!
end
def go_to_import_page

View File

@ -3,9 +3,11 @@
module QA
RSpec.describe 'Manage', :github, :requires_admin do
describe 'Project import' do
let!(:api_client) { Runtime::API::Client.as_admin }
let!(:group) { Resource::Group.fabricate_via_api! { |resource| resource.api_client = api_client } }
let!(:user) do
let(:github_repo) { 'gitlab-qa-github/test-project' }
let(:imported_project_name) { 'imported-project' }
let(:api_client) { Runtime::API::Client.as_admin }
let(:group) { Resource::Group.fabricate_via_api! { |resource| resource.api_client = api_client } }
let(:user) do
Resource::User.fabricate_via_api! do |resource|
resource.api_client = api_client
resource.hard_delete_on_api_removal = true
@ -13,16 +15,25 @@ module QA
end
let(:imported_project) do
Resource::ProjectImportedFromGithub.fabricate_via_browser_ui! do |project|
project.name = 'imported-project'
Resource::ProjectImportedFromGithub.init do |project|
project.import = true
project.add_name_uuid = false
project.name = imported_project_name
project.group = group
project.github_personal_access_token = Runtime::Env.github_access_token
project.github_repository_path = 'gitlab-qa-github/test-project'
project.github_repository_path = github_repo
end
end
before do
group.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
Flow::Login.sign_in(as: user)
Page::Main::Menu.perform(&:go_to_create_project)
Page::Project::New.perform do |project_page|
project_page.click_import_project
project_page.click_github_link
end
end
after do
@ -30,13 +41,24 @@ module QA
end
it 'imports a GitHub repo', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1762' do
Flow::Login.sign_in(as: user)
Page::Project::Import::Github.perform do |import_page|
import_page.add_personal_access_token(Runtime::Env.github_access_token)
import_page.import!(github_repo, group.full_path, imported_project_name)
imported_project # import the project
aggregate_failures do
expect(import_page).to have_imported_project(github_repo)
# validate button is present instead of navigating to avoid dealing with multiple tabs
# which makes the test more complicated
expect(import_page).to have_go_to_project_button(github_repo)
end
end
imported_project.reload!.visit!
Page::Project::Show.perform do |project|
expect(project).to have_content(imported_project.name)
expect(project).to have_content('This test project is used for automated GitHub import by GitLab QA.')
aggregate_failures do
expect(project).to have_content(imported_project_name)
expect(project).to have_content('This test project is used for automated GitHub import by GitLab QA.')
end
end
end
end

View File

@ -254,6 +254,7 @@ RSpec.describe 'User uses header search field', :js do
href = search_path(search: term)
href.concat("&project_id=#{project_id}") if project_id
href.concat("&group_id=#{group_id}") if group_id
href.concat("&nav_source=navbar")
".dropdown a[href='#{href}']"
end

View File

@ -10,7 +10,7 @@
*/
const useMockLocation = (fn) => {
const origWindowLocation = window.location;
let currentWindowLocation;
let currentWindowLocation = origWindowLocation;
Object.defineProperty(window, 'location', {
get: () => currentWindowLocation,

View File

@ -1,4 +1,5 @@
import setWindowLocation from './set_window_location_helper';
import { TEST_HOST } from './test_constants';
describe('helpers/set_window_location_helper', () => {
const originalLocation = window.location.href;
@ -103,4 +104,30 @@ describe('helpers/set_window_location_helper', () => {
);
});
});
// This set of tests relies on Jest executing tests in source order, which is
// at the time of writing the only order they will execute, by design.
// See https://github.com/facebook/jest/issues/4386 for more details.
describe('window.location resetting by global beforeEach', () => {
const overridden = 'https://gdk.test:1234/';
const initial = `${TEST_HOST}/`;
it('works before an override', () => {
expect(window.location.href).toBe(initial);
});
describe('overriding', () => {
beforeEach(() => {
setWindowLocation(overridden);
});
it('works', () => {
expect(window.location.href).toBe(overridden);
});
});
it('works after an override', () => {
expect(window.location.href).toBe(initial);
});
});
});

View File

@ -259,6 +259,8 @@ describe('diffs/components/app', () => {
});
it('marks current diff file based on currently highlighted row', async () => {
window.location.hash = 'ABC_123';
createComponent({
shouldShow: true,
});
@ -429,9 +431,8 @@ describe('diffs/components/app', () => {
jest.spyOn(wrapper.vm, 'refetchDiffData').mockImplementation(() => {});
jest.spyOn(wrapper.vm, 'adjustView').mockImplementation(() => {});
};
const location = window.location.href;
beforeAll(() => {
beforeEach(() => {
setWindowLocation(COMMIT_URL);
document.title = 'My Title';
});
@ -440,10 +441,6 @@ describe('diffs/components/app', () => {
jest.spyOn(urlUtils, 'updateHistory');
});
afterAll(() => {
setWindowLocation(location);
});
it('when the commit changes and the app is not loading it should update the history, refetch the diff data, and update the view', async () => {
createComponent({}, ({ state }) => {
state.diffs.commit = { ...state.diffs.commit, id: 'OLD' };

View File

@ -448,7 +448,7 @@ describe('monitoring/utils', () => {
input | urlParams
${[]} | ${''}
${[{ name: 'env', value: 'prod' }]} | ${'?var-env=prod'}
${[{ name: 'env1', value: 'prod' }, { name: 'env2', value: null }]} | ${'?var-env=prod&var-env1=prod'}
${[{ name: 'env1', value: 'prod' }, { name: 'env2', value: null }]} | ${'?var-env1=prod'}
`(
'setCustomVariablesFromUrl updates history with query "$urlParams" with input $input',
({ input, urlParams }) => {

View File

@ -35,9 +35,19 @@ exports[`PackageTitle renders with tags 1`] = `
size="16"
/>
<gl-sprintf-stub
message="v%{version} published %{timeAgo}"
/>
<span
data-testid="sub-header"
>
v
1.0.0
published
<time-ago-tooltip-stub
class="gl-ml-2"
cssclass=""
time="2020-08-17T14:23:32Z"
tooltipplacement="top"
/>
</span>
</div>
</div>
</div>
@ -123,9 +133,19 @@ exports[`PackageTitle renders without tags 1`] = `
size="16"
/>
<gl-sprintf-stub
message="v%{version} published %{timeAgo}"
/>
<span
data-testid="sub-header"
>
v
1.0.0
published
<time-ago-tooltip-stub
class="gl-ml-2"
cssclass=""
time="2020-08-17T14:23:32Z"
tooltipplacement="top"
/>
</span>
</div>
</div>
</div>

View File

@ -1,3 +1,4 @@
import { GlIcon, GlSprintf } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PackageTags from '~/packages/shared/components/package_tags.vue';
@ -9,6 +10,7 @@ import {
PACKAGE_TYPE_NUGET,
} from '~/packages_and_registries/package_registry/constants';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { packageData, packageFiles, packageTags, packagePipelines } from '../../mock_data';
@ -26,6 +28,7 @@ describe('PackageTitle', () => {
propsData: { packageEntity },
stubs: {
TitleArea,
GlSprintf,
},
});
return wrapper.vm.$nextTick();
@ -38,6 +41,9 @@ describe('PackageTitle', () => {
const findPackageRef = () => wrapper.findByTestId('package-ref');
const findPackageTags = () => wrapper.findComponent(PackageTags);
const findPackageBadges = () => wrapper.findAllByTestId('tag-badge');
const findSubHeaderIcon = () => wrapper.findComponent(GlIcon);
const findSubHeaderText = () => wrapper.findByTestId('sub-header');
const findSubHeaderTimeAgo = () => wrapper.findComponent(TimeAgoTooltip);
afterEach(() => {
wrapper.destroy();
@ -59,6 +65,7 @@ describe('PackageTitle', () => {
it('with tags on mobile', async () => {
jest.spyOn(GlBreakpointInstance, 'isDesktop').mockReturnValue(false);
await createComponent();
await wrapper.vm.$nextTick();
expect(findPackageBadges()).toHaveLength(packageTags().length);
@ -93,6 +100,25 @@ describe('PackageTitle', () => {
});
});
describe('sub-header', () => {
it('has the eye icon', async () => {
await createComponent();
expect(findSubHeaderIcon().props('name')).toBe('eye');
});
it('has a text showing version', async () => {
await createComponent();
expect(findSubHeaderText().text()).toMatchInterpolatedText('v 1.0.0 published');
});
it('has a time ago tooltip component', async () => {
await createComponent();
expect(findSubHeaderTimeAgo().props('time')).toBe(packageWithTags.createdAt);
});
});
describe.each`
packageType | text
${PACKAGE_TYPE_CONAN} | ${'Conan'}

View File

@ -86,19 +86,22 @@ export const STALE_STORED_DATA = [
export const MOCK_FRESH_DATA_RES = { name: 'fresh' };
export const PROMISE_ALL_EXPECTED_MUTATIONS = {
initGroups: {
export const PRELOAD_EXPECTED_MUTATIONS = [
{
type: types.LOAD_FREQUENT_ITEMS,
payload: { key: GROUPS_LOCAL_STORAGE_KEY, data: FRESH_STORED_DATA },
},
{
type: types.LOAD_FREQUENT_ITEMS,
payload: { key: PROJECTS_LOCAL_STORAGE_KEY, data: FRESH_STORED_DATA },
},
];
export const PROMISE_ALL_EXPECTED_MUTATIONS = {
resGroups: {
type: types.LOAD_FREQUENT_ITEMS,
payload: { key: GROUPS_LOCAL_STORAGE_KEY, data: [MOCK_FRESH_DATA_RES, MOCK_FRESH_DATA_RES] },
},
initProjects: {
type: types.LOAD_FREQUENT_ITEMS,
payload: { key: PROJECTS_LOCAL_STORAGE_KEY, data: FRESH_STORED_DATA },
},
resProjects: {
type: types.LOAD_FREQUENT_ITEMS,
payload: { key: PROJECTS_LOCAL_STORAGE_KEY, data: [MOCK_FRESH_DATA_RES, MOCK_FRESH_DATA_RES] },

View File

@ -17,6 +17,7 @@ import {
MOCK_GROUP,
FRESH_STORED_DATA,
MOCK_FRESH_DATA_RES,
PRELOAD_EXPECTED_MUTATIONS,
PROMISE_ALL_EXPECTED_MUTATIONS,
} from '../mock_data';
@ -68,31 +69,31 @@ describe('Global Search Store Actions', () => {
});
describe.each`
action | axiosMock | type | expectedMutations | flashCallCount | lsKey
${actions.loadFrequentGroups} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.initGroups, PROMISE_ALL_EXPECTED_MUTATIONS.resGroups]} | ${0} | ${GROUPS_LOCAL_STORAGE_KEY}
${actions.loadFrequentGroups} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.initGroups]} | ${1} | ${GROUPS_LOCAL_STORAGE_KEY}
${actions.loadFrequentProjects} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.initProjects, PROMISE_ALL_EXPECTED_MUTATIONS.resProjects]} | ${0} | ${PROJECTS_LOCAL_STORAGE_KEY}
${actions.loadFrequentProjects} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.initProjects]} | ${1} | ${PROJECTS_LOCAL_STORAGE_KEY}
`(
'Promise.all calls',
({ action, axiosMock, type, expectedMutations, flashCallCount, lsKey }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
beforeEach(() => {
storeUtils.loadDataFromLS = jest.fn().mockReturnValue(FRESH_STORED_DATA);
mock[axiosMock.method]().reply(axiosMock.code, MOCK_FRESH_DATA_RES);
});
action | axiosMock | type | expectedMutations | flashCallCount
${actions.loadFrequentGroups} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resGroups]} | ${0}
${actions.loadFrequentGroups} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${[]} | ${1}
${actions.loadFrequentProjects} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resProjects]} | ${0}
${actions.loadFrequentProjects} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${[]} | ${1}
`('Promise.all calls', ({ action, axiosMock, type, expectedMutations, flashCallCount }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
beforeEach(() => {
state.frequentItems = {
[GROUPS_LOCAL_STORAGE_KEY]: FRESH_STORED_DATA,
[PROJECTS_LOCAL_STORAGE_KEY]: FRESH_STORED_DATA,
};
it(`should dispatch the correct mutations`, () => {
return testAction({ action, state, expectedMutations }).then(() => {
expect(storeUtils.loadDataFromLS).toHaveBeenCalledWith(lsKey);
flashCallback(flashCallCount);
});
mock[axiosMock.method]().reply(axiosMock.code, MOCK_FRESH_DATA_RES);
});
it(`should dispatch the correct mutations`, () => {
return testAction({ action, state, expectedMutations }).then(() => {
flashCallback(flashCallCount);
});
});
});
},
);
});
});
describe('getGroupsData', () => {
const mockCommit = () => {};
@ -182,14 +183,38 @@ describe('Global Search Store Actions', () => {
});
});
describe('setFrequentGroup', () => {
describe('preloadStoredFrequentItems', () => {
beforeEach(() => {
storeUtils.setFrequentItemToLS = jest.fn();
storeUtils.loadDataFromLS = jest.fn().mockReturnValue(FRESH_STORED_DATA);
});
it(`calls setFrequentItemToLS with ${GROUPS_LOCAL_STORAGE_KEY} and item data`, async () => {
it('calls preloadStoredFrequentItems for both groups and projects and commits LOAD_FREQUENT_ITEMS', async () => {
await testAction({
action: actions.preloadStoredFrequentItems,
state,
expectedMutations: PRELOAD_EXPECTED_MUTATIONS,
});
expect(storeUtils.loadDataFromLS).toHaveBeenCalledTimes(2);
expect(storeUtils.loadDataFromLS).toHaveBeenCalledWith(GROUPS_LOCAL_STORAGE_KEY);
expect(storeUtils.loadDataFromLS).toHaveBeenCalledWith(PROJECTS_LOCAL_STORAGE_KEY);
});
});
describe('setFrequentGroup', () => {
beforeEach(() => {
storeUtils.setFrequentItemToLS = jest.fn().mockReturnValue(FRESH_STORED_DATA);
});
it(`calls setFrequentItemToLS with ${GROUPS_LOCAL_STORAGE_KEY} and item data then commits LOAD_FREQUENT_ITEMS`, async () => {
await testAction({
action: actions.setFrequentGroup,
expectedMutations: [
{
type: types.LOAD_FREQUENT_ITEMS,
payload: { key: GROUPS_LOCAL_STORAGE_KEY, data: FRESH_STORED_DATA },
},
],
payload: MOCK_GROUP,
state,
});
@ -204,12 +229,18 @@ describe('Global Search Store Actions', () => {
describe('setFrequentProject', () => {
beforeEach(() => {
storeUtils.setFrequentItemToLS = jest.fn();
storeUtils.setFrequentItemToLS = jest.fn().mockReturnValue(FRESH_STORED_DATA);
});
it(`calls setFrequentItemToLS with ${PROJECTS_LOCAL_STORAGE_KEY} and item data`, async () => {
await testAction({
action: actions.setFrequentProject,
expectedMutations: [
{
type: types.LOAD_FREQUENT_ITEMS,
payload: { key: PROJECTS_LOCAL_STORAGE_KEY, data: FRESH_STORED_DATA },
},
],
payload: MOCK_PROJECT,
state,
});

View File

@ -51,19 +51,25 @@ describe('Global Search Store Utils', () => {
describe('setFrequentItemToLS', () => {
const frequentItems = {};
let res;
describe('with existing data', () => {
describe(`when frequency is less than ${MAX_FREQUENCY}`, () => {
beforeEach(() => {
frequentItems[MOCK_LS_KEY] = [{ ...MOCK_GROUPS[0], frequency: 1, lastUsed: PREV_TIME }];
setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]);
res = setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]);
});
it('adds 1 to the frequency, tracks lastUsed, and calls localStorage.setItem', () => {
it('adds 1 to the frequency, tracks lastUsed, calls localStorage.setItem and returns the array', () => {
const updatedFrequentItems = [
{ ...MOCK_GROUPS[0], frequency: 2, lastUsed: CURRENT_TIME },
];
expect(localStorage.setItem).toHaveBeenCalledWith(
MOCK_LS_KEY,
JSON.stringify([{ ...MOCK_GROUPS[0], frequency: 2, lastUsed: CURRENT_TIME }]),
JSON.stringify(updatedFrequentItems),
);
expect(res).toEqual(updatedFrequentItems);
});
});
@ -72,16 +78,19 @@ describe('Global Search Store Utils', () => {
frequentItems[MOCK_LS_KEY] = [
{ ...MOCK_GROUPS[0], frequency: MAX_FREQUENCY, lastUsed: PREV_TIME },
];
setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]);
res = setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]);
});
it(`does not further increase frequency past ${MAX_FREQUENCY}, tracks lastUsed, and calls localStorage.setItem`, () => {
it(`does not further increase frequency past ${MAX_FREQUENCY}, tracks lastUsed, calls localStorage.setItem, and returns the array`, () => {
const updatedFrequentItems = [
{ ...MOCK_GROUPS[0], frequency: MAX_FREQUENCY, lastUsed: CURRENT_TIME },
];
expect(localStorage.setItem).toHaveBeenCalledWith(
MOCK_LS_KEY,
JSON.stringify([
{ ...MOCK_GROUPS[0], frequency: MAX_FREQUENCY, lastUsed: CURRENT_TIME },
]),
JSON.stringify(updatedFrequentItems),
);
expect(res).toEqual(updatedFrequentItems);
});
});
});
@ -89,14 +98,17 @@ describe('Global Search Store Utils', () => {
describe('with no existing data', () => {
beforeEach(() => {
frequentItems[MOCK_LS_KEY] = [];
setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]);
res = setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]);
});
it('adds a new entry with frequency 1, tracks lastUsed, and calls localStorage.setItem', () => {
it('adds a new entry with frequency 1, tracks lastUsed, calls localStorage.setItem, and returns the array', () => {
const updatedFrequentItems = [{ ...MOCK_GROUPS[0], frequency: 1, lastUsed: CURRENT_TIME }];
expect(localStorage.setItem).toHaveBeenCalledWith(
MOCK_LS_KEY,
JSON.stringify([{ ...MOCK_GROUPS[0], frequency: 1, lastUsed: CURRENT_TIME }]),
JSON.stringify(updatedFrequentItems),
);
expect(res).toEqual(updatedFrequentItems);
});
});
@ -107,18 +119,21 @@ describe('Global Search Store Utils', () => {
{ id: 2, frequency: 1, lastUsed: PREV_TIME },
{ id: 3, frequency: 1, lastUsed: PREV_TIME },
];
setFrequentItemToLS(MOCK_LS_KEY, frequentItems, { id: 3 });
res = setFrequentItemToLS(MOCK_LS_KEY, frequentItems, { id: 3 });
});
it('sorts the array by most frequent and lastUsed', () => {
it('sorts the array by most frequent and lastUsed and returns the array', () => {
const updatedFrequentItems = [
{ id: 3, frequency: 2, lastUsed: CURRENT_TIME },
{ id: 1, frequency: 2, lastUsed: PREV_TIME },
{ id: 2, frequency: 1, lastUsed: PREV_TIME },
];
expect(localStorage.setItem).toHaveBeenCalledWith(
MOCK_LS_KEY,
JSON.stringify([
{ id: 3, frequency: 2, lastUsed: CURRENT_TIME },
{ id: 1, frequency: 2, lastUsed: PREV_TIME },
{ id: 2, frequency: 1, lastUsed: PREV_TIME },
]),
JSON.stringify(updatedFrequentItems),
);
expect(res).toEqual(updatedFrequentItems);
});
});
@ -131,31 +146,35 @@ describe('Global Search Store Utils', () => {
{ id: 4, frequency: 2, lastUsed: PREV_TIME },
{ id: 5, frequency: 1, lastUsed: PREV_TIME },
];
setFrequentItemToLS(MOCK_LS_KEY, frequentItems, { id: 6 });
res = setFrequentItemToLS(MOCK_LS_KEY, frequentItems, { id: 6 });
});
it('removes the last item in the array', () => {
it('removes the last item in the array and returns the array', () => {
const updatedFrequentItems = [
{ id: 1, frequency: 5, lastUsed: PREV_TIME },
{ id: 2, frequency: 4, lastUsed: PREV_TIME },
{ id: 3, frequency: 3, lastUsed: PREV_TIME },
{ id: 4, frequency: 2, lastUsed: PREV_TIME },
{ id: 6, frequency: 1, lastUsed: CURRENT_TIME },
];
expect(localStorage.setItem).toHaveBeenCalledWith(
MOCK_LS_KEY,
JSON.stringify([
{ id: 1, frequency: 5, lastUsed: PREV_TIME },
{ id: 2, frequency: 4, lastUsed: PREV_TIME },
{ id: 3, frequency: 3, lastUsed: PREV_TIME },
{ id: 4, frequency: 2, lastUsed: PREV_TIME },
{ id: 6, frequency: 1, lastUsed: CURRENT_TIME },
]),
JSON.stringify(updatedFrequentItems),
);
expect(res).toEqual(updatedFrequentItems);
});
});
describe('with null data loaded in', () => {
beforeEach(() => {
frequentItems[MOCK_LS_KEY] = null;
setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]);
res = setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]);
});
it('wipes local storage', () => {
it('wipes local storage and returns empty array', () => {
expect(localStorage.removeItem).toHaveBeenCalledWith(MOCK_LS_KEY);
expect(res).toEqual([]);
});
});
@ -163,14 +182,17 @@ describe('Global Search Store Utils', () => {
beforeEach(() => {
const MOCK_ADDITIONAL_DATA_GROUP = { ...MOCK_GROUPS[0], extraData: 'test' };
frequentItems[MOCK_LS_KEY] = [];
setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_ADDITIONAL_DATA_GROUP);
res = setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_ADDITIONAL_DATA_GROUP);
});
it('parses out extra data for LS', () => {
it('parses out extra data for LS and returns the array', () => {
const updatedFrequentItems = [{ ...MOCK_GROUPS[0], frequency: 1, lastUsed: CURRENT_TIME }];
expect(localStorage.setItem).toHaveBeenCalledWith(
MOCK_LS_KEY,
JSON.stringify([{ ...MOCK_GROUPS[0], frequency: 1, lastUsed: CURRENT_TIME }]),
JSON.stringify(updatedFrequentItems),
);
expect(res).toEqual(updatedFrequentItems);
});
});
});

View File

@ -1,13 +1,13 @@
import { GlForm, GlSearchBoxByType, GlButton } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { MOCK_QUERY } from 'jest/search/mock_data';
import GlobalSearchTopbar from '~/search/topbar/components/app.vue';
import GroupFilter from '~/search/topbar/components/group_filter.vue';
import ProjectFilter from '~/search/topbar/components/project_filter.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
Vue.use(Vuex);
describe('GlobalSearchTopbar', () => {
let wrapper;
@ -15,6 +15,7 @@ describe('GlobalSearchTopbar', () => {
const actionSpies = {
applyQuery: jest.fn(),
setQuery: jest.fn(),
preloadStoredFrequentItems: jest.fn(),
};
const createComponent = (initialState) => {
@ -27,14 +28,12 @@ describe('GlobalSearchTopbar', () => {
});
wrapper = shallowMount(GlobalSearchTopbar, {
localVue,
store,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findTopbarForm = () => wrapper.find(GlForm);
@ -110,4 +109,14 @@ describe('GlobalSearchTopbar', () => {
expect(actionSpies.applyQuery).toHaveBeenCalled();
});
});
describe('onCreate', () => {
beforeEach(() => {
createComponent();
});
it('calls preloadStoredFrequentItems', () => {
expect(actionSpies.preloadStoredFrequentItems).toHaveBeenCalled();
});
});
});

View File

@ -51,7 +51,6 @@ describe('GroupFilter', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findSearchableDropdown = () => wrapper.find(SearchableDropdown);
@ -89,10 +88,11 @@ describe('GroupFilter', () => {
findSearchableDropdown().vm.$emit('change', ANY_OPTION);
});
it('calls setUrlParams with group null, project id null, and then calls visitUrl', () => {
it('calls setUrlParams with group null, project id null, nav_source null, and then calls visitUrl', () => {
expect(setUrlParams).toHaveBeenCalledWith({
[GROUP_DATA.queryParam]: null,
[PROJECT_DATA.queryParam]: null,
nav_source: null,
});
expect(visitUrl).toHaveBeenCalled();
@ -108,10 +108,11 @@ describe('GroupFilter', () => {
findSearchableDropdown().vm.$emit('change', MOCK_GROUP);
});
it('calls setUrlParams with group id, project id null, and then calls visitUrl', () => {
it('calls setUrlParams with group id, project id null, nav_source null, and then calls visitUrl', () => {
expect(setUrlParams).toHaveBeenCalledWith({
[GROUP_DATA.queryParam]: MOCK_GROUP.id,
[PROJECT_DATA.queryParam]: null,
nav_source: null,
});
expect(visitUrl).toHaveBeenCalled();
@ -156,4 +157,31 @@ describe('GroupFilter', () => {
});
});
});
describe.each`
navSource | initialData | callMethod
${null} | ${null} | ${false}
${null} | ${MOCK_GROUP} | ${false}
${'navbar'} | ${null} | ${false}
${'navbar'} | ${MOCK_GROUP} | ${true}
`('onCreate', ({ navSource, initialData, callMethod }) => {
describe(`when nav_source is ${navSource} and ${
initialData ? 'has' : 'does not have'
} an initial group`, () => {
beforeEach(() => {
createComponent({ query: { ...MOCK_QUERY, nav_source: navSource } }, { initialData });
});
it(`${callMethod ? 'does' : 'does not'} call setFrequentGroup`, () => {
if (callMethod) {
expect(actionSpies.setFrequentGroup).toHaveBeenCalledWith(
expect.any(Object),
initialData,
);
} else {
expect(actionSpies.setFrequentGroup).not.toHaveBeenCalled();
}
});
});
});
});

View File

@ -51,7 +51,6 @@ describe('ProjectFilter', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findSearchableDropdown = () => wrapper.find(SearchableDropdown);
@ -89,9 +88,10 @@ describe('ProjectFilter', () => {
findSearchableDropdown().vm.$emit('change', ANY_OPTION);
});
it('calls setUrlParams with null, no group id, then calls visitUrl', () => {
it('calls setUrlParams with null, no group id, nav_source null, then calls visitUrl', () => {
expect(setUrlParams).toHaveBeenCalledWith({
[PROJECT_DATA.queryParam]: null,
nav_source: null,
});
expect(visitUrl).toHaveBeenCalled();
});
@ -106,10 +106,11 @@ describe('ProjectFilter', () => {
findSearchableDropdown().vm.$emit('change', MOCK_PROJECT);
});
it('calls setUrlParams with project id, group id, then calls visitUrl', () => {
it('calls setUrlParams with project id, group id, nav_source null, then calls visitUrl', () => {
expect(setUrlParams).toHaveBeenCalledWith({
[GROUP_DATA.queryParam]: MOCK_PROJECT.namespace.id,
[PROJECT_DATA.queryParam]: MOCK_PROJECT.id,
nav_source: null,
});
expect(visitUrl).toHaveBeenCalled();
});
@ -157,4 +158,31 @@ describe('ProjectFilter', () => {
});
});
});
describe.each`
navSource | initialData | callMethod
${null} | ${null} | ${false}
${null} | ${MOCK_PROJECT} | ${false}
${'navbar'} | ${null} | ${false}
${'navbar'} | ${MOCK_PROJECT} | ${true}
`('onCreate', ({ navSource, initialData, callMethod }) => {
describe(`when nav_source is ${navSource} and ${
initialData ? 'has' : 'does not have'
} an initial project`, () => {
beforeEach(() => {
createComponent({ query: { ...MOCK_QUERY, nav_source: navSource } }, { initialData });
});
it(`${callMethod ? 'does' : 'does not'} call setFrequentProject`, () => {
if (callMethod) {
expect(actionSpies.setFrequentProject).toHaveBeenCalledWith(
expect.any(Object),
initialData,
);
} else {
expect(actionSpies.setFrequentProject).not.toHaveBeenCalled();
}
});
});
});
});

View File

@ -3,6 +3,8 @@ import * as jqueryMatchers from 'custom-jquery-matchers';
import Vue from 'vue';
import 'jquery';
import { setGlobalDateToFakeDate } from 'helpers/fake_date';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import Translate from '~/vue_shared/translate';
import { getJSONFixture, loadHTMLFixture, setHTMLFixture } from './__helpers__/fixtures';
import { initializeTestTimeout } from './__helpers__/timeout';
@ -88,8 +90,13 @@ Object.assign(global, {
},
});
// make sure that each test actually tests something
// see https://jestjs.io/docs/en/expect#expecthasassertions
beforeEach(() => {
// make sure that each test actually tests something
// see https://jestjs.io/docs/en/expect#expecthasassertions
expect.hasAssertions();
// Reset the mocked window.location. This ensures tests don't interfere with
// each other, and removes the need to tidy up if it was changed for a given
// test.
setWindowLocation(TEST_HOST);
});

View File

@ -1,5 +1,4 @@
import { shallowMount } from '@vue/test-utils';
import setWindowLocation from 'helpers/set_window_location_helper';
import { historyPushState } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import UrlSyncComponent from '~/vue_shared/components/url_sync.vue';
@ -15,9 +14,6 @@ jest.mock('~/lib/utils/common_utils', () => ({
describe('url sync component', () => {
let wrapper;
const mockQuery = { group_id: '5014437163714', project_ids: ['5014437608314'] };
const TEST_HOST = 'http://testhost/';
setWindowLocation(TEST_HOST);
const findButton = () => wrapper.find('button');
@ -35,7 +31,9 @@ describe('url sync component', () => {
const expectUrlSync = (query, times, mergeUrlParamsReturnValue) => {
expect(mergeUrlParams).toHaveBeenCalledTimes(times);
expect(mergeUrlParams).toHaveBeenCalledWith(query, TEST_HOST, { spreadArrays: true });
expect(mergeUrlParams).toHaveBeenCalledWith(query, window.location.href, {
spreadArrays: true,
});
expect(historyPushState).toHaveBeenCalledTimes(times);
expect(historyPushState).toHaveBeenCalledWith(mergeUrlParamsReturnValue);

View File

@ -496,18 +496,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do
expect(find_user_from_web_access_token(:archive)).to eq(user)
end
context 'when allow_archive_as_web_access_format feature flag is disabled' do
before do
stub_feature_flags(allow_archive_as_web_access_format: false)
end
it 'returns nil for ARCHIVE requests' do
set_header('SCRIPT_NAME', '/-/archive/main.zip')
expect(find_user_from_web_access_token(:archive)).to be_nil
end
end
context 'for API requests' do
it 'returns the user' do
set_header('SCRIPT_NAME', '/api/endpoint')

View File

@ -66,8 +66,34 @@ RSpec.describe Gitlab::Kas do
end
describe '.tunnel_url' do
it 'returns gitlab_kas external_url with proxy path appended' do
expect(described_class.tunnel_url).to eq(Gitlab.config.gitlab_kas.external_url + '/k8s-proxy')
before do
stub_config(gitlab_kas: { external_url: external_url })
end
subject { described_class.tunnel_url }
context 'external_url uses wss://' do
let(:external_url) { 'wss://kas.gitlab.example.com' }
it { is_expected.to eq('https://kas.gitlab.example.com/k8s-proxy') }
end
context 'external_url uses ws://' do
let(:external_url) { 'ws://kas.gitlab.example.com' }
it { is_expected.to eq('http://kas.gitlab.example.com/k8s-proxy') }
end
context 'external_url uses grpcs://' do
let(:external_url) { 'grpcs://kas.gitlab.example.com' }
it { is_expected.to eq('https://kas.gitlab.example.com/k8s-proxy') }
end
context 'external_url uses grpc://' do
let(:external_url) { 'grpc://kas.gitlab.example.com' }
it { is_expected.to eq('http://kas.gitlab.example.com/k8s-proxy') }
end
end