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/file_collection/merge_request_diff_base.rb'
- 'lib/gitlab/diff/line.rb' - 'lib/gitlab/diff/line.rb'
- 'lib/gitlab/encoding_helper.rb' - 'lib/gitlab/encoding_helper.rb'
- 'lib/gitlab/error_tracking/error_repository/active_record_strategy.rb'
- 'lib/gitlab/fogbugz_import/importer.rb' - 'lib/gitlab/fogbugz_import/importer.rb'
- 'lib/gitlab/git/commit_stats.rb' - 'lib/gitlab/git/commit_stats.rb'
- 'lib/gitlab/git/conflict/parser.rb' - 'lib/gitlab/git/conflict/parser.rb'

View File

@ -79,7 +79,6 @@ Naming/InclusiveLanguage:
- 'spec/rubocop/cop/graphql/id_type_spec.rb' - 'spec/rubocop/cop/graphql/id_type_spec.rb'
- 'spec/services/application_settings/update_service_spec.rb' - 'spec/services/application_settings/update_service_spec.rb'
- 'spec/services/design_management/generate_image_versions_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/services/projects/download_service_spec.rb'
- 'spec/support/import_export/export_file_helper.rb' - 'spec/support/import_export/export_file_helper.rb'
- 'spec/support/shared_contexts/upload_type_check_shared_context.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/draft_notes/publish_service_spec.rb'
- 'spec/services/environments/schedule_to_delete_review_apps_service_spec.rb' - 'spec/services/environments/schedule_to_delete_review_apps_service_spec.rb'
- 'spec/services/environments/stop_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/error_tracking/list_projects_service_spec.rb'
- 'spec/services/events/destroy_service_spec.rb' - 'spec/services/events/destroy_service_spec.rb'
- 'spec/services/feature_flags/update_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 { createConsumer } from '@rails/actioncable';
import ConnectionMonitor from './actioncable_connection_monitor';
const consumer = createConsumer(); export default createConsumer();
if (consumer.connection) {
consumer.connection.monitor = new ConnectionMonitor(consumer.connection);
}
export default consumer;

View File

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

View File

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

View File

