Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-08 15:13:35 +00:00
parent 6a38034714
commit 05db4ead6d
79 changed files with 1189 additions and 312 deletions

View file

@ -534,6 +534,50 @@ rspec:feature-flags:
run_timed_command "bundle exec scripts/used-feature-flags";
fi
rspec:skipped-flaky-tests-report:
extends:
- .default-retry
- .rails:rules:skipped-flaky-tests-report
image: ruby:2.7-alpine
stage: post-test
# We cannot use needs since it would mean needing 84 jobs (since most are parallelized)
# so we use `dependencies` here.
dependencies:
# FOSS/EE jobs
- rspec migration pg12
- rspec unit pg12
- rspec integration pg12
- rspec system pg12
# FOSS/EE minimal jobs
- rspec migration pg12 minimal
- rspec unit pg12 minimal
- rspec integration pg12 minimal
- rspec system pg12 minimal
# EE jobs
- rspec-ee migration pg12
- rspec-ee unit pg12
- rspec-ee integration pg12
- rspec-ee system pg12
# EE minimal jobs
- rspec-ee migration pg12 minimal
- rspec-ee unit pg12 minimal
- rspec-ee integration pg12 minimal
- rspec-ee system pg12 minimal
# Geo jobs
- rspec-ee unit pg12 geo
- rspec-ee integration pg12 geo
- rspec-ee system pg12 geo
# Geo minimal jobs
- rspec-ee unit pg12 geo minimal
- rspec-ee integration pg12 geo minimal
- rspec-ee system pg12 geo minimal
script:
- cat rspec_flaky/skipped_flaky_tests_*_report.txt >> skipped_flaky_tests_report.txt
artifacts:
expire_in: 31d
paths:
- skipped_flaky_tests_report.txt
# EE/FOSS: default refs (MRs, default branch, schedules) jobs #
#######################################################

View file

@ -1352,6 +1352,13 @@
when: never
- changes: *code-backstage-patterns
.rails:rules:skipped-flaky-tests-report:
rules:
- <<: *if-not-ee
when: never
- if: '$SKIP_FLAKY_TESTS_AUTOMATICALLY == "true"'
changes: *code-backstage-patterns
#########################
# Static analysis rules #
#########################

View file

@ -1 +1 @@
7b9cd199b0851fd1b6615e0798f2aafddafd63cb
460a880c6993ab5f76cac951fccc02efd5cbd444

View file

@ -342,7 +342,7 @@ group :development do
gem 'lefthook', '~> 0.7.0', require: false
gem 'solargraph', '~> 0.43', require: false
gem 'letter_opener_web', '~> 1.4.1'
gem 'letter_opener_web', '~> 2.0.0'
# Better errors handler
gem 'better_errors', '~> 2.9.0'

View file

@ -700,10 +700,11 @@ GEM
lefthook (0.7.5)
letter_opener (1.7.0)
launchy (~> 2.2)
letter_opener_web (1.4.1)
actionmailer (>= 3.2)
letter_opener (~> 1.0)
railties (>= 3.2)
letter_opener_web (2.0.0)
actionmailer (>= 5.2)
letter_opener (~> 1.7)
railties (>= 5.2)
rexml
libyajl2 (1.2.0)
license_finder (6.0.0)
bundler
@ -1516,7 +1517,7 @@ DEPENDENCIES
kramdown (~> 2.3.1)
kubeclient (~> 4.9.2)
lefthook (~> 0.7.0)
letter_opener_web (~> 1.4.1)
letter_opener_web (~> 2.0.0)
license_finder (~> 6.0)
licensee (~> 9.14.1)
lockbox (~> 0.6.2)

View file

@ -1,10 +0,0 @@
query GroupBoardIterations($fullPath: ID!, $title: String) {
group(fullPath: $fullPath) {
iterations(includeAncestors: true, title: $title) {
nodes {
id
title
}
}
}
}

View file

@ -1,10 +0,0 @@
query ProjectBoardIterations($fullPath: ID!, $title: String) {
project(fullPath: $fullPath) {
iterations(includeAncestors: true, title: $title) {
nodes {
id
title
}
}
}
}

View file

