Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-07-07 12:09:35 +00:00
parent f4a9d976cf
commit 2abeca2d92
56 changed files with 1149 additions and 114 deletions

View File

@ -809,7 +809,6 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/app/assets/javascripts/authentication/ @gitlab-org/manage/authentication-and-authorization
/app/assets/javascripts/ide/components/shared/tokened_input.vue @gitlab-org/manage/authentication-and-authorization
/app/assets/javascripts/invite_members/components/members_token_select.vue @gitlab-org/manage/authentication-and-authorization
/app/assets/javascripts/logs/components/tokens/ @gitlab-org/manage/authentication-and-authorization
/app/assets/javascripts/packages_and_registries/package_registry/components/list/tokens/ @gitlab-org/manage/authentication-and-authorization
/app/assets/javascripts/pages/admin/impersonation_tokens/ @gitlab-org/manage/authentication-and-authorization
/app/assets/javascripts/pages/groups/settings/access_tokens/ @gitlab-org/manage/authentication-and-authorization

View File

@ -5,7 +5,7 @@ review-cleanup:
extends:
- .default-retry
- .review:rules:review-cleanup
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:gitlab-gcloud-helm3.5-kubectl1.17
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/ruby-3.0:gcloud-383-kubectl-1.23-helm-3.5
stage: prepare
environment:
name: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # No separator for SCHEDULE_TYPE so it's compatible as before and looks nice without it

View File

@ -1729,6 +1729,9 @@
rules:
- <<: *if-not-ee
when: never
- <<: *if-dot-com-gitlab-org-merge-request
when: manual
allow_failure: true
- <<: *if-dot-com-gitlab-org-schedule
allow_failure: true

View File

@ -0,0 +1,7 @@
#import "../fragments/user.fragment.graphql"
query currentUser {
currentUser {
...User
}
}

View File

@ -1,3 +1,5 @@
import { initWorkItemsRoot } from '~/work_items/index';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
initWorkItemsRoot();
initInviteMembersModal();

View File

@ -2,7 +2,7 @@ import produce from 'immer';
import VueApollo from 'vue-apollo';
import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql';
import createDefaultClient from '~/lib/graphql';
import { temporaryConfig } from '~/work_items/graphql/provider';
import { temporaryConfig, resolvers as workItemResolvers } from '~/work_items/graphql/provider';
const resolvers = {
Mutation: {
@ -13,6 +13,7 @@ const resolvers = {
});
cache.writeQuery({ query: getIssueStateQuery, data });
},
...workItemResolvers.Mutation,
},
};

View File

