Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-06-22 15:09:14 +00:00
parent a27c21b5af
commit 3a8d221b7e
72 changed files with 755 additions and 1804 deletions

View File

@ -456,7 +456,6 @@ Lint/UnusedMethodArgument:
- 'lib/gitlab/diff/file_collection/merge_request_diff_base.rb'
- 'lib/gitlab/diff/line.rb'
- 'lib/gitlab/encoding_helper.rb'
- 'lib/gitlab/error_tracking/error_repository/active_record_strategy.rb'
- 'lib/gitlab/fogbugz_import/importer.rb'
- 'lib/gitlab/git/commit_stats.rb'
- 'lib/gitlab/git/conflict/parser.rb'

View File

@ -79,7 +79,6 @@ Naming/InclusiveLanguage:
- 'spec/rubocop/cop/graphql/id_type_spec.rb'
- 'spec/services/application_settings/update_service_spec.rb'
- 'spec/services/design_management/generate_image_versions_service_spec.rb'
- 'spec/services/error_tracking/list_issues_service_spec.rb'
- 'spec/services/projects/download_service_spec.rb'
- 'spec/support/import_export/export_file_helper.rb'
- 'spec/support/shared_contexts/upload_type_check_shared_context.rb'

View File

@ -2670,7 +2670,6 @@ RSpec/ContextWording:
- 'spec/services/draft_notes/publish_service_spec.rb'
- 'spec/services/environments/schedule_to_delete_review_apps_service_spec.rb'
- 'spec/services/environments/stop_service_spec.rb'
- 'spec/services/error_tracking/list_issues_service_spec.rb'
- 'spec/services/error_tracking/list_projects_service_spec.rb'
- 'spec/services/events/destroy_service_spec.rb'
- 'spec/services/feature_flags/update_service_spec.rb'

View File

@ -1,142 +0,0 @@
/* eslint-disable no-restricted-globals */
import { logger } from '@rails/actioncable';
// This is based on https://github.com/rails/rails/blob/5a477890c809d4a17dc0dede43c6b8cef81d8175/actioncable/app/javascript/action_cable/connection_monitor.js
// so that we can take advantage of the improved reconnection logic. We can remove this once we upgrade @rails/actioncable to a version that includes this.
// Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting
// revival reconnections if things go astray. Internal class, not intended for direct user manipulation.
const now = () => new Date().getTime();
const secondsSince = (time) => (now() - time) / 1000;
class ConnectionMonitor {
constructor(connection) {
this.visibilityDidChange = this.visibilityDidChange.bind(this);
this.connection = connection;
this.reconnectAttempts = 0;
}
start() {
if (!this.isRunning()) {
this.startedAt = now();
delete this.stoppedAt;
this.startPolling();
addEventListener('visibilitychange', this.visibilityDidChange);
logger.log(
`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`,
);
}
}
stop() {
if (this.isRunning()) {
this.stoppedAt = now();
this.stopPolling();
removeEventListener('visibilitychange', this.visibilityDidChange);
logger.log('ConnectionMonitor stopped');
}
}
isRunning() {
return this.startedAt && !this.stoppedAt;
}
recordPing() {
this.pingedAt = now();
}
recordConnect() {
this.reconnectAttempts = 0;
this.recordPing();
delete this.disconnectedAt;
logger.log('ConnectionMonitor recorded connect');
}
recordDisconnect() {
this.disconnectedAt = now();
logger.log('ConnectionMonitor recorded disconnect');
}
// Private
startPolling() {
this.stopPolling();
this.poll();
}
stopPolling() {
clearTimeout(this.pollTimeout);
}
poll() {
this.pollTimeout = setTimeout(() => {
this.reconnectIfStale();
this.poll();
}, this.getPollInterval());
}
getPollInterval() {
const { staleThreshold, reconnectionBackoffRate } = this.constructor;
const backoff = (1 + reconnectionBackoffRate) ** Math.min(this.reconnectAttempts, 10);
const jitterMax = this.reconnectAttempts === 0 ? 1.0 : reconnectionBackoffRate;
const jitter = jitterMax * Math.random();
return staleThreshold * 1000 * backoff * (1 + jitter);
}
reconnectIfStale() {
if (this.connectionIsStale()) {
logger.log(
`ConnectionMonitor detected stale connection. reconnectAttempts = ${
this.reconnectAttempts
}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${
this.constructor.staleThreshold
} s`,
);
this.reconnectAttempts += 1;
if (this.disconnectedRecently()) {
logger.log(
`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(
this.disconnectedAt,
)} s`,
);
} else {
logger.log('ConnectionMonitor reopening');
this.connection.reopen();
}
}
}
get refreshedAt() {
return this.pingedAt ? this.pingedAt : this.startedAt;
}
connectionIsStale() {
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
}
disconnectedRecently() {
return (
this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold
);
}
visibilityDidChange() {
if (document.visibilityState === 'visible') {
setTimeout(() => {
if (this.connectionIsStale() || !this.connection.isOpen()) {
logger.log(
`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`,
);
this.connection.reopen();
}
}, 200);
}
}
}
ConnectionMonitor.staleThreshold = 6; // Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
ConnectionMonitor.reconnectionBackoffRate = 0.15;
export default ConnectionMonitor;

View File

@ -1,10 +1,3 @@
import { createConsumer } from '@rails/actioncable';
import ConnectionMonitor from './actioncable_connection_monitor';
const consumer = createConsumer();
if (consumer.connection) {
consumer.connection.monitor = new ConnectionMonitor(consumer.connection);
}
export default consumer;
export default createConsumer();

View File