@ -36,13 +36,11 @@ import {
} from '../boards_util';
import { gqlClient } from '../graphql';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import groupBoardIterationsQuery from '../graphql/group_board_iterations.query.graphql';
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
import projectBoardIterationsQuery from '../graphql/project_board_iterations.query.graphql';
import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';
import * as types from './mutation_types';
@ -203,52 +201,6 @@ export default {
});
},
fetchIterations({ state, commit }, title) {
commit(types.RECEIVE_ITERATIONS_REQUEST);
const { fullPath, boardType } = state;
const variables = {
fullPath,
title,
};
let query;
if (boardType === BoardType.project) {
query = projectBoardIterationsQuery;
}
if (boardType === BoardType.group) {
query = groupBoardIterationsQuery;
}
if (!query) {
// eslint-disable-next-line @gitlab/require-i18n-strings
throw new Error('Unknown board type');
}
return gqlClient
.query({
query,
variables,
})
.then(({ data }) => {
const errors = data[boardType]?.errors;
const iterations = data[boardType]?.iterations.nodes;
if (errors?.[0]) {
throw new Error(errors[0]);
}
commit(types.RECEIVE_ITERATIONS_SUCCESS, iterations);
return iterations;
})
.catch((e) => {
commit(types.RECEIVE_ITERATIONS_FAILURE);
throw e;
});
},
fetchMilestones({ state, commit }, searchTerm) {
commit(types.RECEIVE_MILESTONES_REQUEST);

View file

@ -41,7 +41,3 @@ export const ADD_LIST_TO_HIGHLIGHTED_LISTS = 'ADD_LIST_TO_HIGHLIGHTED_LISTS';
export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS';
export const RESET_BOARD_ITEM_SELECTION = 'RESET_BOARD_ITEM_SELECTION';
export const SET_ERROR = 'SET_ERROR';
export const RECEIVE_ITERATIONS_REQUEST = 'RECEIVE_ITERATIONS_REQUEST';
export const RECEIVE_ITERATIONS_SUCCESS = 'RECEIVE_ITERATIONS_SUCCESS';
export const RECEIVE_ITERATIONS_FAILURE = 'RECEIVE_ITERATIONS_FAILURE';

View file

@ -64,20 +64,6 @@ export default {
);
},
[mutationTypes.RECEIVE_ITERATIONS_REQUEST](state) {
state.iterationsLoading = true;
},
[mutationTypes.RECEIVE_ITERATIONS_SUCCESS](state, iterations) {
state.iterations = iterations;
state.iterationsLoading = false;
},
[mutationTypes.RECEIVE_ITERATIONS_FAILURE](state) {
state.iterationsLoading = false;
state.error = __('Failed to load iterations.');
},
[mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) {
state.activeId = id;
state.sidebarType = sidebarType;

View file

@ -44,6 +44,7 @@ import {
TRACKING_MULTIPLE_FILES_MODE,
} from '../constants';
import { discussionIntersectionObserverHandlerFactory } from '../utils/discussions';
import diffsEventHub from '../event_hub';
import { reviewStatuses } from '../utils/file_reviews';
import { diffsApp } from '../utils/performance';
@ -86,6 +87,9 @@ export default {
ALERT_MERGE_CONFLICT,
ALERT_COLLAPSED_FILES,
},
provide: {
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
},
props: {
endpoint: {
type: String,

View file

@ -0,0 +1,76 @@
function normalize(processable) {
const { entry } = processable;
const offset = entry.rootBounds.bottom - entry.boundingClientRect.top;
const direction =
offset < 0 ? 'Up' : 'Down'; /* eslint-disable-line @gitlab/require-i18n-strings */
return {
...processable,
entry: {
time: entry.time,
type: entry.isIntersecting ? 'intersection' : `scroll${direction}`,
},
};
}
function sort({ entry: alpha }, { entry: beta }) {
const diff = alpha.time - beta.time;
let order = 0;
if (diff < 0) {
order = -1;
} else if (diff > 0) {
order = 1;
} else if (alpha.type === 'intersection' && beta.type === 'scrollUp') {
order = 2;
} else if (alpha.type === 'scrollUp' && beta.type === 'intersection') {
order = -2;
}
return order;
}
function filter(entry) {
return entry.type !== 'scrollDown';
}
export function discussionIntersectionObserverHandlerFactory() {
let unprocessed = [];
let timer = null;
return (processable) => {
unprocessed.push(processable);
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
unprocessed
.map(normalize)
.filter(filter)
.sort(sort)
.forEach((discussionObservationContainer) => {
const {
entry: { type },
currentDiscussion,
isFirstUnresolved,
isDiffsPage,
functions: { setCurrentDiscussionId, getPreviousUnresolvedDiscussionId },
} = discussionObservationContainer;
if (type === 'intersection') {
setCurrentDiscussionId(currentDiscussion.id);
} else if (type === 'scrollUp') {
setCurrentDiscussionId(
isFirstUnresolved
? null
: getPreviousUnresolvedDiscussionId(currentDiscussion.id, isDiffsPage),
);
}
});
unprocessed = [];
}, 0);
};
}

View file

@ -1,5 +1,6 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import { GlIntersectionObserver } from '@gitlab/ui';
import { __ } from '~/locale';
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
@ -16,7 +17,9 @@ export default {
ToggleRepliesWidget,
NoteEditedText,
DiscussionNotesRepliesWrapper,
GlIntersectionObserver,
},
inject: ['discussionObserverHandler'],
props: {
discussion: {
type: Object,
@ -54,7 +57,11 @@ export default {
},
},
computed: {
...mapGetters(['userCanReply']),
...mapGetters([
'userCanReply',
'previousUnresolvedDiscussionId',
'firstUnresolvedDiscussionId',
]),
hasReplies() {
return Boolean(this.replies.length);
},
@ -77,9 +84,20 @@ export default {
url: this.discussion.discussion_path,
};
},
isFirstUnresolved() {
return this.firstUnresolvedDiscussionId === this.discussion.id;
},
},
observerOptions: {
threshold: 0,
rootMargin: '0px 0px -50% 0px',
},
methods: {
...mapActions(['toggleDiscussion', 'setSelectedCommentPositionHover']),
...mapActions([
'toggleDiscussion',
'setSelectedCommentPositionHover',
'setCurrentDiscussionId',
]),
componentName(note) {
if (note.isPlaceholderNote) {
if (note.placeholderType === SYSTEM_NOTE) {
@ -110,6 +128,18 @@ export default {
this.setSelectedCommentPositionHover();
}
},
observerTriggered(entry) {
this.discussionObserverHandler({
entry,
isFirstUnresolved: this.isFirstUnresolved,
currentDiscussion: { ...this.discussion },
isDiffsPage: !this.isOverviewTab,
functions: {
setCurrentDiscussionId: this.setCurrentDiscussionId,
getPreviousUnresolvedDiscussionId: this.previousUnresolvedDiscussionId,
},
});
},
},
};
</script>
@ -122,33 +152,35 @@ export default {
@mouseleave="handleMouseLeave(discussion)"
>
<template v-if="shouldGroupReplies">
<component
:is="componentName(firstNote)"
:note="componentData(firstNote)"
:line="line || diffLine"
:discussion-file="discussion.diff_file"
:commit="commit"
:help-page-path="helpPagePath"
:show-reply-button="userCanReply"
:discussion-root="true"
:discussion-resolve-path="discussion.resolve_path"
:is-overview-tab="isOverviewTab"
@handleDeleteNote="$emit('deleteNote')"
@startReplying="$emit('startReplying')"
>
<template #discussion-resolved-text>
<note-edited-text
v-if="discussion.resolved"
:edited-at="discussion.resolved_at"
:edited-by="discussion.resolved_by"
:action-text="resolvedText"
class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
/>
</template>
<template #avatar-badge>
<slot name="avatar-badge"></slot>
</template>
</component>
<gl-intersection-observer :options="$options.observerOptions" @update="observerTriggered">
<component
:is="componentName(firstNote)"
:note="componentData(firstNote)"
:line="line || diffLine"
:discussion-file="discussion.diff_file"
:commit="commit"
:help-page-path="helpPagePath"
:show-reply-button="userCanReply"
:discussion-root="true"
:discussion-resolve-path="discussion.resolve_path"
:is-overview-tab="isOverviewTab"
@handleDeleteNote="$emit('deleteNote')"
@startReplying="$emit('startReplying')"
>
<template #discussion-resolved-text>
<note-edited-text
v-if="discussion.resolved"
:edited-at="discussion.resolved_at"
:edited-by="discussion.resolved_by"
:action-text="resolvedText"
class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
/>
</template>
<template #avatar-badge>
<slot name="avatar-badge"></slot>
</template>
</component>
</gl-intersection-observer>
<discussion-notes-replies-wrapper :is-diff-discussion="discussion.diff_discussion">
<toggle-replies-widget
v-if="hasReplies"

View file

@ -1,5 +1,5 @@
import initDevOpsScore from '~/analytics/devops_report/devops_score';
import initDevOpsScoreDisabledServicePing from '~/analytics/devops_report/devops_score_disabled_service_ping';
import initDevOpsScore from '~/analytics/devops_reports/devops_score';
import initDevOpsScoreDisabledServicePing from '~/analytics/devops_reports/devops_score_disabled_service_ping';
initDevOpsScoreDisabledServicePing();
initDevOpsScore();

View file

@ -227,7 +227,7 @@
// IMPORTANT PERFORMANCE OPTIMIZATION
//
// When viewinng a blame with many commits a lot of content is rendered on the page.
// Two selectors below ensure that we only render what is visible to the user, thus reducing TBT in the browser.
// content-visibility rules below ensure that we only render what is visible to the user, thus reducing TBT in the browser.
.commit {
content-visibility: auto;
contain-intrinsic-size: 1px 3em;
@ -237,6 +237,10 @@
content-visibility: auto;
contain-intrinsic-size: 1px 1.1875rem;
}
.line-numbers {
content-visibility: auto;
}
}
&.logs {

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
module IssuesHelper
include Issues::IssueTypeHelpers
def issue_css_classes(issue)
classes = ["issue"]
classes << "closed" if issue.closed?

View file

@ -41,8 +41,6 @@ module AlertEventLifecycle
scope :firing, -> { where(status: status_value_for(:firing)) }
scope :resolved, -> { where(status: status_value_for(:resolved)) }
scope :count_by_project_id, -> { group(:project_id).count }
def self.status_value_for(name)
state_machines[:status].states[name].value
end

View file

@ -92,7 +92,6 @@ module Issuable
scope :recent, -> { reorder(id: :desc) }
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :opened, -> { with_state(:opened) }
scope :only_opened, -> { with_state(:opened) }
scope :closed, -> { with_state(:closed) }
# rubocop:disable GitlabSecurity/SqlInjection

View file

@ -14,7 +14,6 @@ module Milestoneable
validate :milestone_is_valid
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :any_milestone, -> { where.not(milestone_id: nil) }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
scope :without_particular_milestone, ->(title) { left_outer_joins(:milestone).where("milestones.title != ? OR milestone_id IS NULL", title) }

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
module AuthorizedProjectUpdate
class ProjectAccessChangedService
def initialize(project_ids)
@project_ids = Array.wrap(project_ids)
end
def execute(blocking: true)
bulk_args = @project_ids.map { |id| [id] }
if blocking
AuthorizedProjectUpdate::ProjectRecalculateWorker.bulk_perform_and_wait(bulk_args)
else
AuthorizedProjectUpdate::ProjectRecalculateWorker.bulk_perform_async(bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
end
end
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
module Issues
module IssueTypeHelpers
# @param object [Issue, Project]
# @param issue_type [String, Symbol]
def create_issue_type_allowed?(object, issue_type)
WorkItem::Type.base_types.key?(issue_type.to_s) &&
can?(current_user, :"create_#{issue_type}", object)
end
end
end

View file

@ -175,21 +175,18 @@ module Groups
end
def refresh_project_authorizations
ProjectAuthorization.where(project_id: @group.all_projects.select(:id)).delete_all # rubocop: disable CodeReuse/ActiveRecord
projects_to_update = Set.new
# refresh authorized projects for current_user immediately
current_user.refresh_authorized_projects
# schedule refreshing projects for all the members of the group
@group.refresh_members_authorized_projects
# All projects in this hierarchy need to have their project authorizations recalculated
@group.all_projects.each_batch { |prjs| projects_to_update.merge(prjs.ids) } # rubocop: disable CodeReuse/ActiveRecord
# When a group is transferred, it also affects who gets access to the projects shared to
# the subgroups within its hierarchy, so we also schedule jobs that refresh authorizations for all such shared projects.
project_group_shares_within_the_hierarchy = ProjectGroupLink.in_group(group.self_and_descendants.select(:id))
project_group_shares_within_the_hierarchy.find_each do |project_group_link|
AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project_group_link.project_id)
ProjectGroupLink.in_group(@group.self_and_descendants.select(:id)).each_batch do |project_group_links|
projects_to_update.merge(project_group_links.pluck(:project_id)) # rubocop: disable CodeReuse/ActiveRecord
end
AuthorizedProjectUpdate::ProjectAccessChangedService.new(projects_to_update.to_a).execute unless projects_to_update.empty?
end
def raise_transfer_error(message)

View file

@ -3,6 +3,7 @@
module Issues
class BaseService < ::IssuableBaseService
include IncidentManagement::UsageData
include IssueTypeHelpers
def hook_data(issue, action, old_associations: {})
hook_data = issue.to_hook_data(current_user, old_associations: old_associations)
@ -44,7 +45,7 @@ module Issues
def filter_params(issue)
super
params.delete(:issue_type) unless issue_type_allowed?(issue)
params.delete(:issue_type) unless create_issue_type_allowed?(issue, params[:issue_type])
filter_incident_label(issue) if params[:issue_type]
moved_issue = params.delete(:moved_issue)
@ -89,12 +90,6 @@ module Issues
Milestones::IssuesCountService.new(milestone).delete_cache
end
# @param object [Issue, Project]
def issue_type_allowed?(object)
WorkItem::Type.base_types.key?(params[:issue_type]) &&
can?(current_user, :"create_#{params[:issue_type]}", object)
end
# @param issue [Issue]
def filter_incident_label(issue)
return unless add_incident_label?(issue) || remove_incident_label?(issue)

View file

@ -80,7 +80,7 @@ module Issues
]
allowed_params << :milestone_id if can?(current_user, :admin_issue, project)
allowed_params << :issue_type if issue_type_allowed?(project)
allowed_params << :issue_type if create_issue_type_allowed?(project, params[:issue_type])
params.slice(*allowed_params)
end

View file

@ -6,5 +6,5 @@
- if show_adoption?
= render_if_exists 'admin/dev_ops_report/devops_tabs'
- else
= render 'report'
= render 'score'

View file

@ -18,17 +18,19 @@
= sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon')
.dropdown-content{ data: { testid: 'issue-type-select-dropdown' } }
%ul
%li.js-filter-issuable-type
= link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do
#{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_("Issue")}
%li.js-filter-issuable-type{ data: { track: { action: "select_issue_type_incident", label: "select_issue_type_incident_dropdown_option" } } }
= link_to new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }), class: ("is-active" if issuable.incident?) do
#{sprite_icon(work_item_type_icon(:incident), css_class: 'gl-icon')} #{_("Incident")}
- if create_issue_type_allowed?(@project, :issue)
%li.js-filter-issuable-type
= link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do
#{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_('Issue')}
- if create_issue_type_allowed?(@project, :incident)
%li.js-filter-issuable-type{ data: { track: { action: "select_issue_type_incident", label: "select_issue_type_incident_dropdown_option" } } }
= link_to new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }), class: ("is-active" if issuable.incident?) do
#{sprite_icon(work_item_type_icon(:incident), css_class: 'gl-icon')} #{_('Incident')}
#js-type-popover
- if issuable.incident?
%p.form-text.text-muted
- incident_docs_url = help_page_path('operations/incident_management/incidents.md')
- incident_docs_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: incident_docs_url }
= _('A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents.').html_safe % { incident_docs_start: incident_docs_start, incident_docs_end: '</a>'.html_safe }
- incident_docs_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">', url: incident_docs_url)
= format(_('A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents.'), incident_docs_start: incident_docs_start, incident_docs_end: '</a>').html_safe

