diff --git a/app/assets/javascripts/admin/users/components/user_actions.vue b/app/assets/javascripts/admin/users/components/user_actions.vue index f5d21ece138..829174d7593 100644 --- a/app/assets/javascripts/admin/users/components/user_actions.vue +++ b/app/assets/javascripts/admin/users/components/user_actions.vue @@ -69,7 +69,6 @@ export default { editButtonAttrs() { return { 'data-testid': 'edit', - icon: 'pencil-square', href: this.userPaths.edit, }; }, @@ -101,6 +100,7 @@ export default { @@ -108,10 +108,9 @@ export default {
- {{ $options.i18n.agent }} - - {{ $options.i18n.connectWithAgent }} - - - {{ $options.i18n.certificate }} + + {{ $options.i18n.createNewCluster }} diff --git a/app/assets/javascripts/clusters_list/components/clusters_main_view.vue b/app/assets/javascripts/clusters_list/components/clusters_main_view.vue index db725ca0a06..eaaff74286a 100644 --- a/app/assets/javascripts/clusters_list/components/clusters_main_view.vue +++ b/app/assets/javascripts/clusters_list/components/clusters_main_view.vue @@ -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 > { - 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, + }, + }); + }, + }); }; diff --git a/app/assets/javascripts/clusters_list/load_clusters.js b/app/assets/javascripts/clusters_list/load_clusters.js deleted file mode 100644 index 1bb3ea546b2..00000000000 --- a/app/assets/javascripts/clusters_list/load_clusters.js +++ /dev/null @@ -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); - }, - }); -}; diff --git a/app/assets/javascripts/clusters_list/load_main_view.js b/app/assets/javascripts/clusters_list/load_main_view.js deleted file mode 100644 index d52b1d4a64d..00000000000 --- a/app/assets/javascripts/clusters_list/load_main_view.js +++ /dev/null @@ -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, - }, - }); - }, - }); -}; diff --git a/app/assets/javascripts/header_search/index.js b/app/assets/javascripts/header_search/index.js index d7e21f55ea5..4af8513ecdb 100644 --- a/app/assets/javascripts/header_search/index.js +++ b/app/assets/javascripts/header_search/index.js @@ -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); }, diff --git a/app/assets/javascripts/header_search/store/index.js b/app/assets/javascripts/header_search/store/index.js index 06cca4be8a7..b83433c5b49 100644 --- a/app/assets/javascripts/header_search/store/index.js +++ b/app/assets/javascripts/header_search/store/index.js @@ -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)); diff --git a/app/assets/javascripts/header_search/store/state.js b/app/assets/javascripts/header_search/store/state.js index 89814c00027..bebdbc7b92e 100644 --- a/app/assets/javascripts/header_search/store/state.js +++ b/app/assets/javascripts/header_search/store/state.js @@ -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, diff --git a/app/assets/javascripts/issues/show/components/delete_issue_modal.vue b/app/assets/javascripts/issues/show/components/delete_issue_modal.vue index 26862346b86..47b09bd6aa0 100644 --- a/app/assets/javascripts/issues/show/components/delete_issue_modal.vue +++ b/app/assets/javascripts/issues/show/components/delete_issue_modal.vue @@ -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, }; }, diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue index 92f0948ec48..adf449aca7b 100644 --- a/app/assets/javascripts/issues/show/components/header_actions.vue +++ b/app/assets/javascripts/issues/show/components/header_actions.vue @@ -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 { {{ deleteButtonText }} diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index f78b4da181e..efc425a1972 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -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 { diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss index 7b4f68e7a44..e06c71dccf0 100644 --- a/app/assets/stylesheets/framework/contextual_sidebar.scss +++ b/app/assets/stylesheets/framework/contextual_sidebar.scss @@ -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; - } } } diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index 8738b712af6..00195f553dc 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -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; } diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index afeaf16e4b6..6d66e207bdc 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -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; } diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index ccc155ddbf5..d1c409d071e 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -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 diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index 1475a26ca09..ae890685dc6 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -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)) diff --git a/app/models/analytics/cycle_analytics/aggregation.rb b/app/models/analytics/cycle_analytics/aggregation.rb index 22a816d9f44..d43293f40ea 100644 --- a/app/models/analytics/cycle_analytics/aggregation.rb +++ b/app/models/analytics/cycle_analytics/aggregation.rb @@ -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) diff --git a/app/models/concerns/runners_token_prefixable.rb b/app/models/concerns/runners_token_prefixable.rb new file mode 100644 index 00000000000..99bbbece7c7 --- /dev/null +++ b/app/models/concerns/runners_token_prefixable.rb @@ -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 diff --git a/app/models/group.rb b/app/models/group.rb index 46ab26dac67..e9fb4c36ba6 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index 14e7bf6d975..a435ca9e1e2 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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? diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index ca14d898d79..e429a16d5ec 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -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) diff --git a/app/views/clusters/clusters/_cluster_list.html.haml b/app/views/clusters/clusters/_cluster_list.html.haml index e5e1b68225e..8d92dc30a76 100644 --- a/app/views/clusters/clusters/_cluster_list.html.haml +++ b/app/views/clusters/clusters/_cluster_list.html.haml @@ -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) } diff --git a/app/views/layouts/_header_search.html.haml b/app/views/layouts/_header_search.html.haml new file mode 100644 index 00000000000..d2fe9a9a6ee --- /dev/null +++ b/app/views/layouts/_header_search.html.haml @@ -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' diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 567da7993b0..7256b9ed467 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -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' } diff --git a/config/feature_flags/development/groups_runners_token_prefix.yml b/config/feature_flags/development/groups_runners_token_prefix.yml deleted file mode 100644 index 87b87266673..00000000000 --- a/config/feature_flags/development/groups_runners_token_prefix.yml +++ /dev/null @@ -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 diff --git a/config/feature_flags/development/projects_runners_token_prefix.yml b/config/feature_flags/development/projects_runners_token_prefix.yml deleted file mode 100644 index 5dd21d115f6..00000000000 --- a/config/feature_flags/development/projects_runners_token_prefix.yml +++ /dev/null @@ -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 diff --git a/doc/administration/gitaly/recovery.md b/doc/administration/gitaly/recovery.md index 8e56e218d74..a7166f7e62e 100644 --- a/doc/administration/gitaly/recovery.md +++ b/doc/administration/gitaly/recovery.md @@ -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 -repository -replicate-immediately +sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml track-repository -virtual-storage -authoritative-storage -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: diff --git a/doc/ssh/index.md b/doc/ssh/index.md index 1de213a796b..846e5c369bb 100644 --- a/doc/ssh/index.md +++ b/doc/ssh/index.md @@ -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`. diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index 6757a043321..aedbfe503c8 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -181,7 +181,7 @@ table.supported-languages ul { Java - 8, 11, 13, 14, 15, or 16 + 8, 11, 13, 14, 15, 16, or 17 Gradle1
    @@ -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)1 | [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)2, [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)1 | [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)2, [7.3.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.26.0/config/.tool-versions#L5)2 | [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)3, [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)3 | +| 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)4, [2018.11.26](https://gitlab.com/gitlab-org/security-products/tests/python-pipenv/-/blob/master/Pipfile) |
    1. - The installed version of Bundler is only used for the bundler-audit analyzer, and is not used for gemnasium + The pre-installed version of Bundler is only used for the bundler-audit analyzer, and is not used for gemnasium.

    2. +

      + 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 gradlew + (Gradle wrapper) file or not: +

      +
        +
      • +

        + If your project does not use a gradlew file, then the analyzer automatically switches to one of the + pre-installed Gradle versions, based on the version of Java specified by the + DS_JAVA_VERSION variable. +

        +

        You can view the + Gradle Java compatibility matrix 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. +

        +
      • +
      • +

        + If your project does use a gradlew file, then the version of Gradle pre-installed in the analyzer image is + ignored, and the version specified in your gradlew file is used instead. +

        +
      • +
      +
    3. +
    4. + +

      + These tests confirms that if a gradlew file does not exist, the version of Gradle pre-installed in the analyzer image is used. +

      +
    5. +
    6. +

      This test confirms that if a Pipfile.lock file is found, it will be used by Gemnasium to scan the exact package versions listed in this file.

      @@ -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`. | diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index 0e01a47872f..f9a4726c21e 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -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. diff --git a/lib/gitlab/analytics/cycle_analytics/request_params.rb b/lib/gitlab/analytics/cycle_analytics/request_params.rb index bc9d94ef09c..bc270fd784a 100644 --- a/lib/gitlab/analytics/cycle_analytics/request_params.rb +++ b/lib/gitlab/analytics/cycle_analytics/request_params.rb @@ -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, diff --git a/lib/gitlab/omniauth_initializer.rb b/lib/gitlab/omniauth_initializer.rb index a9ff186c7cb..f4984e11c14 100644 --- a/lib/gitlab/omniauth_initializer.rb +++ b/lib/gitlab/omniauth_initializer.rb @@ -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 diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb index c0108d85365..a0b42598962 100644 --- a/qa/qa/page/dashboard/projects.rb +++ b/qa/qa/page/dashboard/projects.rb @@ -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 diff --git a/qa/qa/page/modal/delete_issue.rb b/qa/qa/page/modal/delete_issue.rb new file mode 100644 index 00000000000..9b51e969b48 --- /dev/null +++ b/qa/qa/page/modal/delete_issue.rb @@ -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 diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb index b37210f4d3f..fe468de60cd 100644 --- a/qa/qa/page/project/issue/show.rb +++ b/qa/qa/page/project/issue/show.rb @@ -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 diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/personal_project_permissions_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/personal_project_permissions_spec.rb new file mode 100644 index 00000000000..2aefa1c39ed --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/project/personal_project_permissions_spec.rb @@ -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 diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb index 1485edcd97d..ce3c9af22f1 100644 --- a/spec/fast_spec_helper.rb +++ b/spec/fast_spec_helper.rb @@ -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' diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb index 0397e72502a..baa691d244e 100644 --- a/spec/features/global_search_spec.rb +++ b/spec/features/global_search_spec.rb @@ -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 diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js index 43313424553..b90a30b5b89 100644 --- a/spec/frontend/admin/users/components/user_actions_spec.js +++ b/spec/frontend/admin/users/components/user_actions_spec.js @@ -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); - }); }); }); diff --git a/spec/frontend/clusters_list/components/clusters_actions_spec.js b/spec/frontend/clusters_list/components/clusters_actions_spec.js index 331690fc642..2cbd12a680b 100644 --- a/spec/frontend/clusters_list/components/clusters_actions_spec.js +++ b/spec/frontend/clusters_list/components/clusters_actions_spec.js @@ -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); + }); }); }); diff --git a/spec/frontend/clusters_list/components/clusters_main_view_spec.js b/spec/frontend/clusters_list/components/clusters_main_view_spec.js index 4306a906737..20754ffe5b7 100644 --- a/spec/frontend/clusters_list/components/clusters_main_view_spec.js +++ b/spec/frontend/clusters_list/components/clusters_main_view_spec.js @@ -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); + }); + }); }); diff --git a/spec/helpers/clusters_helper_spec.rb b/spec/helpers/clusters_helper_spec.rb index 18d233fcd63..433de813e3d 100644 --- a/spec/helpers/clusters_helper_spec.rb +++ b/spec/helpers/clusters_helper_spec.rb @@ -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 diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb index 42ae5844b95..8b959cf787f 100644 --- a/spec/lib/gitlab/omniauth_initializer_spec.rb +++ b/spec/lib/gitlab/omniauth_initializer_spec.rb @@ -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' } diff --git a/spec/models/analytics/cycle_analytics/aggregation_spec.rb b/spec/models/analytics/cycle_analytics/aggregation_spec.rb index e4832cf3f27..cb439430dd4 100644 --- a/spec/models/analytics/cycle_analytics/aggregation_spec.rb +++ b/spec/models/analytics/cycle_analytics/aggregation_spec.rb @@ -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 diff --git a/spec/models/concerns/runners_token_prefixable_spec.rb b/spec/models/concerns/runners_token_prefixable_spec.rb new file mode 100644 index 00000000000..29e7b8cf4f4 --- /dev/null +++ b/spec/models/concerns/runners_token_prefixable_spec.rb @@ -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 diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index 5fec8ad1113..d7bfcc3f579 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -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 diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 98fad544abd..6a8f9826481 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -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 diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index d2d06b64cb3..b251e8dfe9e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -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) diff --git a/spec/rubocop/cop/database/establish_connection_spec.rb b/spec/rubocop/cop/database/establish_connection_spec.rb index a3c27d33cb0..3919872b5e7 100644 --- a/spec/rubocop/cop/database/establish_connection_spec.rb +++ b/spec/rubocop/cop/database/establish_connection_spec.rb @@ -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 diff --git a/spec/support/shared_examples/models/runners_token_prefix_shared_examples.rb b/spec/support/shared_examples/models/runners_token_prefix_shared_examples.rb deleted file mode 100644 index bbce67ae7b9..00000000000 --- a/spec/support/shared_examples/models/runners_token_prefix_shared_examples.rb +++ /dev/null @@ -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 diff --git a/spec/views/layouts/_header_search.html.haml_spec.rb b/spec/views/layouts/_header_search.html.haml_spec.rb new file mode 100644 index 00000000000..3ab4ae6a483 --- /dev/null +++ b/spec/views/layouts/_header_search.html.haml_spec.rb @@ -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