@ -11,7 +11,6 @@ import {
I18N_CREATE_RUNNER_LINK, I18N_CREATE_RUNNER_LINK,
I18N_STILL_USING_REGISTRATION_TOKENS, I18N_STILL_USING_REGISTRATION_TOKENS,
I18N_CONTACT_ADMIN_TO_REGISTER, I18N_CONTACT_ADMIN_TO_REGISTER,
I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
I18N_NO_RESULTS, I18N_NO_RESULTS,
I18N_EDIT_YOUR_SEARCH, I18N_EDIT_YOUR_SEARCH,
} from '~/ci/runner/constants'; } from '~/ci/runner/constants';
@ -44,15 +43,6 @@ export default {
default: null, 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', modalId: 'runners-empty-state-instructions-modal',
svgHeight: 145, svgHeight: 145,
EMPTY_STATE_SVG_URL, EMPTY_STATE_SVG_URL,
@ -63,7 +53,6 @@ export default {
I18N_CREATE_RUNNER_LINK, I18N_CREATE_RUNNER_LINK,
I18N_STILL_USING_REGISTRATION_TOKENS, I18N_STILL_USING_REGISTRATION_TOKENS,
I18N_CONTACT_ADMIN_TO_REGISTER, I18N_CONTACT_ADMIN_TO_REGISTER,
I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
I18N_NO_RESULTS, I18N_NO_RESULTS,
I18N_EDIT_YOUR_SEARCH, I18N_EDIT_YOUR_SEARCH,
}; };
@ -85,7 +74,6 @@ export default {
> >
<template #description> <template #description>
{{ $options.I18N_RUNNERS_ARE_AGENTS }} {{ $options.I18N_RUNNERS_ARE_AGENTS }}
<template v-if="shouldShowCreateRunnerWorkflow">
<gl-sprintf v-if="newRunnerPath" :message="$options.I18N_CREATE_RUNNER_LINK"> <gl-sprintf v-if="newRunnerPath" :message="$options.I18N_CREATE_RUNNER_LINK">
<template #link="{ content }"> <template #link="{ content }">
<gl-link :href="newRunnerPath">{{ content }}</gl-link> <gl-link :href="newRunnerPath">{{ content }}</gl-link>
@ -105,21 +93,5 @@ export default {
{{ $options.I18N_CONTACT_ADMIN_TO_REGISTER }} {{ $options.I18N_CONTACT_ADMIN_TO_REGISTER }}
</template> </template>
</template> </template>
<gl-sprintf
v-else-if="registrationToken"
:message="$options.I18N_FOLLOW_REGISTRATION_INSTRUCTIONS"
>
<template #link="{ content }">
<gl-link v-gl-modal="$options.modalId">{{ content }}</gl-link>
<runner-instructions-modal
:modal-id="$options.modalId"
:registration-token="registrationToken"
/>
</template>
</gl-sprintf>
<template v-else>
{{ $options.I18N_CONTACT_ADMIN_TO_REGISTER }}
</template>
</template>
</gl-empty-state> </gl-empty-state>
</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__( export const I18N_CONTACT_ADMIN_TO_REGISTER = s__(
'Runners|To register new runners, contact your administrator.', '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 // No runners found
export const I18N_NO_RESULTS = s__('Runners|No results found'); export const I18N_NO_RESULTS = s__('Runners|No results found');

View File

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

View File

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

View File

@ -122,14 +122,6 @@ export default {
return null; return null;
} }
}, },
showDeprecationAlert() {
return (
// create_runner_workflow_for_admin
this.glFeatures.createRunnerWorkflowForAdmin ||
// create_runner_workflow_for_namespace
this.glFeatures.createRunnerWorkflowForNamespace
);
},
}, },
updated() { updated() {
// Refocus on dom changes, after loading data // Refocus on dom changes, after loading data
@ -200,12 +192,7 @@ export default {
v-on="$listeners" v-on="$listeners"
@shown="onShown" @shown="onShown"
> >
<gl-alert <gl-alert :title="$options.i18n.deprecationAlertTitle" variant="warning" :dismissible="false">
v-if="showDeprecationAlert"
:title="$options.i18n.deprecationAlertTitle"
variant="warning"
:dismissible="false"
>
<gl-sprintf :message="$options.i18n.deprecationAlertContent"> <gl-sprintf :message="$options.i18n.deprecationAlertContent">
<template #link="{ content }"> <template #link="{ content }">
<gl-link target="_blank" :href="$options.LEGACY_REGISTER_HELP_URL" <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 :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 feature_category :runner
urgency :low urgency :low
@ -23,12 +19,10 @@ class Admin::RunnersController < Admin::ApplicationController
end end
def new def new
render_404 unless Feature.enabled?(:create_runner_workflow_for_admin, current_user)
end end
def register def register
render_404 unless Feature.enabled?(:create_runner_workflow_for_admin, current_user) && render_404 unless runner.registration_available?
runner.registration_available?
end end
def update 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 :authorize_update_runner!, only: [:edit, :update, :destroy, :pause, :resume]
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show, :register] 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 feature_category :runner
urgency :low urgency :low
@ -35,11 +31,10 @@ class Groups::RunnersController < Groups::ApplicationController
end end
def new def new
render_404 unless create_runner_workflow_for_namespace_enabled?
end end
def register def register
render_404 unless create_runner_workflow_for_namespace_enabled? && runner.registration_available? render_404 unless runner.registration_available?
end end
private private
@ -67,10 +62,6 @@ class Groups::RunnersController < Groups::ApplicationController
render_404 render_404
end end
def create_runner_workflow_for_namespace_enabled?
Feature.enabled?(:create_runner_workflow_for_namespace, group)
end
end end
Groups::RunnersController.prepend_mod Groups::RunnersController.prepend_mod

View File

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

View File

@ -15,7 +15,6 @@ module Projects
before_action do before_action do
push_frontend_feature_flag(:ci_variables_pages, current_user) push_frontend_feature_flag(:ci_variables_pages, current_user)
push_frontend_feature_flag(:ci_limit_environment_scope, @project) 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, @project)
push_frontend_feature_flag(:frozen_outbound_job_token_scopes_override, @project) push_frontend_feature_flag(:frozen_outbound_job_token_scopes_override, @project)
end end

View File

@ -37,8 +37,6 @@ module Mutations
parse_gid(**args) parse_gid(**args)
check_feature_flag(**args)
super super
end end
@ -79,28 +77,6 @@ module Mutations
GitlabSchema.parse_gid(args[:project_id], expected_type: ::Project) GitlabSchema.parse_gid(args[:project_id], expected_type: ::Project)
end end
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 end
end end

View File

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

View File

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

View File

@ -253,10 +253,6 @@ class ProjectPolicy < BasePolicy
!Gitlab.config.terraform_state.enabled !Gitlab.config.terraform_state.enabled
end end
condition(:create_runner_workflow_enabled) do
Feature.enabled?(:create_runner_workflow_for_namespace, project.namespace)
end
condition(:namespace_catalog_available) { namespace_catalog_available? } condition(:namespace_catalog_available) { namespace_catalog_available? }
# `:read_project` may be prevented in EE, but `:read_project_for_iids` should # `:read_project` may be prevented in EE, but `:read_project_for_iids` should
@ -886,10 +882,6 @@ class ProjectPolicy < BasePolicy
enable :read_code enable :read_code
end end
rule { ~create_runner_workflow_enabled }.policy do
prevent :create_runner
end
# Should be matched with GroupPolicy#read_internal_note # Should be matched with GroupPolicy#read_internal_note
rule { admin | can?(:reporter_access) }.enable :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) - if Feature.enabled?(:edit_user_profile_vue, current_user)
.js-user-profile .js-user-profile
- else - 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| = 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|
.row.js-search-settings-section .js-search-settings-section.gl-pb-6
.col-lg-4.profile-settings-sidebar .profile-settings-sidebar
%h4.gl-mt-0 %h4.gl-my-0
= s_("Profiles|Public avatar") = s_("Profiles|Public avatar")
%p %p.gl-text-secondary
- if @user.avatar? - if @user.avatar?
- if gravatar_enabled? - 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 } = 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,7 +26,6 @@
- if current_appearance&.profile_image_guidelines? - if current_appearance&.profile_image_guidelines?
.md .md
= brand_profile_image_guidelines = brand_profile_image_guidelines
.col-lg-8
.avatar-image .avatar-image
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = 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') = render Pajamas::AvatarComponent.new(@user, size: 96, alt: "", class: 'gl-float-left gl-mr-5')
@ -44,13 +43,12 @@
button_options: { class: 'gl-mt-3', data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") } }, button_options: { class: 'gl-mt-3', data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") } },
method: :delete) do method: :delete) do
= s_("Profiles|Remove avatar") = s_("Profiles|Remove avatar")
.col-lg-12
%hr .js-search-settings-section.gl-border-t.gl-py-6
.row.js-search-settings-section .profile-settings-sidebar
.col-lg-4.profile-settings-sidebar %h4.gl-my-0= s_("Profiles|Current status")
%h4.gl-mt-0= s_("Profiles|Current status") %p.gl-text-secondary= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
%p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.") .gl-max-w-80
.col-lg-8
#js-user-profile-set-status-form #js-user-profile-set-status-form
= f.fields_for :status, @user.status do |status_form| = f.fields_for :status, @user.status do |status_form|
= status_form.hidden_field :emoji, data: { js_name: 'emoji' } = status_form.hidden_field :emoji, data: { js_name: 'emoji' }
@ -59,30 +57,25 @@
= status_form.hidden_field :clear_status_after, = status_form.hidden_field :clear_status_after,
value: user_clear_status_at(@user), value: user_clear_status_at(@user),
data: { js_name: 'clearStatusAfter' } data: { js_name: 'clearStatusAfter' }
.col-lg-12
%hr .user-time-preferences.js-search-settings-section.gl-border-t.gl-py-6
.row.user-time-preferences.js-search-settings-section .profile-settings-sidebar
.col-lg-4.profile-settings-sidebar %h4.gl-my-0= s_("Profiles|Time settings")
%h4.gl-mt-0= s_("Profiles|Time settings") %p.gl-text-secondary= s_("Profiles|Set your local time zone.")
%p= s_("Profiles|Set your local time zone.")
.col-lg-8
= f.label :user_timezone, _("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-timezone-dropdown{ data: { timezone_data: timezone_data.to_json, initial_value: @user.timezone, name: 'user[timezone]' } }
.col-lg-12
%hr .js-search-settings-section.gl-border-t.gl-py-6
.row.js-search-settings-section .profile-settings-sidebar
.col-lg-4.profile-settings-sidebar %h4.gl-my-0
%h4.gl-mt-0
= s_("Profiles|Main settings") = s_("Profiles|Main settings")
%p %p.gl-text-secondary
= s_("Profiles|This information will appear on your profile.") = s_("Profiles|This information will appear on your profile.")
- if current_user.ldap_user? - if current_user.ldap_user?
= s_("Profiles|Some options are unavailable for LDAP accounts") = s_("Profiles|Some options are unavailable for LDAP accounts")
.col-lg-8 .form-group.gl-form-group.rspec-full-name.gl-max-w-80
.row
.form-group.gl-form-group.col-md-9.rspec-full-name
= render 'profiles/name', form: f, user: @user = render 'profiles/name', form: f, user: @user
.form-group.gl-form-group.col-md-3 .form-group.gl-form-group.gl-md-form-input-lg
= f.label :id, s_('Profiles|User ID') = f.label :id, s_('Profiles|User ID')
= f.text_field :id, class: 'gl-form-input form-control', readonly: true = f.text_field :id, class: 'gl-form-input form-control', readonly: true
.form-group.gl-form-group .form-group.gl-form-group
@ -145,32 +138,30 @@
= f.text_field :organization, class: 'gl-form-input form-control gl-md-form-input-lg' = f.text_field :organization, class: 'gl-form-input form-control gl-md-form-input-lg'
%small.form-text.text-gl-muted %small.form-text.text-gl-muted
= s_("Profiles|Who you represent or work for.") = s_("Profiles|Who you represent or work for.")
.form-group.gl-form-group .form-group.gl-form-group.gl-mb-6.gl-max-w-80
= f.label :bio, s_('Profiles|Bio') = f.label :bio, s_('Profiles|Bio')
= f.text_area :bio, class: 'gl-form-input gl-form-textarea form-control', rows: 4, maxlength: 250 = f.text_area :bio, class: 'gl-form-input gl-form-textarea form-control', rows: 4, maxlength: 250
%small.form-text.text-gl-muted %small.form-text.text-gl-muted
= s_("Profiles|Tell us about yourself in fewer than 250 characters.") = s_("Profiles|Tell us about yourself in fewer than 250 characters.")
%hr .gl-border-t.gl-pt-6
%fieldset.form-group.gl-form-group %fieldset.form-group.gl-form-group
%legend.col-form-label.col-form-label %legend.col-form-label
= _('Private profile') = _('Private profile')
- private_profile_label = s_("Profiles|Don't display activity-related personal information on your 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') - 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 } = 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 %fieldset.form-group.gl-form-group
%legend.col-form-label.col-form-label %legend.col-form-label
= s_("Profiles|Private contributions") = s_("Profiles|Private contributions")
= f.gitlab_ui_checkbox_component :include_private_contributions, = f.gitlab_ui_checkbox_component :include_private_contributions,
s_('Profiles|Include private contributions on your profile'), 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.") 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 %fieldset.form-group.gl-form-group.gl-mb-0
%legend.col-form-label.col-form-label %legend.col-form-label
= s_("Profiles|Achievements") = s_("Profiles|Achievements")
= f.gitlab_ui_checkbox_component :achievements_enabled, = f.gitlab_ui_checkbox_component :achievements_enabled,
s_('Profiles|Display achievements on your profile') s_('Profiles|Display achievements on your profile')
.row.js-hide-when-nothing-matches-search .js-hide-when-nothing-matches-search.gl-border-t.gl-py-6
.col-lg-12
%hr
= f.submit s_("Profiles|Update profile settings"), class: 'gl-mr-3 js-password-prompt-btn', pajamas_button: true = 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 = render Pajamas::ButtonComponent.new(href: user_path(current_user)) do
= s_('TagsPage|Cancel') = s_('TagsPage|Cancel')

View File

@ -3,7 +3,6 @@
.bs-callout.help-callout .bs-callout.help-callout
%p= s_('Runners|These runners are assigned to this project.') %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) - if can?(current_user, :create_runner, @project)
= render Pajamas::ButtonComponent.new(href: new_project_runner_path(@project), variant: :confirm) do = render Pajamas::ButtonComponent.new(href: new_project_runner_path(@project), variant: :confirm) do
= s_('Runners|New project runner') = s_('Runners|New project runner')
@ -12,17 +11,6 @@
- else - else
= _('Please contact an admin to create runners.') = _('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' = 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'
- 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'
%hr %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 data_category: optional
key_path: usage_activity_by_stage_monthly.plan.service_desk_enabled_projects 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_section: ops
product_stage: monitor product_stage: monitor
product_group: respond product_group: respond

View File

@ -1,7 +1,7 @@
--- ---
data_category: optional data_category: optional
key_path: usage_activity_by_stage.plan.service_desk_enabled_projects 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_section: ops
product_stage: monitor product_stage: monitor
product_group: respond product_group: respond

View File

@ -6,6 +6,9 @@ development:
cluster_cache: cluster_cache:
cluster: cluster:
- redis://localhost:7001 - redis://localhost:7001
feature_flag:
cluster:
- redis://localhost:7001
rate_limiting: rate_limiting:
cluster: cluster:
- redis://localhost:7001 - redis://localhost:7001
@ -17,6 +20,9 @@ test:
cluster_cache: cluster_cache:
cluster: cluster:
- redis://localhost:7001 - redis://localhost:7001
feature_flag:
cluster:
- redis://localhost:7001
rate_limiting: rate_limiting:
cluster: cluster:
- redis://localhost:7001 - 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 When `min_concurrency` is greater than `max_concurrency`, it is treated as
being equal to `max_concurrency`. 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. The values vary according to the work each specific deployment of Sidekiq does.
Any other specialized deployments with processes dedicated to specific queues Any other specialized deployments with processes dedicated to specific queues
should have the concurrency tuned according to: 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) > - [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. > - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/389269) in GitLab 16.0.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/415447) in GitLab 16.2. Feature flag `create_runner_workflow_for_admin` removed.
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`.
Prerequisites: 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. > - [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. > - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/393919) in GitLab 16.0.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/415447) in GitLab 16.2. Feature flag `create_runner_workflow_for_admin` removed.
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`.
Prerequisites: 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. > - [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. > - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/393919) in GitLab 16.0.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/415447) in GitLab 16.2. Feature flag `create_runner_workflow_for_admin` removed.
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`.
Prerequisites: Prerequisites:

View File

@ -863,9 +863,9 @@ PUT /namespaces/:id/subscription_add_on_purchase/:add_on_name
| Attribute | Type | Required | Description | | 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 | | `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: Example request:

View File

@ -1,6 +1,6 @@
--- ---
stage: Create stage: Manage
group: Ecosystem 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 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. 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 1. Ensure an account exists in the
[Customers Portal](https://customers.gitlab.com/customers/sign_in) for the user you want to add. [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. 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 ## 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. 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 1. Ensure an account exists in the
[Customers Portal](https://customers.gitlab.com/customers/sign_in) for the user you want to add. [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. 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 ## 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.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) | | `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`: 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" 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 ## Push options for merge requests
You can use Git push options to perform certain actions for merge requests at the same You can use Git push options to perform certain actions for merge requests at the same

View File

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

View File

@ -15,12 +15,7 @@ module Gitlab
# #
# @return [self] # @return [self]
def self.build(project) def self.build(project)
strategy = strategy = OpenApiStrategy.new(project)
if Feature.enabled?(:gitlab_error_tracking, project)
OpenApiStrategy.new(project)
else
ActiveRecordStrategy.new(project)
end
new(strategy) new(strategy)
end 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,6 +11,7 @@ module Gitlab
Gitlab::Redis::Cache.with do |r| Gitlab::Redis::Cache.with do |r|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
r.with_readonly_pipeline do
Gitlab::Redis::CrossSlot::Pipeline.new(r).pipelined do |pipeline| Gitlab::Redis::CrossSlot::Pipeline.new(r).pipelined do |pipeline|
subjects.each do |subject| subjects.each do |subject|
results[subject.cache_key] = new(subject).read(pipeline) results[subject.cache_key] = new(subject).read(pipeline)
@ -18,6 +19,7 @@ module Gitlab
end end
end end
end end
end
results results
end end

View File

@ -43,7 +43,13 @@ module Gitlab
keys = names.map { |name| normalize_key(name, options) } keys = names.map { |name| normalize_key(name, options) }
values = failsafe(:patched_read_multi_mget, returning: {}) do 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 end
names.zip(values).each_with_object({}) do |(name, value), results| names.zip(values).each_with_object({}) do |(name, value), results|

View File

@ -24,6 +24,12 @@ module Gitlab
end end
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 attr_reader :primary_store, :secondary_store, :instance_name
FAILED_TO_READ_ERROR_MESSAGE = 'Failed to read from the redis default_store.' FAILED_TO_READ_ERROR_MESSAGE = 'Failed to read from the redis default_store.'
@ -100,6 +106,25 @@ module Gitlab
validate_stores! validate_stores!
end 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 # rubocop:disable GitlabSecurity/PublicSend
READ_COMMANDS.each do |name| READ_COMMANDS.each do |name|
define_method(name) do |*args, **kwargs, &block| define_method(name) do |*args, **kwargs, &block|
@ -123,7 +148,7 @@ module Gitlab
PIPELINED_COMMANDS.each do |name| PIPELINED_COMMANDS.each do |name|
define_method(name) do |*args, **kwargs, &block| 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) pipelined_both(name, *args, **kwargs, &block)
else else
send_command(default_store, name, *args, **kwargs, &block) send_command(default_store, name, *args, **kwargs, &block)

View File

@ -34249,9 +34249,6 @@ msgstr ""
msgid "Please contact an admin to create runners." msgid "Please contact an admin to create runners."
msgstr "" msgstr ""
msgid "Please contact an admin to register runners."
msgstr ""
msgid "Please contact your GitLab administrator if you think this is an error." msgid "Please contact your GitLab administrator if you think this is an error."
msgstr "" msgstr ""
@ -39678,9 +39675,6 @@ msgstr ""
msgid "Runners|Filter projects" msgid "Runners|Filter projects"
msgstr "" msgstr ""
msgid "Runners|Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner."
msgstr ""
msgid "Runners|Get started with runners" msgid "Runners|Get started with runners"
msgstr "" 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 have_gitlab_http_status(:ok)
expect(response).to render_template(:new) expect(response).to render_template(:new)
end 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 end
describe '#register' do 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) expect(response).to have_gitlab_http_status(:not_found)
end end
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 end
describe '#edit' do describe '#edit' do