View file

@ -7,6 +7,8 @@ module AuthorizedProjectUpdate
data_consistency :always
include Gitlab::ExclusiveLeaseHelpers
prepend WaitableWorker
feature_category :authentication_and_authorization
urgency :high
queue_namespace :authorized_project_update

View file

@ -159,7 +159,10 @@ module ContainerExpirationPolicies
return unless tags_count && cached_tags_count && tags_count != 0
log_extra_metadata_on_done(:cleanup_tags_service_cache_hit_ratio, cached_tags_count / tags_count.to_f)
ratio = cached_tags_count / tags_count.to_f
ratio_as_percentage = (ratio * 100).round(2)
log_extra_metadata_on_done(:cleanup_tags_service_cache_hit_ratio, ratio_as_percentage)
end
def log_truncate(result)

View file

@ -16,6 +16,8 @@ Bundler.require(*Rails.groups)
module Gitlab
class Application < Rails::Application
config.load_defaults 6.1
require_dependency Rails.root.join('lib/gitlab')
require_dependency Rails.root.join('lib/gitlab/utils')
require_dependency Rails.root.join('lib/gitlab/action_cable/config')
@ -37,8 +39,6 @@ module Gitlab
require_dependency Rails.root.join('lib/gitlab/runtime')
require_dependency Rails.root.join('lib/gitlab/patch/legacy_database_config')
config.autoloader = :zeitwerk
# To be removed in 15.0
# This preload is needed to convert legacy `database.yml`
# from `production: adapter: postgresql`
@ -190,11 +190,12 @@ module Gitlab
# regardless if schema_search_path is set, or not.
config.active_record.dump_schemas = :all
# Use new connection handling so that we can use Rails 6.1+ multiple
# database support.
config.active_record.legacy_connection_handling = false
config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob"
# Override default Active Record settings
# We cannot do this in an initializer because some models are already loaded by then
config.active_record.cache_versioning = false
config.active_record.collection_cache_versioning = false
config.active_record.has_many_inversing = false
config.active_record.belongs_to_required_by_default = false
# Enable the asset pipeline
config.assets.enabled = true
@ -380,6 +381,7 @@ module Gitlab
config.cache_store = :redis_cache_store, Gitlab::Redis::Cache.active_support_config
config.active_job.queue_adapter = :sidekiq
config.action_mailer.deliver_later_queue_name = :mailers
# This is needed for gitlab-shell
ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH']

View file

@ -1,7 +0,0 @@
# frozen_string_literal: true
# This file was introduced during upgrading Rails from 5.2 to 6.0.
# This file can be removed when `config.load_defaults 6.0` is introduced.
# Don't force requests from old versions of IE to be UTF-8 encoded.
Rails.application.config.action_view.default_enforce_utf8 = false

View file

@ -2,6 +2,5 @@
# Be sure to restart your server when you modify this file.
Rails.application.config.action_dispatch.use_cookies_with_metadata = true
Rails.application.config.action_dispatch.cookies_serializer =
Gitlab::Utils.to_boolean(ENV['USE_UNSAFE_HYBRID_COOKIES']) ? :hybrid : :json

View file

@ -1,24 +0,0 @@
# frozen_string_literal: true
# Remove this `if` condition when upgraded to rails 5.0.
# The body must be kept.
# Be sure to restart your server when you modify this file.
#
# This file contains migration options to ease your Rails 5.0 upgrade.
#
# Once upgraded flip defaults one by one to migrate to the new default.
#
# Read the Guide for Upgrading Ruby on Rails for more info on each option.
# Enable per-form CSRF tokens. Previous versions had false.
Rails.application.config.action_controller.per_form_csrf_tokens = false
# Enable origin-checking CSRF mitigation. Previous versions had false.
Rails.application.config.action_controller.forgery_protection_origin_check = false
# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`.
# Previous versions had false.
ActiveSupport.to_time_preserves_timezone = false
# Require `belongs_to` associations by default. Previous versions had false.
Rails.application.config.active_record.belongs_to_required_by_default = false

View file

@ -0,0 +1,35 @@
# frozen_string_literal: true
# This contains configuration from Rails upgrades to override the new defaults so that we
# keep existing behavior.
#
# For boolean values, the new default is the opposite of the value being set in this file.
# For other types, the new default is noted in the comments. These are also documented in
# https://guides.rubyonrails.org/configuring.html#results-of-config-load-defaults
#
# To switch a setting to the new default value, we just need to delete the specific line here.
Rails.application.configure do
# Rails 6.1
config.action_dispatch.cookies_same_site_protection = nil # New default is :lax
config.action_dispatch.ssl_default_redirect_status = nil # New default is 308
ActiveSupport.utc_to_local_returns_utc_offset_times = false
config.action_controller.urlsafe_csrf_tokens = false
config.action_view.preload_links_header = false
# Rails 5.2
config.action_dispatch.use_authenticated_cookie_encryption = false
config.active_support.use_authenticated_message_encryption = false
config.active_support.hash_digest_class = ::Digest::MD5 # New default is ::Digest::SHA1
config.action_controller.default_protect_from_forgery = false
config.action_view.form_with_generates_ids = false
# Rails 5.1
config.assets.unknown_asset_fallback = true
# Rails 5.0
config.action_controller.per_form_csrf_tokens = false
config.action_controller.forgery_protection_origin_check = false
ActiveSupport.to_time_preserves_timezone = false
config.ssl_options = {} # New default is { hsts: { subdomains: true } }
end

View file

@ -0,0 +1,112 @@
/* eslint-disable no-underscore-dangle */
const yaml = require('js-yaml');
const PLUGIN_NAME = 'GraphqlKnownOperationsPlugin';
const GRAPHQL_PATH_REGEX = /(query|mutation)\.graphql$/;
const OPERATION_NAME_SOURCE_REGEX = /^\s*module\.exports.*oneQuery.*"(\w+)"/gm;
/**
* Returns whether a given webpack module is a "graphql" module
*/
const isGraphqlModule = (module) => {
return GRAPHQL_PATH_REGEX.test(module.resource);
};
/**
* Returns graphql operation names we can parse from the given module
*
* Since webpack gives us the source **after** the graphql-tag/loader runs,
* we can look for specific lines we're guaranteed to have from the
* graphql-tag/loader.
*/
const getOperationNames = (module) => {
const originalSource = module.originalSource();
if (!originalSource) {
return [];
}
const matches = originalSource.source().toString().matchAll(OPERATION_NAME_SOURCE_REGEX);
return Array.from(matches).map((match) => match[1]);
};
const createFileContents = (knownOperations) => {
const sourceData = Array.from(knownOperations.values()).sort((a, b) => a.localeCompare(b));
return yaml.dump(sourceData);
};
/**
* Creates a webpack4 compatible "RawSource"
*
* Inspired from https://sourcegraph.com/github.com/FormidableLabs/webpack-stats-plugin@e050ff8c362d5ddd45c66ade724d4a397ace3e5c/-/blob/lib/stats-writer-plugin.js?L144
*/
const createWebpackRawSource = (source) => {
const buff = Buffer.from(source, 'utf-8');
return {
source() {
return buff;
},
size() {
return buff.length;
},
};
};
const onSucceedModule = ({ module, knownOperations }) => {
if (!isGraphqlModule(module)) {
return;
}
getOperationNames(module).forEach((x) => knownOperations.add(x));
};
const onCompilerEmit = ({ compilation, knownOperations, filename }) => {
const contents = createFileContents(knownOperations);
const source = createWebpackRawSource(contents);
const asset = compilation.getAsset(filename);
if (asset) {
compilation.updateAsset(filename, source);
} else {
compilation.emitAsset(filename, source);
}
};
/**
* Webpack plugin that outputs a file containing known graphql operations.
*
* A lot of the mechanices was expired from [this example][1].
*
* [1]: https://sourcegraph.com/github.com/FormidableLabs/webpack-stats-plugin@e050ff8c362d5ddd45c66ade724d4a397ace3e5c/-/blob/lib/stats-writer-plugin.js?L136
*/
class GraphqlKnownOperationsPlugin {
constructor({ filename }) {
this._filename = filename;
}
apply(compiler) {
const knownOperations = new Set();
compiler.hooks.emit.tap(PLUGIN_NAME, (compilation) => {
onCompilerEmit({
compilation,
knownOperations,
filename: this._filename,
});
});
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
compilation.hooks.succeedModule.tap(PLUGIN_NAME, (module) => {
onSucceedModule({
module,
knownOperations,
});
});
});
}
}
module.exports = GraphqlKnownOperationsPlugin;