@ -1,8 +1,19 @@
<script>
import { GlTokenSelector, GlIcon, GlAvatar, GlLink, GlSkeletonLoader } from '@gitlab/ui';
import {
GlTokenSelector,
GlIcon,
GlAvatar,
GlLink,
GlSkeletonLoader,
GlButton,
GlDropdownItem,
GlDropdownDivider,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import { n__ } from '~/locale';
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
@ -27,7 +38,11 @@ export default {
GlAvatar,
GlLink,
GlSkeletonLoader,
GlButton,
SidebarParticipant,
InviteMembersTrigger,
GlDropdownItem,
GlDropdownDivider,
},
inject: ['fullPath'],
props: {
@ -47,6 +62,7 @@ export default {
localAssignees: this.assignees.map(addClass),
searchKey: '',
searchUsers: [],
currentUser: null,
};
},
apollo: {
@ -70,6 +86,9 @@ export default {
this.$emit('error', i18n.fetchError);
},
},
currentUser: {
query: currentUserQuery,
},
},
computed: {
assigneeListEmpty() {
@ -78,12 +97,24 @@ export default {
containerClass() {
return !this.isEditing ? 'gl-shadow-none! gl-bg-transparent!' : '';
},
isLoading() {
isLoadingUsers() {
return this.$apollo.queries.searchUsers.loading;
},
assigneeText() {
return n__('WorkItem|Assignee', 'WorkItem|Assignees', this.localAssignees.length);
},
dropdownItems() {
if (this.currentUser && this.searchEmpty) {
if (this.searchUsers.some((user) => user.username === this.currentUser.username)) {
return this.moveCurrentUserToStart(this.searchUsers);
}
return [this.currentUser, ...this.searchUsers];
}
return this.searchUsers;
},
searchEmpty() {
return this.searchKey.length === 0;
},
},
watch: {
assignees(newVal) {
@ -99,15 +130,18 @@ export default {
getUserId(id) {
return getIdFromGraphQLId(id);
},
setAssignees(e) {
handleBlur(e) {
if (isTokenSelectorElement(e.relatedTarget) || !this.isEditing) return;
this.isEditing = false;
this.setAssignees(this.localAssignees);
},
setAssignees(assignees) {
this.$apollo.mutate({
mutation: localUpdateWorkItemMutation,
variables: {
input: {
id: this.workItemId,
assignees: this.localAssignees,
assignees,
},
},
});
@ -132,6 +166,15 @@ export default {
setSearchKey(value) {
this.searchKey = value;
},
moveCurrentUserToStart(users = []) {
if (this.currentUser) {
return [this.currentUser, ...users.filter((user) => user.id !== this.currentUser.id)];
}
return users;
},
closeDropdown() {
this.$refs.tokenSelector.closeDropdown();
},
},
};
</script>
@ -147,13 +190,13 @@ export default {
ref="tokenSelector"
v-model="localAssignees"
:container-class="containerClass"
class="gl-flex-grow-1 gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base col-9 gl-align-self-start"
:dropdown-items="searchUsers"
:loading="isLoading"
class="assignees-selector gl-flex-grow-1 gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base col-9 gl-align-self-start gl-px-0!"
:dropdown-items="dropdownItems"
:loading="isLoadingUsers"
@input="focusTokenSelector"
@text-input="debouncedSearchKeyUpdate"
@focus="handleFocus"
@blur="setAssignees"
@blur="handleBlur"
@mouseover.native="handleMouseOver"
@mouseout.native="handleMouseOut"
>
@ -163,7 +206,15 @@ export default {
data-testid="empty-state"
>
<gl-icon name="profile" />
<span class="gl-ml-2">{{ __('Add assignees') }}</span>
<span class="gl-ml-2 gl-mr-4">{{ __('Add assignees') }}</span>
<gl-button
v-if="currentUser"
size="small"
class="assign-myself"
data-testid="assign-self"
@click.stop="setAssignees([currentUser])"
>{{ __('Assign myself') }}</gl-button
>
</div>
</template>
<template #token-content="{ token }">
@ -189,6 +240,18 @@ export default {
<rect width="280" height="20" x="10" y="130" rx="4" />
</gl-skeleton-loader>
</template>
<template #dropdown-footer>
<gl-dropdown-divider />
<gl-dropdown-item @click="closeDropdown">
<invite-members-trigger
:display-text="__('Invite members')"
trigger-element="side-nav"
icon="plus"
trigger-source="work-item-assignees-dropdown"
classes="gl-display-block gl-text-body! gl-hover-text-decoration-none gl-pb-2"
/>
</gl-dropdown-item>
</template>
</gl-token-selector>
</div>
</template>

View File

@ -25,7 +25,7 @@ export const temporaryConfig = {
nodes: [
{
__typename: 'UserCore',
id: 'gid://gitlab/User/1',
id: 'gid://gitlab/User/10001',
avatarUrl: '',
webUrl: '',
// eslint-disable-next-line @gitlab/require-i18n-strings
@ -34,7 +34,7 @@ export const temporaryConfig = {
},
{
__typename: 'UserCore',
id: 'gid://gitlab/User/2',
id: 'gid://gitlab/User/10002',
avatarUrl: '',
webUrl: '',
// eslint-disable-next-line @gitlab/require-i18n-strings

View File

@ -13,3 +13,13 @@
#weight-widget-input[readonly] {
background-color: var(--white, $white);
}
.work-item-assignees {
.assign-myself {
display: none;
}
.assignees-selector:hover .assign-myself {
display: block;
}
}

View File

@ -131,6 +131,7 @@ module Ci
validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
after_create :keep_around_commits, unless: :importing?
after_find :observe_age_in_minutes, unless: :importing?
use_fast_destroy :job_artifacts
use_fast_destroy :build_trace_chunks
@ -1322,6 +1323,16 @@ module Ci
end
end
def age_in_minutes
return 0 unless persisted?
unless has_attribute?(:created_at)
raise ArgumentError, 'pipeline not fully loaded'
end
(Time.current - created_at).ceil / 60
end
private
def add_message(severity, content)
@ -1372,6 +1383,21 @@ module Ci
project.repository.keep_around(self.sha, self.before_sha)
end
def observe_age_in_minutes
return unless age_metric_enabled?
return unless persisted? && has_attribute?(:created_at)
::Gitlab::Ci::Pipeline::Metrics
.pipeline_age_histogram
.observe({}, age_in_minutes)
end
def age_metric_enabled?
::Gitlab::SafeRequestStore.fetch(:age_metric_enabled) do
::Feature.enabled?(:ci_pipeline_age_histogram, type: :ops)
end
end
# Without using `unscoped`, caller scope is also included into the query.
# Using `unscoped` here will be redundant after Rails 6.1
def object_hierarchy(options = {})

View File

@ -11,8 +11,18 @@ module Deployments
# TODO: Move all buisness logic in `Seed::Deployment` to this class after
# `create_deployment_in_separate_transaction` feature flag has been removed.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/348778
# If build.persisted_environment is a BatchLoader, we need to remove
# the method proxy in order to clone into new item here
# https://github.com/exAspArk/batch-loader/issues/31
environment = if build.persisted_environment.respond_to?(:__sync)
build.persisted_environment.__sync
else
build.persisted_environment
end
deployment = ::Gitlab::Ci::Pipeline::Seed::Deployment
.new(build, build.persisted_environment).to_resource
.new(build, environment).to_resource
return unless deployment

View File

@ -256,7 +256,7 @@ module Members
if user_by_email
find_or_initialize_member_by_user(user_id: user_by_email.id)
else
source.members.build(invite_email: invitee)
source.members_and_requesters.find_or_initialize_by(invite_email: invitee) # rubocop:disable CodeReuse/ActiveRecord
end
end

View File

@ -8,7 +8,7 @@
- if can_update_merge_request
%p
= _('Push commits to the source branch or add previously merged commits to review them.')
%button.btn.gl-button.btn-confirm.add-review-item-modal-trigger{ type: "button", data: { commits_empty: 'true', context_commits_empty: 'true' } }
= render Pajamas::ButtonComponent.new(variant: 'confirm', button_options: { class: 'add-review-item-modal-trigger', data: { commits_empty: 'true', context_commits_empty: 'true' } }) do
= _('Add previously merged commits')
- else
%ol#commits-list.list-unstyled

View File

@ -2,3 +2,4 @@
- add_page_specific_style 'page_bundles/work_items'
#js-work-items{ data: work_items_index_data(@project) }
= render 'projects/invite_members_modal', project: @project

View File

@ -7,7 +7,9 @@ module WaitableWorker
# Schedules multiple jobs and waits for them to be completed.
def bulk_perform_and_wait(args_list, timeout: 10)
# Short-circuit: it's more efficient to do small numbers of jobs inline
return bulk_perform_inline(args_list) if args_list.size <= 3
if args_list.size == 1 || (args_list.size <= 3 && !inline_refresh_only_for_single_element?)
return bulk_perform_inline(args_list)
end
# Don't wait if there's too many jobs to be waited for. Not including the
# waiter allows them to be deduplicated and it skips waiting for jobs that
@ -45,6 +47,10 @@ module WaitableWorker
def async_only_refresh?
Feature.enabled?(:async_only_project_authorizations_refresh)
end
def inline_refresh_only_for_single_element?
Feature.enabled?(:inline_project_authorizations_refresh_only_for_single_element)
end
end
def perform(*args)

View File

@ -0,0 +1,8 @@
---
name: inline_project_authorizations_refresh_only_for_single_element
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91572
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366815
milestone: '15.2'
type: development
group: group::workspace
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: batched_migrations_health_status_autovacuum
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85196
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/360331
milestone: '15.2'
type: ops
group: group::database
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: ci_pipeline_age_histogram
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90027
rollout_issue_url:
milestone: '15.1'
type: ops
group: group::pipeline execution
default_enabled: false

View File

@ -1,5 +1,5 @@
---
key_path: redis_hll_counters.manage.unque_active_users_monthly
key_path: redis_hll_counters.manage.unique_active_users_monthly
description: Users that have a last_activity_on date within the past 28 days
product_section: dev
product_stage: manage

View File

@ -42,6 +42,8 @@ The following metrics are available:
| `gitlab_ci_pipeline_builder_scoped_variables_duration` | Histogram | 14.5 | Time in seconds it takes to create the scoped variables for a CI/CD job
| `gitlab_ci_pipeline_creation_duration_seconds` | Histogram | 13.0 | Time in seconds it takes to create a CI/CD pipeline | |
| `gitlab_ci_pipeline_size_builds` | Histogram | 13.1 | Total number of builds within a pipeline grouped by a pipeline source | `source` |
| `gitlab_ci_runner_authentication_success_total` | Counter | 15.2 | Total number of times that runner authentication has succeeded | `type` |
| `gitlab_ci_runner_authentication_failure_total` | Counter | 15.2 | Total number of times that runner authentication has failed
| `job_waiter_started_total` | Counter | 12.9 | Number of batches of jobs started where a web request is waiting for the jobs to complete | `worker` |
| `job_waiter_timeouts_total` | Counter | 12.9 | Number of batches of jobs that timed out where a web request is waiting for the jobs to complete | `worker` |
| `gitlab_ci_active_jobs` | Histogram | 14.2 | Count of active jobs when pipeline is created | |

View File

@ -12,6 +12,7 @@ module API
JOB_TOKEN_PARAM = :token
def authenticate_runner!
track_runner_authentication
forbidden! unless current_runner
current_runner
@ -42,6 +43,14 @@ module API
end
end
def track_runner_authentication
if current_runner
metrics.increment_runner_authentication_success_counter(runner_type: current_runner.runner_type)
else
metrics.increment_runner_authentication_failure_counter
end
end
# HTTP status codes to terminate the job on GitLab Runner:
# - 403
def authenticate_job!(require_running: true, heartbeat_runner: false)
@ -149,6 +158,10 @@ module API
def request_using_running_job_token?
current_job.present? && current_authenticated_job.present? && current_job != current_authenticated_job
end
def metrics
strong_memoize(:metrics) { ::Gitlab::Ci::Runner::Metrics.new }
end
end
end
end

View File

@ -42,6 +42,15 @@ module Gitlab
::Gitlab::Metrics.histogram(name, comment, labels, buckets)
end
def self.pipeline_age_histogram
name = :gitlab_ci_pipeline_age_minutes
comment = 'Pipeline age histogram'
buckets = [5, 30, 120, 720, 1440, 7200, 21600, 43200, 86400, 172800, 518400, 1036800]
# 5m 30m 2h 12h 24h 5d 15d 30d 60d 180d 360d 2y
::Gitlab::Metrics.histogram(name, comment, {}, buckets)
end
def self.active_jobs_histogram
name = :gitlab_ci_active_jobs
comment = 'Total amount of active jobs'

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
module Gitlab
module Ci
module Runner
class Metrics
extend Gitlab::Utils::StrongMemoize
def increment_runner_authentication_success_counter(runner_type: 'unknown_type')
raise ArgumentError, "unknown runner type: #{runner_type}" unless
::Ci::Runner.runner_types.include? runner_type
self.class.runner_authentication_success_counter.increment(runner_type: runner_type)
end
def increment_runner_authentication_failure_counter
self.class.runner_authentication_failure_counter.increment
end
def self.runner_authentication_success_counter
strong_memoize(:runner_authentication_success) do
name = :gitlab_ci_runner_authentication_success_total
comment = 'Runner authentication success'
labels = { runner_type: nil }
::Gitlab::Metrics.counter(name, comment, labels)
end
end
def self.runner_authentication_failure_counter
strong_memoize(:runner_authentication_failure) do
name = :gitlab_ci_runner_authentication_failure_total
comment = 'Runner authentication failure'
::Gitlab::Metrics.counter(name, comment)
end
end
end
end
end
end

View File

@ -199,10 +199,32 @@ module Gitlab
BatchOptimizer.new(self).optimize!
end
def health_context
HealthStatus::Context.new([table_name])
end
def hold!(until_time: 10.minutes.from_now)
duration_s = (until_time - Time.current).round
Gitlab::AppLogger.info(
message: "#{self} put on hold until #{until_time}",
migration_id: id,
job_class_name: job_class_name,
duration_s: duration_s
)
update!(on_hold_until: until_time)
end
def on_hold?
return false unless on_hold_until
on_hold_until > Time.zone.now
end
def to_s
"BatchedMigration[id: #{id}]"
end
private
def validate_batched_jobs_status

View File

@ -29,7 +29,8 @@ module Gitlab
if next_batched_job = find_or_create_next_batched_job(active_migration)
migration_wrapper.perform(next_batched_job)
active_migration.optimize!
adjust_migration(active_migration)
active_migration.failure! if next_batched_job.failed? && active_migration.should_stop?
else
finish_active_migration(active_migration)
@ -139,6 +140,16 @@ module Gitlab
migration.reload_last_job
end
end
def adjust_migration(active_migration)
signal = HealthStatus.evaluate(active_migration)
if signal.is_a?(HealthStatus::Signals::Stop)
active_migration.hold!
else
active_migration.optimize!
end
end
end
end
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Gitlab
module Database
module BackgroundMigration
module HealthStatus
# Rather than passing along the migration, we use a more explicitly defined context
Context = Struct.new(:tables)
def self.evaluate(migration, indicator = Indicators::AutovacuumActiveOnTable)
signal = begin
indicator.new(migration.health_context).evaluate
rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, migration_id: migration.id,
job_class_name: migration.job_class_name)
Signals::Unknown.new(indicator, reason: "unexpected error: #{e.message} (#{e.class})")
end
log_signal(signal, migration) if signal.log_info?
signal
end
def self.log_signal(signal, migration)
Gitlab::AppLogger.info(
message: "#{migration} signaled: #{signal}",
migration_id: migration.id
)
end
end
end
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module Gitlab
module Database
module BackgroundMigration
module HealthStatus
module Indicators
end
end
end
end
end

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
module Gitlab
module Database
module BackgroundMigration
module HealthStatus
module Indicators
class AutovacuumActiveOnTable
def initialize(context)
@context = context
end
def evaluate
return Signals::NotAvailable.new(self.class, reason: 'indicator disabled') unless enabled?
autovacuum_active_on = active_autovacuums_for(context.tables)
if autovacuum_active_on.empty?
Signals::Normal.new(self.class, reason: 'no autovacuum running on any relevant tables')
else
Signals::Stop.new(self.class, reason: "autovacuum running on: #{autovacuum_active_on.join(', ')}")
end
end
private
attr_reader :context
def enabled?
Feature.enabled?(:batched_migrations_health_status_autovacuum, type: :ops)
end
def active_autovacuums_for(tables)
Gitlab::Database::PostgresAutovacuumActivity.for_tables(tables)
end
end
end
end
end
end
end

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
module Gitlab
module Database
module BackgroundMigration
module HealthStatus
module Signals
# Base class for a signal
class Base
attr_reader :indicator_class, :reason
def initialize(indicator_class, reason:)
@indicator_class = indicator_class
@reason = reason
end
def to_s
"#{short_name} (indicator: #{indicator_class}; reason: #{reason})"
end
# :nocov:
def log_info?
false
end
# :nocov:
private
def short_name
self.class.name.demodulize
end
end
# A Signals::Stop is an indication to put a migration on hold or stop it entirely:
# In general, we want to slow down or pause the migration.
class Stop < Base
# :nocov:
def log_info?
true
end
# :nocov:
end
# A Signals::Normal indicates normal system state: We carry on with the migration
# and may even attempt to optimize its throughput etc.
class Normal < Base; end
# When given an Signals::Unknown, something unexpected happened while
# we evaluated system indicators.
class Unknown < Base
# :nocov:
def log_info?
true
end
# :nocov:
end
# No signal could be determined, e.g. because the indicator
# was disabled.
class NotAvailable < Base; end
end
end
end
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
module Gitlab
module Database
class PostgresAutovacuumActivity < SharedModel
self.table_name = 'postgres_autovacuum_activity'
self.primary_key = 'table_identifier'
def self.for_tables(tables)
Gitlab::Database::LoadBalancing::Session.current.use_primary do
where('schema = current_schema()').where(table: tables)
end
end
def to_s
"table #{table_identifier} (started: #{vacuum_start})"
end
end
end
end

View File

@ -5129,6 +5129,9 @@ msgstr ""
msgid "Assign milestone"
msgstr ""
msgid "Assign myself"
msgstr ""
msgid "Assign reviewer"
msgstr ""

View File

@ -3,7 +3,7 @@
source scripts/utils.sh
function setup_gcp_dependencies() {
apk add jq
apt-get update && apt-get install -y jq
gcloud auth activate-service-account --key-file="${REVIEW_APPS_GCP_CREDENTIALS}"
gcloud config set project "${REVIEW_APPS_GCP_PROJECT}"

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
FactoryBot.define do
factory :postgres_autovacuum_activity, class: 'Gitlab::Database::PostgresAutovacuumActivity' do
table_identifier { "#{schema}.#{table}" }
schema { 'public' }
table { 'projects' }
vacuum_start { Time.zone.now - 3.minutes }
end
end

View File

@ -13,7 +13,7 @@ FactoryBot.define do
trait(:maintainer) { group_access { Gitlab::Access::MAINTAINER } }
after(:create) do |project_group_link, evaluator|
project_group_link.group.refresh_members_authorized_projects
AuthorizedProjectUpdate::ProjectRecalculateService.new(project_group_link.project).execute
end
end
end

View File

@ -98,7 +98,9 @@ FactoryBot.define do
project.add_owner(project.first_owner)
end
project.group&.refresh_members_authorized_projects
if project.group
AuthorizedProjectUpdate::ProjectRecalculateService.new(project).execute
end
# assign the delegated `#ci_cd_settings` attributes after create
project.group_runners_enabled = evaluator.group_runners_enabled unless evaluator.group_runners_enabled.nil?

View File

@ -4,13 +4,22 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stripTypenames } from 'helpers/graphql_helpers';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
import { i18n } from '~/work_items/constants';
import { temporaryConfig, resolvers } from '~/work_items/graphql/provider';
import { projectMembersResponse, mockAssignees, workItemQueryResponse } from '../mock_data';
import {
projectMembersResponseWithCurrentUser,
mockAssignees,
workItemQueryResponse,
currentUserResponse,
currentUserNullResponse,
projectMembersResponseWithoutCurrentUser,
} from '../mock_data';
Vue.use(VueApollo);
@ -24,17 +33,31 @@ describe('WorkItemAssignees component', () => {
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findEmptyState = () => wrapper.findByTestId('empty-state');
const findAssignSelfButton = () => wrapper.findByTestId('assign-self');
const successSearchQueryHandler = jest
.fn()
.mockResolvedValue(projectMembersResponseWithCurrentUser);
const successCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserResponse);
const noCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserNullResponse);
const successSearchQueryHandler = jest.fn().mockResolvedValue(projectMembersResponse);
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const createComponent = ({
assignees = mockAssignees,
searchQueryHandler = successSearchQueryHandler,
currentUserQueryHandler = successCurrentUserQueryHandler,
} = {}) => {
const apolloProvider = createMockApollo([[userSearchQuery, searchQueryHandler]], resolvers, {
typePolicies: temporaryConfig.cacheConfig.typePolicies,
});
const apolloProvider = createMockApollo(
[
[userSearchQuery, searchQueryHandler],
[currentUserQuery, currentUserQueryHandler],
],
resolvers,
{
typePolicies: temporaryConfig.cacheConfig.typePolicies,
},
);
apolloProvider.clients.defaultClient.writeQuery({
query: workItemQuery,
@ -171,4 +194,95 @@ describe('WorkItemAssignees component', () => {
expect.objectContaining({ search: searchKey }),
);
});
it('does not show `Assign myself` button if current user is loading', () => {
createComponent();
findTokenSelector().trigger('mouseover');
expect(findAssignSelfButton().exists()).toBe(false);
});
it('does not show `Assign myself` button if work item has assignees', async () => {
createComponent();
await waitForPromises();
findTokenSelector().trigger('mouseover');
expect(findAssignSelfButton().exists()).toBe(false);
});
it('does now show `Assign myself` button if user is not logged in', async () => {
createComponent({ currentUserQueryHandler: noCurrentUserQueryHandler, assignees: [] });
await waitForPromises();
findTokenSelector().trigger('mouseover');
expect(findAssignSelfButton().exists()).toBe(false);
});
describe('when user is logged in and there are no assignees', () => {
beforeEach(() => {
createComponent({ assignees: [] });
return waitForPromises();
});
it('renders `Assign myself` button', async () => {
findTokenSelector().trigger('mouseover');
expect(findAssignSelfButton().exists()).toBe(true);
});
it('calls update work item assignees mutation with current user as a variable on button click', () => {
// TODO: replace this test as soon as we have a real mutation implemented
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementation(jest.fn());
findTokenSelector().trigger('mouseover');
findAssignSelfButton().vm.$emit('click', new MouseEvent('click'));
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
input: {
assignees: [stripTypenames(currentUserResponse.data.currentUser)],
id: workItemId,
},
},
}),
);
});
});
it('moves current user to the top of dropdown items if user is a project member', async () => {
createComponent();
await waitForPromises();
expect(findTokenSelector().props('dropdownItems')[0]).toEqual(
expect.objectContaining({
...stripTypenames(currentUserResponse.data.currentUser),
}),
);
});
describe('when current user is not in the list of project members', () => {
const searchQueryHandler = jest
.fn()
.mockResolvedValue(projectMembersResponseWithoutCurrentUser);
beforeEach(() => {
createComponent({ searchQueryHandler });
return waitForPromises();
});
it('adds current user to the top of dropdown items', () => {
expect(findTokenSelector().props('dropdownItems')[0]).toEqual(
stripTypenames(currentUserResponse.data.currentUser),
);
});
it('does not add current user if search is not empty', async () => {
findTokenSelector().vm.$emit('text-input', 'test');
await waitForPromises();
expect(findTokenSelector().props('dropdownItems')[0]).not.toEqual(
stripTypenames(currentUserResponse.data.currentUser),
);
});
});
});

View File

@ -333,13 +333,25 @@ export const availableWorkItemsResponse = {
},
};
export const projectMembersResponse = {
export const projectMembersResponseWithCurrentUser = {
data: {
workspace: {
id: '1',
__typename: 'Project',
users: {
nodes: [
{
id: 'user-2',
user: {
__typename: 'UserCore',
id: 'gid://gitlab/User/5',
avatarUrl: '/avatar2',
name: 'rookie',
username: 'rookie',
webUrl: 'rookie',
status: null,
},
},
{
id: 'user-1',
user: {
@ -353,6 +365,19 @@ export const projectMembersResponse = {
status: null,
},
},
],
},
},
},
};
export const projectMembersResponseWithoutCurrentUser = {
data: {
workspace: {
id: '1',
__typename: 'Project',
users: {
nodes: [
{
id: 'user-2',
user: {
@ -389,3 +414,23 @@ export const mockAssignees = [
username: 'ruthfull',
},
];
export const currentUserResponse = {
data: {
currentUser: {
__typename: 'UserCore',
id: 'gid://gitlab/User/1',
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
name: 'Administrator',
username: 'root',
webUrl: '/root',
},
},
};
export const currentUserNullResponse = {
data: {
currentUser: null,
},
};

View File

@ -66,4 +66,30 @@ RSpec.describe API::Ci::Helpers::Runner do
expect(helper.current_runner).to eq(runner)
end
end
describe '#track_runner_authentication', :prometheus do
subject { helper.track_runner_authentication }
let(:runner) { create(:ci_runner, token: 'foo') }
it 'increments gitlab_ci_runner_authentication_success_total' do
allow(helper).to receive(:params).and_return(token: runner.token)
success_counter = ::Gitlab::Ci::Runner::Metrics.runner_authentication_success_counter
failure_counter = ::Gitlab::Ci::Runner::Metrics.runner_authentication_failure_counter
expect { subject }.to change { success_counter.get(runner_type: 'instance_type') }.by(1)
.and not_change { success_counter.get(runner_type: 'project_type') }
.and not_change { failure_counter.get }
end
it 'increments gitlab_ci_runner_authentication_failure_total' do
allow(helper).to receive(:params).and_return(token: 'invalid')
success_counter = ::Gitlab::Ci::Runner::Metrics.runner_authentication_success_counter
failure_counter = ::Gitlab::Ci::Runner::Metrics.runner_authentication_failure_counter
expect { subject }.to change { failure_counter.get }.by(1)
.and not_change { success_counter.get(runner_type: 'instance_type') }
.and not_change { success_counter.get(runner_type: 'project_type') }
end
end
end

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Runner::Metrics, :prometheus do
subject { described_class.new }
describe '#increment_runner_authentication_success_counter' do
it 'increments count for same type' do
expect { subject.increment_runner_authentication_success_counter(runner_type: 'instance_type') }
.to change { described_class.runner_authentication_success_counter.get(runner_type: 'instance_type') }.by(1)
end
it 'does not increment count for different type' do
expect { subject.increment_runner_authentication_success_counter(runner_type: 'group_type') }
.to not_change { described_class.runner_authentication_success_counter.get(runner_type: 'project_type') }
end
it 'does not increment failure count' do
expect { subject.increment_runner_authentication_success_counter(runner_type: 'project_type') }
.to not_change { described_class.runner_authentication_failure_counter.get }
end
it 'throws ArgumentError for invalid runner type' do
expect { subject.increment_runner_authentication_success_counter(runner_type: 'unknown_type') }
.to raise_error(ArgumentError, 'unknown runner type: unknown_type')
end
end
describe '#increment_runner_authentication_failure_counter' do
it 'increments count' do
expect { subject.increment_runner_authentication_failure_counter }
.to change { described_class.runner_authentication_failure_counter.get }.by(1)
end
it 'does not increment success count' do
expect { subject.increment_runner_authentication_failure_counter }
.to not_change { described_class.runner_authentication_success_counter.get(runner_type: 'instance_type') }
end
end
end

View File

@ -14,6 +14,11 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
end
end
before do
allow(Gitlab::Database::BackgroundMigration::HealthStatus).to receive(:evaluate)
.and_return(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Normal)
end
describe '#run_migration_job' do
shared_examples_for 'it has completed the migration' do
it 'does not create and run a migration job' do
@ -59,13 +64,48 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
sub_batch_size: migration.sub_batch_size)
end
it 'optimizes the migration after executing the job' do
migration.update!(min_value: event1.id, max_value: event2.id)
context 'migration health' do
let(:health_status) { Gitlab::Database::BackgroundMigration::HealthStatus }
let(:stop_signal) { health_status::Signals::Stop.new(:indicator, reason: 'Take a break') }
let(:normal_signal) { health_status::Signals::Normal.new(:indicator, reason: 'All good') }
let(:not_available_signal) { health_status::Signals::NotAvailable.new(:indicator, reason: 'Indicator is disabled') }
let(:unknown_signal) { health_status::Signals::Unknown.new(:indicator, reason: 'Something went wrong') }
expect(migration_wrapper).to receive(:perform).ordered
expect(migration).to receive(:optimize!).ordered
before do
migration.update!(min_value: event1.id, max_value: event2.id)
expect(migration_wrapper).to receive(:perform)
end
runner.run_migration_job(migration)
it 'puts migration on hold on stop signal' do
expect(health_status).to receive(:evaluate).and_return(stop_signal)
expect { runner.run_migration_job(migration) }.to change { migration.on_hold? }
.from(false).to(true)
end
it 'optimizes migration on normal signal' do
expect(health_status).to receive(:evaluate).and_return(normal_signal)
expect(migration).to receive(:optimize!)
expect { runner.run_migration_job(migration) }.not_to change { migration.on_hold? }
end
it 'optimizes migration on no signal' do
expect(health_status).to receive(:evaluate).and_return(not_available_signal)
expect(migration).to receive(:optimize!)
expect { runner.run_migration_job(migration) }.not_to change { migration.on_hold? }
end
it 'optimizes migration on unknown signal' do
expect(health_status).to receive(:evaluate).and_return(unknown_signal)
expect(migration).to receive(:optimize!)
expect { runner.run_migration_job(migration) }.not_to change { migration.on_hold? }
end
end
end

View File

@ -572,6 +572,30 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
end
end
describe '#on_hold?', :freeze_time do
subject { migration.on_hold? }
let(:migration) { create(:batched_background_migration) }
it 'returns false if no on_hold_until is set' do
migration.on_hold_until = nil
expect(subject).to be_falsey
end
it 'returns false if on_hold_until has passed' do
migration.on_hold_until = 1.minute.ago
expect(subject).to be_falsey
end
it 'returns true if on_hold_until is in the future' do
migration.on_hold_until = 1.minute.from_now
expect(subject).to be_truthy
end
end
describe '.for_configuration' do
let!(:attributes) do
{

View File

@ -0,0 +1,61 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::BackgroundMigration::HealthStatus::Indicators::AutovacuumActiveOnTable do
include Database::DatabaseHelpers
let(:connection) { Gitlab::Database.database_base_models[:main].connection }
around do |example|
Gitlab::Database::SharedModel.using_connection(connection) do
example.run
end
end
describe '#evaluate' do
subject { described_class.new(context).evaluate }
before do
swapout_view_for_table(:postgres_autovacuum_activity)
end
let(:context) { Gitlab::Database::BackgroundMigration::HealthStatus::Context.new(tables) }
let(:tables) { [table] }
let(:table) { 'users' }
context 'without autovacuum activity' do
it 'returns Normal signal' do
expect(subject).to be_a(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Normal)
end
it 'remembers the indicator class' do
expect(subject.indicator_class).to eq(described_class)
end
end
context 'with autovacuum activity' do
before do
create(:postgres_autovacuum_activity, table: table, table_identifier: "public.#{table}")
end
it 'returns Stop signal' do
expect(subject).to be_a(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Stop)
end
it 'explains why' do
expect(subject.reason).to include('autovacuum running on: table public.users')
end
it 'remembers the indicator class' do
expect(subject.indicator_class).to eq(described_class)
end
it 'returns NoSignal signal in case the feature flag is disabled' do
stub_feature_flags(batched_migrations_health_status_autovacuum: false)
expect(subject).to be_a(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::NotAvailable)
end
end
end
end

View File

@ -0,0 +1,77 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::BackgroundMigration::HealthStatus do
let(:connection) { Gitlab::Database.database_base_models[:main].connection }
around do |example|
Gitlab::Database::SharedModel.using_connection(connection) do
example.run
end
end
describe '.evaluate' do
subject(:evaluate) { described_class.evaluate(migration, indicator_class) }
let(:migration) { build(:batched_background_migration, :active) }
let(:health_status) { 'Gitlab::Database::BackgroundMigration::HealthStatus' }
let(:indicator_class) { class_double("#{health_status}::Indicators::AutovacuumActiveOnTable") }
let(:indicator) { instance_double("#{health_status}::Indicators::AutovacuumActiveOnTable") }
before do
allow(indicator_class).to receive(:new).with(migration.health_context).and_return(indicator)
end
it 'returns a signal' do
signal = instance_double("#{health_status}::Signals::Normal", log_info?: false)
expect(indicator).to receive(:evaluate).and_return(signal)
expect(evaluate).to eq(signal)
end
it 'logs interesting signals' do
signal = instance_double("#{health_status}::Signals::Stop", log_info?: true)
expect(indicator).to receive(:evaluate).and_return(signal)
expect(described_class).to receive(:log_signal).with(signal, migration)
evaluate
end
it 'does not log signals of no interest' do
signal = instance_double("#{health_status}::Signals::Normal", log_info?: false)
expect(indicator).to receive(:evaluate).and_return(signal)
expect(described_class).not_to receive(:log_signal)
evaluate
end
context 'on indicator error' do
let(:error) { RuntimeError.new('everything broken') }
before do
expect(indicator).to receive(:evaluate).and_raise(error)
end
it 'does not fail' do
expect { evaluate }.not_to raise_error
end
it 'returns Unknown signal' do
expect(evaluate).to be_an_instance_of(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Unknown)
expect(evaluate.reason).to eq("unexpected error: everything broken (RuntimeError)")
end
it 'reports the exception to error tracking' do
expect(Gitlab::ErrorTracking).to receive(:track_exception)
.with(error, migration_id: migration.id, job_class_name: migration.job_class_name)
evaluate
end
end
end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::PostgresAutovacuumActivity, type: :model do
include Database::DatabaseHelpers
it { is_expected.to be_a Gitlab::Database::SharedModel }
describe '.for_tables' do
subject { described_class.for_tables(tables) }
let(:tables) { %w[foo test] }
before do
swapout_view_for_table(:postgres_autovacuum_activity)
# unrelated
create(:postgres_autovacuum_activity, table: 'bar')
tables.each do |table|
create(:postgres_autovacuum_activity, table: table)
end
expect(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_primary).and_yield
end
it 'returns autovacuum activity for queries tables' do
expect(subject.map(&:table).sort).to eq(tables)
end
end
end

View File

@ -118,6 +118,38 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
describe 'pipeline age metric' do
let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }
let(:pipeline_age_histogram) do
::Gitlab::Ci::Pipeline::Metrics.pipeline_age_histogram
end
context 'when pipeline age histogram is enabled' do
before do
stub_feature_flags(ci_pipeline_age_histogram: true)
end
it 'observes pipeline age' do
expect(pipeline_age_histogram).to receive(:observe)
described_class.find(pipeline.id)
end
end
context 'when pipeline age histogram is disabled' do
before do
stub_feature_flags(ci_pipeline_age_histogram: false)
end
it 'observes pipeline age' do
expect(pipeline_age_histogram).not_to receive(:observe)
described_class.find(pipeline.id)
end
end
end
describe '#set_status' do
let(:pipeline) { build(:ci_empty_pipeline, :created) }
@ -4979,4 +5011,34 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
end
describe '#age_in_minutes' do
let(:pipeline) { build(:ci_pipeline) }
context 'when pipeline has not been persisted' do
it 'returns zero' do
expect(pipeline.age_in_minutes).to eq 0
end
end
context 'when pipeline has been saved' do
it 'returns pipeline age in minutes' do
pipeline.save!
travel_to(pipeline.created_at + 2.hours) do
expect(pipeline.age_in_minutes).to eq 120
end
end
end
context 'when pipeline has been loaded without all attributes' do
it 'raises an exception' do
pipeline.save!
pipeline_id = Ci::Pipeline.where(id: pipeline.id).select(:id).first
expect { pipeline_id.age_in_minutes }.to raise_error(ArgumentError)
end
end
end
end

View File

@ -104,6 +104,7 @@ RSpec.describe 'factories' do
factories_based_on_view = %i[
postgres_index
postgres_index_bloat_estimate
postgres_autovacuum_activity
].to_set.freeze
without_fd, with_fd = FactoryBot.factories

View File

@ -69,6 +69,20 @@ RSpec.describe API::Invitations do
end
end
context 'when invitee is already an invited member' do
it 'updates the member for that email' do
member = source.add_developer(email)
expect do
post invitations_url(source, maintainer),
params: { email: email, access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:created)
end.to change { member.reset.access_level }.from(Member::DEVELOPER).to(Member::MAINTAINER)
.and not_change { source.members.invite.count }
end
end
it 'adds a new member by email' do
expect do
post invitations_url(source, maintainer),

View File

@ -25,6 +25,7 @@ RSpec.describe Deployments::CreateForBuildService do
expect(build.deployment.deployable).to eq(build)
expect(build.deployment.deployable_type).to eq('CommitStatus')
expect(build.deployment.environment).to eq(build.persisted_environment)
expect(build.deployment.valid?).to be_truthy
end
context 'when creation failure occures' do

View File

@ -601,8 +601,8 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
}.from(0).to(1)
end
it 'performs authorizations job immediately' do
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_inline)
it 'performs authorizations job' do
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_async)
transfer_service.execute(new_parent_group)
end

View File

@ -146,12 +146,14 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
end
context 'when passing an existing invite user id' do
let(:user_id) { create(:project_member, :invited, project: source).invite_email }
let(:invited_member) { create(:project_member, :guest, :invited, project: source) }
let(:user_id) { invited_member.invite_email }
let(:access_level) { ProjectMember::MAINTAINER }
it 'does not add a member' do
expect(execute_service[:status]).to eq(:error)
expect(execute_service[:message]).to eq("The member's email address has already been taken")
expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false)
it 'allows already invited members to be re-invited by email and updates the member access' do
expect(execute_service[:status]).to eq(:success)
expect(invited_member.reset.access_level).to eq ProjectMember::MAINTAINER
expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true)
end
end

View File

@ -353,15 +353,16 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
context 'when member already exists' do
context 'with email' do
let!(:invited_member) { create(:project_member, :invited, project: project) }
let(:params) { { email: "#{invited_member.invite_email},#{project_user.email}" } }
let!(:invited_member) { create(:project_member, :guest, :invited, project: project) }
let(:params) do
{ email: "#{invited_member.invite_email},#{project_user.email}", access_level: ProjectMember::MAINTAINER }
end
it 'adds new email and returns an error for the already invited email' do
it 'adds new email and allows already invited members to be re-invited by email and updates the member access' do
expect_to_create_members(count: 1)
expect(result[:status]).to eq(:error)
expect(result[:message][invited_member.invite_email])
.to eq("The member's email address has already been taken")
expect(result[:status]).to eq(:success)
expect(project.users).to include project_user
expect(invited_member.reset.access_level).to eq ProjectMember::MAINTAINER
end
end

View File

@ -4,8 +4,10 @@ module Database
module DatabaseHelpers
# In order to directly work with views using factories,
# we can swapout the view for a table of identical structure.
def swapout_view_for_table(view)
ActiveRecord::Base.connection.execute(<<~SQL.squish)
def swapout_view_for_table(view, connection: nil)
connection ||= ActiveRecord::Base.connection
connection.execute(<<~SQL.squish)
CREATE TABLE #{view}_copy (LIKE #{view});
DROP VIEW #{view};
ALTER TABLE #{view}_copy RENAME TO #{view};

View File

@ -78,22 +78,21 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
end
context 'when member is already a member by email' do
it 'fails with an error', :js do
it 'updates the member for that email', :js do
visit members_page_path
invite_member('test@example.com', role: 'Developer')
invite_member('test@example.com', role: 'Reporter', refresh: false)
expect(page).to have_selector(invite_modal_selector)
expect(page).to have_content("The member's email address has already been taken")
expect(page).not_to have_selector(invite_modal_selector)
page.refresh
click_link 'Invited'
page.within find_invited_member_row('test@example.com') do
expect(page).to have_button('Developer')
expect(page).to have_button('Reporter')
end
end
end

View File

@ -299,37 +299,51 @@ RSpec.shared_examples_for "member creation" do
end
context 'when member already exists' do
before do
source.add_member(user, :developer)
end
context 'when member is a user' do
before do
source.add_member(user, :developer)
end
context 'with no current_user' do
it 'updates the member' do
expect(source.users).to include(user)
context 'with no current_user' do
it 'updates the member' do
expect(source.users).to include(user)
described_class.add_member(source, user, :maintainer)
described_class.add_member(source, user, :maintainer)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
context 'when current_user can update member', :enable_admin_mode do
it 'updates the member' do
expect(source.users).to include(user)
described_class.add_member(source, user, :maintainer, current_user: admin)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
context 'when current_user cannot update member' do
it 'does not update the member' do
expect(source.users).to include(user)
described_class.add_member(source, user, :maintainer, current_user: user)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
end
end
end
context 'when current_user can update member', :enable_admin_mode do
it 'updates the member' do
expect(source.users).to include(user)
context 'when member is an invite by email' do
let_it_be(:email) { 'user@email.com' }
let_it_be(:existing_member) { source.add_developer(email) }
described_class.add_member(source, user, :maintainer, current_user: admin)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
context 'when current_user cannot update member' do
it 'does not update the member' do
expect(source.users).to include(user)
described_class.add_member(source, user, :maintainer, current_user: user)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
it 'updates the member for that email' do
expect do
described_class.add_member(source, email, :maintainer)
end.to change { existing_member.reset.access_level }.from(Member::DEVELOPER).to(Member::MAINTAINER)
.and not_change { source.members.invite.count }
end
end
end

View File

@ -229,6 +229,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
describe 'executing an entire migration', :freeze_time, if: Gitlab::Database.has_config?(tracking_database) do
include Gitlab::Database::DynamicModelHelpers
include Database::DatabaseHelpers
let(:migration_class) do
Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do
@ -347,5 +348,20 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
it 'does not update non-matching records in the range' do
expect { full_migration_run }.not_to change { example_data.where('status <> 1 AND some_column <> 0').count }
end
context 'health status' do
subject(:migration_run) { described_class.new.perform }
it 'puts migration on hold when there is autovaccum activity on related tables' do
swapout_view_for_table(:postgres_autovacuum_activity, connection: connection)
create(
:postgres_autovacuum_activity,
table: migration.table_name,
table_identifier: "public.#{migration.table_name}"
)
expect { migration_run }.to change { migration.reload.on_hold? }.from(false).to(true)
end
end
end
end

View File

@ -29,54 +29,81 @@ RSpec.describe WaitableWorker do
subject(:job) { worker.new }
describe '.bulk_perform_and_wait' do
it 'schedules the jobs and waits for them to complete' do
worker.bulk_perform_and_wait([[1], [2]])
context '1 job' do
it 'inlines the job' do
args_list = [[1]]
expect(worker).to receive(:bulk_perform_inline).with(args_list).and_call_original
expect(Gitlab::AppJsonLogger).to(
receive(:info).with(a_hash_including('message' => 'running inline',
'class' => 'Gitlab::Foo::Bar::DummyWorker',
'job_status' => 'running',
'queue' => 'foo_bar_dummy'))
.once)
expect(worker.counter).to eq(3)
end
worker.bulk_perform_and_wait(args_list)
it 'inlines workloads <= 3 jobs' do
args_list = [[1], [2], [3]]
expect(worker).to receive(:bulk_perform_inline).with(args_list).and_call_original
expect(Gitlab::AppJsonLogger).to(
receive(:info).with(a_hash_including('message' => 'running inline',
'class' => 'Gitlab::Foo::Bar::DummyWorker',
'job_status' => 'running',
'queue' => 'foo_bar_dummy'))
.exactly(3).times)
worker.bulk_perform_and_wait(args_list)
expect(worker.counter).to eq(6)
end
context 'when the feature flag `async_only_project_authorizations_refresh` is turned off' do
before do
stub_feature_flags(async_only_project_authorizations_refresh: false)
end
it 'runs > 3 jobs using sidekiq and a waiter key' do
expect(worker).to receive(:bulk_perform_async)
.with([[1, anything], [2, anything], [3, anything], [4, anything]])
worker.bulk_perform_and_wait([[1], [2], [3], [4]])
expect(worker.counter).to eq(1)
end
end
it 'runs > 3 jobs using sidekiq and no waiter key' do
arguments = 1.upto(5).map { |i| [i] }
context 'between 2 and 3 jobs' do
it 'runs the jobs asynchronously' do
arguments = [[1], [2], [3]]
expect(worker).to receive(:bulk_perform_async).with(arguments)
expect(worker).to receive(:bulk_perform_async).with(arguments)
worker.bulk_perform_and_wait(arguments, timeout: 2)
worker.bulk_perform_and_wait(arguments)
end
context 'when the feature flag `inline_project_authorizations_refresh_only_for_single_element` is turned off' do
before do
stub_feature_flags(inline_project_authorizations_refresh_only_for_single_element: false)
end
it 'inlines the jobs' do
args_list = [[1], [2], [3]]
expect(worker).to receive(:bulk_perform_inline).with(args_list).and_call_original
expect(Gitlab::AppJsonLogger).to(
receive(:info).with(a_hash_including('message' => 'running inline',
'class' => 'Gitlab::Foo::Bar::DummyWorker',
'job_status' => 'running',
'queue' => 'foo_bar_dummy'))
.exactly(3).times)
worker.bulk_perform_and_wait(args_list)
end
end
end
it 'runs > 10 * timeout jobs using sidekiq and no waiter key' do
arguments = 1.upto(21).map { |i| [i] }
context '>= 4 jobs' do
it 'runs jobs using sidekiq and no waiter key' do
arguments = 1.upto(5).map { |i| [i] }
expect(worker).to receive(:bulk_perform_async).with(arguments)
expect(worker).to receive(:bulk_perform_async).with(arguments)
worker.bulk_perform_and_wait(arguments, timeout: 2)
worker.bulk_perform_and_wait(arguments, timeout: 2)
end
it 'runs > 10 * timeout jobs using sidekiq and no waiter key' do
arguments = 1.upto(21).map { |i| [i] }
expect(worker).to receive(:bulk_perform_async).with(arguments)
worker.bulk_perform_and_wait(arguments, timeout: 2)
end
context 'when the feature flag `async_only_project_authorizations_refresh` is turned off' do
before do
stub_feature_flags(async_only_project_authorizations_refresh: false)
end
it 'runs > 3 jobs using sidekiq and a waiter key' do
expect(worker).to receive(:bulk_perform_async)
.with([[1, anything], [2, anything], [3, anything], [4, anything]])
worker.bulk_perform_and_wait([[1], [2], [3], [4]])
end
end
end
end