View File

@ -65,11 +65,6 @@ RSpec.describe Groups::RunnersController, feature_category: :runner_fleet do
end end
describe '#new' do describe '#new' do
context 'when create_runner_workflow_for_namespace is enabled' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: [group])
end
context 'when user is owner' do context 'when user is owner' do
before do before do
group.add_owner(user) group.add_owner(user)
@ -96,33 +91,9 @@ RSpec.describe Groups::RunnersController, feature_category: :runner_fleet do
end end
end end
context 'when create_runner_workflow_for_namespace is disabled' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: false)
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 }
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
describe '#register' do describe '#register' do
subject(:register) { get :register, params: { group_id: group, id: new_runner } } subject(:register) { get :register, params: { group_id: group, id: new_runner } }
context 'when create_runner_workflow_for_namespace is enabled' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: [group])
end
context 'when user is owner' do context 'when user is owner' do
before do before do
group.add_owner(user) group.add_owner(user)
@ -167,27 +138,6 @@ RSpec.describe Groups::RunnersController, feature_category: :runner_fleet do
end 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) }
before do
stub_feature_flags(create_runner_workflow_for_namespace: false)
end
context 'when user is owner' do
before do
group.add_owner(user)
end
it 'returns :not_found' do
register
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
describe '#show' do describe '#show' do
context 'when user is owner' do context 'when user is owner' do
before do before do

View File

@ -28,11 +28,6 @@ RSpec.describe Projects::RunnersController, feature_category: :runner_fleet do
} }
end end
context 'when create_runner_workflow_for_namespace is enabled' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
end
context 'when user is maintainer' do context 'when user is maintainer' do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
@ -59,33 +54,9 @@ RSpec.describe Projects::RunnersController, feature_category: :runner_fleet do
end end
end end
context 'when create_runner_workflow_for_namespace is disabled' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: false)
end
context 'when user is maintainer' do
before do
project.add_maintainer(user)
end
it 'renders a 404' do
get :new, params: params
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
describe '#register' do describe '#register' do
subject(:register) { get :register, params: { namespace_id: project.namespace, project_id: project, id: new_runner } } 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
before do
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
end
context 'when user is maintainer' do context 'when user is maintainer' do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
@ -130,27 +101,6 @@ RSpec.describe Projects::RunnersController, feature_category: :runner_fleet do
end 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) }
before do
stub_feature_flags(create_runner_workflow_for_namespace: false)
end
context 'when user is maintainer' do
before do
project.add_maintainer(user)
end
it 'returns :not_found' do
register
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
describe '#update' do describe '#update' do
it 'updates the runner and ticks the queue' do it 'updates the runner and ticks the queue' do
new_desc = runner.description.swapcase new_desc = runner.description.swapcase

View File