View file

@ -24,6 +24,7 @@ const IS_JH = require('./helpers/is_jh_env');
const vendorDllHash = require('./helpers/vendor_dll_hash');
const MonacoWebpackPlugin = require('./plugins/monaco_webpack');
const GraphqlKnownOperationsPlugin = require('./plugins/graphql_known_operations_plugin');
const ROOT_PATH = path.resolve(__dirname, '..');
const SUPPORTED_BROWSERS = fs.readFileSync(path.join(ROOT_PATH, '.browserslistrc'), 'utf-8');
@ -456,6 +457,8 @@ module.exports = {
globalAPI: true,
}),
new GraphqlKnownOperationsPlugin({ filename: 'graphql_known_operations.yml' }),
// fix legacy jQuery plugins which depend on globals
new webpack.ProvidePlugin({
$: 'jquery',

View file

@ -1298,6 +1298,7 @@ docker build:
- `rules: changes` works the same way as [`only: changes` and `except: changes`](#onlychanges--exceptchanges).
- You can use `when: never` to implement a rule similar to [`except:changes`](#onlychanges--exceptchanges).
- `changes` resolves to `true` if any of the matching files are changed (an `OR` operation).
#### `rules:exists`
@ -1330,6 +1331,7 @@ job:
file paths. After the 10,000th check, rules with patterned globs always match.
In other words, the `exists` rule always assumes a match in projects with more
than 10,000 files.
- `exists` resolves to `true` if any of the listed files are found (an `OR` operation).
#### `rules:allow_failure`
@ -1567,7 +1569,7 @@ docker build:
**Additional details**:
- If any of the matching files are changed (an `OR` operation), `changes` resolves to `true`.
- `changes` resolves to `true` if any of the matching files are changed (an `OR` operation).
- If you use refs other than `branches`, `external_pull_requests`, or `merge_requests`,
`changes` can't determine if a given file is new or old and always returns `true`.
- If you use `only: changes` with other refs, jobs ignore the changes and always run.

View file

@ -166,6 +166,13 @@ Our current RSpec tests parallelization setup is as follows:
After that, the next pipeline uses the up-to-date `knapsack/report-master.json` file.
### Flaky tests
Tests that are [known to be flaky](testing_guide/flaky_tests.md#automatic-retries-and-flaky-tests-detection) are:
- skipped if the `$SKIP_FLAKY_TESTS_AUTOMATICALLY` variable is set to `true` (`false` by default)
- run if `$SKIP_FLAKY_TESTS_AUTOMATICALLY` variable is not set to `true` or if the `~"pipeline:run-flaky-tests"` label is set on the MR
### Monitoring
The GitLab test suite is [monitored](performance.md#rspec-profiling) for the `main` branch, and any branch

View file

@ -85,6 +85,7 @@ the Agent in subsequent steps.
In GitLab:
1. Ensure that [GitLab CI/CD is enabled in your project](../../../../ci/enable_or_disable_ci.md#enable-cicd-in-a-project).
1. From your project's sidebar, select **Infrastructure > Kubernetes clusters**.
1. Select the **GitLab Agent managed clusters** tab.
1. Select **Integrate with the GitLab Agent**.

View file

@ -29,13 +29,12 @@ Learn more about how GitLab can help you run [Infrastructure as Code](iac/index.
## Integrated Kubernetes management
GitLab has special integrations with Kubernetes to help you deploy, manage and troubleshoot
third-party or custom applications in Kubernetes clusters. Auto DevOps provides a full
DevSecOps pipeline by default targeted at Kubernetes based deployments. To support
all the GitLab features, GitLab offers a cluster management project for easy onboarding.
The deploy boards provide quick insights into your cluster, including pod logs tailing.
The GitLab integration with Kubernetes helps you to install, configure, manage, deploy, and troubleshoot
cluster applications. With the GitLab Kubernetes Agent, you can connect clusters behind a firewall,
have real-time access to API endpoints, perform pull-beased or push-based deployments for production
and non-production environments, and much more.
Learn more about the [GitLab integration with Kubernetes](clusters/index.md).
Learn more about the [GitLab Kubernetes Agent](../clusters/agent/index.md).
## Runbooks in GitLab

View file

@ -0,0 +1,40 @@
# frozen_string_literal: true
module Gitlab
module Graphql
class KnownOperations
Operation = Struct.new(:name) do
def to_caller_id
"graphql:#{name}"
end
end
ANONYMOUS = Operation.new("anonymous").freeze
UNKNOWN = Operation.new("unknown").freeze
def self.default
@default ||= self.new(Gitlab::Webpack::GraphqlKnownOperations.load)
end
def initialize(operation_names)
@operation_hash = operation_names
.map { |name| Operation.new(name).freeze }
.concat([ANONYMOUS, UNKNOWN])
.index_by(&:name)
end
# Returns the known operation from the given ::GraphQL::Query object
def from_query(query)
operation_name = query.selected_operation_name
return ANONYMOUS unless operation_name
@operation_hash[operation_name] || UNKNOWN
end
def operations
@operation_hash.values
end
end
end
end

View file

@ -6,11 +6,13 @@ module Gitlab
module SidekiqConfig
FOSS_QUEUE_CONFIG_PATH = 'app/workers/all_queues.yml'
EE_QUEUE_CONFIG_PATH = 'ee/app/workers/all_queues.yml'
JH_QUEUE_CONFIG_PATH = 'jh/app/workers/all_queues.yml'
SIDEKIQ_QUEUES_PATH = 'config/sidekiq_queues.yml'
QUEUE_CONFIG_PATHS = [
FOSS_QUEUE_CONFIG_PATH,
(EE_QUEUE_CONFIG_PATH if Gitlab.ee?)
(EE_QUEUE_CONFIG_PATH if Gitlab.ee?),
(JH_QUEUE_CONFIG_PATH if Gitlab.jh?)
].compact.freeze
# This maps workers not in our application code to queues. We need
@ -33,7 +35,7 @@ module Gitlab
weight: 2,
tags: []
)
}.transform_values { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: false) }.freeze
}.transform_values { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: false, jh: false) }.freeze
class << self
include Gitlab::SidekiqConfig::CliMethods
@ -58,10 +60,14 @@ module Gitlab
@workers ||= begin
result = []
result.concat(DEFAULT_WORKERS.values)
result.concat(find_workers(Rails.root.join('app', 'workers'), ee: false))
result.concat(find_workers(Rails.root.join('app', 'workers'), ee: false, jh: false))
if Gitlab.ee?
result.concat(find_workers(Rails.root.join('ee', 'app', 'workers'), ee: true))
result.concat(find_workers(Rails.root.join('ee', 'app', 'workers'), ee: true, jh: false))
end
if Gitlab.jh?
result.concat(find_workers(Rails.root.join('jh', 'app', 'workers'), ee: false, jh: true))
end
result
@ -69,16 +75,26 @@ module Gitlab
end
def workers_for_all_queues_yml
workers.partition(&:ee?).reverse.map(&:sort)
workers.each_with_object([[], [], []]) do |worker, array|
if worker.jh?
array[2].push(worker)
elsif worker.ee?
array[1].push(worker)
else
array[0].push(worker)
end
end.map(&:sort)
end
# YAML.load_file is OK here as we control the file contents
def all_queues_yml_outdated?
foss_workers, ee_workers = workers_for_all_queues_yml
foss_workers, ee_workers, jh_workers = workers_for_all_queues_yml
return true if foss_workers != YAML.load_file(FOSS_QUEUE_CONFIG_PATH)
Gitlab.ee? && ee_workers != YAML.load_file(EE_QUEUE_CONFIG_PATH)
return true if Gitlab.ee? && ee_workers != YAML.load_file(EE_QUEUE_CONFIG_PATH)
Gitlab.jh? && File.exist?(JH_QUEUE_CONFIG_PATH) && jh_workers != YAML.load_file(JH_QUEUE_CONFIG_PATH)
end
def queues_for_sidekiq_queues_yml
@ -120,14 +136,14 @@ module Gitlab
private
def find_workers(root, ee:)
def find_workers(root, ee:, jh:)
concerns = root.join('concerns').to_s
Dir[root.join('**', '*.rb')]
.reject { |path| path.start_with?(concerns) }
.map { |path| worker_from_path(path, root) }
.select { |worker| worker < Sidekiq::Worker }
.map { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: ee) }
.map { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: ee, jh: jh) }
end
def worker_from_path(path, root)

View file

@ -18,6 +18,7 @@ module Gitlab
QUEUE_CONFIG_PATHS = begin
result = %w[app/workers/all_queues.yml]
result << 'ee/app/workers/all_queues.yml' if Gitlab.ee?
result << 'jh/app/workers/all_queues.yml' if Gitlab.jh?
result
end.freeze

View file

@ -13,15 +13,20 @@ module Gitlab
:worker_has_external_dependencies?,
to: :klass
def initialize(klass, ee:)
def initialize(klass, ee:, jh: false)
@klass = klass
@ee = ee
@jh = jh
end
def ee?
@ee
end
def jh?
@jh
end
def ==(other)
to_yaml == case other
when self.class

View file

@ -0,0 +1,65 @@
# frozen_string_literal: true
require 'net/http'
require 'uri'
module Gitlab
module Webpack
class FileLoader
class BaseError < StandardError
attr_reader :original_error, :uri
def initialize(uri, orig)
super orig.message
@uri = uri.to_s
@original_error = orig
end
end
StaticLoadError = Class.new(BaseError)
DevServerLoadError = Class.new(BaseError)
DevServerSSLError = Class.new(BaseError)
def self.load(path)
if Gitlab.config.webpack.dev_server.enabled
self.load_from_dev_server(path)
else
self.load_from_static(path)
end
end
def self.load_from_dev_server(path)
host = Gitlab.config.webpack.dev_server.host
port = Gitlab.config.webpack.dev_server.port
scheme = Gitlab.config.webpack.dev_server.https ? 'https' : 'http'
uri = Addressable::URI.new(scheme: scheme, host: host, port: port, path: self.dev_server_path(path))
# localhost could be blocked via Gitlab::HTTP
response = HTTParty.get(uri.to_s, verify: false) # rubocop:disable Gitlab/HTTParty
return response.body if response.code == 200
raise "HTTP error #{response.code}"
rescue OpenSSL::SSL::SSLError, EOFError => e
raise DevServerSSLError.new(uri, e)
rescue StandardError => e
raise DevServerLoadError.new(uri, e)
end
def self.load_from_static(path)
file_uri = ::Rails.root.join(
Gitlab.config.webpack.output_dir,
path
)
File.read(file_uri)
rescue StandardError => e
raise StaticLoadError.new(file_uri, e)
end
def self.dev_server_path(path)
"/#{Gitlab.config.webpack.public_path}/#{path}"
end
end
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Gitlab
module Webpack
class GraphqlKnownOperations
class << self
include Gitlab::Utils::StrongMemoize
def clear_memoization!
clear_memoization(:graphql_known_operations)
end
def load
strong_memoize(:graphql_known_operations) do
data = ::Gitlab::Webpack::FileLoader.load("graphql_known_operations.yml")
YAML.safe_load(data)
rescue StandardError
[]
end
end
end
end
end
end

View file

@ -1,8 +1,5 @@
# frozen_string_literal: true
require 'net/http'
require 'uri'
module Gitlab
module Webpack
class Manifest
@ -78,49 +75,16 @@ module Gitlab
end
def load_manifest
data = if Gitlab.config.webpack.dev_server.enabled
load_dev_server_manifest
else
load_static_manifest
end
data = Gitlab::Webpack::FileLoader.load(Gitlab.config.webpack.manifest_filename)
Gitlab::Json.parse(data)
end
def load_dev_server_manifest
host = Gitlab.config.webpack.dev_server.host
port = Gitlab.config.webpack.dev_server.port
scheme = Gitlab.config.webpack.dev_server.https ? 'https' : 'http'
uri = Addressable::URI.new(scheme: scheme, host: host, port: port, path: dev_server_path)
# localhost could be blocked via Gitlab::HTTP
response = HTTParty.get(uri.to_s, verify: false) # rubocop:disable Gitlab/HTTParty
return response.body if response.code == 200
raise "HTTP error #{response.code}"
rescue OpenSSL::SSL::SSLError, EOFError => e
rescue Gitlab::Webpack::FileLoader::StaticLoadError => e
raise ManifestLoadError.new("Could not load compiled manifest from #{e.uri}.\n\nHave you run `rake gitlab:assets:compile`?", e.original_error)
rescue Gitlab::Webpack::FileLoader::DevServerSSLError => e
ssl_status = Gitlab.config.webpack.dev_server.https ? ' over SSL' : ''
raise ManifestLoadError.new("Could not connect to webpack-dev-server at #{uri}#{ssl_status}.\n\nIs SSL enabled? Check that settings in `gitlab.yml` and webpack-dev-server match.", e)
rescue StandardError => e
raise ManifestLoadError.new("Could not load manifest from webpack-dev-server at #{uri}.\n\nIs webpack-dev-server running? Try running `gdk status webpack` or `gdk tail webpack`.", e)
end
def load_static_manifest
File.read(static_manifest_path)
rescue StandardError => e
raise ManifestLoadError.new("Could not load compiled manifest from #{static_manifest_path}.\n\nHave you run `rake gitlab:assets:compile`?", e)
end
def static_manifest_path
::Rails.root.join(
Gitlab.config.webpack.output_dir,
Gitlab.config.webpack.manifest_filename
)
end
def dev_server_path
"/#{Gitlab.config.webpack.public_path}/#{Gitlab.config.webpack.manifest_filename}"
raise ManifestLoadError.new("Could not connect to webpack-dev-server at #{e.uri}#{ssl_status}.\n\nIs SSL enabled? Check that settings in `gitlab.yml` and webpack-dev-server match.", e.original_error)
rescue Gitlab::Webpack::FileLoader::DevServerLoadError => e
raise ManifestLoadError.new("Could not load manifest from webpack-dev-server at #{e.uri}.\n\nIs webpack-dev-server running? Try running `gdk status webpack` or `gdk tail webpack`.", e.original_error)
end
end
end

View file

@ -100,7 +100,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Google Cloud'),
link: project_google_cloud_index_path(context.project),
active_routes: {},
active_routes: { controller: :google_cloud },
item_id: :google_cloud
)
end