@ -126,10 +126,6 @@ export default {
isSearchFiltered() {
return isSearchFiltered(this.search);
},
shouldShowCreateRunnerWorkflow() {
// create_runner_workflow_for_admin feature flag
return this.glFeatures.createRunnerWorkflowForAdmin;
},
},
watch: {
search: {
@ -193,7 +189,7 @@ export default {
/>
<div class="gl-w-full gl-md-w-auto gl-display-flex">
<gl-button v-if="shouldShowCreateRunnerWorkflow" :href="newRunnerPath" variant="confirm">
<gl-button :href="newRunnerPath" variant="confirm">
{{ s__('Runners|New instance runner') }}
</gl-button>
<registration-dropdown

View File

@ -20,6 +20,9 @@ export default {
showInstallationInstructions: s__(
'Runners|Show runner installation and registration instructions',
),
supportForRegistrationTokensDeprecated: s__(
'Runners|Support for registration tokens is deprecated',
),
},
components: {
GlDropdown,
@ -51,14 +54,6 @@ export default {
};
},
computed: {
isDeprecated() {
// Show a compact version when used as secondary option
// create_runner_workflow_for_admin or create_runner_workflow_for_namespace
return (
this.glFeatures?.createRunnerWorkflowForAdmin ||
this.glFeatures?.createRunnerWorkflowForNamespace
);
},
actionText() {
switch (this.type) {
case INSTANCE_TYPE:
@ -71,30 +66,6 @@ export default {
return I18N_REGISTER_RUNNER;
}
},
dropdownText() {
if (this.isDeprecated) {
return '';
}
return this.actionText;
},
dropdownToggleClass() {
if (this.isDeprecated) {
return ['gl-px-3!'];
}
return [];
},
dropdownCategory() {
if (this.isDeprecated) {
return 'tertiary';
}
return 'primary';
},
dropdownVariant() {
if (this.isDeprecated) {
return 'default';
}
return 'confirm';
},
},
methods: {
onShowInstructionsClick() {
@ -113,22 +84,21 @@ export default {
<gl-dropdown
ref="runnerRegistrationDropdown"
menu-class="gl-w-auto!"
:text="dropdownText"
:toggle-class="dropdownToggleClass"
:variant="dropdownVariant"
:category="dropdownCategory"
toggle-class="gl-px-3!"
variant="default"
category="tertiary"
v-bind="$attrs"
>
<template v-if="isDeprecated" #button-content>
<template #button-content>
<span class="gl-sr-only">{{ actionText }}</span>
<gl-icon name="ellipsis_v" />
</template>
<gl-dropdown-form class="gl-p-4!">
<registration-token input-id="token-value" :value="currentRegistrationToken">
<template v-if="isDeprecated" #label-description>
<template #label-description>
<gl-icon name="warning" class="gl-text-orange-500" />
<span class="gl-text-secondary">
{{ s__('Runners|Support for registration tokens is deprecated') }}
{{ $options.i18n.supportForRegistrationTokensDeprecated }}
</span>
</template>
</registration-token>

View File

@ -11,7 +11,6 @@ import {
I18N_CREATE_RUNNER_LINK,
I18N_STILL_USING_REGISTRATION_TOKENS,
I18N_CONTACT_ADMIN_TO_REGISTER,
I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
I18N_NO_RESULTS,
I18N_EDIT_YOUR_SEARCH,
} from '~/ci/runner/constants';
@ -44,15 +43,6 @@ export default {
default: null,
},
},
computed: {
shouldShowCreateRunnerWorkflow() {
// create_runner_workflow_for_admin or create_runner_workflow_for_namespace
return (
this.glFeatures?.createRunnerWorkflowForAdmin ||
this.glFeatures?.createRunnerWorkflowForNamespace
);
},
},
modalId: 'runners-empty-state-instructions-modal',
svgHeight: 145,
EMPTY_STATE_SVG_URL,
@ -63,7 +53,6 @@ export default {
I18N_CREATE_RUNNER_LINK,
I18N_STILL_USING_REGISTRATION_TOKENS,
I18N_CONTACT_ADMIN_TO_REGISTER,
I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
I18N_NO_RESULTS,
I18N_EDIT_YOUR_SEARCH,
};
@ -85,39 +74,22 @@ export default {
>
<template #description>
{{ $options.I18N_RUNNERS_ARE_AGENTS }}
<template v-if="shouldShowCreateRunnerWorkflow">
<gl-sprintf v-if="newRunnerPath" :message="$options.I18N_CREATE_RUNNER_LINK">
<template #link="{ content }">
<gl-link :href="newRunnerPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
<template v-if="registrationToken">
<br />
<gl-link v-gl-modal="$options.modalId">{{
$options.I18N_STILL_USING_REGISTRATION_TOKENS
}}</gl-link>
<runner-instructions-modal
:modal-id="$options.modalId"
:registration-token="registrationToken"
/>
</template>
<template v-if="!newRunnerPath && !registrationToken">
{{ $options.I18N_CONTACT_ADMIN_TO_REGISTER }}
</template>
</template>
<gl-sprintf
v-else-if="registrationToken"
:message="$options.I18N_FOLLOW_REGISTRATION_INSTRUCTIONS"
>
<gl-sprintf v-if="newRunnerPath" :message="$options.I18N_CREATE_RUNNER_LINK">
<template #link="{ content }">
<gl-link v-gl-modal="$options.modalId">{{ content }}</gl-link>
<runner-instructions-modal
:modal-id="$options.modalId"
:registration-token="registrationToken"
/>
<gl-link :href="newRunnerPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
<template v-else>
<template v-if="registrationToken">
<br />
<gl-link v-gl-modal="$options.modalId">{{
$options.I18N_STILL_USING_REGISTRATION_TOKENS
}}</gl-link>
<runner-instructions-modal
:modal-id="$options.modalId"
:registration-token="registrationToken"
/>
</template>
<template v-if="!newRunnerPath && !registrationToken">
{{ $options.I18N_CONTACT_ADMIN_TO_REGISTER }}
</template>
</template>

View File

@ -117,9 +117,6 @@ export const I18N_STILL_USING_REGISTRATION_TOKENS = s__('Runners|Still using reg
export const I18N_CONTACT_ADMIN_TO_REGISTER = s__(
'Runners|To register new runners, contact your administrator.',
);
export const I18N_FOLLOW_REGISTRATION_INSTRUCTIONS = s__(
'Runners|Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.',
);
// No runners found
export const I18N_NO_RESULTS = s__('Runners|No results found');

View File

@ -155,10 +155,6 @@ export default {
isSearchFiltered() {
return isSearchFiltered(this.search);
},
shouldShowCreateRunnerWorkflow() {
// create_runner_workflow_for_namespace feature flag
return this.glFeatures.createRunnerWorkflowForNamespace;
},
},
watch: {
search: {
@ -231,11 +227,7 @@ export default {
/>
<div class="gl-w-full gl-md-w-auto gl-display-flex">
<gl-button
v-if="shouldShowCreateRunnerWorkflow && newRunnerPath"
:href="newRunnerPath"
variant="confirm"
>
<gl-button v-if="newRunnerPath" :href="newRunnerPath" variant="confirm">
{{ s__('Runners|New group runner') }}
</gl-button>
<registration-dropdown

View File

@ -19,12 +19,12 @@ export default {
locked: {
icon: 'lock',
class: 'value',
iconClass: 'is-active',
displayText: __('Locked'),
},
unlocked: {
class: ['no-value hide-collapsed'],
icon: 'lock-open',
iconClass: '',
displayText: __('Unlocked'),
},
components: {
EditForm,
@ -49,8 +49,6 @@ export default {
issueCapitalized: __('Issue'),
mergeRequest: __('merge request'),
mergeRequestCapitalized: __('Merge request'),
locked: __('Locked'),
unlocked: __('Unlocked'),
lockingMergeRequest: __('Locking %{issuableDisplayName}'),
unlockingMergeRequest: __('Unlocking %{issuableDisplayName}'),
lockMergeRequest: __('Lock %{issuableDisplayName}'),
@ -84,10 +82,7 @@ export default {
return this.getNoteableData.discussion_locked;
},
lockStatus() {
return this.isLocked ? this.$options.i18n.locked : this.$options.i18n.unlocked;
},
tooltipLabel() {
return this.isLocked ? this.$options.i18n.locked : this.$options.i18n.unlocked;
return this.isLocked ? this.$options.locked : this.$options.unlocked;
},
lockToggleInProgressText() {
return this.isLocked ? this.unlockingMergeRequestText : this.lockingMergeRequestText;
@ -205,7 +200,7 @@ export default {
</gl-disclosure-dropdown-item>
<div v-else class="block issuable-sidebar-item lock">
<div
v-gl-tooltip.left.viewport="{ title: tooltipLabel }"
v-gl-tooltip.left.viewport="{ title: lockStatus.displayText }"
class="sidebar-collapsed-icon"
data-testid="sidebar-collapse-icon"
@click="toggleForm"
@ -239,7 +234,7 @@ export default {
/>
<div data-testid="lock-status" class="sidebar-item-value" :class="lockStatus.class">
{{ lockStatus }}
{{ lockStatus.displayText }}
</div>
</div>
</div>

View File

@ -122,14 +122,6 @@ export default {
return null;
}
},
showDeprecationAlert() {
return (
// create_runner_workflow_for_admin
this.glFeatures.createRunnerWorkflowForAdmin ||
// create_runner_workflow_for_namespace
this.glFeatures.createRunnerWorkflowForNamespace
);
},
},
updated() {
// Refocus on dom changes, after loading data
@ -200,12 +192,7 @@ export default {
v-on="$listeners"
@shown="onShown"
>
<gl-alert
v-if="showDeprecationAlert"
:title="$options.i18n.deprecationAlertTitle"
variant="warning"
:dismissible="false"
>
<gl-alert :title="$options.i18n.deprecationAlertTitle" variant="warning" :dismissible="false">
<gl-sprintf :message="$options.i18n.deprecationAlertContent">
<template #link="{ content }">
<gl-link target="_blank" :href="$options.LEGACY_REGISTER_HELP_URL"

View File

@ -5,10 +5,6 @@ class Admin::RunnersController < Admin::ApplicationController
before_action :runner, except: [:index, :new, :tag_list, :runner_setup_scripts]
before_action only: [:index] do
push_frontend_feature_flag(:create_runner_workflow_for_admin, current_user)
end
feature_category :runner
urgency :low
@ -23,12 +19,10 @@ class Admin::RunnersController < Admin::ApplicationController
end
def new
render_404 unless Feature.enabled?(:create_runner_workflow_for_admin, current_user)
end
def register
render_404 unless Feature.enabled?(:create_runner_workflow_for_admin, current_user) &&
runner.registration_available?
render_404 unless runner.registration_available?
end
def update

View File

@ -6,10 +6,6 @@ class Groups::RunnersController < Groups::ApplicationController
before_action :authorize_update_runner!, only: [:edit, :update, :destroy, :pause, :resume]
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show, :register]
before_action only: [:index] do
push_frontend_feature_flag(:create_runner_workflow_for_namespace, group)
end
feature_category :runner
urgency :low
@ -35,11 +31,10 @@ class Groups::RunnersController < Groups::ApplicationController
end
def new
render_404 unless create_runner_workflow_for_namespace_enabled?
end
def register
render_404 unless create_runner_workflow_for_namespace_enabled? && runner.registration_available?
render_404 unless runner.registration_available?
end
private
@ -67,10 +62,6 @@ class Groups::RunnersController < Groups::ApplicationController
render_404
end
def create_runner_workflow_for_namespace_enabled?
Feature.enabled?(:create_runner_workflow_for_namespace, group)
end
end
Groups::RunnersController.prepend_mod

View File

@ -24,11 +24,10 @@ class Projects::RunnersController < Projects::ApplicationController
end
def new
render_404 unless create_runner_workflow_for_namespace_enabled?
end
def register
render_404 unless create_runner_workflow_for_namespace_enabled? && runner.registration_available?
render_404 unless runner.registration_available?
end
def destroy
@ -84,8 +83,4 @@ class Projects::RunnersController < Projects::ApplicationController
def runner_params
params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
end
def create_runner_workflow_for_namespace_enabled?
Feature.enabled?(:create_runner_workflow_for_namespace, project.namespace)
end
end

View File

@ -15,7 +15,6 @@ module Projects
before_action do
push_frontend_feature_flag(:ci_variables_pages, current_user)
push_frontend_feature_flag(:ci_limit_environment_scope, @project)
push_frontend_feature_flag(:create_runner_workflow_for_namespace, @project.namespace)
push_frontend_feature_flag(:frozen_outbound_job_token_scopes, @project)
push_frontend_feature_flag(:frozen_outbound_job_token_scopes_override, @project)
end

View File

@ -37,8 +37,6 @@ module Mutations
parse_gid(**args)
check_feature_flag(**args)
super
end
@ -79,28 +77,6 @@ module Mutations
GitlabSchema.parse_gid(args[:project_id], expected_type: ::Project)
end
end
def check_feature_flag(**args)
case args[:runner_type]
when 'instance_type'
if Feature.disabled?(:create_runner_workflow_for_admin, current_user)
raise Gitlab::Graphql::Errors::ResourceNotAvailable,
'`create_runner_workflow_for_admin` feature flag is disabled.'
end
when 'group_type'
namespace = find_object(**args).sync
if Feature.disabled?(:create_runner_workflow_for_namespace, namespace)
raise Gitlab::Graphql::Errors::ResourceNotAvailable,
'`create_runner_workflow_for_namespace` feature flag is disabled.'
end
when 'project_type'
project = find_object(**args).sync
if project && Feature.disabled?(:create_runner_workflow_for_namespace, project.namespace)
raise Gitlab::Graphql::Errors::ResourceNotAvailable,
'`create_runner_workflow_for_namespace` feature flag is disabled.'
end
end
end
end
end
end

View File

@ -22,10 +22,6 @@ class GlobalPolicy < BasePolicy
condition(:project_bot, scope: :user) { @user&.project_bot? }
condition(:migration_bot, scope: :user) { @user&.migration_bot? }
condition(:create_runner_workflow_enabled, scope: :user) do
Feature.enabled?(:create_runner_workflow_for_admin, @user)
end
condition(:service_account, scope: :user) { @user&.service_account? }
rule { anonymous }.policy do
@ -128,10 +124,6 @@ class GlobalPolicy < BasePolicy
enable :create_instance_runner
end
rule { ~create_runner_workflow_enabled }.policy do
prevent :create_instance_runner
end
# We can't use `read_statistics` because the user may have different permissions for different projects
rule { admin }.enable :use_project_statistics_filters

View File

@ -97,10 +97,6 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
with_scope :subject
condition(:crm_enabled, score: 0, scope: :subject) { @subject.crm_enabled? }
condition(:create_runner_workflow_enabled) do
Feature.enabled?(:create_runner_workflow_for_namespace, group)
end
condition(:achievements_enabled, scope: :subject) do
Feature.enabled?(:achievements, @subject)
end
@ -375,10 +371,6 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :admin_observability
end
rule { ~create_runner_workflow_enabled }.policy do
prevent :create_runner
end
# Should be matched with ProjectPolicy#read_internal_note
rule { admin | reporter }.enable :read_internal_note

View File

@ -253,10 +253,6 @@ class ProjectPolicy < BasePolicy
!Gitlab.config.terraform_state.enabled
end
condition(:create_runner_workflow_enabled) do
Feature.enabled?(:create_runner_workflow_for_namespace, project.namespace)
end
condition(:namespace_catalog_available) { namespace_catalog_available? }
# `:read_project` may be prevented in EE, but `:read_project_for_iids` should
@ -886,10 +882,6 @@ class ProjectPolicy < BasePolicy
enable :read_code
end
rule { ~create_runner_workflow_enabled }.policy do
prevent :create_runner
end
# Should be matched with GroupPolicy#read_internal_note
rule { admin | can?(:reporter_access) }.enable :read_internal_note

View File

@ -7,12 +7,12 @@
- if Feature.enabled?(:edit_user_profile_vue, current_user)
.js-user-profile
- else
= gitlab_ui_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user js-edit-user gl-mt-3 js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
.row.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= gitlab_ui_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user js-edit-user js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
.js-search-settings-section.gl-pb-6
.profile-settings-sidebar
%h4.gl-my-0
= s_("Profiles|Public avatar")
%p
%p.gl-text-secondary
- if @user.avatar?
- if gravatar_enabled?
= s_("Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}").html_safe % { gravatar_link: gravatar_link }
@ -26,31 +26,29 @@
- if current_appearance&.profile_image_guidelines?
.md
= brand_profile_image_guidelines
.col-lg-8
.avatar-image
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= render Pajamas::AvatarComponent.new(@user, size: 96, alt: "", class: 'gl-float-left gl-mr-5')
%h5.gl-mt-0= s_("Profiles|Upload new avatar")
.gl-display-flex.gl-align-items-center.gl-my-3
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-choose-user-avatar-button' }) do
= s_("Profiles|Choose file...")
%span.gl-ml-3.js-avatar-filename= s_("Profiles|No file chosen.")
= f.file_field :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*'
.gl-text-gray-500= s_("Profiles|The maximum file size allowed is 200KB.")
- if @user.avatar?
= render Pajamas::ButtonComponent.new(variant: :danger,
category: :secondary,
href: profile_avatar_path,
button_options: { class: 'gl-mt-3', data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") } },
method: :delete) do
= s_("Profiles|Remove avatar")
.col-lg-12
%hr
.row.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0= s_("Profiles|Current status")
%p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
.col-lg-8
.avatar-image
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= render Pajamas::AvatarComponent.new(@user, size: 96, alt: "", class: 'gl-float-left gl-mr-5')
%h5.gl-mt-0= s_("Profiles|Upload new avatar")
.gl-display-flex.gl-align-items-center.gl-my-3
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-choose-user-avatar-button' }) do
= s_("Profiles|Choose file...")
%span.gl-ml-3.js-avatar-filename= s_("Profiles|No file chosen.")
= f.file_field :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*'
.gl-text-gray-500= s_("Profiles|The maximum file size allowed is 200KB.")
- if @user.avatar?
= render Pajamas::ButtonComponent.new(variant: :danger,
category: :secondary,
href: profile_avatar_path,
button_options: { class: 'gl-mt-3', data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") } },
method: :delete) do
= s_("Profiles|Remove avatar")
.js-search-settings-section.gl-border-t.gl-py-6
.profile-settings-sidebar
%h4.gl-my-0= s_("Profiles|Current status")
%p.gl-text-secondary= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
.gl-max-w-80
#js-user-profile-set-status-form
= f.fields_for :status, @user.status do |status_form|
= status_form.hidden_field :emoji, data: { js_name: 'emoji' }
@ -59,121 +57,114 @@
= status_form.hidden_field :clear_status_after,
value: user_clear_status_at(@user),
data: { js_name: 'clearStatusAfter' }
.col-lg-12
%hr
.row.user-time-preferences.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0= s_("Profiles|Time settings")
%p= s_("Profiles|Set your local time zone.")
.col-lg-8
= f.label :user_timezone, _("Time zone")
.js-timezone-dropdown{ data: { timezone_data: timezone_data.to_json, initial_value: @user.timezone, name: 'user[timezone]' } }
.col-lg-12
%hr
.row.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
.user-time-preferences.js-search-settings-section.gl-border-t.gl-py-6
.profile-settings-sidebar
%h4.gl-my-0= s_("Profiles|Time settings")
%p.gl-text-secondary= s_("Profiles|Set your local time zone.")
= f.label :user_timezone, _("Time zone")
.js-timezone-dropdown{ data: { timezone_data: timezone_data.to_json, initial_value: @user.timezone, name: 'user[timezone]' } }
.js-search-settings-section.gl-border-t.gl-py-6
.profile-settings-sidebar
%h4.gl-my-0
= s_("Profiles|Main settings")
%p
%p.gl-text-secondary
= s_("Profiles|This information will appear on your profile.")
- if current_user.ldap_user?
= s_("Profiles|Some options are unavailable for LDAP accounts")
.col-lg-8
.row
.form-group.gl-form-group.col-md-9.rspec-full-name
= render 'profiles/name', form: f, user: @user
.form-group.gl-form-group.col-md-3
= f.label :id, s_('Profiles|User ID')
= f.text_field :id, class: 'gl-form-input form-control', readonly: true
.form-group.gl-form-group
= f.label :pronouns, s_('Profiles|Pronouns')
= f.text_field :pronouns, class: 'gl-form-input form-control gl-md-form-input-lg'
%small.form-text.text-gl-muted
= s_("Profiles|Enter your pronouns to let people know how to refer to you.")
.form-group.gl-form-group
= f.label :pronunciation, s_('Profiles|Pronunciation')
= f.text_field :pronunciation, class: 'gl-form-input form-control gl-md-form-input-lg'
%small.form-text.text-gl-muted
= s_("Profiles|Enter how your name is pronounced to help people address you correctly.")
= render_if_exists 'profiles/extra_settings', form: f
= render_if_exists 'profiles/email_settings', form: f
.form-group.gl-form-group
= f.label :skype
= f.text_field :skype, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|username")
.form-group.gl-form-group
= f.label :linkedin
= f.text_field :linkedin, class: 'gl-form-input form-control gl-md-form-input-lg'
%small.form-text.text-gl-muted
= s_("Profiles|Your LinkedIn profile name from linkedin.com/in/profilename")
.form-group.gl-form-group
= f.label :twitter
= f.text_field :twitter, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|@username")
.form-group.gl-form-group
- external_accounts_help_url = help_page_path('user/profile/index', anchor: 'add-external-accounts-to-your-user-profile-page')
- external_accounts_link = link_to '', external_accounts_help_url, target: "_blank", rel: "noopener noreferrer"
- external_accounts_docs_link = safe_format(s_('Profiles|Your Discord user ID. %{external_accounts_link_start}Learn more.%{external_accounts_link_end}'), tag_pair(external_accounts_link, :external_accounts_link_start, :external_accounts_link_end))
- min_discord_length = 17
- max_discord_length = 20
= f.label :discord
= f.text_field :discord,
class: 'gl-form-input form-control gl-md-form-input-lg js-validate-length',
placeholder: s_("Profiles|User ID"),
data: { min_length: min_discord_length,
min_length_message: s_('Profiles|Discord ID is too short (minimum is %{min_length} characters).') % { min_length: min_discord_length },
max_length: max_discord_length,
max_length_message: s_('Profiles|Discord ID is too long (maximum is %{max_length} characters).') % { max_length: max_discord_length },
allow_empty: true}
%small.form-text.text-gl-muted
= external_accounts_docs_link
.form-group.gl-form-group.rspec-full-name.gl-max-w-80
= render 'profiles/name', form: f, user: @user
.form-group.gl-form-group.gl-md-form-input-lg
= f.label :id, s_('Profiles|User ID')
= f.text_field :id, class: 'gl-form-input form-control', readonly: true
.form-group.gl-form-group
= f.label :pronouns, s_('Profiles|Pronouns')
= f.text_field :pronouns, class: 'gl-form-input form-control gl-md-form-input-lg'
%small.form-text.text-gl-muted
= s_("Profiles|Enter your pronouns to let people know how to refer to you.")
.form-group.gl-form-group
= f.label :pronunciation, s_('Profiles|Pronunciation')
= f.text_field :pronunciation, class: 'gl-form-input form-control gl-md-form-input-lg'
%small.form-text.text-gl-muted
= s_("Profiles|Enter how your name is pronounced to help people address you correctly.")
= render_if_exists 'profiles/extra_settings', form: f
= render_if_exists 'profiles/email_settings', form: f
.form-group.gl-form-group
= f.label :skype
= f.text_field :skype, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|username")
.form-group.gl-form-group
= f.label :linkedin
= f.text_field :linkedin, class: 'gl-form-input form-control gl-md-form-input-lg'
%small.form-text.text-gl-muted
= s_("Profiles|Your LinkedIn profile name from linkedin.com/in/profilename")
.form-group.gl-form-group
= f.label :twitter
= f.text_field :twitter, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|@username")
.form-group.gl-form-group
- external_accounts_help_url = help_page_path('user/profile/index', anchor: 'add-external-accounts-to-your-user-profile-page')
- external_accounts_link = link_to '', external_accounts_help_url, target: "_blank", rel: "noopener noreferrer"
- external_accounts_docs_link = safe_format(s_('Profiles|Your Discord user ID. %{external_accounts_link_start}Learn more.%{external_accounts_link_end}'), tag_pair(external_accounts_link, :external_accounts_link_start, :external_accounts_link_end))
- min_discord_length = 17
- max_discord_length = 20
= f.label :discord
= f.text_field :discord,
class: 'gl-form-input form-control gl-md-form-input-lg js-validate-length',
placeholder: s_("Profiles|User ID"),
data: { min_length: min_discord_length,
min_length_message: s_('Profiles|Discord ID is too short (minimum is %{min_length} characters).') % { min_length: min_discord_length },
max_length: max_discord_length,
max_length_message: s_('Profiles|Discord ID is too long (maximum is %{max_length} characters).') % { max_length: max_discord_length },
allow_empty: true}
%small.form-text.text-gl-muted
= external_accounts_docs_link
.form-group.gl-form-group
= f.label :website_url, s_('Profiles|Website url')
= f.text_field :website_url, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|https://website.com")
.form-group.gl-form-group
= f.label :location, s_('Profiles|Location')
- if @user.read_only_attribute?(:location)
= f.text_field :location, class: 'gl-form-input form-control gl-md-form-input-lg', readonly: true
%small.form-text.text-gl-muted
= s_("Profiles|Your location was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:location) }
- else
= f.text_field :location, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|City, country")
.form-group.gl-form-group
= f.label :job_title, s_('Profiles|Job title')
= f.text_field :job_title, class: 'gl-form-input form-control gl-md-form-input-lg'
.form-group.gl-form-group
= f.label :organization, s_('Profiles|Organization')
= f.text_field :organization, class: 'gl-form-input form-control gl-md-form-input-lg'
.form-group.gl-form-group
= f.label :website_url, s_('Profiles|Website url')
= f.text_field :website_url, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|https://website.com")
.form-group.gl-form-group
= f.label :location, s_('Profiles|Location')
- if @user.read_only_attribute?(:location)
= f.text_field :location, class: 'gl-form-input form-control gl-md-form-input-lg', readonly: true
%small.form-text.text-gl-muted
= s_("Profiles|Who you represent or work for.")
.form-group.gl-form-group
= f.label :bio, s_('Profiles|Bio')
= f.text_area :bio, class: 'gl-form-input gl-form-textarea form-control', rows: 4, maxlength: 250
%small.form-text.text-gl-muted
= s_("Profiles|Tell us about yourself in fewer than 250 characters.")
%hr
= s_("Profiles|Your location was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:location) }
- else
= f.text_field :location, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|City, country")
.form-group.gl-form-group
= f.label :job_title, s_('Profiles|Job title')
= f.text_field :job_title, class: 'gl-form-input form-control gl-md-form-input-lg'
.form-group.gl-form-group
= f.label :organization, s_('Profiles|Organization')
= f.text_field :organization, class: 'gl-form-input form-control gl-md-form-input-lg'
%small.form-text.text-gl-muted
= s_("Profiles|Who you represent or work for.")
.form-group.gl-form-group.gl-mb-6.gl-max-w-80
= f.label :bio, s_('Profiles|Bio')
= f.text_area :bio, class: 'gl-form-input gl-form-textarea form-control', rows: 4, maxlength: 250
%small.form-text.text-gl-muted
= s_("Profiles|Tell us about yourself in fewer than 250 characters.")
.gl-border-t.gl-pt-6
%fieldset.form-group.gl-form-group
%legend.col-form-label.col-form-label
%legend.col-form-label
= _('Private profile')
- private_profile_label = s_("Profiles|Don't display activity-related personal information on your profile.")
- private_profile_help_link = link_to sprite_icon('question-o'), help_page_path('user/profile/index.md', anchor: 'make-your-user-profile-page-private')
= f.gitlab_ui_checkbox_component :private_profile, '%{private_profile_label} %{private_profile_help_link}'.html_safe % { private_profile_label: private_profile_label, private_profile_help_link: private_profile_help_link.html_safe }
%fieldset.form-group.gl-form-group
%legend.col-form-label.col-form-label
%legend.col-form-label
= s_("Profiles|Private contributions")
= f.gitlab_ui_checkbox_component :include_private_contributions,
s_('Profiles|Include private contributions on your profile'),
help_text: s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information.")
%fieldset.form-group.gl-form-group
%legend.col-form-label.col-form-label
%fieldset.form-group.gl-form-group.gl-mb-0
%legend.col-form-label
= s_("Profiles|Achievements")
= f.gitlab_ui_checkbox_component :achievements_enabled,
s_('Profiles|Display achievements on your profile')
.row.js-hide-when-nothing-matches-search
.col-lg-12
%hr
= f.submit s_("Profiles|Update profile settings"), class: 'gl-mr-3 js-password-prompt-btn', pajamas_button: true
= render Pajamas::ButtonComponent.new(href: user_path(current_user)) do
= s_('TagsPage|Cancel')
.js-hide-when-nothing-matches-search.gl-border-t.gl-py-6
= f.submit s_("Profiles|Update profile settings"), class: 'gl-mr-3 js-password-prompt-btn', pajamas_button: true
= render Pajamas::ButtonComponent.new(href: user_path(current_user)) do
= s_('TagsPage|Cancel')
#password-prompt-modal

View File

@ -3,26 +3,14 @@
.bs-callout.help-callout
%p= s_('Runners|These runners are assigned to this project.')
- if Feature.enabled?(:create_runner_workflow_for_namespace, @project.namespace)
- if can?(current_user, :create_runner, @project)
= render Pajamas::ButtonComponent.new(href: new_project_runner_path(@project), variant: :confirm) do
= s_('Runners|New project runner')
.gl-display-inline
#js-project-runner-registration-dropdown{ data: { registration_token: @project.runners_token, project_id: @project.id } }
- else
= _('Please contact an admin to create runners.')
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'restrict-runner-registration-by-all-users-in-an-instance'), target: '_blank', rel: 'noopener noreferrer'
- if can?(current_user, :create_runner, @project)
= render Pajamas::ButtonComponent.new(href: new_project_runner_path(@project), variant: :confirm) do
= s_('Runners|New project runner')
.gl-display-inline
#js-project-runner-registration-dropdown{ data: { registration_token: @project.runners_token, project_id: @project.id } }
- else
- if can?(current_user, :register_project_runners, @project)
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: @project.runners_token,
type: _('project'),
reset_token_url: reset_registration_token_namespace_project_settings_ci_cd_path,
project_path: @project.path_with_namespace,
group_path: '' }
- else
= _('Please contact an admin to register runners.')
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'restrict-runner-registration-by-all-users-in-an-instance'), target: '_blank', rel: 'noopener noreferrer'
= _('Please contact an admin to create runners.')
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'restrict-runner-registration-by-all-users-in-an-instance'), target: '_blank', rel: 'noopener noreferrer'
%hr

View File

@ -1,8 +0,0 @@
---
name: create_runner_workflow_for_admin
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109497
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/389269
milestone: '15.9'
type: development
group: group::runner
default_enabled: true

View File

@ -1,8 +0,0 @@
---
name: create_runner_workflow_for_namespace
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113535
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/393919
milestone: '15.10'
type: development
group: group::runner
default_enabled: true

View File

@ -1,7 +1,7 @@
---
data_category: optional
key_path: usage_activity_by_stage_monthly.plan.service_desk_enabled_projects
description: Count creator ids from projects with service desk enabled
description: Count unique users who created projects (creator_id) with service desk enabled
product_section: ops
product_stage: monitor
product_group: respond

View File

@ -1,7 +1,7 @@
---
data_category: optional
key_path: usage_activity_by_stage.plan.service_desk_enabled_projects
description: Count creator ids from projects with service desk enabled
description: Count unique users who created projects (creator_id) with service desk enabled
product_section: ops
product_stage: monitor
product_group: respond

View File

@ -6,6 +6,9 @@ development:
cluster_cache:
cluster:
- redis://localhost:7001
feature_flag:
cluster:
- redis://localhost:7001
rate_limiting:
cluster:
- redis://localhost:7001
@ -17,6 +20,9 @@ test:
cluster_cache:
cluster:
- redis://localhost:7001
feature_flag:
cluster:
- redis://localhost:7001
rate_limiting:
cluster:
- redis://localhost:7001

View File

@ -105,8 +105,6 @@ concurrency is set to:
When `min_concurrency` is greater than `max_concurrency`, it is treated as
being equal to `max_concurrency`.
You can find example values used by GitLab.com by searching for `concurrency:`
in [the Helm charts](https://gitlab.com/gitlab-com/gl-infra/k8s-workloads/gitlab-com/-/blob/master/releases/gitlab/values/gprd.yaml.gotmpl).
The values vary according to the work each specific deployment of Sidekiq does.
Any other specialized deployments with processes dedicated to specific queues
should have the concurrency tuned according to:

View File

@ -38,10 +38,7 @@ If you are using GitLab.com:
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/383139) in GitLab 15.10. Deployed behind the `create_runner_workflow_for_admin` [flag](../../administration/feature_flags.md)
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/389269) in GitLab 16.0.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature,
ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `create_runner_workflow_for_admin`.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/415447) in GitLab 16.2. Feature flag `create_runner_workflow_for_admin` removed.
Prerequisites:
@ -195,10 +192,7 @@ Group runners process jobs by using a first in, first out ([FIFO](https://en.wik
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/383143) in GitLab 15.10. Deployed behind the `create_runner_workflow_for_namespace` [flag](../../administration/feature_flags.md). Disabled by default.
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/393919) in GitLab 16.0.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature,
ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `create_runner_workflow_for_namespace`.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/415447) in GitLab 16.2. Feature flag `create_runner_workflow_for_admin` removed.
Prerequisites:
@ -328,10 +322,7 @@ A fork *does* copy the CI/CD settings of the cloned repository.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/383143) in GitLab 15.10. Deployed behind the `create_runner_workflow_for_namespace` [flag](../../administration/feature_flags.md). Disabled by default.
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/393919) in GitLab 16.0.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature,
ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `create_runner_workflow_for_namespace`.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/415447) in GitLab 16.2. Feature flag `create_runner_workflow_for_admin` removed.
Prerequisites:

View File

@ -863,9 +863,9 @@ PUT /namespaces/:id/subscription_add_on_purchase/:add_on_name
| Attribute | Type | Required | Description |
|:------------|:--------|:---------|:------------|
| `quantity` | integer | yes | Amount of units in the subscription add-on purchase (Example: Number of seats for a code suggestions add-on) |
| `quantity` | integer | no | Amount of units in the subscription add-on purchase (Example: Number of seats for a code suggestions add-on) |
| `expires_on` | date | yes | Expiration date of the subscription add-on purchase |
| `purchase_xid` | string | yes | Identifier for the subscription add-on purchase (Example: Subscription name for a code suggestions add-on) |
| `purchase_xid` | string | no | Identifier for the subscription add-on purchase (Example: Subscription name for a code suggestions add-on) |
Example request:

View File

@ -1,6 +1,6 @@
---
stage: Create
group: Ecosystem
stage: Manage
group: Authentication and Authorization
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---

View File

@ -347,12 +347,13 @@ If you have difficulty during the renewal process, contact the
Contacts can renew a subscription, cancel a subscription, or transfer the subscription to a different namespace.
To change the contacts:
For information about how to transfer ownership of the Customers Portal account to another person, see
[Change account owner information](../customers_portal.md#change-account-owner-information).
To add a secondary contact for your subscription:
1. Ensure an account exists in the
[Customers Portal](https://customers.gitlab.com/customers/sign_in) for the user you want to add.
1. Verify you have access to at least one of
[these requirements](https://about.gitlab.com/handbook/support/license-and-renewals/workflows/customersdot/associating_purchases.html).
1. [Create a ticket with the Support team](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=360000071293). Include any relevant material in your request.
## Compute

View File

@ -428,12 +428,13 @@ The new tier takes effect when the new license is activated.
Contacts can renew a subscription, cancel a subscription, or transfer the subscription to a different namespace.
To change the contacts:
For information about how to transfer ownership of the Customers Portal account to another person, see
[Change account owner information](../customers_portal.md#change-account-owner-information).
To add a secondary contact for your subscription:
1. Ensure an account exists in the
[Customers Portal](https://customers.gitlab.com/customers/sign_in) for the user you want to add.
1. Verify you have access to at least one of
[these requirements](https://about.gitlab.com/handbook/support/license-and-renewals/workflows/customersdot/associating_purchases.html).
1. [Create a ticket with the Support team](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=360000071293). Include any relevant material in your request.
## Subscription expiry

View File

@ -37,6 +37,7 @@ You can use push options to skip a CI/CD pipeline, or pass CI/CD variables.
| ------------------------------ | ------------------------------------------------------------------------------------------- |---------------------- |
| `ci.skip` | Do not create a CI pipeline for the latest push. Only skips branch pipelines and not [merge request pipelines](../../ci/pipelines/merge_request_pipelines.md). This does not skip pipelines for CI integrations, such as Jenkins. | [11.7](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/15643) |
| `ci.variable="<name>=<value>"` | Provide [CI/CD variables](../../ci/variables/index.md) to be used in a CI pipeline, if one is created due to the push. Only passes variables to branch pipelines and not [merge request pipelines](../../ci/pipelines/merge_request_pipelines.md). | [12.6](https://gitlab.com/gitlab-org/gitlab/-/issues/27983) |
| `integrations.skip_ci` | Skip push events for CI integrations, such as Atlassian Bamboo, Buildkite, Drone, Jenkins, and JetBrains TeamCity. | [16.2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123837) |
An example of using `ci.skip`:
@ -50,6 +51,12 @@ An example of passing some CI/CD variables for a pipeline:
git push -o ci.variable="MAX_RETRIES=10" -o ci.variable="MAX_TIME=600"
```
An example of using `integrations.skip_ci`:
```shell
git push -o integrations.skip_ci
```
## Push options for merge requests
You can use Git push options to perform certain actions for merge requests at the same

View File

@ -42,8 +42,10 @@ module Gitlab
with_redis do |redis|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
if Gitlab::Redis::ClusterUtil.cluster?(redis)
Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
keys.each { |key| pipeline.get(key) }
redis.with_readonly_pipeline do
Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
keys.each { |key| pipeline.get(key) }
end
end
else
redis.mget(keys)

View File

@ -15,12 +15,7 @@ module Gitlab
#
# @return [self]
def self.build(project)
strategy =
if Feature.enabled?(:gitlab_error_tracking, project)
OpenApiStrategy.new(project)
else
ActiveRecordStrategy.new(project)
end
strategy = OpenApiStrategy.new(project)
new(strategy)
end

View File

@ -1,117 +0,0 @@
# frozen_string_literal: true
module Gitlab
module ErrorTracking
class ErrorRepository
class ActiveRecordStrategy
def initialize(project)
@project = project
end
def report_error(
name:, description:, actor:, platform:,
environment:, level:, occurred_at:, payload:
)
error = project_errors.report_error(
name: name, # Example: ActionView::MissingTemplate
description: description, # Example: Missing template posts/show in...
actor: actor, # Example: PostsController#show
platform: platform, # Example: ruby
timestamp: occurred_at
)
# The payload field contains all the data on error including stacktrace in jsonb.
# Together with occurred_at these are 2 main attributes that we need to save here.
error.events.create!(
environment: environment,
description: description,
level: level,
occurred_at: occurred_at,
payload: payload
)
rescue ActiveRecord::ActiveRecordError => e
handle_exceptions(e)
end
def find_error(id)
project_error(id).to_sentry_detailed_error
rescue ActiveRecord::ActiveRecordError => e
handle_exceptions(e)
end
def list_errors(filters:, query:, sort:, limit:, cursor:)
errors = project_errors
errors = filter_by_status(errors, filters[:status])
errors = sort(errors, sort)
errors = errors.keyset_paginate(cursor: cursor, per_page: limit)
# query is not supported
pagination = ErrorRepository::Pagination.new(errors.cursor_for_next_page, errors.cursor_for_previous_page)
[errors.map(&:to_sentry_error), pagination]
end
def last_event_for(id)
project_error(id).last_event&.to_sentry_error_event
rescue ActiveRecord::ActiveRecordError => e
handle_exceptions(e)
end
def update_error(id, **attributes)
project_error(id).update(attributes)
end
def dsn_url(public_key)
gitlab = Settings.gitlab
custom_port = Settings.gitlab_on_standard_port? ? nil : ":#{gitlab.port}"
base_url = [
gitlab.protocol,
"://",
public_key,
'@',
gitlab.host,
custom_port,
gitlab.relative_url_root
].join('')
"#{base_url}/api/v4/error_tracking/collector/#{project.id}"
end
private
attr_reader :project
def project_errors
::ErrorTracking::Error.where(project: project) # rubocop:disable CodeReuse/ActiveRecord
end
def project_error(id)
project_errors.find(id)
end
def filter_by_status(errors, status)
return errors unless ::ErrorTracking::Error.statuses.key?(status)
errors.for_status(status)
end
def sort(errors, sort)
return errors.order_id_desc unless sort
errors.sort_by_attribute(sort)
end
def handle_exceptions(exception)
case exception
when ActiveRecord::RecordInvalid
raise RecordInvalidError, exception.message
else
raise DatabaseError, exception.message
end
end
end
end
end
end

View File

@ -11,9 +11,11 @@ module Gitlab
Gitlab::Redis::Cache.with do |r|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
Gitlab::Redis::CrossSlot::Pipeline.new(r).pipelined do |pipeline|
subjects.each do |subject|
results[subject.cache_key] = new(subject).read(pipeline)
r.with_readonly_pipeline do
Gitlab::Redis::CrossSlot::Pipeline.new(r).pipelined do |pipeline|
subjects.each do |subject|
results[subject.cache_key] = new(subject).read(pipeline)
end
end
end
end

View File

@ -43,7 +43,13 @@ module Gitlab
keys = names.map { |name| normalize_key(name, options) }
values = failsafe(:patched_read_multi_mget, returning: {}) do
redis.with { |c| pipeline_mget(c, keys) }
redis.with do |c|
if c.is_a?(Gitlab::Redis::MultiStore)
c.with_readonly_pipeline { pipeline_mget(c, keys) }
else
pipeline_mget(c, keys)
end
end
end
names.zip(values).each_with_object({}) do |(name, value), results|

View File

@ -24,6 +24,12 @@ module Gitlab
end
end
class NestedReadonlyPipelineError < StandardError
def message
'Nested use of with_readonly_pipeline is detected.'
end
end
attr_reader :primary_store, :secondary_store, :instance_name
FAILED_TO_READ_ERROR_MESSAGE = 'Failed to read from the redis default_store.'
@ -100,6 +106,25 @@ module Gitlab
validate_stores!
end
# Pipelines are sent to both instances by default since
# they could execute both read and write commands.
#
# But for pipelines that only consists of read commands, this method
# can be used to scope the pipeline and send it only to the default store.
def with_readonly_pipeline
raise NestedReadonlyPipelineError if readonly_pipeline?
Thread.current[:readonly_pipeline] = true
yield
ensure
Thread.current[:readonly_pipeline] = false
end
def readonly_pipeline?
Thread.current[:readonly_pipeline].present?
end
# rubocop:disable GitlabSecurity/PublicSend
READ_COMMANDS.each do |name|
define_method(name) do |*args, **kwargs, &block|
@ -123,7 +148,7 @@ module Gitlab
PIPELINED_COMMANDS.each do |name|
define_method(name) do |*args, **kwargs, &block|
if use_primary_and_secondary_stores?
if use_primary_and_secondary_stores? && !readonly_pipeline?
pipelined_both(name, *args, **kwargs, &block)
else
send_command(default_store, name, *args, **kwargs, &block)

View File

@ -34249,9 +34249,6 @@ msgstr ""
msgid "Please contact an admin to create runners."
msgstr ""
msgid "Please contact an admin to register runners."
msgstr ""
msgid "Please contact your GitLab administrator if you think this is an error."
msgstr ""
@ -39678,9 +39675,6 @@ msgstr ""
msgid "Runners|Filter projects"
msgstr ""
msgid "Runners|Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner."
msgstr ""
msgid "Runners|Get started with runners"
msgstr ""

View File

@ -41,18 +41,6 @@ RSpec.describe Admin::RunnersController, feature_category: :runner_fleet do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:new)
end
context 'when create_runner_workflow_for_admin is disabled' do
before do
stub_feature_flags(create_runner_workflow_for_admin: false)
end
it 'returns :not_found' do
get :new
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe '#register' do
@ -78,20 +66,6 @@ RSpec.describe Admin::RunnersController, feature_category: :runner_fleet do
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when create_runner_workflow_for_admin is disabled' do
let_it_be(:new_runner) { create(:ci_runner, registration_type: :authenticated_user) }
before do
stub_feature_flags(create_runner_workflow_for_admin: false)
end
it 'returns :not_found' do
register
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe '#edit' do

View File

@ -65,52 +65,28 @@ RSpec.describe Groups::RunnersController, feature_category: :runner_fleet do
end
describe '#new' do
context 'when create_runner_workflow_for_namespace is enabled' do
context 'when user is owner' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: [group])
group.add_owner(user)
end
context 'when user is owner' do
before do
group.add_owner(user)
end
it 'renders new with 200 status code' do
get :new, params: { group_id: group }
it 'renders new with 200 status code' do
get :new, params: { group_id: group }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:new)
end
end
context 'when user is not owner' do
before do
group.add_maintainer(user)
end
it 'renders a 404' do
get :new, params: { group_id: group }
expect(response).to have_gitlab_http_status(:not_found)
end
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:new)
end
end
context 'when create_runner_workflow_for_namespace is disabled' do
context 'when user is not owner' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: false)
group.add_maintainer(user)
end
context 'when user is owner' do
before do
group.add_owner(user)
end
it 'renders a 404' do
get :new, params: { group_id: group }
it 'renders a 404' do
get :new, params: { group_id: group }
expect(response).to have_gitlab_http_status(:not_found)
end
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
@ -118,66 +94,40 @@ RSpec.describe Groups::RunnersController, feature_category: :runner_fleet do
describe '#register' do
subject(:register) { get :register, params: { group_id: group, id: new_runner } }
context 'when create_runner_workflow_for_namespace is enabled' do
context 'when user is owner' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: [group])
group.add_owner(user)
end
context 'when user is owner' do
before do
group.add_owner(user)
end
context 'when runner can be registered after creation' do
let_it_be(:new_runner) { create(:ci_runner, :group, groups: [group], registration_type: :authenticated_user) }
context 'when runner can be registered after creation' do
let_it_be(:new_runner) { create(:ci_runner, :group, groups: [group], registration_type: :authenticated_user) }
it 'renders a :register template' do
register
it 'renders a :register template' do
register
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:register)
end
end
context 'when runner cannot be registered after creation' do
let_it_be(:new_runner) { runner }
it 'returns :not_found' do
register
expect(response).to have_gitlab_http_status(:not_found)
end
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:register)
end
end
context 'when user is not owner' do
before do
group.add_maintainer(user)
end
context 'when runner cannot be registered after creation' do
let_it_be(:new_runner) { runner }
context 'when runner can be registered after creation' do
let_it_be(:new_runner) { create(:ci_runner, :group, groups: [group], registration_type: :authenticated_user) }
it 'returns :not_found' do
register
it 'returns :not_found' do
register
expect(response).to have_gitlab_http_status(:not_found)
end
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'when create_runner_workflow_for_namespace is disabled' do
let_it_be(:new_runner) { create(:ci_runner, :group, groups: [group], registration_type: :authenticated_user) }
context 'when user is not owner' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: false)
group.add_maintainer(user)
end
context 'when user is owner' do
before do
group.add_owner(user)
end
context 'when runner can be registered after creation' do
let_it_be(:new_runner) { create(:ci_runner, :group, groups: [group], registration_type: :authenticated_user) }
it 'returns :not_found' do
register

View File

@ -28,52 +28,28 @@ RSpec.describe Projects::RunnersController, feature_category: :runner_fleet do
}
end
context 'when create_runner_workflow_for_namespace is enabled' do
context 'when user is maintainer' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
project.add_maintainer(user)
end
context 'when user is maintainer' do
before do
project.add_maintainer(user)
end
it 'renders new with 200 status code' do
get :new, params: params
it 'renders new with 200 status code' do
get :new, params: params
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:new)
end
end
context 'when user is not maintainer' do
before do
project.add_developer(user)
end
it 'renders a 404' do
get :new, params: params
expect(response).to have_gitlab_http_status(:not_found)
end
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:new)
end
end
context 'when create_runner_workflow_for_namespace is disabled' do
context 'when user is not maintainer' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: false)
project.add_developer(user)
end
context 'when user is maintainer' do
before do
project.add_maintainer(user)
end
it 'renders a 404' do
get :new, params: params
it 'renders a 404' do
get :new, params: params
expect(response).to have_gitlab_http_status(:not_found)
end
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
@ -81,66 +57,40 @@ RSpec.describe Projects::RunnersController, feature_category: :runner_fleet do
describe '#register' do
subject(:register) { get :register, params: { namespace_id: project.namespace, project_id: project, id: new_runner } }
context 'when create_runner_workflow_for_namespace is enabled' do
context 'when user is maintainer' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
project.add_maintainer(user)
end
context 'when user is maintainer' do
before do
project.add_maintainer(user)
end
context 'when runner can be registered after creation' do
let_it_be(:new_runner) { create(:ci_runner, :project, projects: [project], registration_type: :authenticated_user) }
context 'when runner can be registered after creation' do
let_it_be(:new_runner) { create(:ci_runner, :project, projects: [project], registration_type: :authenticated_user) }
it 'renders a :register template' do
register
it 'renders a :register template' do
register
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:register)
end
end
context 'when runner cannot be registered after creation' do
let_it_be(:new_runner) { runner }
it 'returns :not_found' do
register
expect(response).to have_gitlab_http_status(:not_found)
end
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:register)
end
end
context 'when user is not maintainer' do
before do
project.add_developer(user)
end
context 'when runner cannot be registered after creation' do
let_it_be(:new_runner) { runner }
context 'when runner can be registered after creation' do
let_it_be(:new_runner) { create(:ci_runner, :project, projects: [project], registration_type: :authenticated_user) }
it 'returns :not_found' do
register
it 'returns :not_found' do
register
expect(response).to have_gitlab_http_status(:not_found)
end
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'when create_runner_workflow_for_namespace is disabled' do
let_it_be(:new_runner) { create(:ci_runner, :project, projects: [project], registration_type: :authenticated_user) }
context 'when user is not maintainer' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: false)
project.add_developer(user)
end
context 'when user is maintainer' do
before do
project.add_maintainer(user)
end
context 'when runner can be registered after creation' do
let_it_be(:new_runner) { create(:ci_runner, :project, projects: [project], registration_type: :authenticated_user) }
it 'returns :not_found' do
register

View File

@ -32,30 +32,13 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
end
describe "runners registration" do
context 'when create_runner_workflow_for_namespace is enabled' do
before do
stub_feature_flags(create_runner_workflow_for_admin: true)
visit admin_runners_path
end
it_behaves_like "shows and resets runner registration token" do
let(:dropdown_text) { s_('Runners|Register an instance runner') }
let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token }
end
before do
visit admin_runners_path
end
context 'when create_runner_workflow_for_namespace is disabled' do
before do
stub_feature_flags(create_runner_workflow_for_admin: false)
visit admin_runners_path
end
it_behaves_like "shows and resets runner registration token" do
let(:dropdown_text) { s_('Runners|Register an instance runner') }
let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token }
end
it_behaves_like "shows and resets runner registration token" do
let(:dropdown_text) { s_('Runners|Register an instance runner') }
let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token }
end
end

View File

@ -16,21 +16,6 @@ RSpec.describe "Group Runners", feature_category: :runner_fleet do
end
describe "Group runners page", :js do
describe "legacy runners registration" do
let_it_be(:group_registration_token) { group.runners_token }
before do
stub_feature_flags(create_runner_workflow_for_namespace: false)
visit group_runners_path(group)
end
it_behaves_like "shows and resets runner registration token" do
let(:dropdown_text) { 'Register a group runner' }
let(:registration_token) { group_registration_token }
end
end
context "with no runners" do
before do
visit group_runners_path(group)

View File

@ -14,17 +14,11 @@ RSpec.describe 'Runners', feature_category: :runner_fleet do
stub_feature_flags(project_runners_vue_ui: false)
end
context 'when user views runners page' do
let_it_be(:project) { create(:project) }
context 'with user as project maintainer' do
let_it_be(:project) { create(:project).tap { |project| project.add_maintainer(user) } }
before do
project.add_maintainer(user)
end
context 'when create_runner_workflow_for_namespace is enabled', :js do
context 'when user views runners page', :js do
before do
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
visit project_runners_path(project)
end
@ -38,58 +32,18 @@ RSpec.describe 'Runners', feature_category: :runner_fleet do
end
end
context 'when user views new runner page' do
context 'when create_runner_workflow_for_namespace is enabled', :js do
before do
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
visit new_project_runner_path(project)
end
it_behaves_like 'creates runner and shows register page' do
let(:register_path_pattern) { register_project_runner_path(project, '.*') }
end
it 'shows the locked field' do
expect(page).to have_selector('input[type="checkbox"][name="locked"]')
expect(page).to have_content(_('Lock to current projects'))
end
end
end
context 'when create_runner_workflow_for_namespace is disabled' do
context 'when user views new runner page', :js do
before do
stub_feature_flags(create_runner_workflow_for_namespace: false)
visit new_project_runner_path(project)
end
it 'user can see a link with instructions on how to install GitLab Runner' do
visit project_runners_path(project)
expect(page).to have_link('Install GitLab Runner and ensure it\'s running.', href: "https://docs.gitlab.com/runner/install/")
it_behaves_like 'creates runner and shows register page' do
let(:register_path_pattern) { register_project_runner_path(project, '.*') }
end
describe 'runners registration token' do
let!(:token) { project.runners_token }
before do
visit project_runners_path(project)
end
it 'has a registration token' do
expect(page.find('#registration_token')).to have_content(token)
end
describe 'reload registration token' do
let(:page_token) { find('#registration_token').text }
before do
click_link 'Reset registration token'
end
it 'changes registration token' do
expect(page_token).not_to eq token
end
end
it 'shows the locked field' do
expect(page).to have_selector('input[type="checkbox"][name="locked"]')
expect(page).to have_content(_('Lock to current projects'))
end
end
end

View File

@ -1,79 +0,0 @@
import ConnectionMonitor from '~/actioncable_connection_monitor';
describe('ConnectionMonitor', () => {
let monitor;
beforeEach(() => {
monitor = new ConnectionMonitor({});
});
describe('#getPollInterval', () => {
beforeEach(() => {
Math.originalRandom = Math.random;
});
afterEach(() => {
Math.random = Math.originalRandom;
});
const { staleThreshold, reconnectionBackoffRate } = ConnectionMonitor;
const backoffFactor = 1 + reconnectionBackoffRate;
const ms = 1000;
it('uses exponential backoff', () => {
Math.random = () => 0;
monitor.reconnectAttempts = 0;
expect(monitor.getPollInterval()).toEqual(staleThreshold * ms);
monitor.reconnectAttempts = 1;
expect(monitor.getPollInterval()).toEqual(staleThreshold * backoffFactor * ms);
monitor.reconnectAttempts = 2;
expect(monitor.getPollInterval()).toEqual(
staleThreshold * backoffFactor * backoffFactor * ms,
);
});
it('caps exponential backoff after some number of reconnection attempts', () => {
Math.random = () => 0;
monitor.reconnectAttempts = 42;
const cappedPollInterval = monitor.getPollInterval();
monitor.reconnectAttempts = 9001;
expect(monitor.getPollInterval()).toEqual(cappedPollInterval);
});
it('uses 100% jitter when 0 reconnection attempts', () => {
Math.random = () => 0;
expect(monitor.getPollInterval()).toEqual(staleThreshold * ms);
Math.random = () => 0.5;
expect(monitor.getPollInterval()).toEqual(staleThreshold * 1.5 * ms);
});
it('uses reconnectionBackoffRate for jitter when >0 reconnection attempts', () => {
monitor.reconnectAttempts = 1;
Math.random = () => 0.25;
expect(monitor.getPollInterval()).toEqual(
staleThreshold * backoffFactor * (1 + reconnectionBackoffRate * 0.25) * ms,
);
Math.random = () => 0.5;
expect(monitor.getPollInterval()).toEqual(
staleThreshold * backoffFactor * (1 + reconnectionBackoffRate * 0.5) * ms,
);
});
it('applies jitter after capped exponential backoff', () => {
monitor.reconnectAttempts = 9001;
Math.random = () => 0;
const withoutJitter = monitor.getPollInterval();
Math.random = () => 0.5;
const withJitter = monitor.getPollInterval();
expect(withJitter).toBeGreaterThan(withoutJitter);
});
});
});

View File

@ -41,7 +41,9 @@ describe('RegistrationDropdown', () => {
const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
const findRegistrationToken = () => wrapper.findComponent(RegistrationToken);
const findRegistrationTokenInput = () =>
wrapper.findByLabelText(RegistrationToken.i18n.registrationToken);
wrapper.findByLabelText(
`${RegistrationToken.i18n.registrationToken} ${RegistrationDropdown.i18n.supportForRegistrationTokensDeprecated}`,
);
const findTokenResetDropdownItem = () =>
wrapper.findComponent(RegistrationTokenResetDropdownItem);
const findModal = () => wrapper.findComponent(GlModal);
@ -107,12 +109,12 @@ describe('RegistrationDropdown', () => {
createComponent();
expect(findDropdown().props()).toMatchObject({
category: 'primary',
variant: 'confirm',
category: 'tertiary',
variant: 'default',
});
expect(findDropdown().attributes()).toMatchObject({
toggleclass: '',
toggleclass: 'gl-px-3!',
});
});
@ -217,14 +219,9 @@ describe('RegistrationDropdown', () => {
});
});
describe.each([
{ createRunnerWorkflowForAdmin: true },
{ createRunnerWorkflowForNamespace: true },
])('When showing a "deprecated" warning', (glFeatures) => {
describe('When showing a "deprecated" warning', () => {
it('passes deprecated variant props and attributes to dropdown', () => {
createComponent({
provide: { glFeatures },
});
createComponent();
expect(findDropdown().props()).toMatchObject({
category: 'tertiary',
@ -249,12 +246,7 @@ describe('RegistrationDropdown', () => {
});
it('shows warning text', () => {
createComponent(
{
provide: { glFeatures },
},
mountExtended,
);
createComponent({}, mountExtended);
const text = wrapper.findByText(s__('Runners|Support for registration tokens is deprecated'));
@ -262,12 +254,7 @@ describe('RegistrationDropdown', () => {
});
it('button shows ellipsis icon', () => {
createComponent(
{
provide: { glFeatures },
},
mountExtended,
);
createComponent({}, mountExtended);
expect(findDropdownBtn().findComponent(GlIcon).props('name')).toBe('ellipsis_v');
expect(findDropdownBtn().findAllComponents(GlIcon)).toHaveLength(1);

View File

@ -10,7 +10,6 @@ import {
I18N_CREATE_RUNNER_LINK,
I18N_STILL_USING_REGISTRATION_TOKENS,
I18N_CONTACT_ADMIN_TO_REGISTER,
I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
I18N_NO_RESULTS,
I18N_EDIT_YOUR_SEARCH,
} from '~/ci/runner/constants';
@ -59,137 +58,85 @@ describe('RunnerListEmptyState', () => {
});
describe('when search is not filtered', () => {
describe.each([
{ createRunnerWorkflowForAdmin: true },
{ createRunnerWorkflowForNamespace: true },
])('when createRunnerWorkflow is enabled by %o', (currentGlFeatures) => {
beforeEach(() => {
glFeatures = currentGlFeatures;
});
describe.each`
newRunnerPath | registrationToken | expectedMessages
${mockNewRunnerPath} | ${mockRegistrationToken} | ${[I18N_CREATE_RUNNER_LINK, I18N_STILL_USING_REGISTRATION_TOKENS]}
${mockNewRunnerPath} | ${null} | ${[I18N_CREATE_RUNNER_LINK]}
${null} | ${mockRegistrationToken} | ${[I18N_STILL_USING_REGISTRATION_TOKENS]}
${null} | ${null} | ${[I18N_CONTACT_ADMIN_TO_REGISTER]}
`(
'when newRunnerPath is $newRunnerPath and registrationToken is $registrationToken',
({ newRunnerPath, registrationToken, expectedMessages }) => {
beforeEach(() => {
createComponent({
props: {
newRunnerPath,
registrationToken,
},
});
});
it('shows title', () => {
expectTitleToBe(I18N_GET_STARTED);
});
it('renders an illustration', () => {
expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
});
it(`shows description: "${expectedMessages.join(' ')}"`, () => {
expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, ...expectedMessages]);
});
},
);
describe('with newRunnerPath and registration token', () => {
describe.each`
newRunnerPath | registrationToken | expectedMessages
${mockNewRunnerPath} | ${mockRegistrationToken} | ${[I18N_CREATE_RUNNER_LINK, I18N_STILL_USING_REGISTRATION_TOKENS]}
${mockNewRunnerPath} | ${null} | ${[I18N_CREATE_RUNNER_LINK]}
${null} | ${mockRegistrationToken} | ${[I18N_STILL_USING_REGISTRATION_TOKENS]}
${null} | ${null} | ${[I18N_CONTACT_ADMIN_TO_REGISTER]}
`(
'when newRunnerPath is $newRunnerPath and registrationToken is $registrationToken',
({ newRunnerPath, registrationToken, expectedMessages }) => {
beforeEach(() => {
createComponent({
props: {
registrationToken: mockRegistrationToken,
newRunnerPath: mockNewRunnerPath,
newRunnerPath,
registrationToken,
},
});
});
it('shows links to the new runner page and registration instructions', () => {
expect(findLinks().at(0).attributes('href')).toBe(mockNewRunnerPath);
const { value } = getBinding(findLinks().at(1).element, 'gl-modal');
expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
});
});
describe('with newRunnerPath and no registration token', () => {
beforeEach(() => {
createComponent({
props: {
registrationToken: mockRegistrationToken,
newRunnerPath: null,
},
});
});
it('opens a runner registration instructions modal with a link', () => {
const { value } = getBinding(findLink().element, 'gl-modal');
expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
});
});
describe('with no newRunnerPath nor registration token', () => {
beforeEach(() => {
createComponent({
props: {
registrationToken: null,
newRunnerPath: null,
},
});
});
it('has no link', () => {
expect(findLink().exists()).toBe(false);
});
});
});
describe('when createRunnerWorkflow is disabled', () => {
describe('when there is a registration token', () => {
beforeEach(() => {
createComponent({
props: {
registrationToken: mockRegistrationToken,
},
});
it('shows title', () => {
expectTitleToBe(I18N_GET_STARTED);
});
it('renders an illustration', () => {
expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
});
it('opens a runner registration instructions modal with a link', () => {
const { value } = getBinding(findLink().element, 'gl-modal');
expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
it(`shows description: "${expectedMessages.join(' ')}"`, () => {
expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, ...expectedMessages]);
});
},
);
it('displays text with registration instructions', () => {
expectTitleToBe(I18N_GET_STARTED);
expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_FOLLOW_REGISTRATION_INSTRUCTIONS]);
describe('with newRunnerPath and registration token', () => {
beforeEach(() => {
createComponent({
props: {
registrationToken: mockRegistrationToken,
newRunnerPath: mockNewRunnerPath,
},
});
});
describe('when there is no registration token', () => {
beforeEach(() => {
createComponent({ props: { registrationToken: null } });
});
it('shows links to the new runner page and registration instructions', () => {
expect(findLinks().at(0).attributes('href')).toBe(mockNewRunnerPath);
it('displays "contact admin" text', () => {
expectTitleToBe(I18N_GET_STARTED);
const { value } = getBinding(findLinks().at(1).element, 'gl-modal');
expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
});
});
expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_CONTACT_ADMIN_TO_REGISTER]);
describe('with newRunnerPath and no registration token', () => {
beforeEach(() => {
createComponent({
props: {
registrationToken: mockRegistrationToken,
newRunnerPath: null,
},
});
});
it('has no registration instructions link', () => {
expect(findLink().exists()).toBe(false);
it('opens a runner registration instructions modal with a link', () => {
const { value } = getBinding(findLink().element, 'gl-modal');
expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
});
});
describe('with no newRunnerPath nor registration token', () => {
beforeEach(() => {
createComponent({
props: {
registrationToken: null,
newRunnerPath: null,
},
});
});
it('has no link', () => {
expect(findLink().exists()).toBe(false);
});
});
});

View File

@ -483,35 +483,15 @@ describe('GroupRunnersApp', () => {
expect(findRegistrationDropdown().exists()).toBe(true);
});
it('when create_runner_workflow_for_namespace is enabled', () => {
it('shows the create runner button', () => {
createComponent({
props: {
newRunnerPath,
},
provide: {
glFeatures: {
createRunnerWorkflowForNamespace: true,
},
},
});
expect(findNewRunnerBtn().attributes('href')).toBe(newRunnerPath);
});
it('when create_runner_workflow_for_namespace is disabled', () => {
createComponent({
props: {
newRunnerPath,
},
provide: {
glFeatures: {
createRunnerWorkflowForNamespace: false,
},
},
});
expect(findNewRunnerBtn().exists()).toBe(false);
});
});
describe('when user has no permission to register group runner', () => {
@ -524,16 +504,11 @@ describe('GroupRunnersApp', () => {
expect(findRegistrationDropdown().exists()).toBe(false);
});
it('when create_runner_workflow_for_namespace is enabled', () => {
it('shows the create runner button', () => {
createComponent({
props: {
newRunnerPath: null,
},
provide: {
glFeatures: {
createRunnerWorkflowForNamespace: true,
},
},
});
expect(findNewRunnerBtn().exists()).toBe(false);

View File

@ -3,6 +3,8 @@ import { shallowMount } from '@vue/test-utils';
import TaskListItemActions from '~/issues/show/components/task_list_item_actions.vue';
import eventHub from '~/issues/show/event_hub';
jest.mock('~/issues/show/event_hub');
describe('TaskListItemActions component', () => {
let wrapper;
@ -37,16 +39,12 @@ describe('TaskListItemActions component', () => {
});
it('emits event when `Convert to task` dropdown item is clicked', () => {
jest.spyOn(eventHub, '$emit');
findConvertToTaskItem().vm.$emit('action');
expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10');
});
it('emits event when `Delete` dropdown item is clicked', () => {
jest.spyOn(eventHub, '$emit');
findDeleteItem().vm.$emit('action');
expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10');

View File

@ -1,38 +1,37 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import GfmAutoComplete from '~/gfm_auto_complete';
import { TYPE_ISSUE } from '~/issues/constants';
import RelatedIssuableInput from '~/related_issues/components/related_issuable_input.vue';
import { PathIdSeparator } from '~/related_issues/constants';
jest.mock('ee_else_ce/gfm_auto_complete', () => {
return function gfmAutoComplete() {
return {
constructor() {},
setup() {},
};
};
});
jest.mock('~/gfm_auto_complete');
describe('RelatedIssuableInput', () => {
let propsData;
let wrapper;
beforeEach(() => {
propsData = {
inputValue: '',
references: [],
pathIdSeparator: PathIdSeparator.Issue,
issuableType: TYPE_ISSUE,
autoCompleteSources: {
issues: `${TEST_HOST}/h5bp/html5-boilerplate/-/autocomplete_sources/issues`,
const autoCompleteSources = {
issues: `${TEST_HOST}/h5bp/html5-boilerplate/-/autocomplete_sources/issues`,
};
const mountComponent = (props = {}) => {
wrapper = shallowMount(RelatedIssuableInput, {
propsData: {
inputValue: '',
references: [],
pathIdSeparator: PathIdSeparator.Issue,
issuableType: TYPE_ISSUE,
autoCompleteSources,
...props,
},
};
});
attachTo: document.body,
});
};
describe('autocomplete', () => {
describe('with autoCompleteSources', () => {
it('shows placeholder text', () => {
const wrapper = shallowMount(RelatedIssuableInput, { propsData });
mountComponent();
expect(wrapper.findComponent({ ref: 'input' }).element.placeholder).toBe(
'Paste issue link or <#issue id>',
@ -40,51 +39,32 @@ describe('RelatedIssuableInput', () => {
});
it('has GfmAutoComplete', () => {
const wrapper = shallowMount(RelatedIssuableInput, { propsData });
mountComponent();
expect(wrapper.vm.gfmAutoComplete).toBeDefined();
expect(GfmAutoComplete).toHaveBeenCalledWith(autoCompleteSources);
});
});
describe('with no autoCompleteSources', () => {
it('shows placeholder text', () => {
const wrapper = shallowMount(RelatedIssuableInput, {
propsData: {
...propsData,
references: ['!1', '!2'],
},
});
mountComponent({ references: ['!1', '!2'] });
expect(wrapper.findComponent({ ref: 'input' }).element.value).toBe('');
});
it('does not have GfmAutoComplete', () => {
const wrapper = shallowMount(RelatedIssuableInput, {
propsData: {
...propsData,
autoCompleteSources: {},
},
});
mountComponent({ autoCompleteSources: {} });
expect(wrapper.vm.gfmAutoComplete).not.toBeDefined();
expect(GfmAutoComplete).not.toHaveBeenCalled();
});
});
});
describe('focus', () => {
it('when clicking anywhere on the input wrapper it should focus the input', async () => {
const wrapper = shallowMount(RelatedIssuableInput, {
propsData: {
...propsData,
references: ['foo', 'bar'],
},
// We need to attach to document, so that `document.activeElement` is properly set in jsdom
attachTo: document.body,
});
mountComponent({ references: ['foo', 'bar'] });
wrapper.find('li').trigger('click');
await nextTick();
await wrapper.find('li').trigger('click');
expect(document.activeElement).toBe(wrapper.findComponent({ ref: 'input' }).element);
});
@ -92,11 +72,7 @@ describe('RelatedIssuableInput', () => {
describe('when filling in the input', () => {
it('emits addIssuableFormInput with data', () => {
const wrapper = shallowMount(RelatedIssuableInput, {
propsData,
});
wrapper.vm.$emit = jest.fn();
mountComponent();
const newInputValue = 'filling in things';
const untouchedRawReferences = newInputValue.trim().split(/\s/);
@ -108,12 +84,16 @@ describe('RelatedIssuableInput', () => {
input.element.selectionEnd = newInputValue.length;
input.trigger('input');
expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormInput', {
newValue: newInputValue,
caretPos: newInputValue.length,
untouchedRawReferences,
touchedReference,
});
expect(wrapper.emitted('addIssuableFormInput')).toEqual([
[
{
newValue: newInputValue,
caretPos: newInputValue.length,
untouchedRawReferences,
touchedReference,
},
],
]);
});
});
});

View File

@ -1,3 +1,4 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
@ -154,6 +155,13 @@ describe('IssuableLockForm', () => {
expect(tooltip).toBeDefined();
expect(tooltip.value.title).toBe(isLocked ? 'Locked' : 'Unlocked');
});
it('renders lock icon', () => {
const icon = findSidebarCollapseIcon().findComponent(GlIcon).props('name');
const expected = isLocked ? 'lock' : 'lock-open';
expect(icon).toBe(expected);
});
});
});
});

View File

@ -148,3 +148,16 @@ export const getMrTimelogsQueryResponse = {
},
},
};
export const deleteTimelogMutationResponse = {
data: {
timelogDelete: {
errors: [],
timelog: {
id: 'gid://gitlab/Issue/148',
issue: {},
mergeRequest: {},
},
},
},
};

View File

@ -12,6 +12,7 @@ import getIssueTimelogsQuery from '~/sidebar/queries/get_issue_timelogs.query.gr
import getMrTimelogsQuery from '~/sidebar/queries/get_mr_timelogs.query.graphql';
import deleteTimelogMutation from '~/sidebar/queries/delete_timelog.mutation.graphql';
import {
deleteTimelogMutationResponse,
getIssueTimelogsQueryResponse,
getMrTimelogsQueryResponse,
timelogToRemoveId,
@ -22,7 +23,7 @@ jest.mock('~/alert');
describe('Issuable Time Tracking Report', () => {
Vue.use(VueApollo);
let wrapper;
let fakeApollo;
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findDeleteButton = () => wrapper.findByTestId('deleteButton');
const successIssueQueryHandler = jest.fn().mockResolvedValue(getIssueTimelogsQueryResponse);
@ -30,30 +31,27 @@ describe('Issuable Time Tracking Report', () => {
const mountComponent = ({
queryHandler = successIssueQueryHandler,
mutationHandler,
issuableType = 'issue',
mountFunction = shallowMount,
limitToHours = false,
} = {}) => {
fakeApollo = createMockApollo([
[getIssueTimelogsQuery, queryHandler],
[getMrTimelogsQuery, queryHandler],
]);
wrapper = extendedWrapper(
mountFunction(Report, {
apolloProvider: createMockApollo([
[getIssueTimelogsQuery, queryHandler],
[getMrTimelogsQuery, queryHandler],
[deleteTimelogMutation, mutationHandler],
]),
provide: {
issuableId: 1,
issuableType,
},
propsData: { limitToHours, issuableId: '1' },
apolloProvider: fakeApollo,
}),
);
};
afterEach(() => {
fakeApollo = null;
});
it('should render loading spinner', () => {
mountComponent();
@ -135,50 +133,27 @@ describe('Issuable Time Tracking Report', () => {
});
describe('when clicking on the delete timelog button', () => {
beforeEach(() => {
mountComponent({ mountFunction: mount });
});
it('calls `$apollo.mutate` with deleteTimelogMutation mutation and removes the row', async () => {
const mutateSpy = jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: {
timelogDelete: {
errors: [],
},
},
});
const mutateSpy = jest.fn().mockResolvedValue(deleteTimelogMutationResponse);
mountComponent({ mutationHandler: mutateSpy, mountFunction: mount });
await waitForPromises();
await findDeleteButton().trigger('click');
await waitForPromises();
expect(createAlert).not.toHaveBeenCalled();
expect(mutateSpy).toHaveBeenCalledWith({
mutation: deleteTimelogMutation,
variables: {
input: {
id: timelogToRemoveId,
},
},
});
expect(mutateSpy).toHaveBeenCalledWith({ input: { id: timelogToRemoveId } });
});
it('calls `createAlert` with errorMessage and does not remove the row on promise reject', async () => {
const mutateSpy = jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({});
const mutateSpy = jest.fn().mockRejectedValue({});
mountComponent({ mutationHandler: mutateSpy, mountFunction: mount });
await waitForPromises();
await findDeleteButton().trigger('click');
await waitForPromises();
expect(mutateSpy).toHaveBeenCalledWith({
mutation: deleteTimelogMutation,
variables: {
input: {
id: timelogToRemoveId,
},
},
});
expect(mutateSpy).toHaveBeenCalledWith({ input: { id: timelogToRemoveId } });
expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while removing the timelog.',
captureError: true,

View File

@ -39,7 +39,6 @@ describe('CrmContactToken', () => {
Vue.use(VueApollo);
let wrapper;
let fakeApollo;
const getBaseToken = () => wrapper.findComponent(BaseToken);
@ -58,9 +57,8 @@ describe('CrmContactToken', () => {
listeners = {},
queryHandler = searchGroupCrmContactsQueryHandler,
} = {}) => {
fakeApollo = createMockApollo([[searchCrmContactsQuery, queryHandler]]);
wrapper = mount(CrmContactToken, {
apolloProvider: createMockApollo([[searchCrmContactsQuery, queryHandler]]),
propsData: {
config,
value,
@ -75,14 +73,9 @@ describe('CrmContactToken', () => {
},
stubs,
listeners,
apolloProvider: fakeApollo,
});
};
afterEach(() => {
fakeApollo = null;
});
describe('methods', () => {
describe('fetchContacts', () => {
describe('for groups', () => {
@ -160,9 +153,7 @@ describe('CrmContactToken', () => {
});
it('calls `createAlert` with alert error message when request fails', async () => {
mountComponent();
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
getBaseToken().vm.$emit('fetch-suggestions');
await waitForPromises();
@ -173,12 +164,9 @@ describe('CrmContactToken', () => {
});
it('sets `loading` to false when request completes', async () => {
mountComponent();
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
getBaseToken().vm.$emit('fetch-suggestions');
await waitForPromises();
expect(getBaseToken().props('suggestionsLoading')).toBe(false);
@ -195,13 +183,7 @@ describe('CrmContactToken', () => {
value: { data: '1' },
});
const baseTokenEl = wrapper.findComponent(BaseToken);
expect(baseTokenEl.exists()).toBe(true);
expect(baseTokenEl.props()).toMatchObject({
suggestions: mockCrmContacts,
getActiveTokenValue: wrapper.vm.getActiveContact,
});
expect(getBaseToken().props('suggestions')).toEqual(mockCrmContacts);
});
it.each(mockCrmContacts)('renders token item when value is selected', (contact) => {
@ -270,12 +252,9 @@ describe('CrmContactToken', () => {
it('emits listeners in the base-token', () => {
const mockInput = jest.fn();
mountComponent({
listeners: {
input: mockInput,
},
});
wrapper.findComponent(BaseToken).vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
mountComponent({ listeners: { input: mockInput } });
getBaseToken().vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
});

View File

@ -39,7 +39,6 @@ describe('CrmOrganizationToken', () => {
Vue.use(VueApollo);
let wrapper;
let fakeApollo;
const getBaseToken = () => wrapper.findComponent(BaseToken);
@ -58,8 +57,8 @@ describe('CrmOrganizationToken', () => {
listeners = {},
queryHandler = searchGroupCrmOrganizationsQueryHandler,
} = {}) => {
fakeApollo = createMockApollo([[searchCrmOrganizationsQuery, queryHandler]]);
wrapper = mount(CrmOrganizationToken, {
apolloProvider: createMockApollo([[searchCrmOrganizationsQuery, queryHandler]]),
propsData: {
config,
value,
@ -74,14 +73,9 @@ describe('CrmOrganizationToken', () => {
},
stubs,
listeners,
apolloProvider: fakeApollo,
});
};
afterEach(() => {
fakeApollo = null;
});
describe('methods', () => {
describe('fetchOrganizations', () => {
describe('for groups', () => {
@ -159,9 +153,7 @@ describe('CrmOrganizationToken', () => {
});
it('calls `createAlert` when request fails', async () => {
mountComponent();
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
getBaseToken().vm.$emit('fetch-suggestions');
await waitForPromises();
@ -172,9 +164,7 @@ describe('CrmOrganizationToken', () => {
});
it('sets `loading` to false when request completes', async () => {
mountComponent();
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
getBaseToken().vm.$emit('fetch-suggestions');
@ -194,13 +184,7 @@ describe('CrmOrganizationToken', () => {
value: { data: '1' },
});
const baseTokenEl = wrapper.findComponent(BaseToken);
expect(baseTokenEl.exists()).toBe(true);
expect(baseTokenEl.props()).toMatchObject({
suggestions: mockCrmOrganizations,
getActiveTokenValue: wrapper.vm.getActiveOrganization,
});
expect(getBaseToken().props('suggestions')).toEqual(mockCrmOrganizations);
});
it.each(mockCrmOrganizations)('renders token item when value is selected', (organization) => {
@ -269,12 +253,9 @@ describe('CrmOrganizationToken', () => {
it('emits listeners in the base-token', () => {
const mockInput = jest.fn();
mountComponent({
listeners: {
input: mockInput,
},
});
wrapper.findComponent(BaseToken).vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
mountComponent({ listeners: { input: mockInput } });
getBaseToken().vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
});

View File

@ -90,14 +90,6 @@ describe('RunnerInstructionsModal component', () => {
await waitForPromises();
});
it('should not show alert', () => {
expect(findAlert().exists()).toBe(false);
});
it('should not show deprecation alert', () => {
expect(findAlert('warning').exists()).toBe(false);
});
it('should contain a number of platforms buttons', () => {
expect(runnerPlatformsHandler).toHaveBeenCalledWith({});
@ -112,19 +104,8 @@ describe('RunnerInstructionsModal component', () => {
expect(architectures).toEqual(mockPlatformList[0].architectures.nodes);
});
describe.each`
glFeatures | deprecationAlertExists
${{}} | ${false}
${{ createRunnerWorkflowForAdmin: true }} | ${true}
${{ createRunnerWorkflowForNamespace: true }} | ${true}
`('with features $glFeatures', ({ glFeatures, deprecationAlertExists }) => {
beforeEach(() => {
createComponent({ provide: { glFeatures } });
});
it(`alert is ${deprecationAlertExists ? 'shown' : 'not shown'}`, () => {
expect(findAlert('warning').exists()).toBe(deprecationAlertExists);
});
it('alert is shown', () => {
expect(findAlert('warning').exists()).toBe(true);
});
describe('when the modal resizes', () => {

View File

@ -1,10 +1,10 @@
import { GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql';
import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
import { TYPE_MERGE_REQUEST } from '~/issues/constants';
@ -44,20 +44,20 @@ Vue.use(VueApollo);
describe('User select dropdown', () => {
let wrapper;
let fakeApollo;
const hideDropdownMock = jest.fn();
const findSearchField = () => wrapper.findComponent(GlSearchBoxByType);
const findParticipantsLoading = () => wrapper.find('[data-testid="loading-participants"]');
const findSelectedParticipants = () => wrapper.findAll('[data-testid="selected-participant"]');
const findParticipantsLoading = () => wrapper.findByTestId('loading-participants');
const findSelectedParticipants = () => wrapper.findAllByTestId('selected-participant');
const findSelectedParticipantByIndex = (index) =>
findSelectedParticipants().at(index).findComponent(SidebarParticipant);
const findUnselectedParticipants = () =>
wrapper.findAll('[data-testid="unselected-participant"]');
const findUnselectedParticipants = () => wrapper.findAllByTestId('unselected-participant');
const findUnselectedParticipantByIndex = (index) =>
findUnselectedParticipants().at(index).findComponent(SidebarParticipant);
const findCurrentUser = () => wrapper.findAll('[data-testid="current-user"]');
const findIssuableAuthor = () => wrapper.findAll('[data-testid="issuable-author"]');
const findUnassignLink = () => wrapper.find('[data-testid="unassign"]');
const findEmptySearchResults = () => wrapper.find('[data-testid="empty-results"]');
const findCurrentUser = () => wrapper.findAllByTestId('current-user');
const findIssuableAuthor = () => wrapper.findAllByTestId('issuable-author');
const findUnassignLink = () => wrapper.findByTestId('unassign');
const findEmptySearchResults = () => wrapper.findAllByTestId('empty-results');
const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(projectMembersResponse);
const participantsQueryHandlerSuccess = jest.fn().mockResolvedValue(participantsQueryResponse);
@ -72,7 +72,7 @@ describe('User select dropdown', () => {
[searchUsersQueryOnMR, jest.fn().mockResolvedValue(searchResponseOnMR)],
[getIssueParticipantsQuery, participantsQueryHandler],
]);
wrapper = shallowMount(UserSelect, {
wrapper = shallowMountExtended(UserSelect, {
apolloProvider: fakeApollo,
propsData: {
headerText: 'test',
@ -97,7 +97,7 @@ describe('User select dropdown', () => {
</div>
`,
methods: {
hide: jest.fn(),
hide: hideDropdownMock,
},
},
},
@ -106,6 +106,7 @@ describe('User select dropdown', () => {
afterEach(() => {
fakeApollo = null;
hideDropdownMock.mockClear();
});
it('renders a loading spinner if participants are loading', () => {
@ -290,12 +291,12 @@ describe('User select dropdown', () => {
value: [assignee],
},
});
wrapper.vm.$refs.dropdown.hide = jest.fn();
await waitForPromises();
findUnassignLink().trigger('click');
expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalledTimes(1);
expect(hideDropdownMock).toHaveBeenCalledTimes(1);
});
it('emits an empty array after unselecting the only selected assignee', async () => {

View File

@ -65,9 +65,7 @@ RSpec.describe Gitlab::MarkdownCache::Redis::Extension, :clean_gitlab_redis_cach
Gitlab::Redis::Cache.with do |redis|
expect(redis).to receive(:pipelined).and_call_original
times = Gitlab::Redis::ClusterUtil.cluster?(redis) ? 2 : 1
expect_next_instances_of(Redis::PipelinedConnection, times) do |pipeline|
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
expect(pipeline).to receive(:mapped_hmget).once.and_call_original
end
end

View File

@ -832,6 +832,30 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
subject
end
end
context 'when with_readonly_pipeline is used' do
it 'calls the default store only' do
expect(primary_store).to receive(:send).and_call_original
expect(secondary_store).not_to receive(:send).and_call_original
multi_store.with_readonly_pipeline { subject }
end
context 'when used in a nested manner' do
subject(:nested_subject) do
multi_store.with_readonly_pipeline do
multi_store.with_readonly_pipeline { subject }
end
end
it 'raises error' do
expect { nested_subject }.to raise_error(Gitlab::Redis::MultiStore::NestedReadonlyPipelineError)
expect { nested_subject }.to raise_error { |e|
expect(e.message).to eq('Nested use of with_readonly_pipeline is detected.')
}
end
end
end
end
end

View File

@ -694,59 +694,5 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_disallowed(:create_instance_runner) }
end
context 'create_runner_workflow_for_admin flag disabled' do
before do
stub_feature_flags(create_runner_workflow_for_admin: false)
end
context 'admin' do
let(:current_user) { admin_user }
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_disallowed(:create_instance_runner) }
end
context 'when admin mode is disabled' do
it { is_expected.to be_disallowed(:create_instance_runner) }
end
end
context 'with project_bot' do
let(:current_user) { project_bot }
it { is_expected.to be_disallowed(:create_instance_runner) }
end
context 'with migration_bot' do
let(:current_user) { migration_bot }
it { is_expected.to be_disallowed(:create_instance_runner) }
end
context 'with security_bot' do
let(:current_user) { security_bot }
it { is_expected.to be_disallowed(:create_instance_runner) }
end
context 'with llm_bot' do
let(:current_user) { llm_bot }
it { is_expected.to be_disallowed(:create_instance_runners) }
end
context 'with regular user' do
let(:current_user) { user }
it { is_expected.to be_disallowed(:create_instance_runner) }
end
context 'with anonymous' do
let(:current_user) { nil }
it { is_expected.to be_disallowed(:create_instance_runner) }
end
end
end
end

View File

@ -1483,155 +1483,81 @@ RSpec.describe GroupPolicy, feature_category: :system_access do
end
end
context 'create_runner_workflow_for_namespace flag enabled' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: [group])
end
context 'admin' do
let(:current_user) { admin }
context 'admin' do
let(:current_user) { admin }
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:create_runner) }
context 'with specific group runner registration disabled' do
before do
group.runner_registration_enabled = false
end
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:create_runner) }
end
context 'with specific group runner registration disabled' do
before do
group.runner_registration_enabled = false
end
context 'with group runner registration disabled' do
before do
stub_application_setting(valid_runner_registrars: ['project'])
group.runner_registration_enabled = runner_registration_enabled
end
context 'with specific group runner registration enabled' do
let(:runner_registration_enabled) { true }
it { is_expected.to be_allowed(:create_runner) }
end
context 'with group runner registration disabled' do
before do
stub_application_setting(valid_runner_registrars: ['project'])
group.runner_registration_enabled = runner_registration_enabled
end
context 'with specific group runner registration disabled' do
let(:runner_registration_enabled) { false }
context 'with specific group runner registration enabled' do
let(:runner_registration_enabled) { true }
it { is_expected.to be_allowed(:create_runner) }
end
context 'with specific group runner registration disabled' do
let(:runner_registration_enabled) { false }
it { is_expected.to be_allowed(:create_runner) }
end
it { is_expected.to be_allowed(:create_runner) }
end
end
context 'when admin mode is disabled' do
it { is_expected.to be_disallowed(:create_runner) }
end
end
context 'with owner' do
let(:current_user) { owner }
it { is_expected.to be_allowed(:create_runner) }
it_behaves_like 'disallowed when group runner registration disabled'
end
context 'with maintainer' do
let(:current_user) { maintainer }
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with reporter' do
let(:current_user) { reporter }
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with guest' do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with developer' do
let(:current_user) { developer }
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with anonymous' do
let(:current_user) { nil }
context 'when admin mode is disabled' do
it { is_expected.to be_disallowed(:create_runner) }
end
end
context 'with create_runner_workflow_for_namespace flag disabled' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: [other_group])
end
context 'with owner' do
let(:current_user) { owner }
let_it_be(:other_group) { create(:group) }
it { is_expected.to be_allowed(:create_runner) }
context 'admin' do
let(:current_user) { admin }
it_behaves_like 'disallowed when group runner registration disabled'
end
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_disallowed(:create_runner) }
context 'with maintainer' do
let(:current_user) { maintainer }
context 'with specific group runner registration disabled' do
before do
group.runner_registration_enabled = false
end
it { is_expected.to be_disallowed(:create_runner) }
end
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with reporter' do
let(:current_user) { reporter }
it_behaves_like 'disallowed when group runner registration disabled'
end
it { is_expected.to be_disallowed(:create_runner) }
end
context 'when admin mode is disabled' do
it { is_expected.to be_disallowed(:create_runner) }
end
end
context 'with guest' do
let(:current_user) { guest }
context 'with owner' do
let(:current_user) { owner }
it { is_expected.to be_disallowed(:create_runner) }
end
it { is_expected.to be_disallowed(:create_runner) }
context 'with developer' do
let(:current_user) { developer }
it_behaves_like 'disallowed when group runner registration disabled'
end
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with maintainer' do
let(:current_user) { maintainer }
context 'with anonymous' do
let(:current_user) { nil }
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with reporter' do
let(:current_user) { reporter }
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with guest' do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with developer' do
let(:current_user) { developer }
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with anonymous' do
let(:current_user) { nil }
it { is_expected.to be_disallowed(:create_runner) }
end
it { is_expected.to be_disallowed(:create_runner) }
end
end

View File

@ -2879,42 +2879,10 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
describe 'create_runner' do
context 'create_runner_workflow_for_namespace flag enabled' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
end
context 'admin' do
let(:current_user) { admin }
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:create_runner) }
context 'with project runner registration disabled' do
before do
stub_application_setting(valid_runner_registrars: ['group'])
end
it { is_expected.to be_allowed(:create_runner) }
end
context 'with specific project runner registration disabled' do
before do
project.update!(runner_registration_enabled: false)
end
it { is_expected.to be_allowed(:create_runner) }
end
end
context 'when admin mode is disabled' do
it { is_expected.to be_disallowed(:create_runner) }
end
end
context 'with owner' do
let(:current_user) { owner }
context 'admin' do
let(:current_user) { admin }
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:create_runner) }
context 'with project runner registration disabled' do
@ -2922,7 +2890,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
stub_application_setting(valid_runner_registrars: ['group'])
end
it { is_expected.to be_disallowed(:create_runner) }
it { is_expected.to be_allowed(:create_runner) }
end
context 'with specific project runner registration disabled' do
@ -2930,125 +2898,65 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
project.update!(runner_registration_enabled: false)
end
it { is_expected.to be_disallowed(:create_runner) }
it { is_expected.to be_allowed(:create_runner) }
end
end
context 'with maintainer' do
let(:current_user) { maintainer }
it { is_expected.to be_allowed(:create_runner) }
context 'when admin mode is disabled' do
it { is_expected.to be_disallowed(:create_runner) }
end
end
context 'with reporter' do
let(:current_user) { reporter }
context 'with owner' do
let(:current_user) { owner }
it { is_expected.to be_allowed(:create_runner) }
context 'with project runner registration disabled' do
before do
stub_application_setting(valid_runner_registrars: ['group'])
end
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with guest' do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with developer' do
let(:current_user) { developer }
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with anonymous' do
let(:current_user) { nil }
context 'with specific project runner registration disabled' do
before do
project.update!(runner_registration_enabled: false)
end
it { is_expected.to be_disallowed(:create_runner) }
end
end
context 'create_runner_workflow_for_namespace flag disabled' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: [group])
end
context 'with maintainer' do
let(:current_user) { maintainer }
context 'admin' do
let(:current_user) { admin }
it { is_expected.to be_allowed(:create_runner) }
end
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_disallowed(:create_runner) }
context 'with reporter' do
let(:current_user) { reporter }
context 'with project runner registration disabled' do
before do
stub_application_setting(valid_runner_registrars: ['group'])
end
it { is_expected.to be_disallowed(:create_runner) }
end
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with guest' do
let(:current_user) { guest }
context 'with specific project runner registration disabled' do
before do
project.update!(runner_registration_enabled: false)
end
it { is_expected.to be_disallowed(:create_runner) }
end
it { is_expected.to be_disallowed(:create_runner) }
end
end
context 'with developer' do
let(:current_user) { developer }
context 'when admin mode is disabled' do
it { is_expected.to be_disallowed(:create_runner) }
end
end
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with owner' do
let(:current_user) { owner }
context 'with anonymous' do
let(:current_user) { nil }
it { is_expected.to be_disallowed(:create_runner) }
context 'with project runner registration disabled' do
before do
stub_application_setting(valid_runner_registrars: ['group'])
end
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with specific project runner registration disabled' do
before do
project.update!(runner_registration_enabled: false)
end
it { is_expected.to be_disallowed(:create_runner) }
end
end
context 'with maintainer' do
let(:current_user) { maintainer }
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with reporter' do
let(:current_user) { reporter }
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with guest' do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with developer' do
let(:current_user) { developer }
it { is_expected.to be_disallowed(:create_runner) }
end
context 'with anonymous' do
let(:current_user) { nil }
it { is_expected.to be_disallowed(:create_runner) }
end
it { is_expected.to be_disallowed(:create_runner) }
end
end

View File

@ -95,18 +95,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
end
end
shared_context 'when :create_runner_workflow_for_namespace feature flag is disabled' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: [other_group])
end
it 'returns an error' do
post_graphql_mutation(mutation, current_user: current_user)
expect_graphql_errors_to_include('`create_runner_workflow_for_namespace` feature flag is disabled.')
end
end
shared_examples 'when runner is created successfully' do
it do
expected_args = { user: current_user, params: anything }
@ -139,18 +127,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
context 'when user has permissions', :enable_admin_mode do
let(:current_user) { admin }
context 'when :create_runner_workflow_for_admin feature flag is disabled' do
before do
stub_feature_flags(create_runner_workflow_for_admin: false)
end
it 'returns an error' do
post_graphql_mutation(mutation, current_user: current_user)
expect_graphql_errors_to_include('`create_runner_workflow_for_admin` feature flag is disabled.')
end
end
it_behaves_like 'when runner is created successfully'
it_behaves_like 'when model is invalid returns error'
end
@ -164,17 +140,12 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
}
end
before do
stub_feature_flags(create_runner_workflow_for_namespace: [group])
end
it_behaves_like 'when user does not have permissions'
context 'when user has permissions' do
context 'when user is group owner' do
let(:current_user) { group_owner }
it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
it_behaves_like 'when runner is created successfully'
it_behaves_like 'when model is invalid returns error'
@ -226,7 +197,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
context 'when user is admin in admin mode', :enable_admin_mode do
let(:current_user) { admin }
it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
it_behaves_like 'when runner is created successfully'
it_behaves_like 'when model is invalid returns error'
end
@ -249,7 +219,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
context 'when user is group owner' do
let(:current_user) { group_owner }
it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
it_behaves_like 'when runner is created successfully'
it_behaves_like 'when model is invalid returns error'
@ -304,7 +273,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
context 'when user is admin in admin mode', :enable_admin_mode do
let(:current_user) { admin }
it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
it_behaves_like 'when runner is created successfully'
it_behaves_like 'when model is invalid returns error'
end

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe ErrorTracking::IssueDetailsService, feature_category: :error_tracking do
include_context 'sentry error tracking context'
subject { described_class.new(project, user, params) }
subject(:service) { described_class.new(project, user, params) }
describe '#execute' do
context 'with authorized user' do
@ -41,26 +41,41 @@ RSpec.describe ErrorTracking::IssueDetailsService, feature_category: :error_trac
include_examples 'error tracking service http status handling', :issue_details
context 'with integrated error tracking' do
let_it_be(:error) { create(:error_tracking_error, project: project) }
let(:params) { { issue_id: error.id } }
let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
let(:params) { { issue_id: issue_id } }
before do
error_tracking_setting.update!(integrated: true)
allow(service).to receive(:error_repository).and_return(error_repository)
end
it 'returns the error in detailed format' do
expect(result[:status]).to eq(:success)
expect(result[:issue].to_json).to eq(error.to_sentry_detailed_error.to_json)
context 'when error is found' do
let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
let(:issue_id) { error.fingerprint }
before do
allow(error_repository).to receive(:find_error).with(issue_id).and_return(error)
end
it 'returns the error in detailed format' do
expect(result[:status]).to eq(:success)
expect(result[:issue]).to eq(error)
end
end
context 'when error does not exist' do
let(:params) { { issue_id: non_existing_record_id } }
let(:issue_id) { non_existing_record_id }
before do
allow(error_repository).to receive(:find_error).with(issue_id)
.and_raise(Gitlab::ErrorTracking::ErrorRepository::DatabaseError.new('Error not found'))
end
it 'returns the error in detailed format' do
expect(result).to match(
status: :error,
message: /Couldn't find ErrorTracking::Error/,
message: /Error not found/,
http_status: :bad_request
)
end

View File

@ -7,7 +7,7 @@ RSpec.describe ErrorTracking::IssueLatestEventService, feature_category: :error_
let(:params) { {} }
subject { described_class.new(project, user, params) }
subject(:service) { described_class.new(project, user, params) }
describe '#execute' do
context 'with authorized user' do
@ -29,27 +29,42 @@ RSpec.describe ErrorTracking::IssueLatestEventService, feature_category: :error_
include_examples 'error tracking service http status handling', :issue_latest_event
context 'with integrated error tracking' do
let_it_be(:error) { create(:error_tracking_error, project: project) }
let_it_be(:event) { create(:error_tracking_error_event, error: error) }
let(:params) { { issue_id: error.id } }
let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
let(:params) { { issue_id: issue_id } }
before do
error_tracking_setting.update!(integrated: true)
allow(service).to receive(:error_repository).and_return(error_repository)
end
it 'returns the latest event in expected format' do
expect(result[:status]).to eq(:success)
expect(result[:latest_event].to_json).to eq(event.to_sentry_error_event.to_json)
context 'when error is found' do
let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
let(:event) { build_stubbed(:error_tracking_open_api_error_event, fingerprint: error.fingerprint) }
let(:issue_id) { error.fingerprint }
before do
allow(error_repository).to receive(:last_event_for).with(issue_id).and_return(event)
end
it 'returns the latest event in expected format' do
expect(result[:status]).to eq(:success)
expect(result[:latest_event]).to eq(event)
end
end
context 'when error does not exist' do
let(:params) { { issue_id: non_existing_record_id } }
let(:issue_id) { non_existing_record_id }
before do
allow(error_repository).to receive(:last_event_for).with(issue_id)
.and_raise(Gitlab::ErrorTracking::ErrorRepository::DatabaseError.new('Error not found'))
end
it 'returns the error in detailed format' do
expect(result).to match(
status: :error,
message: /Couldn't find ErrorTracking::Error/,
message: /Error not found/,
http_status: :bad_request
)
end

View File

@ -113,17 +113,45 @@ RSpec.describe ErrorTracking::IssueUpdateService, feature_category: :error_track
include_examples 'error tracking service sentry error handling', :update_issue
context 'with integrated error tracking' do
let(:error) { create(:error_tracking_error, project: project) }
let(:arguments) { { issue_id: error.id, status: 'resolved' } }
let(:update_issue_response) { { updated: true, status: :success, closed_issue_iid: nil } }
let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
let(:issue_id) { error.fingerprint }
let(:arguments) { { issue_id: issue_id, status: 'resolved' } }
before do
error_tracking_setting.update!(integrated: true)
allow(update_service).to receive(:error_repository).and_return(error_repository)
allow(error_repository).to receive(:update_error)
.with(issue_id, status: 'resolved').and_return(updated)
end
it 'resolves the error and responds with expected format' do
expect(update_service.execute).to eq(update_issue_response)
expect(error.reload.status).to eq('resolved')
context 'when update succeeded' do
let(:updated) { true }
it 'returns success with updated true' do
expect(project.error_tracking_setting).to receive(:expire_issues_cache)
expect(update_service.execute).to eq(
status: :success,
updated: true,
closed_issue_iid: nil
)
end
end
context 'when update failed' do
let(:updated) { false }
it 'returns success with updated false' do
expect(project.error_tracking_setting).to receive(:expire_issues_cache)
expect(update_service.execute).to eq(
status: :success,
updated: false,
closed_issue_iid: nil
)
end
end
end
end

View File

@ -7,10 +7,10 @@ RSpec.describe ErrorTracking::ListIssuesService, feature_category: :error_tracki
let(:params) { {} }
subject { described_class.new(project, user, params) }
subject(:service) { described_class.new(project, user, params) }
describe '#execute' do
context 'Sentry backend' do
context 'with Sentry backend' do
let(:params) { { search_term: 'something', sort: 'last_seen', cursor: 'some-cursor' } }
let(:list_sentry_issues_args) do
@ -42,7 +42,7 @@ RSpec.describe ErrorTracking::ListIssuesService, feature_category: :error_tracki
expect(result).to eq(status: :success, pagination: {}, issues: issues)
end
it 'returns bad request for an issue_status not on the whitelist' do
it 'returns bad request with invalid issue_status' do
params[:issue_status] = 'assigned'
expect(error_tracking_setting).not_to receive(:list_sentry_issues)
@ -65,22 +65,84 @@ RSpec.describe ErrorTracking::ListIssuesService, feature_category: :error_tracki
end
end
context 'GitLab backend' do
let_it_be(:error1) { create(:error_tracking_error, name: 'foo', project: project) }
let_it_be(:error2) { create(:error_tracking_error, name: 'bar', project: project) }
context 'with integrated error tracking' do
let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
let(:errors) { [] }
let(:pagination) { Gitlab::ErrorTracking::ErrorRepository::Pagination.new(nil, nil) }
let(:opts) { default_opts }
let(:params) { { limit: '1' } }
let(:default_opts) do
{
filters: { status: described_class::DEFAULT_ISSUE_STATUS },
query: nil,
sort: described_class::DEFAULT_SORT,
limit: described_class::DEFAULT_LIMIT,
cursor: nil
}
end
let(:params) { {} }
before do
error_tracking_setting.update!(integrated: true)
allow(service).to receive(:error_repository).and_return(error_repository)
end
it 'returns the error in expected format' do
expect(result[:status]).to eq(:success)
expect(result[:issues].size).to eq(1)
expect(result[:issues].first.to_json).to eq(error2.to_sentry_error.to_json)
expect(result[:pagination][:next][:cursor]).to be_present
expect(result[:pagination][:previous]).to be_nil
context 'when errors are found' do
let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
let(:errors) { [error] }
before do
allow(error_repository).to receive(:list_errors)
.with(**opts)
.and_return([errors, pagination])
end
context 'without params' do
it 'returns the errors without pagination' do
expect(result[:status]).to eq(:success)
expect(result[:issues]).to eq(errors)
expect(result[:pagination]).to eq({})
expect(error_repository).to have_received(:list_errors).with(**opts)
end
end
context 'with pagination' do
context 'with next page' do
before do
pagination.next = 'next cursor'
end
it 'has next cursor' do
expect(result[:pagination]).to eq(next: { cursor: 'next cursor' })
end
end
context 'with prev page' do
before do
pagination.prev = 'prev cursor'
end
it 'has prev cursor' do
expect(result[:pagination]).to eq(previous: { cursor: 'prev cursor' })
end
end
context 'with next and prev page' do
before do
pagination.next = 'next cursor'
pagination.prev = 'prev cursor'
end
it 'has both cursors' do
expect(result[:pagination]).to eq(
next: { cursor: 'next cursor' },
previous: { cursor: 'prev cursor' }
)
end
end
end
end
end
end

View File

@ -9123,10 +9123,6 @@
- './spec/services/environments/schedule_to_delete_review_apps_service_spec.rb'
- './spec/services/environments/stop_service_spec.rb'
- './spec/services/error_tracking/base_service_spec.rb'
- './spec/services/error_tracking/issue_details_service_spec.rb'
- './spec/services/error_tracking/issue_latest_event_service_spec.rb'
- './spec/services/error_tracking/issue_update_service_spec.rb'
- './spec/services/error_tracking/list_issues_service_spec.rb'
- './spec/services/error_tracking/list_projects_service_spec.rb'
- './spec/services/event_create_service_spec.rb'
- './spec/services/events/destroy_service_spec.rb'

View File

@ -15,66 +15,27 @@ RSpec.describe 'projects/runners/_project_runners.html.haml', feature_category:
allow(view).to receive(:reset_registration_token_namespace_project_settings_ci_cd_path).and_return('banana_url')
end
context 'when create_runner_workflow_for_namespace is disabled' do
context 'when user can create project runner' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: false)
allow(view).to receive(:can?).with(user, :create_runner, project).and_return(true)
end
context 'when project runner registration is allowed' do
before do
stub_application_setting(valid_runner_registrars: ['project'])
allow(view).to receive(:can?).with(user, :register_project_runners, project).and_return(true)
end
it 'renders the New project runner button' do
render 'projects/runners/project_runners', project: project
it 'enables the Remove project button for a project' do
render 'projects/runners/project_runners', project: project
expect(rendered).to have_selector '#js-install-runner'
expect(rendered).not_to have_content 'Please contact an admin to register runners.'
end
end
context 'when project runner registration is not allowed' do
before do
stub_application_setting(valid_runner_registrars: ['group'])
end
it 'does not enable the Remove project button for a project' do
render 'projects/runners/project_runners', project: project
expect(rendered).to have_content 'Please contact an admin to register runners.'
expect(rendered).not_to have_selector '#js-install-runner'
end
expect(rendered).to have_link(s_('Runners|New project runner'), href: new_project_runner_path(project))
end
end
context 'when create_runner_workflow_for_namespace is enabled' do
context 'when user cannot create project runner' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: project.namespace)
allow(view).to receive(:can?).with(user, :create_runner, project).and_return(false)
end
context 'when user can create project runner' do
before do
allow(view).to receive(:can?).with(user, :create_runner, project).and_return(true)
end
it 'does not render the New project runner button' do
render 'projects/runners/project_runners', project: project
it 'renders the New project runner button' do
render 'projects/runners/project_runners', project: project
expect(rendered).to have_link(s_('Runners|New project runner'), href: new_project_runner_path(project))
end
end
context 'when user cannot create project runner' do
before do
allow(view).to receive(:can?).with(user, :create_runner, project).and_return(false)
end
it 'does not render the New project runner button' do
render 'projects/runners/project_runners', project: project
expect(rendered).not_to have_link(s_('Runners|New project runner'))
end
expect(rendered).not_to have_link(s_('Runners|New project runner'))
end
end
end