@ -32,10 +32,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
end end
describe "runners registration" do describe "runners registration" do
context 'when create_runner_workflow_for_namespace is enabled' do
before do before do
stub_feature_flags(create_runner_workflow_for_admin: true)
visit admin_runners_path visit admin_runners_path
end end
@ -45,20 +42,6 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
end end
end 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
end
end
context "when there are runners" do context "when there are runners" do
context "with an instance runner" do context "with an instance runner" do
let_it_be(:instance_runner) { create(:ci_runner, :instance) } let_it_be(:instance_runner) { create(:ci_runner, :instance) }

View File

@ -16,21 +16,6 @@ RSpec.describe "Group Runners", feature_category: :runner_fleet do
end end
describe "Group runners page", :js do 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 context "with no runners" do
before do before do
visit group_runners_path(group) 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) stub_feature_flags(project_runners_vue_ui: false)
end end
context 'when user views runners page' do context 'with user as project maintainer' do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project).tap { |project| project.add_maintainer(user) } }
context 'when user views runners page', :js do
before do before do
project.add_maintainer(user)
end
context 'when create_runner_workflow_for_namespace is enabled', :js do
before do
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
visit project_runners_path(project) visit project_runners_path(project)
end end
@ -38,11 +32,8 @@ RSpec.describe 'Runners', feature_category: :runner_fleet do
end end
end end
context 'when user views new runner page' do context 'when user views new runner page', :js do
context 'when create_runner_workflow_for_namespace is enabled', :js do
before do before do
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
visit new_project_runner_path(project) visit new_project_runner_path(project)
end end
@ -57,43 +48,6 @@ RSpec.describe 'Runners', feature_category: :runner_fleet do
end end
end end
context 'when create_runner_workflow_for_namespace is disabled' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: false)
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/")
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
end
end
end
context 'when a project has enabled shared_runners' do context 'when a project has enabled shared_runners' do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }

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

View File