View file

@ -36,13 +36,17 @@ namespace :gitlab do
# Do not edit it manually!
BANNER
foss_workers, ee_workers = Gitlab::SidekiqConfig.workers_for_all_queues_yml
foss_workers, ee_workers, jh_workers = Gitlab::SidekiqConfig.workers_for_all_queues_yml
write_yaml(Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH, banner, foss_workers)
if Gitlab.ee?
write_yaml(Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH, banner, ee_workers)
end
if Gitlab.jh?
write_yaml(Gitlab::SidekiqConfig::JH_QUEUE_CONFIG_PATH, banner, jh_workers)
end
end
desc 'GitLab | Sidekiq | Validate that all_queues.yml matches worker definitions'
@ -57,6 +61,7 @@ namespace :gitlab do
- #{Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH}
- #{Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH}
#{"- " + Gitlab::SidekiqConfig::JH_QUEUE_CONFIG_PATH if Gitlab.jh?}
MSG
end

View file

@ -4037,6 +4037,9 @@ msgid_plural "ApplicationSettings|Approve %d users"
msgstr[0] ""
msgstr[1] ""
msgid "ApplicationSettings|Approve users"
msgstr ""
msgid "ApplicationSettings|Approve users in the pending approval status?"
msgstr ""
@ -4045,6 +4048,9 @@ msgid_plural "ApplicationSettings|By making this change, you will automatically
msgstr[0] ""
msgstr[1] ""
msgid "ApplicationSettings|By making this change, you will automatically approve all users in pending approval status."
msgstr ""
msgid "ApplicationSettings|Denied domains for sign-ups"
msgstr ""

View file

@ -227,10 +227,10 @@ GEM
watir (6.19.1)
regexp_parser (>= 1.2, < 3)
selenium-webdriver (>= 3.142.7)
webdrivers (4.7.0)
webdrivers (5.0.0)
nokogiri (~> 1.6)
rubyzip (>= 1.3.0)
selenium-webdriver (> 3.141, < 5.0)
selenium-webdriver (~> 4.0)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.4.2)
@ -263,10 +263,10 @@ DEPENDENCIES
rspec-retry (~> 0.6.1)
rspec_junit_formatter (~> 0.4.1)
ruby-debug-ide (~> 0.7.0)
selenium-webdriver (~> 4.0.0.rc1)
selenium-webdriver (~> 4.0)
timecop (~> 0.9.1)
webdrivers (~> 4.6)
webdrivers (~> 5.0)
zeitwerk (~> 2.4)
BUNDLED WITH
2.2.29
2.2.30

