Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-10 03:08:56 +00:00
parent 237ead18b9
commit 981fb44c36
53 changed files with 990 additions and 430 deletions

View File

@ -69,7 +69,6 @@ export default {
editButtonAttrs() {
return {
'data-testid': 'edit',
icon: 'pencil-square',
href: this.userPaths.edit,
};
},
@ -101,6 +100,7 @@ export default {
<gl-button
v-else
v-gl-tooltip="$options.i18n.edit"
icon="pencil-square"
v-bind="editButtonAttrs"
:aria-label="$options.i18n.edit"
/>
@ -108,10 +108,9 @@ export default {
<div v-if="hasDropdownActions" class="gl-p-2">
<gl-dropdown
v-gl-tooltip="$options.i18n.userAdministration"
data-testid="dropdown-toggle"
:text="$options.i18n.userAdministration"
:text-sr-only="!showButtonLabels"
icon="ellipsis_h"
icon="ellipsis_v"
data-qa-selector="user_actions_dropdown_toggle"
:data-qa-username="user.username"
no-caret

View File

@ -23,11 +23,21 @@ export default {
GlModalDirective,
GlTooltip: GlTooltipDirective,
},
inject: ['newClusterPath', 'addClusterPath', 'canAddCluster'],
inject: ['newClusterPath', 'addClusterPath', 'canAddCluster', 'displayClusterAgents'],
computed: {
tooltip() {
const { connectWithAgent, dropdownDisabledHint } = this.$options.i18n;
return this.canAddCluster ? connectWithAgent : dropdownDisabledHint;
const { connectWithAgent, connectExistingCluster, dropdownDisabledHint } = this.$options.i18n;
if (!this.canAddCluster) {
return dropdownDisabledHint;
} else if (this.displayClusterAgents) {
return connectWithAgent;
}
return connectExistingCluster;
},
shouldTriggerModal() {
return this.canAddCluster && this.displayClusterAgents;
},
},
};
@ -37,24 +47,27 @@ export default {
<div class="nav-controls gl-ml-auto">
<gl-dropdown
ref="dropdown"
v-gl-modal-directive="canAddCluster && $options.INSTALL_AGENT_MODAL_ID"
v-gl-modal-directive="shouldTriggerModal && $options.INSTALL_AGENT_MODAL_ID"
v-gl-tooltip="tooltip"
category="primary"
variant="confirm"
:text="$options.i18n.actionsButton"
:disabled="!canAddCluster"
split
:split="displayClusterAgents"
right
>
<gl-dropdown-section-header>{{ $options.i18n.agent }}</gl-dropdown-section-header>
<gl-dropdown-item
v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
data-testid="connect-new-agent-link"
>
{{ $options.i18n.connectWithAgent }}
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-section-header>{{ $options.i18n.certificate }}</gl-dropdown-section-header>
<template v-if="displayClusterAgents">
<gl-dropdown-section-header>{{ $options.i18n.agent }}</gl-dropdown-section-header>
<gl-dropdown-item
v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
data-testid="connect-new-agent-link"
>
{{ $options.i18n.connectWithAgent }}
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-section-header>{{ $options.i18n.certificate }}</gl-dropdown-section-header>
</template>
<gl-dropdown-item :href="newClusterPath" data-testid="new-cluster-link" @click.stop>
{{ $options.i18n.createNewCluster }}
</gl-dropdown-item>

View File

@ -3,6 +3,7 @@ import { GlTabs, GlTab } from '@gitlab/ui';
import Tracking from '~/tracking';
import {
CLUSTERS_TABS,
CERTIFICATE_TAB,
MAX_CLUSTERS_LIST,
MAX_LIST_COUNT,
AGENT,
@ -29,6 +30,7 @@ export default {
},
CLUSTERS_TABS,
mixins: [trackingMixin],
inject: ['displayClusterAgents'],
props: {
defaultBranchName: {
default: '.noBranch',
@ -42,6 +44,11 @@ export default {
maxAgents: MAX_CLUSTERS_LIST,
};
},
computed: {
clusterTabs() {
return this.displayClusterAgents ? CLUSTERS_TABS : [CERTIFICATE_TAB];
},
},
watch: {
selectedTabIndex(val) {
this.onTabChange(val);
@ -49,10 +56,10 @@ export default {
},
methods: {
setSelectedTab(tabName) {
this.selectedTabIndex = CLUSTERS_TABS.findIndex((tab) => tab.queryParamValue === tabName);
this.selectedTabIndex = this.clusterTabs.findIndex((tab) => tab.queryParamValue === tabName);
},
onTabChange(tab) {
const tabName = CLUSTERS_TABS[tab].queryParamValue;
const tabName = this.clusterTabs[tab].queryParamValue;
this.maxAgents = tabName === AGENT ? MAX_LIST_COUNT : MAX_CLUSTERS_LIST;
this.track(EVENT_ACTIONS_CHANGE, { property: tabName });
@ -69,7 +76,7 @@ export default {
lazy
>
<gl-tab
v-for="(tab, idx) in $options.CLUSTERS_TABS"
v-for="(tab, idx) in clusterTabs"
:key="idx"
:title="tab.title"
:query-param-value="tab.queryParamValue"

View File

@ -232,6 +232,12 @@ export const CERTIFICATE_BASED_CARD_INFO = {
export const MAX_CLUSTERS_LIST = 6;
export const CERTIFICATE_TAB = {
title: s__('ClusterAgents|Certificate'),
component: 'clusters',
queryParamValue: 'certificate_based',
};
export const CLUSTERS_TABS = [
{
title: s__('ClusterAgents|All'),
@ -243,11 +249,7 @@ export const CLUSTERS_TABS = [
component: 'agents',
queryParamValue: 'agent',
},
{
title: s__('ClusterAgents|Certificate'),
component: 'clusters',
queryParamValue: 'certificate_based',
},
CERTIFICATE_TAB,
];
export const CLUSTERS_ACTIONS = {

View File

@ -1,13 +1,61 @@
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import loadClusters from './load_clusters';
import loadMainView from './load_main_view';
import { parseBoolean } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
import ClustersMainView from './components/clusters_main_view.vue';
import { createStore } from './store';
Vue.use(GlToast);
Vue.use(VueApollo);
export default () => {
loadClusters(Vue);
loadMainView(Vue, VueApollo);
const el = document.querySelector('.js-clusters-main-view');
if (!el) {
return null;
}
const defaultClient = createDefaultClient();
const {
emptyStateImage,
defaultBranchName,
projectPath,
kasAddress,
newClusterPath,
addClusterPath,
emptyStateHelpText,
clustersEmptyStateImage,
canAddCluster,
canAdminCluster,
gitlabVersion,
displayClusterAgents,
} = el.dataset;
return new Vue({
el,
apolloProvider: new VueApollo({ defaultClient }),
provide: {
emptyStateImage,
projectPath,
kasAddress,
newClusterPath,
addClusterPath,
emptyStateHelpText,
clustersEmptyStateImage,
canAddCluster: parseBoolean(canAddCluster),
canAdminCluster: parseBoolean(canAdminCluster),
gitlabVersion,
displayClusterAgents: parseBoolean(displayClusterAgents),
},
store: createStore(el.dataset),
render(createElement) {
return createElement(ClustersMainView, {
props: {
defaultBranchName,
},
});
},
});
};

View File

@ -1,25 +0,0 @@
import Clusters from './components/clusters.vue';
import { createStore } from './store';
export default (Vue) => {
const el = document.querySelector('#js-clusters-list-app');
if (!el) {
return null;
}
const { emptyStateHelpText, newClusterPath, clustersEmptyStateImage } = el.dataset;
return new Vue({
el,
provide: {
emptyStateHelpText,
newClusterPath,
clustersEmptyStateImage,
},
store: createStore(el.dataset),
render(createElement) {
return createElement(Clusters);
},
});
};

View File

@ -1,57 +0,0 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { parseBoolean } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
import ClustersMainView from './components/clusters_main_view.vue';
import { createStore } from './store';
Vue.use(VueApollo);
export default () => {
const el = document.querySelector('.js-clusters-main-view');
if (!el) {
return null;
}
const defaultClient = createDefaultClient();
const {
emptyStateImage,
defaultBranchName,
projectPath,
kasAddress,
newClusterPath,
addClusterPath,
emptyStateHelpText,
clustersEmptyStateImage,
canAddCluster,
canAdminCluster,
gitlabVersion,
} = el.dataset;
return new Vue({
el,
apolloProvider: new VueApollo({ defaultClient }),
provide: {
emptyStateImage,
projectPath,
kasAddress,
newClusterPath,
addClusterPath,
emptyStateHelpText,
clustersEmptyStateImage,
canAddCluster: parseBoolean(canAddCluster),
canAdminCluster: parseBoolean(canAdminCluster),
gitlabVersion,
},
store: createStore(el.dataset),
render(createElement) {
return createElement(ClustersMainView, {
props: {
defaultBranchName,
},
});
},
});
};

View File

@ -5,7 +5,7 @@ import createStore from './store';
Vue.use(Translate);
export const initHeaderSearchApp = () => {
export const initHeaderSearchApp = (search = '') => {
const el = document.getElementById('js-header-search');
if (!el) {
@ -18,7 +18,7 @@ export const initHeaderSearchApp = () => {
return new Vue({
el,
store: createStore({ searchPath, issuesPath, mrPath, autocompletePath, searchContext }),
store: createStore({ searchPath, issuesPath, mrPath, autocompletePath, searchContext, search }),
render(createElement) {
return createElement(HeaderSearchApp);
},

View File

@ -13,11 +13,12 @@ export const getStoreConfig = ({
mrPath,
autocompletePath,
searchContext,
search,
}) => ({
actions,
getters,
mutations,
state: createState({ searchPath, issuesPath, mrPath, autocompletePath, searchContext }),
state: createState({ searchPath, issuesPath, mrPath, autocompletePath, searchContext, search }),
});
const createStore = (config) => new Vuex.Store(getStoreConfig(config));

View File

@ -1,10 +1,17 @@
const createState = ({ searchPath, issuesPath, mrPath, autocompletePath, searchContext }) => ({
const createState = ({
searchPath,
issuesPath,
mrPath,
autocompletePath,
searchContext,
search: '',
search,
}) => ({
searchPath,
issuesPath,
mrPath,
autocompletePath,
searchContext,
search,
autocompleteOptions: [],
autocompleteError: false,
loading: false,

View File

@ -31,7 +31,10 @@ export default {
computed: {
actionPrimary() {
return {
attributes: { variant: 'danger' },
attributes: {
variant: 'danger',
'data-qa-selector': 'confirm_delete_issue_button',
},
text: this.title,
};
},

View File

@ -290,6 +290,7 @@ export default {
class="gl-display-none gl-sm-display-inline-flex! gl-ml-3"
icon="ellipsis_v"
category="tertiary"
data-qa-selector="issue_actions_ellipsis_dropdown"
:text="dropdownText"
:text-sr-only="true"
data-testid="desktop-dropdown"
@ -323,6 +324,7 @@ export default {
<gl-dropdown-item
v-gl-modal="$options.deleteModalId"
variant="danger"
data-qa-selector="delete_issue_button"
@click="track('click_dropdown')"
>
{{ deleteButtonText }}

View File

@ -116,16 +116,18 @@ function deferredInitialisation() {
);
}
const search = document.querySelector('#search');
if (search) {
search.addEventListener(
const searchInputBox = document.querySelector('#search');
if (searchInputBox) {
searchInputBox.addEventListener(
'focus',
() => {
if (gon.features?.newHeaderSearch) {
import(/* webpackChunkName: 'globalSearch' */ '~/header_search')
.then(async ({ initHeaderSearchApp }) => {
await initHeaderSearchApp();
document.querySelector('#search').focus();
// In case the user started searching before we bootstrapped, let's pass the search along.
const initialSearchValue = searchInputBox.value;
await initHeaderSearchApp(initialSearchValue);
searchInputBox.focus();
})
.catch(() => {});
} else {

View File

@ -360,27 +360,10 @@
}
> li {
// TODO: Remove this block once all sidebar badges use gl_badge_tag
// https://gitlab.com/gitlab-org/gitlab/-/issues/350061
.badge.badge-pill:not(.gl-badge) {
@include gl-rounded-lg;
@include gl-py-1;
@include gl-px-3;
background-color: $blue-100;
color: $blue-700;
}
&.active {
.sidebar-sub-level-items:not(.is-fly-out-only) {
display: block;
}
// TODO: Remove this block once all sidebar badges use gl_badge_tag
// https://gitlab.com/gitlab-org/gitlab/-/issues/350061
.badge.badge-pill:not(.gl-badge) {
@include gl-font-weight-normal;
color: $blue-700;
}
}
}

View File

@ -1380,24 +1380,11 @@ input {
border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items > li .badge.badge-pill:not(.gl-badge) {
border-radius: 0.5rem;
padding-top: 0.125rem;
padding-bottom: 0.125rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
background-color: #064787;
color: #9dc7f1;
}
.sidebar-top-level-items
> li.active
.sidebar-sub-level-items:not(.is-fly-out-only) {
display: block;
}
.sidebar-top-level-items > li.active .badge.badge-pill:not(.gl-badge) {
font-weight: 400;
color: #9dc7f1;
}
.sidebar-top-level-items li > a.gl-link {
color: #fafafa;
}

View File

@ -1365,24 +1365,11 @@ input {
border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items > li .badge.badge-pill:not(.gl-badge) {
border-radius: 0.5rem;
padding-top: 0.125rem;
padding-bottom: 0.125rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
background-color: #cbe2f9;
color: #0b5cad;
}
.sidebar-top-level-items
> li.active
.sidebar-sub-level-items:not(.is-fly-out-only) {
display: block;
}
.sidebar-top-level-items > li.active .badge.badge-pill:not(.gl-badge) {
font-weight: 400;
color: #0b5cad;
}
.sidebar-top-level-items li > a.gl-link {
color: #303030;
}

View File

@ -12,10 +12,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
# Overridden from Doorkeeper::AuthorizationsController to
# include the call to session.delete
def new
logger.info("#{self.class.name}#new: pre_auth_params['scope'] = #{pre_auth_params['scope'].inspect}")
if pre_auth.authorizable?
logger.info("#{self.class.name}#new: pre_auth.scopes = #{pre_auth.scopes.to_a.inspect}")
if skip_authorization? || matching_token?
auth = authorization.authorize
parsed_redirect_uri = URI.parse(auth.redirect_uri)
@ -46,15 +43,9 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
auth_type = params.delete('gl_auth_type')
return unless auth_type == 'login'
logger.info("#{self.class.name}: BEFORE application has read_user: #{application_has_read_user_scope?}")
logger.info("#{self.class.name}: BEFORE scope = #{params['scope'].inspect}")
ensure_read_user_scope!
params['scope'] = Gitlab::Auth::READ_USER_SCOPE.to_s if application_has_read_user_scope?
logger.info("#{self.class.name}: AFTER application has read_user: #{application_has_read_user_scope?}")
logger.info("#{self.class.name}: AFTER scope = #{params['scope'].inspect}")
end
# Configure the application to support read_user scope, if it already

View File

@ -28,8 +28,10 @@ module ClustersHelper
clusters_empty_state_image: image_path('illustrations/empty-state/empty-state-clusters.svg'),
empty_state_help_text: clusterable.empty_state_help_text,
new_cluster_path: clusterable.new_path(tab: 'create'),
add_cluster_path: clusterable.new_path(tab: 'add'),
can_add_cluster: clusterable.can_add_cluster?.to_s,
can_admin_cluster: clusterable.can_admin_cluster?.to_s
can_admin_cluster: clusterable.can_admin_cluster?.to_s,
display_cluster_agents: display_cluster_agents?(clusterable).to_s
}
end
@ -38,7 +40,6 @@ module ClustersHelper
default_branch_name: clusterable.default_branch,
empty_state_image: image_path('illustrations/empty-state/empty-state-agents.svg'),
project_path: clusterable.full_path,
add_cluster_path: clusterable.new_path(tab: 'add'),
kas_address: Gitlab::Kas.external_url,
gitlab_version: Gitlab.version_info
}.merge(js_clusters_list_data(clusterable))

View File

@ -10,11 +10,41 @@ class Analytics::CycleAnalytics::Aggregation < ApplicationRecord
scope :priority_order, -> { order('last_incremental_run_at ASC NULLS FIRST') }
scope :enabled, -> { where('enabled IS TRUE') }
def estimated_next_run_at
return unless enabled
return if last_incremental_run_at.nil?
estimation = (last_incremental_run_at - earliest_last_run_at) + average_aggregation_duration
estimation < 1 ? nil : estimation.from_now
end
def self.safe_create_for_group(group)
top_level_group = group.root_ancestor
return if Analytics::CycleAnalytics::Aggregation.exists?(group_id: top_level_group.id)
aggregation = find_by(group_id: top_level_group.id)
return aggregation if aggregation.present?
insert({ group_id: top_level_group.id }, unique_by: :group_id)
find_by(group_id: top_level_group.id)
end
private
def average_aggregation_duration
return 0.seconds if incremental_runtimes_in_seconds.empty?
average = incremental_runtimes_in_seconds.sum.fdiv(incremental_runtimes_in_seconds.size)
average.seconds
end
def earliest_last_run_at
max = self.class.select(:last_incremental_run_at)
.where(enabled: true)
.where.not(last_incremental_run_at: nil)
.priority_order
.limit(1)
.to_sql
connection.select_value("(#{max})")
end
def self.load_batch(last_run_at, batch_size = 100)

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
module RunnersTokenPrefixable
# Prefix for runners_token which can be used to invalidate existing tokens.
# The value chosen here is GR (for Gitlab Runner) combined with the rotation
# date (20220225) decimal to hex encoded.
RUNNERS_TOKEN_PREFIX = 'GR1348941'
end

View File

@ -22,11 +22,6 @@ class Group < Namespace
extend ::Gitlab::Utils::Override
# Prefix for runners_token which can be used to invalidate existing tokens.
# The value chosen here is GR (for Gitlab Runner) combined with the rotation
# date (20220225) decimal to hex encoded.
RUNNERS_TOKEN_PREFIX = 'GR1348941'
def self.sti_name
'Group'
end
@ -124,7 +119,7 @@ class Group < Namespace
add_authentication_token_field :runners_token,
encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
prefix: ->(instance) { instance.runners_token_prefix }
prefix: RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
after_create :post_create_hook
after_destroy :post_destroy_hook
@ -678,13 +673,9 @@ class Group < Namespace
ensure_runners_token!
end
def runners_token_prefix
Feature.enabled?(:groups_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
end
override :format_runners_token
def format_runners_token(token)
"#{runners_token_prefix}#{token}"
"#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}#{token}"
end
def project_creation_level

View File

@ -90,11 +90,6 @@ class Project < ApplicationRecord
DEFAULT_SQUASH_COMMIT_TEMPLATE = '%{title}'
# Prefix for runners_token which can be used to invalidate existing tokens.
# The value chosen here is GR (for Gitlab Runner) combined with the rotation
# date (20220225) decimal to hex encoded.
RUNNERS_TOKEN_PREFIX = 'GR1348941'
cache_markdown_field :description, pipeline: :description
default_value_for :packages_enabled, true
@ -117,7 +112,7 @@ class Project < ApplicationRecord
add_authentication_token_field :runners_token,
encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
prefix: ->(instance) { instance.runners_token_prefix }
prefix: RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
@ -1887,13 +1882,9 @@ class Project < ApplicationRecord
ensure_runners_token!
end
def runners_token_prefix
Feature.enabled?(:projects_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
end
override :format_runners_token
def format_runners_token(token)
"#{runners_token_prefix}#{token}"
"#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}#{token}"
end
def pages_deployed?

View File

@ -27,8 +27,6 @@
= render_if_exists 'admin/users/gma_user_badge'
.gl-my-3.gl-display-flex.gl-flex-wrap.gl-my-n2.gl-mx-n2
.gl-p-2
#js-admin-user-actions{ data: admin_user_actions_data_attributes(@user) }
- if @user != current_user
.gl-p-2
- if impersonation_enabled? && @user.can?(:log_in)
@ -36,6 +34,8 @@
- if can_force_email_confirmation?(@user)
%button.btn.gl-button.btn-info.js-confirm-modal-button{ data: confirm_user_data(@user) }
= _('Confirm user')
.gl-p-2
#js-admin-user-actions{ data: admin_user_actions_data_attributes(@user) }
= gl_tabs_nav do
= gl_tab_link_to _("Account"), admin_user_path(@user)
= gl_tab_link_to _("Groups and projects"), projects_admin_user_path(@user)

View File

@ -7,4 +7,4 @@
%span.btn.gl-button.btn-confirm.js-add-cluster.disabled.gl-py-2
= s_("ClusterIntegration|Connect cluster with certificate")
#js-clusters-list-app{ data: js_clusters_list_data(clusterable) }
.js-clusters-main-view{ data: js_clusters_list_data(clusterable) }

View File

@ -0,0 +1,24 @@
#js-header-search.header-search{ data: { 'search-context' => header_search_context.to_json,
'search-path' => search_path,
'issues-path' => issues_dashboard_path,
'mr-path' => merge_requests_dashboard_path,
'autocomplete-path' => search_autocomplete_path } }
= form_tag search_path, method: :get do |_f|
.gl-search-box-by-type
= sprite_icon('search', css_class: 'gl-search-box-by-type-search-icon gl-icon')
%input{ id: 'search', name: 'search', type: "text", placeholder: s_('GlobalSearch|Search GitLab'), class: 'form-control gl-form-input gl-search-box-by-type-input', autocomplete: 'off' }
= hidden_field_tag :group_id, header_search_context[:group][:id] if header_search_context[:group]
= hidden_field_tag :project_id, header_search_context[:project][:id] if header_search_context[:project]
- if header_search_context[:group] || header_search_context[:project]
= hidden_field_tag :scope, header_search_context[:scope]
= hidden_field_tag :search_code, header_search_context[:code_search]
= hidden_field_tag :snippets, header_search_context[:for_snippets]
= hidden_field_tag :repository_ref, header_search_context[:ref]
= hidden_field_tag :nav_source, 'navbar'
-# workaround for non-JS feature specs, see spec/support/helpers/search_helpers.rb
- if ENV['RAILS_ENV'] == 'test'
%noscript= button_tag 'Search'

View File

@ -41,14 +41,7 @@
%li.nav-item.header-search-new.d-none.d-lg-block.m-auto
- unless current_controller?(:search)
- if Feature.enabled?(:new_header_search)
#js-header-search.header-search{ data: { 'search-context' => header_search_context.to_json,
'search-path' => search_path,
'issues-path' => issues_dashboard_path,
'mr-path' => merge_requests_dashboard_path,
'autocomplete-path' => search_autocomplete_path } }
.gl-search-box-by-type
= sprite_icon('search', css_class: 'gl-search-box-by-type-search-icon gl-icon')
%input{ type: "text", placeholder: s_('GlobalSearch|Search GitLab'), class: 'form-control gl-form-input gl-search-box-by-type-input', id: 'search', autocomplete: 'off' }
= render 'layouts/header_search'
- else
= render 'layouts/search'
%li.nav-item{ class: 'd-none d-sm-inline-block d-lg-none' }

View File

@ -1,8 +0,0 @@
---
name: groups_runners_token_prefix
introduced_by_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
milestone: '14.9'
type: development
group: group::database
default_enabled: true

View File

@ -1,8 +0,0 @@
---
name: projects_runners_token_prefix
introduced_by_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
milestone: '14.9'
type: development
group: group::database
default_enabled: true

View File

@ -377,7 +377,7 @@ sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.t
The `track-repository` Praefect sub-command adds repositories on disk to the Praefect database to be tracked.
```shell
sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml track-repository -virtual-storage <virtual-storage> -repository <repository> -replicate-immediately
sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml track-repository -virtual-storage <virtual-storage> -authoritative-storage <storage-name> -repository <repository> -replicate-immediately
```
- `-virtual-storage` is the virtual storage the repository is located in. Virtual storages are configured in `/etc/gitlab/gitlab.rb` under `praefect['virtual_storages]` and looks like the following:

View File

@ -479,3 +479,19 @@ ssh: Could not resolve hostname gitlab.example.com: nodename nor servname provid
```
If you receive this error, restart your terminal and try the command again.
### `Key enrollment failed: invalid format` error
You may receive the following error when [generating an SSH key pair for a FIDO/U2F hardware security key](#generate-an-ssh-key-pair-for-a-fidou2f-hardware-security-key):
```shell
Key enrollment failed: invalid format
```
You can troubleshoot this by trying the following:
- Run the `ssh-keygen` command using `sudo`.
- Verify your IDO/U2F hardware security key supports
the key type provided.
- Verify the version of OpenSSH is 8.2 or greater by
running `ssh -v`.

View File

@ -181,7 +181,7 @@ table.supported-languages ul {
</tr>
<tr>
<td rowspan="2">Java</td>
<td rowspan="2">8, 11, 13, 14, 15, or 16</td>
<td rowspan="2">8, 11, 13, 14, 15, 16, or 17</td>
<td><a href="https://gradle.org/">Gradle</a><sup><b><a href="#notes-regarding-supported-languages-and-package-managers-1">1</a></b></sup></td>
<td>
<ul>
@ -335,26 +335,60 @@ To support the following package managers, the GitLab analyzers proceed in two s
1. Execute the package manager or a specific task, to export the dependency information.
1. Parse the exported dependency information.
| Package Manager | Preinstalled Versions | Tested Versions |
| ------ | ------ | ------ |
| Bundler | [2.1.4](https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit/-/blob/v2.11.3/Dockerfile#L15)<sup><b><a href="#exported-dependency-information-notes-1">1</a></b></sup> | [1.17.3](https://gitlab.com/gitlab-org/security-products/tests/ruby-bundler/-/blob/master/Gemfile.lock#L118), [2.1.4](https://gitlab.com/gitlab-org/security-products/tests/ruby-bundler/-/blob/bundler2-FREEZE/Gemfile.lock#L118) |
| sbt | [1.6.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/config/.tool-versions#L4) | [1.0.4](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L330), [1.1.4](https://gitlab.com/gitlab-org/security-products/tests/scala-sbt-multiproject/-/blob/main/project/build.properties#L1), [1.1.6](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L339), [1.2.8](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L348), [1.3.12](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L357), [1.4.6](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L366), [1.6.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L384) |
| Maven | [3.6.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.23.0/config/.tool-versions#L3) | [3.6.3](https://gitlab.com/gitlab-org/security-products/tests/java-maven/-/blob/master/pom.xml#L3) |
| Gradle | [6.7.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.23.0/config/.tool-versions#L5) | [5.6.4](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/master/gradle/wrapper/gradle-wrapper.properties#L3), [6.5](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14/gradle/wrapper/gradle-wrapper.properties#L3), [6.7-rc-1](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-15/gradle/wrapper/gradle-wrapper.properties#L3), [6.9](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14-gradle-6-9/gradle/wrapper/gradle-wrapper.properties#L3), [7.0-rc-2](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-16/gradle/wrapper/gradle-wrapper.properties#L3) |
| setuptools | [50.3.2](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v2.29.9/Dockerfile#L27) | [57.5.0](https://gitlab.com/gitlab-org/security-products/tests/python-setuptools/-/blob/main/setup.py) |
| pip | [20.2.4](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v2.29.9/Dockerfile#L26) | [20.x](https://gitlab.com/gitlab-org/security-products/tests/python-pip/-/blob/master/requirements.txt) |
| Pipenv | [2018.11.26](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-python/-/blob/v2.18.4/requirements.txt#L13) | [2018.11.26](https://gitlab.com/gitlab-org/security-products/tests/python-pipenv/-/blob/pipfile-lock-FREEZE/Pipfile.lock#L6)<sup><b><a href="#exported-dependency-information-notes-2">2</a></b></sup>, [2018.11.26](https://gitlab.com/gitlab-org/security-products/tests/python-pipenv/-/blob/master/Pipfile) |
| Package Manager | Pre-installed Versions | Tested Versions |
| ------ | ------ | ------ |
| Bundler | [2.1.4](https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit/-/blob/v2.11.3/Dockerfile#L15)<sup><b><a href="#exported-dependency-information-notes-1">1</a></b></sup> | [1.17.3](https://gitlab.com/gitlab-org/security-products/tests/ruby-bundler/-/blob/master/Gemfile.lock#L118), [2.1.4](https://gitlab.com/gitlab-org/security-products/tests/ruby-bundler/-/blob/bundler2-FREEZE/Gemfile.lock#L118) |
| sbt | [1.6.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/config/.tool-versions#L4) | [1.0.4](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L330), [1.1.4](https://gitlab.com/gitlab-org/security-products/tests/scala-sbt-multiproject/-/blob/main/project/build.properties#L1), [1.1.6](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L339), [1.2.8](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L348), [1.3.12](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L357), [1.4.6](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L366), [1.6.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L384) |
| Maven | [3.6.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.23.0/config/.tool-versions#L3) | [3.6.3](https://gitlab.com/gitlab-org/security-products/tests/java-maven/-/blob/master/pom.xml#L3) |
| Gradle | [6.7.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.23.0/config/.tool-versions#L5)<sup><b><a href="#exported-dependency-information-notes-2">2</a></b></sup>, [7.3.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.26.0/config/.tool-versions#L5)<sup><b><a href="#exported-dependency-information-notes-2">2</a></b></sup> | [5.6.4](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/master/gradle/wrapper/gradle-wrapper.properties#L3), [6.5](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14/gradle/wrapper/gradle-wrapper.properties#L3), [6.7-rc-1](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-15/gradle/wrapper/gradle-wrapper.properties#L3), [6.7.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.27.1/.gitlab-ci.yml#L289-297)<sup><b><a href="#exported-dependency-information-notes-3">3</a></b></sup>, [6.9](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14-gradle-6-9/gradle/wrapper/gradle-wrapper.properties#L3), [7.0-rc-2](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-16/gradle/wrapper/gradle-wrapper.properties#L3), [7.3](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14-gradle-7-3/gradle/wrapper/gradle-wrapper.properties#L3), [7.3.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.27.1/.gitlab-ci.yml#L299-317)<sup><b><a href="#exported-dependency-information-notes-3">3</a></b></sup> |
| setuptools | [50.3.2](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v2.29.9/Dockerfile#L27) | [57.5.0](https://gitlab.com/gitlab-org/security-products/tests/python-setuptools/-/blob/main/setup.py) |
| pip | [20.2.4](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v2.29.9/Dockerfile#L26) | [20.x](https://gitlab.com/gitlab-org/security-products/tests/python-pip/-/blob/master/requirements.txt) |
| Pipenv | [2018.11.26](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-python/-/blob/v2.18.4/requirements.txt#L13) | [2018.11.26](https://gitlab.com/gitlab-org/security-products/tests/python-pipenv/-/blob/pipfile-lock-FREEZE/Pipfile.lock#L6)<sup><b><a href="#exported-dependency-information-notes-4">4</a></b></sup>, [2018.11.26](https://gitlab.com/gitlab-org/security-products/tests/python-pipenv/-/blob/master/Pipfile) |
<!-- markdownlint-disable MD044 -->
<ol>
<li>
<a id="exported-dependency-information-notes-1"></a>
<p>
The installed version of <code>Bundler</code> is only used for the <a href="https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit">bundler-audit</a> analyzer, and is not used for <a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">gemnasium</a>
The pre-installed version of <code>Bundler</code> is only used for the <a href="https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit">bundler-audit</a> analyzer, and is not used for <a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">gemnasium</a>.
</p>
</li>
<li>
<a id="exported-dependency-information-notes-2"></a>
<p>
Different versions of Java require different versions of Gradle. The versions of Gradle listed in the above table are pre-installed
in the analyzer image. The version of Gradle used by the analyzer depends on whether your project uses a <code>gradlew</code>
(Gradle wrapper) file or not:
</p>
<ul>
<li>
<p>
If your project <i>does not use</i> a <code>gradlew</code> file, then the analyzer automatically switches to one of the
pre-installed Gradle versions, based on the version of Java specified by the
<a href="#configuring-specific-analyzers-used-by-dependency-scanning"><code>DS_JAVA_VERSION</code></a> variable.
</p>
<p>You can view the
<a href="https://docs.gradle.org/current/userguide/compatibility.html#java">Gradle Java compatibility matrix</a> to see which version
of Gradle is selected for each Java version. Note that we only support switching to one of these pre-installed Gradle versions
for Java versions 13 to 17.
</p>
</li>
<li>
<p>
If your project <i>does use</i> a <code>gradlew</code> file, then the version of Gradle pre-installed in the analyzer image is
ignored, and the version specified in your <code>gradlew</code> file is used instead.
</p>
</li>
</ul>
</li>
<li>
<a id="exported-dependency-information-notes-3"></a>
<p>
These tests confirms that if a <code>gradlew</code> file does not exist, the version of <code>Gradle</code> pre-installed in the analyzer image is used.
</p>
</li>
<li>
<a id="exported-dependency-information-notes-4"></a>
<p>
This test confirms that if a <code>Pipfile.lock</code> file is found, it will be used by <a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a> to scan the exact package versions listed in this file.
</p>
@ -563,7 +597,7 @@ The following variables are used for configuring specific analyzers (used for a
| `GEMNASIUM_DB_REF_NAME` | `gemnasium` | `master` | Branch name for remote repository database. `GEMNASIUM_DB_REMOTE_URL` is required. |
| `DS_REMEDIATE` | `gemnasium` | `"true"` | Enable automatic remediation of vulnerable dependencies. |
| `GEMNASIUM_LIBRARY_SCAN_ENABLED` | `gemnasium` | `"true"` | Enable detecting vulnerabilities in vendored JavaScript libraries. For now, `gemnasium` leverages [`Retire.js`](https://github.com/RetireJS/retire.js) to do this job. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/350512) in GitLab 14.8. |
| `DS_JAVA_VERSION` | `gemnasium-maven` | `11` | Version of Java. Available versions: `8`, `11`, `13`, `14`, `15`, `16`. |
| `DS_JAVA_VERSION` | `gemnasium-maven` | `11` | Version of Java. Available versions: `8`, `11`, `13`, `14`, `15`, `16`, `17`. |
| `MAVEN_CLI_OPTS` | `gemnasium-maven` | `"-DskipTests --batch-mode"` | List of command line arguments that are passed to `maven` by the analyzer. See an example for [using private repositories](../index.md#using-private-maven-repositories). |
| `GRADLE_CLI_OPTS` | `gemnasium-maven` | | List of command line arguments that are passed to `gradle` by the analyzer. |
| `SBT_CLI_OPTS` | `gemnasium-maven` | | List of command-line arguments that the analyzer passes to `sbt`. |

View File

@ -217,7 +217,11 @@ cannot change them:
This ensures that your job uses the settings you intend and that they are not overridden by
project-level pipelines.
##### Avoid parent and child pipelines
##### Avoid parent and child pipelines in GitLab 14.7 and earlier
NOTE:
This advice does not apply to GitLab 14.8 and later because [a fix](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78878) added
compatibility for combining compliance pipelines, and parent and child pipelines.
Compliance pipelines start on the run of _every_ pipeline in a relevant project. This means that if a pipeline in the relevant project
triggers a child pipeline, the compliance pipeline runs first. This can trigger the parent pipeline, instead of the child pipeline.

View File

@ -86,6 +86,7 @@ module Gitlab
def to_data_attributes
{}.tap do |attrs|
attrs[:aggregation] = aggregation_attributes if group
attrs[:group] = group_data_attributes if group
attrs[:value_stream] = value_stream_data_attributes.to_json if value_stream
attrs[:created_after] = created_after.to_date.iso8601
@ -103,6 +104,15 @@ module Gitlab
private
def aggregation_attributes
aggregation = ::Analytics::CycleAnalytics::Aggregation.safe_create_for_group(group)
{
enabled: aggregation.enabled.to_s,
last_run_at: aggregation.last_incremental_run_at,
next_run_at: aggregation.estimated_next_run_at
}
end
def group_data_attributes
{
id: group.id,

View File

@ -3,6 +3,7 @@
module Gitlab
class OmniauthInitializer
OAUTH2_TIMEOUT_SECONDS = 10
ConfigurationError = Class.new(StandardError)
def initialize(devise_config)
@devise_config = devise_config
@ -75,16 +76,29 @@ module Gitlab
provider_arguments << provider[argument] if provider[argument]
end
case provider['args']
arguments = provider.fetch('args', {})
defaults = provider_defaults(provider)
case arguments
when Array
# An Array from the configuration will be expanded.
provider_arguments.concat provider['args']
# An Array from the configuration will be expanded
provider_arguments.concat arguments
provider_arguments << defaults unless defaults.empty?
when Hash
defaults = provider_defaults(provider)
hash_arguments = provider['args'].deep_symbolize_keys.deep_merge(defaults)
hash_arguments = arguments.deep_symbolize_keys.deep_merge(defaults)
normalized = normalize_hash_arguments(hash_arguments)
# A Hash from the configuration will be passed as is.
provider_arguments << normalize_hash_arguments(hash_arguments)
provider_arguments << normalized unless normalized.empty?
else
# this will prevent the application from starting in development mode.
# we still set defaults, and let the application start in prod.
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
ConfigurationError.new("Arguments were provided for #{provider['name']}, but not as an array or a hash"),
provider_name: provider['name'],
arguments_type: arguments.class.name
)
provider_arguments << defaults unless defaults.empty?
end
provider_arguments

View File

@ -23,6 +23,12 @@ module QA
end
end
def filter_by_name(name)
within_element(:project_filter_form) do
fill_in :name, with: name
end
end
def go_to_project(name)
filter_by_name(name)
@ -40,14 +46,6 @@ module QA
def clear_project_filter
fill_element(:project_filter_form, "")
end
private
def filter_by_name(name)
within_element(:project_filter_form) do
fill_in :name, with: name
end
end
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module QA
module Page
module Modal
class DeleteIssue < Base
view 'app/assets/javascripts/issues/show/components/delete_issue_modal.vue' do
element :confirm_delete_issue_button, required: true
end
def confirm_delete_issue
click_element :confirm_delete_issue_button
end
end
end
end
end

View File

@ -18,6 +18,8 @@ module QA
view 'app/assets/javascripts/issues/show/components/header_actions.vue' do
element :close_issue_button
element :reopen_issue_button
element :issue_actions_ellipsis_dropdown
element :delete_issue_button
end
view 'app/assets/javascripts/related_issues/components/add_issuable_form.vue' do
@ -69,6 +71,20 @@ module QA
def has_reopen_issue_button?
has_element?(:reopen_issue_button)
end
def has_delete_issue_button?
click_element(:issue_actions_ellipsis_dropdown)
has_element?(:delete_issue_button)
end
def delete_issue
click_element(:issue_actions_ellipsis_dropdown)
click_element(:delete_issue_button, Page::Modal::DeleteIssue)
Page::Modal::DeleteIssue.perform(&:confirm_delete_issue)
wait_for_requests
end
end
end
end

View File

@ -0,0 +1,98 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Manage' do
describe 'Personal project permissions' do
let!(:owner) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) }
let!(:owner_api_client) { Runtime::API::Client.new(:gitlab, user: owner) }
let!(:project) do
Resource::Project.fabricate_via_api! do |project|
project.api_client = owner_api_client
project.name = 'qa-owner-personal-project'
project.personal_namespace = owner.username
end
end
after do
project&.remove_via_api!
end
context 'when user is added as Owner' do
let(:issue) do
Resource::Issue.fabricate_via_api! do |issue|
issue.api_client = owner_api_client
issue.project = project
issue.title = 'Test Owner deletes issue'
end
end
before do
Flow::Login.sign_in(as: owner)
end
it "has Owner role with Owner permissions", testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352542' do
Page::Dashboard::Projects.perform do |projects|
projects.filter_by_name(project.name)
expect(projects).to have_project_with_access_role(project.name, 'Owner')
end
expect_owner_permissions_allow_delete_issue
end
end
context 'when user is added as Maintainer' do
let(:maintainer) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_2, Runtime::Env.gitlab_qa_password_2) }
let(:issue) do
Resource::Issue.fabricate_via_api! do |issue|
issue.api_client = owner_api_client
issue.project = project
issue.title = 'Test Maintainer deletes issue'
end
end
before do
project.add_member(maintainer, Resource::Members::AccessLevel::MAINTAINER)
Flow::Login.sign_in(as: maintainer)
end
it "has Maintainer role without Owner permissions", testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352607' do
Page::Dashboard::Projects.perform do |projects|
projects.filter_by_name(project.name)
expect(projects).to have_project_with_access_role(project.name, 'Maintainer')
end
expect_maintainer_permissions_do_not_allow_delete_issue
end
end
private
def expect_owner_permissions_allow_delete_issue
expect do
issue.visit!
Page::Project::Issue::Show.perform(&:delete_issue)
Page::Project::Issue::Index.perform do |index|
expect(index).not_to have_issue(issue)
end
end.not_to raise_error
end
def expect_maintainer_permissions_do_not_allow_delete_issue
expect do
issue.visit!
Page::Project::Issue::Show.perform do |issue|
expect(issue).not_to have_delete_issue_button
end
end.not_to raise_error
end
end
end
end

View File

@ -18,6 +18,9 @@ require_relative '../config/settings'
require_relative 'support/rspec'
require 'active_support/all'
require_relative 'simplecov_env'
SimpleCovEnv.start!
unless ActiveSupport::Dependencies.autoload_paths.frozen?
ActiveSupport::Dependencies.autoload_paths << 'lib'
ActiveSupport::Dependencies.autoload_paths << 'ee/lib'

View File

@ -72,6 +72,10 @@ RSpec.describe 'Global search' do
# TODO: Remove this along with feature flag #339348
stub_feature_flags(new_header_search: true)
visit dashboard_projects_path
# intialize javascript loaded input search input field
find('#search').click
find('body').click
end
it 'renders updated search bar' do

View File

@ -77,6 +77,12 @@ describe('AdminUserActions component', () => {
expect(findActionsDropdown().exists()).toBe(true);
});
it('renders the tooltip', () => {
const tooltip = getBinding(findActionsDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(I18N_USER_ACTIONS.userAdministration);
});
describe('when there are actions that require confirmation', () => {
beforeEach(() => {
initComponent({ actions: CONFIRMATION_ACTIONS });
@ -152,7 +158,7 @@ describe('AdminUserActions component', () => {
describe('when `showButtonLabels` prop is `false`', () => {
beforeEach(() => {
initComponent({ actions: [EDIT, ...CONFIRMATION_ACTIONS] });
initComponent({ actions: [EDIT] });
});
it('does not render "Edit" button label', () => {
@ -163,16 +169,11 @@ describe('AdminUserActions component', () => {
expect(tooltip).toBeDefined();
expect(tooltip.value).toBe(I18N_USER_ACTIONS.edit);
});
it('does not render "User administration" dropdown button label', () => {
expect(findActionsDropdown().props('text')).toBe(I18N_USER_ACTIONS.userAdministration);
expect(findActionsDropdown().props('textSrOnly')).toBe(true);
});
});
describe('when `showButtonLabels` prop is `true`', () => {
beforeEach(() => {
initComponent({ actions: [EDIT, ...CONFIRMATION_ACTIONS], showButtonLabels: true });
initComponent({ actions: [EDIT], showButtonLabels: true });
});
it('renders "Edit" button label', () => {
@ -181,10 +182,5 @@ describe('AdminUserActions component', () => {
expect(findEditButton().text()).toBe(I18N_USER_ACTIONS.edit);
expect(tooltip).not.toBeDefined();
});
it('renders "User administration" dropdown button label', () => {
expect(findActionsDropdown().props('text')).toBe(I18N_USER_ACTIONS.userAdministration);
expect(findActionsDropdown().props('textSrOnly')).toBe(false);
});
});
});

View File

@ -14,10 +14,13 @@ describe('ClustersActionsComponent', () => {
newClusterPath,
addClusterPath,
canAddCluster: true,
displayClusterAgents: true,
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findDropdownItemIds = () =>
findDropdownItems().wrappers.map((x) => x.attributes('data-testid'));
const findNewClusterLink = () => wrapper.findByTestId('new-cluster-link');
const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link');
const findConnectNewAgentLink = () => wrapper.findByTestId('connect-new-agent-link');
@ -47,26 +50,11 @@ describe('ClustersActionsComponent', () => {
expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.actionsButton);
});
it('renders a dropdown with 3 actions items', () => {
expect(findDropdownItems()).toHaveLength(3);
});
it('renders correct href attributes for the links', () => {
expect(findNewClusterLink().attributes('href')).toBe(newClusterPath);
expect(findConnectClusterLink().attributes('href')).toBe(addClusterPath);
});
it('renders correct modal id for the agent link', () => {
const binding = getBinding(findConnectNewAgentLink().element, 'gl-modal-directive');
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
it('shows tooltip', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectWithAgent);
});
describe('when user cannot add clusters', () => {
beforeEach(() => {
createWrapper({ canAddCluster: false });
@ -80,5 +68,67 @@ describe('ClustersActionsComponent', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.dropdownDisabledHint);
});
it('does not bind split dropdown button', () => {
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
expect(binding.value).toBe(false);
});
});
describe('when on project level', () => {
it('renders a dropdown with 3 actions items', () => {
expect(findDropdownItemIds()).toEqual([
'connect-new-agent-link',
'new-cluster-link',
'connect-cluster-link',
]);
});
it('renders correct modal id for the agent link', () => {
const binding = getBinding(findConnectNewAgentLink().element, 'gl-modal-directive');
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
it('shows tooltip', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectWithAgent);
});
it('shows split button in dropdown', () => {
expect(findDropdown().props('split')).toBe(true);
});
it('binds split button with modal id', () => {
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
});
describe('when on group or admin level', () => {
beforeEach(() => {
createWrapper({ displayClusterAgents: false });
});
it('renders a dropdown with 2 actions items', () => {
expect(findDropdownItemIds()).toEqual(['new-cluster-link', 'connect-cluster-link']);
});
it('shows tooltip', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectExistingCluster);
});
it('does not show split button in dropdown', () => {
expect(findDropdown().props('split')).toBe(false);
});
it('does not bind dropdown button to modal', () => {
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
expect(binding.value).toBe(false);
});
});
});

View File

@ -7,6 +7,7 @@ import {
AGENT,
CERTIFICATE_BASED,
CLUSTERS_TABS,
CERTIFICATE_TAB,
MAX_CLUSTERS_LIST,
MAX_LIST_COUNT,
EVENT_LABEL_TABS,
@ -23,12 +24,12 @@ describe('ClustersMainViewComponent', () => {
defaultBranchName,
};
beforeEach(() => {
const createWrapper = ({ displayClusterAgents }) => {
wrapper = shallowMountExtended(ClustersMainView, {
propsData,
provide: { displayClusterAgents },
});
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
};
afterEach(() => {
wrapper.destroy();
@ -40,66 +41,90 @@ describe('ClustersMainViewComponent', () => {
const findComponent = () => wrapper.findByTestId('clusters-tab-component');
const findModal = () => wrapper.findComponent(InstallAgentModal);
it('renders `GlTabs` with `syncActiveTabWithQueryParams` and `queryParamName` props set', () => {
expect(findTabs().exists()).toBe(true);
expect(findTabs().props('syncActiveTabWithQueryParams')).toBe(true);
});
describe('when on project level', () => {
beforeEach(() => {
createWrapper({ displayClusterAgents: true });
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it('renders correct number of tabs', () => {
expect(findAllTabs()).toHaveLength(CLUSTERS_TABS.length);
});
it('renders `GlTabs` with `syncActiveTabWithQueryParams` and `queryParamName` props set', () => {
expect(findTabs().exists()).toBe(true);
expect(findTabs().props('syncActiveTabWithQueryParams')).toBe(true);
});
describe('tabs', () => {
it.each`
tabTitle | queryParamValue | lineNumber
${'All'} | ${'all'} | ${0}
${'Agent'} | ${AGENT} | ${1}
${'Certificate'} | ${CERTIFICATE_BASED} | ${2}
it('renders correct number of tabs', () => {
expect(findAllTabs()).toHaveLength(CLUSTERS_TABS.length);
});
describe('tabs', () => {
it.each`
tabTitle | queryParamValue | lineNumber
${'All'} | ${'all'} | ${0}
${'Agent'} | ${AGENT} | ${1}
${'Certificate'} | ${CERTIFICATE_BASED} | ${2}
`(
'renders correct tab title and query param value',
({ tabTitle, queryParamValue, lineNumber }) => {
expect(findGlTabAtIndex(lineNumber).attributes('title')).toBe(tabTitle);
expect(findGlTabAtIndex(lineNumber).props('queryParamValue')).toBe(queryParamValue);
},
);
});
describe.each`
tab | tabName
${'1'} | ${AGENT}
${'2'} | ${CERTIFICATE_BASED}
`(
'renders correct tab title and query param value',
({ tabTitle, queryParamValue, lineNumber }) => {
expect(findGlTabAtIndex(lineNumber).attributes('title')).toBe(tabTitle);
expect(findGlTabAtIndex(lineNumber).props('queryParamValue')).toBe(queryParamValue);
'when the child component emits the tab change event for $tabName tab',
({ tab, tabName }) => {
beforeEach(() => {
findComponent().vm.$emit('changeTab', tabName);
});
it(`changes the tab value to ${tab}`, () => {
expect(findTabs().attributes('value')).toBe(tab);
});
},
);
});
describe.each`
tab | tabName
${'1'} | ${AGENT}
${'2'} | ${CERTIFICATE_BASED}
`('when the child component emits the tab change event for $tabName tab', ({ tab, tabName }) => {
beforeEach(() => {
findComponent().vm.$emit('changeTab', tabName);
});
describe.each`
tab | tabName | maxAgents
${1} | ${AGENT} | ${MAX_LIST_COUNT}
${2} | ${CERTIFICATE_BASED} | ${MAX_CLUSTERS_LIST}
`('when the active tab is $tabName', ({ tab, tabName, maxAgents }) => {
beforeEach(() => {
findTabs().vm.$emit('input', tab);
});
it(`changes the tab value to ${tab}`, () => {
expect(findTabs().attributes('value')).toBe(tab);
});
});
it('passes child-component param to the component', () => {
expect(findComponent().props('defaultBranchName')).toBe(defaultBranchName);
});
describe.each`
tab | tabName | maxAgents
${1} | ${AGENT} | ${MAX_LIST_COUNT}
${2} | ${CERTIFICATE_BASED} | ${MAX_CLUSTERS_LIST}
`('when the active tab is $tabName', ({ tab, tabName, maxAgents }) => {
beforeEach(() => {
findTabs().vm.$emit('input', tab);
});
it(`sets max-agents param to ${maxAgents} and passes it to the modal`, () => {
expect(findModal().props('maxAgents')).toBe(maxAgents);
});
it('passes child-component param to the component', () => {
expect(findComponent().props('defaultBranchName')).toBe(defaultBranchName);
});
it(`sets max-agents param to ${maxAgents} and passes it to the modal`, () => {
expect(findModal().props('maxAgents')).toBe(maxAgents);
});
it(`sends the correct tracking event with the property '${tabName}'`, () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTIONS_CHANGE, {
label: EVENT_LABEL_TABS,
property: tabName,
it(`sends the correct tracking event with the property '${tabName}'`, () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTIONS_CHANGE, {
label: EVENT_LABEL_TABS,
property: tabName,
});
});
});
});
describe('when on group or admin level', () => {
beforeEach(() => {
createWrapper({ displayClusterAgents: false });
});
it('renders correct number of tabs', () => {
expect(findAllTabs()).toHaveLength(1);
});
it('renders correct tab title', () => {
expect(findGlTabAtIndex(0).attributes('title')).toBe(CERTIFICATE_TAB.title);
});
});
});

View File

@ -92,6 +92,10 @@ RSpec.describe ClustersHelper do
expect(subject[:new_cluster_path]).to eq("#{project_path(project)}/-/clusters/new?tab=create")
end
it 'displays add cluster using certificate path' do
expect(subject[:add_cluster_path]).to eq("#{project_path(project)}/-/clusters/new?tab=add")
end
context 'user has no permissions to create a cluster' do
it 'displays that user can\'t add cluster' do
expect(subject[:can_add_cluster]).to eq("false")
@ -114,6 +118,10 @@ RSpec.describe ClustersHelper do
it 'doesn\'t display empty state help text' do
expect(subject[:empty_state_help_text]).to be_nil
end
it 'displays display_cluster_agents as true' do
expect(subject[:display_cluster_agents]).to eq("true")
end
end
context 'group cluster' do
@ -123,6 +131,10 @@ RSpec.describe ClustersHelper do
it 'displays empty state help text' do
expect(subject[:empty_state_help_text]).to eq(s_('ClusterIntegration|Adding an integration to your group will share the cluster across all your projects.'))
end
it 'displays display_cluster_agents as false' do
expect(subject[:display_cluster_agents]).to eq("false")
end
end
end
@ -145,10 +157,6 @@ RSpec.describe ClustersHelper do
expect(subject[:project_path]).to eq(project.full_path)
end
it 'displays add cluster using certificate path' do
expect(subject[:add_cluster_path]).to eq("#{project_path(project)}/-/clusters/new?tab=add")
end
it 'displays kas address' do
expect(subject[:kas_address]).to eq(Gitlab::Kas.external_url)
end

View File

@ -5,7 +5,161 @@ require 'spec_helper'
RSpec.describe Gitlab::OmniauthInitializer do
let(:devise_config) { class_double(Devise) }
subject { described_class.new(devise_config) }
subject(:initializer) { described_class.new(devise_config) }
describe '.arguments_for' do
let(:devise_config) { nil }
let(:arguments) { initializer.send(:arguments_for, provider) }
context 'when there are no args at all' do
let(:provider) { { 'name' => 'unknown' } }
it 'returns an empty array' do
expect(arguments).to eq []
end
end
context 'when there is an app_id and an app_secret' do
let(:provider) { { 'name' => 'unknown', 'app_id' => 1, 'app_secret' => 2 } }
it 'includes both of them, in positional order' do
expect(arguments).to eq [1, 2]
end
end
context 'when there is an app_id and an app_secret, and an array of args' do
let(:provider) do
{
'name' => 'unknown',
'app_id' => 1,
'app_secret' => 2,
'args' => %w[one two three]
}
end
it 'concatenates the args on the end' do
expect(arguments).to eq [1, 2, 'one', 'two', 'three']
end
end
context 'when there is an app_id and an app_secret, and an array of args, and default values' do
let(:provider) do
{
'name' => 'unknown',
'app_id' => 1,
'app_secret' => 2,
'args' => %w[one two three]
}
end
before do
expect(described_class)
.to receive(:default_arguments_for).with('unknown')
.and_return({ default_arg: :some_value })
end
it 'concatenates the args on the end' do
expect(arguments)
.to eq [1, 2, 'one', 'two', 'three', { default_arg: :some_value }]
end
end
context 'when there is an app_id and an app_secret, and a hash of args' do
let(:provider) do
{
'name' => 'unknown',
'app_id' => 1,
'app_secret' => 2,
'args' => { 'foo' => 100, 'bar' => 200, 'nested' => { 'value' => 300 } }
}
end
it 'concatenates the args on the end' do
expect(arguments)
.to eq [1, 2, { foo: 100, bar: 200, nested: { value: 300 } }]
end
end
context 'when there is an app_id and an app_secret, and a hash of args, and default arguments' do
let(:provider) do
{
'name' => 'unknown',
'app_id' => 1,
'app_secret' => 2,
'args' => { 'foo' => 100, 'bar' => 200, 'nested' => { 'value' => 300 } }
}
end
before do
expect(described_class)
.to receive(:default_arguments_for).with('unknown')
.and_return({ default_arg: :some_value })
end
it 'concatenates the args on the end' do
expect(arguments)
.to eq [1, 2, { default_arg: :some_value, foo: 100, bar: 200, nested: { value: 300 } }]
end
end
context 'when there is an app_id and an app_secret, no args, and default values' do
let(:provider) do
{
'name' => 'unknown',
'app_id' => 1,
'app_secret' => 2
}
end
before do
expect(described_class)
.to receive(:default_arguments_for).with('unknown')
.and_return({ default_arg: :some_value })
end
it 'concatenates the args on the end' do
expect(arguments)
.to eq [1, 2, { default_arg: :some_value }]
end
end
context 'when there are args, of an unsupported type' do
let(:provider) do
{
'name' => 'unknown',
'args' => 1
}
end
context 'when there are default arguments' do
before do
expect(described_class)
.to receive(:default_arguments_for).with('unknown')
.and_return({ default_arg: :some_value })
end
it 'tracks a configuration error' do
expect(::Gitlab::ErrorTracking)
.to receive(:track_and_raise_for_dev_exception)
.with(described_class::ConfigurationError, provider_name: 'unknown', arguments_type: 'Integer')
expect(arguments)
.to eq [{ default_arg: :some_value }]
end
end
context 'when there are no default arguments' do
it 'tracks a configuration error' do
expect(::Gitlab::ErrorTracking)
.to receive(:track_and_raise_for_dev_exception)
.with(described_class::ConfigurationError, provider_name: 'unknown', arguments_type: 'Integer')
expect(arguments).to be_empty
end
end
end
end
describe '#execute' do
it 'configures providers from array' do
@ -105,9 +259,48 @@ RSpec.describe Gitlab::OmniauthInitializer do
it 'configures defaults for gitlab' do
conf = {
'name' => 'gitlab',
"args" => {}
"args" => { 'client_options' => { 'site' => generate(:url) } }
}
expect(devise_config).to receive(:omniauth).with(
:gitlab,
client_options: { site: conf.dig('args', 'client_options', 'site') },
authorize_params: { gl_auth_type: 'login' }
)
subject.execute([conf])
end
it 'configures defaults for gitlab, when arguments are not provided' do
conf = { 'name' => 'gitlab' }
expect(devise_config).to receive(:omniauth).with(
:gitlab,
authorize_params: { gl_auth_type: 'login' }
)
subject.execute([conf])
end
it 'configures defaults for gitlab, when array arguments are provided' do
conf = { 'name' => 'gitlab', 'args' => ['a'] }
expect(devise_config).to receive(:omniauth).with(
:gitlab,
'a',
authorize_params: { gl_auth_type: 'login' }
)
subject.execute([conf])
end
it 'tracks a configuration error if the arguments are neither a hash nor an array' do
conf = { 'name' => 'gitlab', 'args' => 17 }
expect(::Gitlab::ErrorTracking)
.to receive(:track_and_raise_for_dev_exception)
.with(described_class::ConfigurationError, provider_name: 'gitlab', arguments_type: 'Integer')
expect(devise_config).to receive(:omniauth).with(
:gitlab,
authorize_params: { gl_auth_type: 'login' }

View File

@ -25,18 +25,16 @@ RSpec.describe Analytics::CycleAnalytics::Aggregation, type: :model do
let_it_be(:subgroup) { create(:group, parent: group) }
it 'creates the aggregation record' do
described_class.safe_create_for_group(group)
record = described_class.safe_create_for_group(group)
record = described_class.find_by(group_id: group)
expect(record).to be_present
expect(record).to be_persisted
end
context 'when non top-level group is given' do
it 'creates the aggregation record for the top-level group' do
described_class.safe_create_for_group(subgroup)
record = described_class.safe_create_for_group(subgroup)
record = described_class.find_by(group_id: group)
expect(record).to be_present
expect(record).to be_persisted
end
end
@ -75,4 +73,56 @@ RSpec.describe Analytics::CycleAnalytics::Aggregation, type: :model do
expect(last_two).to eq([aggregation5, aggregation2])
end
end
describe '#estimated_next_run_at' do
around do |example|
freeze_time { example.run }
end
context 'when aggregation was not yet executed for the given group' do
let(:aggregation) { create(:cycle_analytics_aggregation, last_incremental_run_at: nil) }
it { expect(aggregation.estimated_next_run_at).to eq(nil) }
end
context 'when aggregation was already run' do
let!(:other_aggregation1) { create(:cycle_analytics_aggregation, last_incremental_run_at: 10.minutes.ago) }
let!(:other_aggregation2) { create(:cycle_analytics_aggregation, last_incremental_run_at: 15.minutes.ago) }
let!(:aggregation) { create(:cycle_analytics_aggregation, last_incremental_run_at: 5.minutes.ago) }
it 'returns the duration between the previous run timestamp and the earliest last_incremental_run_at' do
expect(aggregation.estimated_next_run_at).to eq(10.minutes.from_now)
end
context 'when the aggregation has persisted previous runtimes' do
before do
aggregation.update!(incremental_runtimes_in_seconds: [30, 60, 90])
end
it 'adds the runtime to the estimation' do
expect(aggregation.estimated_next_run_at).to eq((10.minutes.seconds + 60.seconds).from_now)
end
end
end
context 'when no records are present in the DB' do
it 'returns nil' do
expect(described_class.new.estimated_next_run_at).to eq(nil)
end
end
context 'when only one aggregation record present' do
let!(:aggregation) { create(:cycle_analytics_aggregation, last_incremental_run_at: 5.minutes.ago) }
it 'returns nil' do
expect(aggregation.estimated_next_run_at).to eq(nil)
end
end
context 'when the aggregation is disabled' do
it 'returns nil' do
expect(described_class.new(enabled: false).estimated_next_run_at).to eq(nil)
end
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe RunnersTokenPrefixable do
describe 'runners token prefix' do
subject { described_class::RUNNERS_TOKEN_PREFIX }
it 'has the correct value' do
expect(subject).to eq('GR1348941')
end
end
end

View File

@ -441,7 +441,7 @@ RSpec.shared_examples 'prefixed token rotation' do
context 'token is not set' do
it 'generates a new token' do
expect(subject).to match(/^#{instance.class::RUNNERS_TOKEN_PREFIX}/)
expect(subject).to match(/^#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}/)
expect(instance).not_to be_persisted
end
end
@ -452,26 +452,14 @@ RSpec.shared_examples 'prefixed token rotation' do
end
it 'generates a new token' do
expect(subject).to match(/^#{instance.class::RUNNERS_TOKEN_PREFIX}/)
expect(subject).to match(/^#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}/)
expect(instance).not_to be_persisted
end
context 'feature flag is disabled' do
before do
flag = "#{described_class.name.downcase.pluralize}_runners_token_prefix"
stub_feature_flags(flag => false)
end
it 'leaves the token unchanged' do
expect { subject }.not_to change(instance, :runners_token)
expect(instance).not_to be_persisted
end
end
end
context 'token is set and matches prefix' do
before do
instance.set_runners_token(instance.class::RUNNERS_TOKEN_PREFIX + '-abcdef')
instance.set_runners_token(RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX + '-abcdef')
end
it 'leaves the token unchanged' do
@ -486,7 +474,7 @@ RSpec.shared_examples 'prefixed token rotation' do
context 'token is not set' do
it 'generates a new token' do
expect(subject).to match(/^#{instance.class::RUNNERS_TOKEN_PREFIX}/)
expect(subject).to match(/^#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}/)
expect(instance).to be_persisted
end
end
@ -497,25 +485,14 @@ RSpec.shared_examples 'prefixed token rotation' do
end
it 'generates a new token' do
expect(subject).to match(/^#{instance.class::RUNNERS_TOKEN_PREFIX}/)
expect(subject).to match(/^#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}/)
expect(instance).to be_persisted
end
context 'feature flag is disabled' do
before do
flag = "#{described_class.name.downcase.pluralize}_runners_token_prefix"
stub_feature_flags(flag => false)
end
it 'leaves the token unchanged' do
expect { subject }.not_to change(instance, :runners_token)
end
end
end
context 'token is set and matches prefix' do
before do
instance.set_runners_token(instance.class::RUNNERS_TOKEN_PREFIX + '-abcdef')
instance.set_runners_token(RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX + '-abcdef')
instance.save!
end

View File

@ -3239,12 +3239,4 @@ RSpec.describe Group do
it_behaves_like 'no effective expiration interval'
end
end
describe '#runners_token' do
let_it_be(:group) { create(:group) }
subject { group }
it_behaves_like 'it has a prefixable runners_token', :groups_runners_token_prefix
end
end

View File

@ -813,8 +813,8 @@ RSpec.describe Project, factory_default: :keep do
end
it 'does not set an random token if one provided' do
project = FactoryBot.create(:project, runners_token: "#{Project::RUNNERS_TOKEN_PREFIX}my-token")
expect(project.runners_token).to eq("#{Project::RUNNERS_TOKEN_PREFIX}my-token")
project = FactoryBot.create(:project, runners_token: "#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}my-token")
expect(project.runners_token).to eq("#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}my-token")
end
end
@ -8077,14 +8077,6 @@ RSpec.describe Project, factory_default: :keep do
end
end
describe '#runners_token' do
let_it_be(:project) { create(:project) }
subject { project }
it_behaves_like 'it has a prefixable runners_token', :projects_runners_token_prefix
end
private
def finish_job(export_job)

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
require 'spec_helper'
require 'fast_spec_helper'
require_relative '../../../../rubocop/cop/database/establish_connection'
RSpec.describe RuboCop::Cop::Database::EstablishConnection do

View File

@ -1,35 +0,0 @@
# frozen_string_literal: true
RSpec.shared_examples 'it has a prefixable runners_token' do |feature_flag|
context 'feature flag enabled' do
before do
stub_feature_flags(feature_flag => [subject])
end
describe '#runners_token' do
it 'has a runners_token_prefix' do
expect(subject.runners_token_prefix).not_to be_empty
end
it 'starts with the runners_token_prefix' do
expect(subject.runners_token).to start_with(subject.runners_token_prefix)
end
end
end
context 'feature flag disabled' do
before do
stub_feature_flags(feature_flag => false)
end
describe '#runners_token' do
it 'does not have a runners_token_prefix' do
expect(subject.runners_token_prefix).to be_empty
end
it 'starts with the runners_token_prefix' do
expect(subject.runners_token).to start_with(subject.runners_token_prefix)
end
end
end
end

View File

@ -0,0 +1,113 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'layouts/_header_search' do
let(:project) { nil }
let(:group) { nil }
let(:scope) { nil }
let(:ref) { nil }
let(:code_search) { false }
let(:for_snippets) { false}
let(:header_search_context) do
{
project: project,
group: group,
scope: scope,
ref: ref,
code_search: code_search,
for_snippets: for_snippets
}
end
before do
allow(view).to receive(:header_search_context).and_return(header_search_context)
end
shared_examples 'hidden fields are properly set' do
context 'when search_context has a scope value' do
let(:scope) { 'issues' }
it 'sets scope input to issues' do
render
expect(rendered).to have_css("input[name='scope'][value='#{scope}']", count: 1, visible: false)
end
end
context 'when search_context has a code_search value' do
let(:code_search) { true }
it 'sets search_code input to true' do
render
expect(rendered).to have_css("input[name='search_code'][value='#{code_search}']", count: 1, visible: false)
end
end
context 'when search_context has a ref value' do
let(:ref) { 'test-branch' }
it 'sets repository_ref input to test-branch' do
render
expect(rendered).to have_css("input[name='repository_ref'][value='#{ref}']", count: 1, visible: false)
end
end
context 'when search_context has a for_snippets value' do
let(:for_snippets) { true }
it 'sets for_snippets input to true' do
render
expect(rendered).to have_css("input[name='snippets'][value='#{for_snippets}']", count: 1, visible: false)
end
end
context 'nav_source' do
it 'always set to navbar' do
render
expect(rendered).to have_css("input[name='nav_source'][value='navbar']", count: 1, visible: false)
end
end
context 'submit button' do
it 'always renders for specs' do
render
expect(rendered).to have_css('noscript button', text: 'Search')
end
end
end
context 'when doing a project level search' do
let(:project) do
{ id: 123, name: 'foo' }
end
it 'sets project_id field' do
render
expect(rendered).to have_css("input[name='project_id'][value='#{project[:id]}']", count: 1, visible: false)
end
it_behaves_like 'hidden fields are properly set'
end
context 'when doing a group level search' do
let(:group) do
{ id: 123, name: 'bar' }
end
it 'sets group_id field' do
render
expect(rendered).to have_css("input[name='group_id'][value='#{group[:id]}']", count: 1, visible: false)
end
it_behaves_like 'hidden fields are properly set'
end
end