@ -10,7 +10,6 @@ import {
I18N_CREATE_RUNNER_LINK, I18N_CREATE_RUNNER_LINK,
I18N_STILL_USING_REGISTRATION_TOKENS, I18N_STILL_USING_REGISTRATION_TOKENS,
I18N_CONTACT_ADMIN_TO_REGISTER, I18N_CONTACT_ADMIN_TO_REGISTER,
I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
I18N_NO_RESULTS, I18N_NO_RESULTS,
I18N_EDIT_YOUR_SEARCH, I18N_EDIT_YOUR_SEARCH,
} from '~/ci/runner/constants'; } from '~/ci/runner/constants';
@ -59,14 +58,6 @@ describe('RunnerListEmptyState', () => {
}); });
describe('when search is not filtered', () => { describe('when search is not filtered', () => {
describe.each([
{ createRunnerWorkflowForAdmin: true },
{ createRunnerWorkflowForNamespace: true },
])('when createRunnerWorkflow is enabled by %o', (currentGlFeatures) => {
beforeEach(() => {
glFeatures = currentGlFeatures;
});
describe.each` describe.each`
newRunnerPath | registrationToken | expectedMessages newRunnerPath | registrationToken | expectedMessages
${mockNewRunnerPath} | ${mockRegistrationToken} | ${[I18N_CREATE_RUNNER_LINK, I18N_STILL_USING_REGISTRATION_TOKENS]} ${mockNewRunnerPath} | ${mockRegistrationToken} | ${[I18N_CREATE_RUNNER_LINK, I18N_STILL_USING_REGISTRATION_TOKENS]}
@ -149,50 +140,6 @@ describe('RunnerListEmptyState', () => {
}); });
}); });
describe('when createRunnerWorkflow is disabled', () => {
describe('when there is a registration token', () => {
beforeEach(() => {
createComponent({
props: {
registrationToken: mockRegistrationToken,
},
});
});
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('displays text with registration instructions', () => {
expectTitleToBe(I18N_GET_STARTED);
expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_FOLLOW_REGISTRATION_INSTRUCTIONS]);
});
});
describe('when there is no registration token', () => {
beforeEach(() => {
createComponent({ props: { registrationToken: null } });
});
it('displays "contact admin" text', () => {
expectTitleToBe(I18N_GET_STARTED);
expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_CONTACT_ADMIN_TO_REGISTER]);
});
it('has no registration instructions link', () => {
expect(findLink().exists()).toBe(false);
});
});
});
});
describe('when search is filtered', () => { describe('when search is filtered', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ props: { isSearchFiltered: true } }); createComponent({ props: { isSearchFiltered: true } });

View File

@ -483,35 +483,15 @@ describe('GroupRunnersApp', () => {
expect(findRegistrationDropdown().exists()).toBe(true); expect(findRegistrationDropdown().exists()).toBe(true);
}); });
it('when create_runner_workflow_for_namespace is enabled', () => { it('shows the create runner button', () => {
createComponent({ createComponent({
props: { props: {
newRunnerPath, newRunnerPath,
}, },
provide: {
glFeatures: {
createRunnerWorkflowForNamespace: true,
},
},
}); });
expect(findNewRunnerBtn().attributes('href')).toBe(newRunnerPath); 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', () => { describe('when user has no permission to register group runner', () => {
@ -524,16 +504,11 @@ describe('GroupRunnersApp', () => {
expect(findRegistrationDropdown().exists()).toBe(false); expect(findRegistrationDropdown().exists()).toBe(false);
}); });
it('when create_runner_workflow_for_namespace is enabled', () => { it('shows the create runner button', () => {
createComponent({ createComponent({
props: { props: {
newRunnerPath: null, newRunnerPath: null,
}, },
provide: {
glFeatures: {
createRunnerWorkflowForNamespace: true,
},
},
}); });
expect(findNewRunnerBtn().exists()).toBe(false); 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 TaskListItemActions from '~/issues/show/components/task_list_item_actions.vue';
import eventHub from '~/issues/show/event_hub'; import eventHub from '~/issues/show/event_hub';
jest.mock('~/issues/show/event_hub');
describe('TaskListItemActions component', () => { describe('TaskListItemActions component', () => {
let wrapper; let wrapper;
@ -37,16 +39,12 @@ describe('TaskListItemActions component', () => {
}); });
it('emits event when `Convert to task` dropdown item is clicked', () => { it('emits event when `Convert to task` dropdown item is clicked', () => {
jest.spyOn(eventHub, '$emit');
findConvertToTaskItem().vm.$emit('action'); findConvertToTaskItem().vm.$emit('action');
expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10'); expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10');
}); });
it('emits event when `Delete` dropdown item is clicked', () => { it('emits event when `Delete` dropdown item is clicked', () => {
jest.spyOn(eventHub, '$emit');
findDeleteItem().vm.$emit('action'); findDeleteItem().vm.$emit('action');
expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10'); 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 { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import GfmAutoComplete from '~/gfm_auto_complete';
import { TYPE_ISSUE } from '~/issues/constants'; import { TYPE_ISSUE } from '~/issues/constants';
import RelatedIssuableInput from '~/related_issues/components/related_issuable_input.vue'; import RelatedIssuableInput from '~/related_issues/components/related_issuable_input.vue';
import { PathIdSeparator } from '~/related_issues/constants'; import { PathIdSeparator } from '~/related_issues/constants';
jest.mock('ee_else_ce/gfm_auto_complete', () => { jest.mock('~/gfm_auto_complete');
return function gfmAutoComplete() {
return {
constructor() {},
setup() {},
};
};
});
describe('RelatedIssuableInput', () => { describe('RelatedIssuableInput', () => {
let propsData; let wrapper;
beforeEach(() => { const autoCompleteSources = {
propsData = { issues: `${TEST_HOST}/h5bp/html5-boilerplate/-/autocomplete_sources/issues`,
};
const mountComponent = (props = {}) => {
wrapper = shallowMount(RelatedIssuableInput, {
propsData: {
inputValue: '', inputValue: '',
references: [], references: [],
pathIdSeparator: PathIdSeparator.Issue, pathIdSeparator: PathIdSeparator.Issue,
issuableType: TYPE_ISSUE, issuableType: TYPE_ISSUE,
autoCompleteSources: { autoCompleteSources,
issues: `${TEST_HOST}/h5bp/html5-boilerplate/-/autocomplete_sources/issues`, ...props,
}, },
}; attachTo: document.body,
}); });
};
describe('autocomplete', () => { describe('autocomplete', () => {
describe('with autoCompleteSources', () => { describe('with autoCompleteSources', () => {
it('shows placeholder text', () => { it('shows placeholder text', () => {
const wrapper = shallowMount(RelatedIssuableInput, { propsData }); mountComponent();
expect(wrapper.findComponent({ ref: 'input' }).element.placeholder).toBe( expect(wrapper.findComponent({ ref: 'input' }).element.placeholder).toBe(
'Paste issue link or <#issue id>', 'Paste issue link or <#issue id>',
@ -40,51 +39,32 @@ describe('RelatedIssuableInput', () => {
}); });
it('has GfmAutoComplete', () => { it('has GfmAutoComplete', () => {
const wrapper = shallowMount(RelatedIssuableInput, { propsData }); mountComponent();
expect(wrapper.vm.gfmAutoComplete).toBeDefined(); expect(GfmAutoComplete).toHaveBeenCalledWith(autoCompleteSources);
}); });
}); });
describe('with no autoCompleteSources', () => { describe('with no autoCompleteSources', () => {
it('shows placeholder text', () => { it('shows placeholder text', () => {
const wrapper = shallowMount(RelatedIssuableInput, { mountComponent({ references: ['!1', '!2'] });
propsData: {
...propsData,
references: ['!1', '!2'],
},
});
expect(wrapper.findComponent({ ref: 'input' }).element.value).toBe(''); expect(wrapper.findComponent({ ref: 'input' }).element.value).toBe('');
}); });
it('does not have GfmAutoComplete', () => { it('does not have GfmAutoComplete', () => {
const wrapper = shallowMount(RelatedIssuableInput, { mountComponent({ autoCompleteSources: {} });
propsData: {
...propsData,
autoCompleteSources: {},
},
});
expect(wrapper.vm.gfmAutoComplete).not.toBeDefined(); expect(GfmAutoComplete).not.toHaveBeenCalled();
}); });
}); });
}); });
describe('focus', () => { describe('focus', () => {
it('when clicking anywhere on the input wrapper it should focus the input', async () => { it('when clicking anywhere on the input wrapper it should focus the input', async () => {
const wrapper = shallowMount(RelatedIssuableInput, { mountComponent({ references: ['foo', 'bar'] });
propsData: {
...propsData,
references: ['foo', 'bar'],
},
// We need to attach to document, so that `document.activeElement` is properly set in jsdom
attachTo: document.body,
});
wrapper.find('li').trigger('click'); await wrapper.find('li').trigger('click');
await nextTick();
expect(document.activeElement).toBe(wrapper.findComponent({ ref: 'input' }).element); expect(document.activeElement).toBe(wrapper.findComponent({ ref: 'input' }).element);
}); });
@ -92,11 +72,7 @@ describe('RelatedIssuableInput', () => {
describe('when filling in the input', () => { describe('when filling in the input', () => {
it('emits addIssuableFormInput with data', () => { it('emits addIssuableFormInput with data', () => {
const wrapper = shallowMount(RelatedIssuableInput, { mountComponent();
propsData,
});
wrapper.vm.$emit = jest.fn();
const newInputValue = 'filling in things'; const newInputValue = 'filling in things';
const untouchedRawReferences = newInputValue.trim().split(/\s/); const untouchedRawReferences = newInputValue.trim().split(/\s/);
@ -108,12 +84,16 @@ describe('RelatedIssuableInput', () => {
input.element.selectionEnd = newInputValue.length; input.element.selectionEnd = newInputValue.length;
input.trigger('input'); input.trigger('input');
expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormInput', { expect(wrapper.emitted('addIssuableFormInput')).toEqual([
[
{
newValue: newInputValue, newValue: newInputValue,
caretPos: newInputValue.length, caretPos: newInputValue.length,
untouchedRawReferences, untouchedRawReferences,
touchedReference, touchedReference,
}); },
],
]);
}); });
}); });
}); });

View File

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

View File

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

View File

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

View File

@ -90,14 +90,6 @@ describe('RunnerInstructionsModal component', () => {
await waitForPromises(); 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', () => { it('should contain a number of platforms buttons', () => {
expect(runnerPlatformsHandler).toHaveBeenCalledWith({}); expect(runnerPlatformsHandler).toHaveBeenCalledWith({});
@ -112,19 +104,8 @@ describe('RunnerInstructionsModal component', () => {
expect(architectures).toEqual(mockPlatformList[0].architectures.nodes); expect(architectures).toEqual(mockPlatformList[0].architectures.nodes);
}); });
describe.each` it('alert is shown', () => {
glFeatures | deprecationAlertExists expect(findAlert('warning').exists()).toBe(true);
${{}} | ${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);
});
}); });
describe('when the modal resizes', () => { describe('when the modal resizes', () => {

View File

@ -1,10 +1,10 @@
import { GlSearchBoxByType } from '@gitlab/ui'; import { GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; 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 searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql';
import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql'; import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
import { TYPE_MERGE_REQUEST } from '~/issues/constants'; import { TYPE_MERGE_REQUEST } from '~/issues/constants';
@ -44,20 +44,20 @@ Vue.use(VueApollo);
describe('User select dropdown', () => { describe('User select dropdown', () => {
let wrapper; let wrapper;
let fakeApollo; let fakeApollo;
const hideDropdownMock = jest.fn();
const findSearchField = () => wrapper.findComponent(GlSearchBoxByType); const findSearchField = () => wrapper.findComponent(GlSearchBoxByType);
const findParticipantsLoading = () => wrapper.find('[data-testid="loading-participants"]'); const findParticipantsLoading = () => wrapper.findByTestId('loading-participants');
const findSelectedParticipants = () => wrapper.findAll('[data-testid="selected-participant"]'); const findSelectedParticipants = () => wrapper.findAllByTestId('selected-participant');
const findSelectedParticipantByIndex = (index) => const findSelectedParticipantByIndex = (index) =>
findSelectedParticipants().at(index).findComponent(SidebarParticipant); findSelectedParticipants().at(index).findComponent(SidebarParticipant);
const findUnselectedParticipants = () => const findUnselectedParticipants = () => wrapper.findAllByTestId('unselected-participant');
wrapper.findAll('[data-testid="unselected-participant"]');
const findUnselectedParticipantByIndex = (index) => const findUnselectedParticipantByIndex = (index) =>
findUnselectedParticipants().at(index).findComponent(SidebarParticipant); findUnselectedParticipants().at(index).findComponent(SidebarParticipant);
const findCurrentUser = () => wrapper.findAll('[data-testid="current-user"]'); const findCurrentUser = () => wrapper.findAllByTestId('current-user');
const findIssuableAuthor = () => wrapper.findAll('[data-testid="issuable-author"]'); const findIssuableAuthor = () => wrapper.findAllByTestId('issuable-author');
const findUnassignLink = () => wrapper.find('[data-testid="unassign"]'); const findUnassignLink = () => wrapper.findByTestId('unassign');
const findEmptySearchResults = () => wrapper.find('[data-testid="empty-results"]'); const findEmptySearchResults = () => wrapper.findAllByTestId('empty-results');
const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(projectMembersResponse); const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(projectMembersResponse);
const participantsQueryHandlerSuccess = jest.fn().mockResolvedValue(participantsQueryResponse); const participantsQueryHandlerSuccess = jest.fn().mockResolvedValue(participantsQueryResponse);
@ -72,7 +72,7 @@ describe('User select dropdown', () => {
[searchUsersQueryOnMR, jest.fn().mockResolvedValue(searchResponseOnMR)], [searchUsersQueryOnMR, jest.fn().mockResolvedValue(searchResponseOnMR)],
[getIssueParticipantsQuery, participantsQueryHandler], [getIssueParticipantsQuery, participantsQueryHandler],
]); ]);
wrapper = shallowMount(UserSelect, { wrapper = shallowMountExtended(UserSelect, {
apolloProvider: fakeApollo, apolloProvider: fakeApollo,
propsData: { propsData: {
headerText: 'test', headerText: 'test',
@ -97,7 +97,7 @@ describe('User select dropdown', () => {
</div> </div>
`, `,
methods: { methods: {
hide: jest.fn(), hide: hideDropdownMock,
}, },
}, },
}, },
@ -106,6 +106,7 @@ describe('User select dropdown', () => {
afterEach(() => { afterEach(() => {
fakeApollo = null; fakeApollo = null;
hideDropdownMock.mockClear();
}); });
it('renders a loading spinner if participants are loading', () => { it('renders a loading spinner if participants are loading', () => {
@ -290,12 +291,12 @@ describe('User select dropdown', () => {
value: [assignee], value: [assignee],
}, },
}); });
wrapper.vm.$refs.dropdown.hide = jest.fn();
await waitForPromises(); await waitForPromises();
findUnassignLink().trigger('click'); 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 () => { 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| Gitlab::Redis::Cache.with do |redis|
expect(redis).to receive(:pipelined).and_call_original expect(redis).to receive(:pipelined).and_call_original
times = Gitlab::Redis::ClusterUtil.cluster?(redis) ? 2 : 1 expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
expect_next_instances_of(Redis::PipelinedConnection, times) do |pipeline|
expect(pipeline).to receive(:mapped_hmget).once.and_call_original expect(pipeline).to receive(:mapped_hmget).once.and_call_original
end end
end end

View File

@ -832,6 +832,30 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
subject subject
end end
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
end end

View File

@ -694,59 +694,5 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_disallowed(:create_instance_runner) } it { is_expected.to be_disallowed(:create_instance_runner) }
end 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
end end

View File

@ -1483,11 +1483,6 @@ RSpec.describe GroupPolicy, feature_category: :system_access do
end end
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 context 'admin' do
let(:current_user) { admin } let(:current_user) { admin }
@ -1566,75 +1561,6 @@ RSpec.describe GroupPolicy, feature_category: :system_access do
end end
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
let_it_be(:other_group) { create(:group) }
context 'admin' do
let(:current_user) { admin }
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_disallowed(:create_runner) }
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_behaves_like 'disallowed when group runner registration disabled'
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_disallowed(: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 }
it { is_expected.to be_disallowed(:create_runner) }
end
end
end
describe 'read_group_all_available_runners' do describe 'read_group_all_available_runners' do
context 'admin' do context 'admin' do
let(:current_user) { admin } let(:current_user) { admin }

View File

@ -2879,11 +2879,6 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end end
describe 'create_runner' do 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 context 'admin' do
let(:current_user) { admin } let(:current_user) { admin }
@ -2965,93 +2960,6 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end end
end end
context 'create_runner_workflow_for_namespace flag disabled' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: [group])
end
context 'admin' do
let(:current_user) { admin }
context 'when admin mode is enabled', :enable_admin_mode do
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 '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_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
end
end
describe 'admin_project_runners' do describe 'admin_project_runners' do
context 'admin' do context 'admin' do
let(:current_user) { admin } let(:current_user) { admin }

View File

@ -95,18 +95,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
end end
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 shared_examples 'when runner is created successfully' do
it do it do
expected_args = { user: current_user, params: anything } 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 context 'when user has permissions', :enable_admin_mode do
let(:current_user) { admin } 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 runner is created successfully'
it_behaves_like 'when model is invalid returns error' it_behaves_like 'when model is invalid returns error'
end end
@ -164,17 +140,12 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
} }
end end
before do
stub_feature_flags(create_runner_workflow_for_namespace: [group])
end
it_behaves_like 'when user does not have permissions' it_behaves_like 'when user does not have permissions'
context 'when user has permissions' do context 'when user has permissions' do
context 'when user is group owner' do context 'when user is group owner' do
let(:current_user) { group_owner } 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 runner is created successfully'
it_behaves_like 'when model is invalid returns error' 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 context 'when user is admin in admin mode', :enable_admin_mode do
let(:current_user) { admin } 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 runner is created successfully'
it_behaves_like 'when model is invalid returns error' it_behaves_like 'when model is invalid returns error'
end end
@ -249,7 +219,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
context 'when user is group owner' do context 'when user is group owner' do
let(:current_user) { group_owner } 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 runner is created successfully'
it_behaves_like 'when model is invalid returns error' 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 context 'when user is admin in admin mode', :enable_admin_mode do
let(:current_user) { admin } 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 runner is created successfully'
it_behaves_like 'when model is invalid returns error' it_behaves_like 'when model is invalid returns error'
end end

View File

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

View File

@ -7,7 +7,7 @@ RSpec.describe ErrorTracking::IssueLatestEventService, feature_category: :error_
let(:params) { {} } let(:params) { {} }
subject { described_class.new(project, user, params) } subject(:service) { described_class.new(project, user, params) }
describe '#execute' do describe '#execute' do
context 'with authorized user' 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 include_examples 'error tracking service http status handling', :issue_latest_event
context 'with integrated error tracking' do context 'with integrated error tracking' do
let_it_be(:error) { create(:error_tracking_error, project: project) } let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
let_it_be(:event) { create(:error_tracking_error_event, error: error) } let(:params) { { issue_id: issue_id } }
let(:params) { { issue_id: error.id } }
before do before do
error_tracking_setting.update!(integrated: true) error_tracking_setting.update!(integrated: true)
allow(service).to receive(:error_repository).and_return(error_repository)
end
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 end
it 'returns the latest event in expected format' do it 'returns the latest event in expected format' do
expect(result[:status]).to eq(:success) expect(result[:status]).to eq(:success)
expect(result[:latest_event].to_json).to eq(event.to_sentry_error_event.to_json) expect(result[:latest_event]).to eq(event)
end
end end
context 'when error does not exist' do 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 it 'returns the error in detailed format' do
expect(result).to match( expect(result).to match(
status: :error, status: :error,
message: /Couldn't find ErrorTracking::Error/, message: /Error not found/,
http_status: :bad_request http_status: :bad_request
) )
end 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 include_examples 'error tracking service sentry error handling', :update_issue
context 'with integrated error tracking' do context 'with integrated error tracking' do
let(:error) { create(:error_tracking_error, project: project) } let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
let(:arguments) { { issue_id: error.id, status: 'resolved' } } let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
let(:update_issue_response) { { updated: true, status: :success, closed_issue_iid: nil } } let(:issue_id) { error.fingerprint }
let(:arguments) { { issue_id: issue_id, status: 'resolved' } }
before do before do
error_tracking_setting.update!(integrated: true) 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 end
it 'resolves the error and responds with expected format' do context 'when update succeeded' do
expect(update_service.execute).to eq(update_issue_response) let(:updated) { true }
expect(error.reload.status).to eq('resolved')
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 end
end end

View File

@ -7,10 +7,10 @@ RSpec.describe ErrorTracking::ListIssuesService, feature_category: :error_tracki
let(:params) { {} } let(:params) { {} }
subject { described_class.new(project, user, params) } subject(:service) { described_class.new(project, user, params) }
describe '#execute' do describe '#execute' do
context 'Sentry backend' do context 'with Sentry backend' do
let(:params) { { search_term: 'something', sort: 'last_seen', cursor: 'some-cursor' } } let(:params) { { search_term: 'something', sort: 'last_seen', cursor: 'some-cursor' } }
let(:list_sentry_issues_args) do 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) expect(result).to eq(status: :success, pagination: {}, issues: issues)
end 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' params[:issue_status] = 'assigned'
expect(error_tracking_setting).not_to receive(:list_sentry_issues) expect(error_tracking_setting).not_to receive(:list_sentry_issues)
@ -65,22 +65,84 @@ RSpec.describe ErrorTracking::ListIssuesService, feature_category: :error_tracki
end end
end end
context 'GitLab backend' do context 'with integrated error tracking' do
let_it_be(:error1) { create(:error_tracking_error, name: 'foo', project: project) } let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
let_it_be(:error2) { create(:error_tracking_error, name: 'bar', project: project) } 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 before do
error_tracking_setting.update!(integrated: true) error_tracking_setting.update!(integrated: true)
allow(service).to receive(:error_repository).and_return(error_repository)
end end
it 'returns the error in expected format' do 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[:status]).to eq(:success)
expect(result[:issues].size).to eq(1) expect(result[:issues]).to eq(errors)
expect(result[:issues].first.to_json).to eq(error2.to_sentry_error.to_json) expect(result[:pagination]).to eq({})
expect(result[:pagination][:next][:cursor]).to be_present expect(error_repository).to have_received(:list_errors).with(**opts)
expect(result[:pagination][:previous]).to be_nil 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 end
end end

View File

@ -9123,10 +9123,6 @@
- './spec/services/environments/schedule_to_delete_review_apps_service_spec.rb' - './spec/services/environments/schedule_to_delete_review_apps_service_spec.rb'
- './spec/services/environments/stop_service_spec.rb' - './spec/services/environments/stop_service_spec.rb'
- './spec/services/error_tracking/base_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/error_tracking/list_projects_service_spec.rb'
- './spec/services/event_create_service_spec.rb' - './spec/services/event_create_service_spec.rb'
- './spec/services/events/destroy_service_spec.rb' - './spec/services/events/destroy_service_spec.rb'

View File

@ -15,44 +15,6 @@ 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') allow(view).to receive(:reset_registration_token_namespace_project_settings_ci_cd_path).and_return('banana_url')
end end
context 'when create_runner_workflow_for_namespace is disabled' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: false)
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 '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
end
end
context 'when create_runner_workflow_for_namespace is enabled' do
before do
stub_feature_flags(create_runner_workflow_for_namespace: project.namespace)
end
context 'when user can create project runner' do context 'when user can create project runner' do
before do before do
allow(view).to receive(:can?).with(user, :create_runner, project).and_return(true) allow(view).to receive(:can?).with(user, :create_runner, project).and_return(true)
@ -77,5 +39,4 @@ RSpec.describe 'projects/runners/_project_runners.html.haml', feature_category:
end end
end end
end end
end
end end