View file

@ -166,6 +166,7 @@ function rspec_paralellized_job() {
export SUITE_FLAKY_RSPEC_REPORT_PATH="${FLAKY_RSPEC_SUITE_REPORT_PATH}"
export FLAKY_RSPEC_REPORT_PATH="rspec_flaky/all_${report_name}_report.json"
export NEW_FLAKY_RSPEC_REPORT_PATH="rspec_flaky/new_${report_name}_report.json"
export SKIPPED_FLAKY_TESTS_REPORT_PATH="rspec_flaky/skipped_flaky_tests_${report_name}_report.txt"
if [[ ! -f $FLAKY_RSPEC_REPORT_PATH ]]; then
echo "{}" > "${FLAKY_RSPEC_REPORT_PATH}"

View file

@ -0,0 +1,29 @@
# frozen_string_literal: true
require 'spec_helper'
# We need to distinguish between known and unknown GraphQL operations. This spec
# tests that we set up Gitlab::Graphql::KnownOperations.default which requires
# integration of FE queries, webpack plugin, and BE.
RSpec.describe 'Graphql known operations', :js do
around do |example|
# Let's make sure we aren't receiving or leaving behind any side-effects
# https://gitlab.com/gitlab-org/gitlab/-/jobs/1743294100
::Gitlab::Graphql::KnownOperations.instance_variable_set(:@default, nil)
::Gitlab::Webpack::GraphqlKnownOperations.clear_memoization!
example.run
::Gitlab::Graphql::KnownOperations.instance_variable_set(:@default, nil)
::Gitlab::Webpack::GraphqlKnownOperations.clear_memoization!
end
it 'collects known Graphql operations from the code', :aggregate_failures do
# Check that we include some arbitrary operation name we expect
known_operations = Gitlab::Graphql::KnownOperations.default.operations.map(&:name)
expect(known_operations).to include("searchProjects")
expect(known_operations.length).to be > 20
expect(known_operations).to all( match(%r{^[a-z]+}i) )
end
end

View file

@ -4,25 +4,29 @@ require 'spec_helper'
RSpec.describe 'New/edit issue', :js do
include ActionView::Helpers::JavaScriptHelper
include FormHelper
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user)}
let_it_be(:user2) { create(:user)}
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
let_it_be(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
let(:current_user) { user }
before_all do
project.add_maintainer(user)
project.add_maintainer(user2)
end
before do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
project.add_maintainer(user)
project.add_maintainer(user2)
sign_in(user)
sign_in(current_user)
end
context 'new issue' do
describe 'new issue' do
before do
visit new_project_issue_path(project)
end
@ -235,29 +239,42 @@ RSpec.describe 'New/edit issue', :js do
end
describe 'displays issue type options in the dropdown' do
shared_examples 'type option is visible' do |label:, identifier:|
it "shows #{identifier} option", :aggregate_failures do
page.within('[data-testid="issue-type-select-dropdown"]') do
expect(page).to have_selector(%([data-testid="issue-type-#{identifier}-icon"]))
expect(page).to have_content(label)
end
end
end
before do
page.within('.issue-form') do
click_button 'Issue'
end
end
it 'correctly displays the Issue type option with an icon', :aggregate_failures do
page.within('[data-testid="issue-type-select-dropdown"]') do
expect(page).to have_selector('[data-testid="issue-type-issue-icon"]')
expect(page).to have_content('Issue')
end
end
it_behaves_like 'type option is visible', label: 'Issue', identifier: :issue
it_behaves_like 'type option is visible', label: 'Incident', identifier: :incident
it 'correctly displays the Incident type option with an icon', :aggregate_failures do
page.within('[data-testid="issue-type-select-dropdown"]') do
expect(page).to have_selector('[data-testid="issue-type-incident-icon"]')
expect(page).to have_content('Incident')
context 'when user is guest' do
let_it_be(:guest) { create(:user) }
let(:current_user) { guest }
before_all do
project.add_guest(guest)
end
it_behaves_like 'type option is visible', label: 'Issue', identifier: :issue
it_behaves_like 'type option is visible', label: 'Incident', identifier: :incident
end
end
describe 'milestone' do
let!(:milestone) { create(:milestone, title: '">&lt;img src=x onerror=alert(document.domain)&gt;', project: project) }
let!(:milestone) do
create(:milestone, title: '">&lt;img src=x onerror=alert(document.domain)&gt;', project: project)
end
it 'escapes milestone' do
click_button 'Milestone'
@ -274,7 +291,7 @@ RSpec.describe 'New/edit issue', :js do
end
end
context 'edit issue' do
describe 'edit issue' do
before do
visit edit_project_issue_path(project, issue)
end
@ -329,7 +346,7 @@ RSpec.describe 'New/edit issue', :js do
end
end
context 'inline edit' do
describe 'inline edit' do
before do
visit project_issue_path(project, issue)
end

View file

@ -1,7 +1,7 @@
import { GlBanner } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DevopsScoreCallout from '~/analytics/devops_report/components/devops_score_callout.vue';
import { INTRO_COOKIE_KEY } from '~/analytics/devops_report/constants';
import DevopsScoreCallout from '~/analytics/devops_reports/components/devops_score_callout.vue';
import { INTRO_COOKIE_KEY } from '~/analytics/devops_reports/constants';
import * as utils from '~/lib/utils/common_utils';
import { devopsReportDocsPath, devopsScoreIntroImagePath } from '../mock_data';

View file

@ -2,8 +2,8 @@ import { GlTable, GlBadge, GlEmptyState } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import DevopsScore from '~/analytics/devops_report/components/devops_score.vue';
import DevopsScoreCallout from '~/analytics/devops_report/components/devops_score_callout.vue';
import DevopsScore from '~/analytics/devops_reports/components/devops_score.vue';
import DevopsScoreCallout from '~/analytics/devops_reports/components/devops_score_callout.vue';
import { devopsScoreMetricsData, noDataImagePath, devopsScoreTableHeaders } from '../mock_data';
describe('DevopsScore', () => {

View file

@ -1,9 +1,9 @@
import { GlEmptyState, GlSprintf } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ServicePingDisabled from '~/analytics/devops_report/components/service_ping_disabled.vue';
import ServicePingDisabled from '~/analytics/devops_reports/components/service_ping_disabled.vue';
describe('~/analytics/devops_report/components/service_ping_disabled.vue', () => {
describe('~/analytics/devops_reports/components/service_ping_disabled.vue', () => {
let wrapper;
afterEach(() => {

View file

@ -1,6 +1,7 @@
import { GlIcon } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
import { createStore } from '~/mr_notes/stores';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
@ -19,6 +20,9 @@ describe('DiffDiscussions', () => {
store = createStore();
wrapper = mount(localVue.extend(DiffDiscussions), {
store,
provide: {
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
},
propsData: {
discussions: getDiscussionsMockData(),
...props,

View file

@ -0,0 +1,133 @@
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
describe('Diff Discussions Utils', () => {
describe('discussionIntersectionObserverHandlerFactory', () => {
it('creates a handler function', () => {
expect(discussionIntersectionObserverHandlerFactory()).toBeInstanceOf(Function);
});
describe('intersection observer handler', () => {
const functions = {
setCurrentDiscussionId: jest.fn(),
getPreviousUnresolvedDiscussionId: jest.fn().mockImplementation((id) => {
return Number(id) - 1;
}),
};
const defaultProcessableWrapper = {
entry: {
time: 0,
isIntersecting: true,
rootBounds: {
bottom: 0,
},
boundingClientRect: {
top: 0,
},
},
currentDiscussion: {
id: 1,
},
isFirstUnresolved: false,
isDiffsPage: true,
};
let handler;
let getMock;
let setMock;
beforeEach(() => {
functions.setCurrentDiscussionId.mockClear();
functions.getPreviousUnresolvedDiscussionId.mockClear();
defaultProcessableWrapper.functions = functions;
setMock = functions.setCurrentDiscussionId.mock;
getMock = functions.getPreviousUnresolvedDiscussionId.mock;
handler = discussionIntersectionObserverHandlerFactory();
});
it('debounces multiple simultaneous requests into one queue', () => {
handler(defaultProcessableWrapper);
handler(defaultProcessableWrapper);
handler(defaultProcessableWrapper);
handler(defaultProcessableWrapper);
expect(setTimeout).toHaveBeenCalledTimes(4);
expect(clearTimeout).toHaveBeenCalledTimes(3);
// By only advancing to one timer, we ensure it's all being batched into one queue
jest.advanceTimersToNextTimer();
expect(functions.setCurrentDiscussionId).toHaveBeenCalledTimes(4);
});
it('properly processes, sorts and executes the correct actions for a set of observed intersections', () => {
handler(defaultProcessableWrapper);
handler({
// This observation is here to be filtered out because it's a scrollDown
...defaultProcessableWrapper,
entry: {
...defaultProcessableWrapper.entry,
isIntersecting: false,
boundingClientRect: { top: 10 },
rootBounds: { bottom: 100 },
},
});
handler({
...defaultProcessableWrapper,
entry: {
...defaultProcessableWrapper.entry,
time: 101,
isIntersecting: false,
rootBounds: { bottom: -100 },
},
currentDiscussion: { id: 20 },
});
handler({
...defaultProcessableWrapper,
entry: {
...defaultProcessableWrapper.entry,
time: 100,
isIntersecting: false,
boundingClientRect: { top: 100 },
},
currentDiscussion: { id: 30 },
isDiffsPage: false,
});
handler({
...defaultProcessableWrapper,
isFirstUnresolved: true,
entry: {
...defaultProcessableWrapper.entry,
time: 100,
isIntersecting: false,
boundingClientRect: { top: 200 },
},
});
jest.advanceTimersToNextTimer();
expect(setMock.calls.length).toBe(4);
expect(setMock.calls[0]).toEqual([1]);
expect(setMock.calls[1]).toEqual([29]);
expect(setMock.calls[2]).toEqual([null]);
expect(setMock.calls[3]).toEqual([19]);
expect(getMock.calls.length).toBe(2);
expect(getMock.calls[0]).toEqual([30, false]);
expect(getMock.calls[1]).toEqual([20, true]);
[
setMock.invocationCallOrder[0],
getMock.invocationCallOrder[0],
setMock.invocationCallOrder[1],
setMock.invocationCallOrder[2],
getMock.invocationCallOrder[1],
setMock.invocationCallOrder[3],
].forEach((order, idx, list) => {
// Compare each invocation sequence to the one before it (except the first one)
expect(list[idx - 1] || -1).toBeLessThan(order);
});
});
});
});
});

View file

@ -1,6 +1,7 @@
import { getByRole } from '@testing-library/dom';
import { shallowMount, mount } from '@vue/test-utils';
import '~/behaviors/markdown/render_gfm';
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import NoteableNote from '~/notes/components/noteable_note.vue';
import { SYSTEM_NOTE } from '~/notes/constants';
@ -26,6 +27,9 @@ describe('DiscussionNotes', () => {
const createComponent = (props, mountingMethod = shallowMount) => {
wrapper = mountingMethod(DiscussionNotes, {
store,
provide: {
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
},
propsData: {
discussion: discussionMock,
isExpanded: false,

View file

@ -3,6 +3,7 @@ import { nextTick } from 'vue';
import discussionWithTwoUnresolvedNotes from 'test_fixtures/merge_requests/resolved_diff_discussion.json';
import { trimText } from 'helpers/text_helper';
import mockDiffFile from 'jest/diffs/mock_data/diff_file';
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
@ -31,6 +32,9 @@ describe('noteable_discussion component', () => {
wrapper = mount(NoteableDiscussion, {
store,
provide: {
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
},
propsData: { discussion: discussionMock },
});
});
@ -167,6 +171,9 @@ describe('noteable_discussion component', () => {
wrapper = mount(NoteableDiscussion, {
store,
provide: {
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
},
propsData: { discussion: discussionMock },
});
});
@ -185,6 +192,9 @@ describe('noteable_discussion component', () => {
wrapper = mount(NoteableDiscussion, {
store,
provide: {
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
},
propsData: { discussion: discussionMock },
});
});

View file

@ -9,6 +9,7 @@ import DraftNote from '~/batch_comments/components/draft_note.vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import axios from '~/lib/utils/axios_utils';
import * as urlUtility from '~/lib/utils/url_utility';
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
import CommentForm from '~/notes/components/comment_form.vue';
import NotesApp from '~/notes/components/notes_app.vue';
import * as constants from '~/notes/constants';
@ -78,6 +79,9 @@ describe('note_app', () => {
</div>`,
},
{
provide: {
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
},
propsData,
store,
},

View file

@ -0,0 +1,72 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'rspec-parameterized'
require "support/graphql/fake_query_type"
RSpec.describe Gitlab::Graphql::KnownOperations do
using RSpec::Parameterized::TableSyntax
# Include duplicated operation names to test that we are unique-ifying them
let(:fake_operations) { %w(foo foo bar bar) }
let(:fake_schema) do
Class.new(GraphQL::Schema) do
query Graphql::FakeQueryType
end
end
subject { described_class.new(fake_operations) }
describe "#from_query" do
where(:query_string, :expected) do
"query { helloWorld }" | described_class::ANONYMOUS
"query fuzzyyy { helloWorld }" | described_class::UNKNOWN
"query foo { helloWorld }" | described_class::Operation.new("foo")
end
with_them do
it "returns known operation name from GraphQL Query" do
query = ::GraphQL::Query.new(fake_schema, query_string)
expect(subject.from_query(query)).to eq(expected)
end
end
end
describe "#operations" do
it "returns array of known operations" do
expect(subject.operations.map(&:name)).to match_array(%w(anonymous unknown foo bar))
end
end
describe "Operation#to_caller_id" do
where(:query_string, :expected) do
"query { helloWorld }" | "graphql:#{described_class::ANONYMOUS.name}"
"query foo { helloWorld }" | "graphql:foo"
end
with_them do
it "formats operation name for caller_id metric property" do
query = ::GraphQL::Query.new(fake_schema, query_string)
expect(subject.from_query(query).to_caller_id).to eq(expected)
end
end
end
describe ".default" do
it "returns a memoization of values from webpack", :aggregate_failures do
# .default could have been referenced in another spec, so we need to clean it up here
described_class.instance_variable_set(:@default, nil)
expect(Gitlab::Webpack::GraphqlKnownOperations).to receive(:load).once.and_return(fake_operations)
2.times { described_class.default }
# Uses reference equality to verify memoization
expect(described_class.default).to equal(described_class.default)
expect(described_class.default).to be_a(described_class)
expect(described_class.default.operations.map(&:name)).to include(*fake_operations)
end
end
end

View file

@ -11,12 +11,12 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
end
def stub_exists(exists: true)
['app/workers/all_queues.yml', 'ee/app/workers/all_queues.yml'].each do |path|
['app/workers/all_queues.yml', 'ee/app/workers/all_queues.yml', 'jh/app/workers/all_queues.yml'].each do |path|
allow(File).to receive(:exist?).with(expand_path(path)).and_return(exists)
end
end
def stub_contents(foss_queues, ee_queues)
def stub_contents(foss_queues, ee_queues, jh_queues)
allow(YAML).to receive(:load_file)
.with(expand_path('app/workers/all_queues.yml'))
.and_return(foss_queues)
@ -24,6 +24,10 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
allow(YAML).to receive(:load_file)
.with(expand_path('ee/app/workers/all_queues.yml'))
.and_return(ee_queues)
allow(YAML).to receive(:load_file)
.with(expand_path('jh/app/workers/all_queues.yml'))
.and_return(jh_queues)
end
before do
@ -45,8 +49,9 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
end
it 'flattens and joins the contents' do
expected_queues = %w[queue_a queue_b]
expected_queues = expected_queues.first(1) unless Gitlab.ee?
expected_queues = %w[queue_a]
expected_queues << 'queue_b' if Gitlab.ee?
expected_queues << 'queue_c' if Gitlab.jh?
expect(described_class.worker_queues(dummy_root))
.to match_array(expected_queues)
@ -55,7 +60,7 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
context 'when the file contains an array of hashes' do
before do
stub_contents([{ name: 'queue_a' }], [{ name: 'queue_b' }])
stub_contents([{ name: 'queue_a' }], [{ name: 'queue_b' }], [{ name: 'queue_c' }])
end
include_examples 'valid file contents'

View file

@ -18,19 +18,26 @@ RSpec.describe Gitlab::SidekiqConfig::Worker do
get_tags: attributes[:tags]
)
described_class.new(inner_worker, ee: false)
described_class.new(inner_worker, ee: false, jh: false)
end
describe '#ee?' do
it 'returns the EE status set on creation' do
expect(described_class.new(double, ee: true)).to be_ee
expect(described_class.new(double, ee: false)).not_to be_ee
expect(described_class.new(double, ee: true, jh: false)).to be_ee
expect(described_class.new(double, ee: false, jh: false)).not_to be_ee
end
end
describe '#jh?' do
it 'returns the JH status set on creation' do
expect(described_class.new(double, ee: false, jh: true)).to be_jh
expect(described_class.new(double, ee: false, jh: false)).not_to be_jh
end
end
describe '#==' do
def worker_with_yaml(yaml)
described_class.new(double, ee: false).tap do |worker|
described_class.new(double, ee: false, jh: false).tap do |worker|
allow(worker).to receive(:to_yaml).and_return(yaml)
end
end
@ -57,7 +64,7 @@ RSpec.describe Gitlab::SidekiqConfig::Worker do
expect(worker).to receive(meth)
described_class.new(worker, ee: false).send(meth)
described_class.new(worker, ee: false, jh: false).send(meth)
end
end
end

View file

@ -0,0 +1,79 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'support/helpers/file_read_helpers'
require 'support/webmock'
RSpec.describe Gitlab::Webpack::FileLoader do
include FileReadHelpers
include WebMock::API
let(:error_file_path) { "error.yml" }
let(:file_path) { "my_test_file.yml" }
let(:file_contents) do
<<-EOF
- hello
- world
- test
EOF
end
before do
allow(Gitlab.config.webpack.dev_server).to receive_messages(host: 'hostname', port: 2000, https: false)
allow(Gitlab.config.webpack).to receive(:public_path).and_return('public_path')
allow(Gitlab.config.webpack).to receive(:output_dir).and_return('webpack_output')
end
context "with dev server enabled" do
before do
allow(Gitlab.config.webpack.dev_server).to receive(:enabled).and_return(true)
stub_request(:get, "http://hostname:2000/public_path/not_found").to_return(status: 404)
stub_request(:get, "http://hostname:2000/public_path/#{file_path}").to_return(body: file_contents, status: 200)
stub_request(:get, "http://hostname:2000/public_path/#{error_file_path}").to_raise(StandardError)
end
it "returns content when respondes succesfully" do
expect(Gitlab::Webpack::FileLoader.load(file_path)).to be(file_contents)
end
it "raises error when 404" do
expect { Gitlab::Webpack::FileLoader.load("not_found") }.to raise_error("HTTP error 404")
end
it "raises error when errors out" do
expect { Gitlab::Webpack::FileLoader.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::DevServerLoadError)
end
end
context "with dev server enabled and https" do
before do
allow(Gitlab.config.webpack.dev_server).to receive(:enabled).and_return(true)
allow(Gitlab.config.webpack.dev_server).to receive(:https).and_return(true)
stub_request(:get, "https://hostname:2000/public_path/#{error_file_path}").to_raise(EOFError)
end
it "raises error if catches SSLError" do
expect { Gitlab::Webpack::FileLoader.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::DevServerSSLError)
end
end
context "with dev server disabled" do
before do
allow(Gitlab.config.webpack.dev_server).to receive(:enabled).and_return(false)
stub_file_read(::Rails.root.join("webpack_output/#{file_path}"), content: file_contents)
stub_file_read(::Rails.root.join("webpack_output/#{error_file_path}"), error: Errno::ENOENT)
end
describe ".load" do
it "returns file content from file path" do
expect(Gitlab::Webpack::FileLoader.load(file_path)).to be(file_contents)
end
it "throws error if file cannot be read" do
expect { Gitlab::Webpack::FileLoader.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::StaticLoadError)
end
end
end
end

View file

@ -0,0 +1,47 @@
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Gitlab::Webpack::GraphqlKnownOperations do
let(:content) do
<<-EOF
- hello
- world
- test
EOF
end
around do |example|
described_class.clear_memoization!
example.run
described_class.clear_memoization!
end
describe ".load" do
context "when file loader returns" do
before do
allow(::Gitlab::Webpack::FileLoader).to receive(:load).with("graphql_known_operations.yml").and_return(content)
end
it "returns memoized value" do
expect(::Gitlab::Webpack::FileLoader).to receive(:load).once
2.times { ::Gitlab::Webpack::GraphqlKnownOperations.load }
expect(::Gitlab::Webpack::GraphqlKnownOperations.load).to eq(%w(hello world test))
end
end
context "when file loader errors" do
before do
allow(::Gitlab::Webpack::FileLoader).to receive(:load).and_raise(StandardError.new("test"))
end
it "returns empty array" do
expect(::Gitlab::Webpack::GraphqlKnownOperations.load).to eq([])
end
end
end
end

View file

@ -51,6 +51,16 @@ RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do
it 'menu link points to Terraform page' do
expect(subject.link).to eq find_menu_item(:terraform).link
end
context 'when Terraform menu is not visible' do
before do
subject.renderable_items.delete(find_menu_item(:terraform))
end
it 'menu link points to Google Cloud page' do
expect(subject.link).to eq find_menu_item(:google_cloud).link
end
end
end
end
@ -89,5 +99,11 @@ RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do
it_behaves_like 'access rights checks'
end
describe 'Google Cloud' do
let(:item_id) { :google_cloud }
it_behaves_like 'access rights checks'
end
end
end

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AuthorizedProjectUpdate::ProjectAccessChangedService do
describe '#execute' do
it 'schedules the project IDs' do
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_and_wait)
.with([[1], [2]])
described_class.new([1, 2]).execute
end
it 'permits non-blocking operation' do
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_async)
.with([[1], [2]])
described_class.new([1, 2]).execute(blocking: false)
end
end
end

View file

@ -593,11 +593,16 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
let_it_be_with_reload(:group) { create(:group, :private, parent: old_parent_group) }
let_it_be(:new_group_member) { create(:user) }
let_it_be(:old_group_member) { create(:user) }
let_it_be(:unique_subgroup_member) { create(:user) }
let_it_be(:direct_project_member) { create(:user) }
before do
new_parent_group.add_maintainer(new_group_member)
old_parent_group.add_maintainer(old_group_member)
subgroup1.add_developer(unique_subgroup_member)
nested_project.add_developer(direct_project_member)
group.refresh_members_authorized_projects
subgroup1.refresh_members_authorized_projects
end
it 'removes old project authorizations' do
@ -613,7 +618,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
end
it 'performs authorizations job immediately' do
expect(AuthorizedProjectsWorker).to receive(:bulk_perform_inline)
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_inline)
transfer_service.execute(new_parent_group)
end
@ -630,14 +635,24 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
ProjectAuthorization.where(project_id: nested_project.id, user_id: new_group_member.id).size
}.from(0).to(1)
end
it 'preserves existing project authorizations for direct project members' do
expect { transfer_service.execute(new_parent_group) }.not_to change {
ProjectAuthorization.where(project_id: nested_project.id, user_id: direct_project_member.id).count
}
end
end
context 'for groups with many members' do
before do
11.times do
new_parent_group.add_maintainer(create(:user))
end
context 'for nested groups with unique members' do
it 'preserves existing project authorizations' do
expect { transfer_service.execute(new_parent_group) }.not_to change {
ProjectAuthorization.where(project_id: nested_project.id, user_id: unique_subgroup_member.id).count
}
end
end
context 'for groups with many projects' do
let_it_be(:project_list) { create_list(:project, 11, :repository, :private, namespace: group) }
it 'adds new project authorizations for the user which makes a transfer' do
transfer_service.execute(new_parent_group)
@ -646,9 +661,21 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
expect(ProjectAuthorization.where(project_id: nested_project.id, user_id: user.id).size).to eq(1)
end
it 'adds project authorizations for users in the new hierarchy' do
expect { transfer_service.execute(new_parent_group) }.to change {
ProjectAuthorization.where(project_id: project_list.map { |project| project.id }, user_id: new_group_member.id).size
}.from(0).to(project_list.count)
end
it 'removes project authorizations for users in the old hierarchy' do
expect { transfer_service.execute(new_parent_group) }.to change {
ProjectAuthorization.where(project_id: project_list.map { |project| project.id }, user_id: old_group_member.id).size
}.from(project_list.count).to(0)
end
it 'schedules authorizations job' do
expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async)
.with(array_including(new_parent_group.members_with_parents.pluck(:user_id).map {|id| [id, anything] }))
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_async)
.with(array_including(group.all_projects.ids.map { |id| [id, anything] }))
transfer_service.execute(new_parent_group)
end

View file

@ -107,9 +107,7 @@ RSpec.configure do |config|
warn `curl -s -o log/goroutines.log http://localhost:9236/debug/pprof/goroutine?debug=2`
end
end
end
unless ENV['CI']
else
# Allow running `:focus` examples locally,
# falling back to all tests when there is no `:focus` example.
config.filter_run focus: true

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
return unless ENV['CI']
return unless ENV['SKIP_FLAKY_TESTS_AUTOMATICALLY'] == "true"
return if ENV['CI_MERGE_REQUEST_LABELS'].include?(/pipeline:run-flaky-tests/)
require_relative '../tooling/rspec_flaky/report'
RSpec.configure do |config|
$flaky_test_example_ids = begin # rubocop:disable Style/GlobalVars
raise "$SUITE_FLAKY_RSPEC_REPORT_PATH is empty." if ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'].to_s.empty?
raise "#{ENV['SUITE_FLAKY_RSPEC_REPORT_PATH']} doesn't exist" unless File.exist?(ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'])
RspecFlaky::Report.load(ENV['SUITE_FLAKY_RSPEC_REPORT_PATH']).map { |_, flaky_test_data| flaky_test_data["example_id"] }
rescue => e # rubocop:disable Style/RescueStandardError
puts e
[]
end
$skipped_flaky_tests_report = [] # rubocop:disable Style/GlobalVars
config.around do |example|
# Skip flaky tests automatically
if $flaky_test_example_ids.include?(example.id) # rubocop:disable Style/GlobalVars
puts "Skipping #{example.id} '#{example.full_description}' because it's flaky."
$skipped_flaky_tests_report << example.id # rubocop:disable Style/GlobalVars
else
example.run
end
end
config.after(:suite) do
next unless ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH']
File.write(ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH'], "#{$skipped_flaky_tests_report.join("\n")}\n") # rubocop:disable Style/GlobalVars
end
end

View file

@ -82,8 +82,9 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
nil | 10 | nil
0 | 5 | nil
10 | 0 | 0
10 | 5 | 0.5
3 | 10 | (10 / 3.to_f)
10 | 5 | 50.0
17 | 3 | 17.65
3 | 10 | 333.33
end
with_them do