Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a27c21b5af
commit
3a8d221b7e
|
|
@ -456,7 +456,6 @@ Lint/UnusedMethodArgument:
|
|||
- 'lib/gitlab/diff/file_collection/merge_request_diff_base.rb'
|
||||
- 'lib/gitlab/diff/line.rb'
|
||||
- 'lib/gitlab/encoding_helper.rb'
|
||||
- 'lib/gitlab/error_tracking/error_repository/active_record_strategy.rb'
|
||||
- 'lib/gitlab/fogbugz_import/importer.rb'
|
||||
- 'lib/gitlab/git/commit_stats.rb'
|
||||
- 'lib/gitlab/git/conflict/parser.rb'
|
||||
|
|
|
|||
|
|
@ -79,7 +79,6 @@ Naming/InclusiveLanguage:
|
|||
- 'spec/rubocop/cop/graphql/id_type_spec.rb'
|
||||
- 'spec/services/application_settings/update_service_spec.rb'
|
||||
- 'spec/services/design_management/generate_image_versions_service_spec.rb'
|
||||
- 'spec/services/error_tracking/list_issues_service_spec.rb'
|
||||
- 'spec/services/projects/download_service_spec.rb'
|
||||
- 'spec/support/import_export/export_file_helper.rb'
|
||||
- 'spec/support/shared_contexts/upload_type_check_shared_context.rb'
|
||||
|
|
|
|||
|
|
@ -2670,7 +2670,6 @@ RSpec/ContextWording:
|
|||
- 'spec/services/draft_notes/publish_service_spec.rb'
|
||||
- 'spec/services/environments/schedule_to_delete_review_apps_service_spec.rb'
|
||||
- 'spec/services/environments/stop_service_spec.rb'
|
||||
- 'spec/services/error_tracking/list_issues_service_spec.rb'
|
||||
- 'spec/services/error_tracking/list_projects_service_spec.rb'
|
||||
- 'spec/services/events/destroy_service_spec.rb'
|
||||
- 'spec/services/feature_flags/update_service_spec.rb'
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -1,10 +1,3 @@
|
|||
import { createConsumer } from '@rails/actioncable';
|
||||
import ConnectionMonitor from './actioncable_connection_monitor';
|
||||
|
||||
const consumer = createConsumer();
|
||||
|
||||
if (consumer.connection) {
|
||||
consumer.connection.monitor = new ConnectionMonitor(consumer.connection);
|
||||
}
|
||||
|
||||
export default consumer;
|
||||
export default createConsumer();
|
||||
|
|
|
|||
|
|
@ -126,10 +126,6 @@ export default {
|
|||
isSearchFiltered() {
|
||||
return isSearchFiltered(this.search);
|
||||
},
|
||||
shouldShowCreateRunnerWorkflow() {
|
||||
// create_runner_workflow_for_admin feature flag
|
||||
return this.glFeatures.createRunnerWorkflowForAdmin;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
search: {
|
||||
|
|
@ -193,7 +189,7 @@ export default {
|
|||
/>
|
||||
|
||||
<div class="gl-w-full gl-md-w-auto gl-display-flex">
|
||||
<gl-button v-if="shouldShowCreateRunnerWorkflow" :href="newRunnerPath" variant="confirm">
|
||||
<gl-button :href="newRunnerPath" variant="confirm">
|
||||
{{ s__('Runners|New instance runner') }}
|
||||
</gl-button>
|
||||
<registration-dropdown
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ export default {
|
|||
showInstallationInstructions: s__(
|
||||
'Runners|Show runner installation and registration instructions',
|
||||
),
|
||||
supportForRegistrationTokensDeprecated: s__(
|
||||
'Runners|Support for registration tokens is deprecated',
|
||||
),
|
||||
},
|
||||
components: {
|
||||
GlDropdown,
|
||||
|
|
@ -51,14 +54,6 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
isDeprecated() {
|
||||
// Show a compact version when used as secondary option
|
||||
// create_runner_workflow_for_admin or create_runner_workflow_for_namespace
|
||||
return (
|
||||
this.glFeatures?.createRunnerWorkflowForAdmin ||
|
||||
this.glFeatures?.createRunnerWorkflowForNamespace
|
||||
);
|
||||
},
|
||||
actionText() {
|
||||
switch (this.type) {
|
||||
case INSTANCE_TYPE:
|
||||
|
|
@ -71,30 +66,6 @@ export default {
|
|||
return I18N_REGISTER_RUNNER;
|
||||
}
|
||||
},
|
||||
dropdownText() {
|
||||
if (this.isDeprecated) {
|
||||
return '';
|
||||
}
|
||||
return this.actionText;
|
||||
},
|
||||
dropdownToggleClass() {
|
||||
if (this.isDeprecated) {
|
||||
return ['gl-px-3!'];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
dropdownCategory() {
|
||||
if (this.isDeprecated) {
|
||||
return 'tertiary';
|
||||
}
|
||||
return 'primary';
|
||||
},
|
||||
dropdownVariant() {
|
||||
if (this.isDeprecated) {
|
||||
return 'default';
|
||||
}
|
||||
return 'confirm';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onShowInstructionsClick() {
|
||||
|
|
@ -113,22 +84,21 @@ export default {
|
|||
<gl-dropdown
|
||||
ref="runnerRegistrationDropdown"
|
||||
menu-class="gl-w-auto!"
|
||||
:text="dropdownText"
|
||||
:toggle-class="dropdownToggleClass"
|
||||
:variant="dropdownVariant"
|
||||
:category="dropdownCategory"
|
||||
toggle-class="gl-px-3!"
|
||||
variant="default"
|
||||
category="tertiary"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template v-if="isDeprecated" #button-content>
|
||||
<template #button-content>
|
||||
<span class="gl-sr-only">{{ actionText }}</span>
|
||||
<gl-icon name="ellipsis_v" />
|
||||
</template>
|
||||
<gl-dropdown-form class="gl-p-4!">
|
||||
<registration-token input-id="token-value" :value="currentRegistrationToken">
|
||||
<template v-if="isDeprecated" #label-description>
|
||||
<template #label-description>
|
||||
<gl-icon name="warning" class="gl-text-orange-500" />
|
||||
<span class="gl-text-secondary">
|
||||
{{ s__('Runners|Support for registration tokens is deprecated') }}
|
||||
{{ $options.i18n.supportForRegistrationTokensDeprecated }}
|
||||
</span>
|
||||
</template>
|
||||
</registration-token>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
I18N_CREATE_RUNNER_LINK,
|
||||
I18N_STILL_USING_REGISTRATION_TOKENS,
|
||||
I18N_CONTACT_ADMIN_TO_REGISTER,
|
||||
I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
|
||||
I18N_NO_RESULTS,
|
||||
I18N_EDIT_YOUR_SEARCH,
|
||||
} from '~/ci/runner/constants';
|
||||
|
|
@ -44,15 +43,6 @@ export default {
|
|||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
shouldShowCreateRunnerWorkflow() {
|
||||
// create_runner_workflow_for_admin or create_runner_workflow_for_namespace
|
||||
return (
|
||||
this.glFeatures?.createRunnerWorkflowForAdmin ||
|
||||
this.glFeatures?.createRunnerWorkflowForNamespace
|
||||
);
|
||||
},
|
||||
},
|
||||
modalId: 'runners-empty-state-instructions-modal',
|
||||
svgHeight: 145,
|
||||
EMPTY_STATE_SVG_URL,
|
||||
|
|
@ -63,7 +53,6 @@ export default {
|
|||
I18N_CREATE_RUNNER_LINK,
|
||||
I18N_STILL_USING_REGISTRATION_TOKENS,
|
||||
I18N_CONTACT_ADMIN_TO_REGISTER,
|
||||
I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
|
||||
I18N_NO_RESULTS,
|
||||
I18N_EDIT_YOUR_SEARCH,
|
||||
};
|
||||
|
|
@ -85,39 +74,22 @@ export default {
|
|||
>
|
||||
<template #description>
|
||||
{{ $options.I18N_RUNNERS_ARE_AGENTS }}
|
||||
<template v-if="shouldShowCreateRunnerWorkflow">
|
||||
<gl-sprintf v-if="newRunnerPath" :message="$options.I18N_CREATE_RUNNER_LINK">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="newRunnerPath">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
<template v-if="registrationToken">
|
||||
<br />
|
||||
<gl-link v-gl-modal="$options.modalId">{{
|
||||
$options.I18N_STILL_USING_REGISTRATION_TOKENS
|
||||
}}</gl-link>
|
||||
<runner-instructions-modal
|
||||
:modal-id="$options.modalId"
|
||||
:registration-token="registrationToken"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="!newRunnerPath && !registrationToken">
|
||||
{{ $options.I18N_CONTACT_ADMIN_TO_REGISTER }}
|
||||
</template>
|
||||
</template>
|
||||
<gl-sprintf
|
||||
v-else-if="registrationToken"
|
||||
:message="$options.I18N_FOLLOW_REGISTRATION_INSTRUCTIONS"
|
||||
>
|
||||
<gl-sprintf v-if="newRunnerPath" :message="$options.I18N_CREATE_RUNNER_LINK">
|
||||
<template #link="{ content }">
|
||||
<gl-link v-gl-modal="$options.modalId">{{ content }}</gl-link>
|
||||
<runner-instructions-modal
|
||||
:modal-id="$options.modalId"
|
||||
:registration-token="registrationToken"
|
||||
/>
|
||||
<gl-link :href="newRunnerPath">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
<template v-else>
|
||||
<template v-if="registrationToken">
|
||||
<br />
|
||||
<gl-link v-gl-modal="$options.modalId">{{
|
||||
$options.I18N_STILL_USING_REGISTRATION_TOKENS
|
||||
}}</gl-link>
|
||||
<runner-instructions-modal
|
||||
:modal-id="$options.modalId"
|
||||
:registration-token="registrationToken"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="!newRunnerPath && !registrationToken">
|
||||
{{ $options.I18N_CONTACT_ADMIN_TO_REGISTER }}
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -117,9 +117,6 @@ export const I18N_STILL_USING_REGISTRATION_TOKENS = s__('Runners|Still using reg
|
|||
export const I18N_CONTACT_ADMIN_TO_REGISTER = s__(
|
||||
'Runners|To register new runners, contact your administrator.',
|
||||
);
|
||||
export const I18N_FOLLOW_REGISTRATION_INSTRUCTIONS = s__(
|
||||
'Runners|Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.',
|
||||
);
|
||||
|
||||
// No runners found
|
||||
export const I18N_NO_RESULTS = s__('Runners|No results found');
|
||||
|
|
|
|||
|
|
@ -155,10 +155,6 @@ export default {
|
|||
isSearchFiltered() {
|
||||
return isSearchFiltered(this.search);
|
||||
},
|
||||
shouldShowCreateRunnerWorkflow() {
|
||||
// create_runner_workflow_for_namespace feature flag
|
||||
return this.glFeatures.createRunnerWorkflowForNamespace;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
search: {
|
||||
|
|
@ -231,11 +227,7 @@ export default {
|
|||
/>
|
||||
|
||||
<div class="gl-w-full gl-md-w-auto gl-display-flex">
|
||||
<gl-button
|
||||
v-if="shouldShowCreateRunnerWorkflow && newRunnerPath"
|
||||
:href="newRunnerPath"
|
||||
variant="confirm"
|
||||
>
|
||||
<gl-button v-if="newRunnerPath" :href="newRunnerPath" variant="confirm">
|
||||
{{ s__('Runners|New group runner') }}
|
||||
</gl-button>
|
||||
<registration-dropdown
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@ export default {
|
|||
locked: {
|
||||
icon: 'lock',
|
||||
class: 'value',
|
||||
iconClass: 'is-active',
|
||||
displayText: __('Locked'),
|
||||
},
|
||||
unlocked: {
|
||||
class: ['no-value hide-collapsed'],
|
||||
icon: 'lock-open',
|
||||
iconClass: '',
|
||||
displayText: __('Unlocked'),
|
||||
},
|
||||
components: {
|
||||
EditForm,
|
||||
|
|
@ -49,8 +49,6 @@ export default {
|
|||
issueCapitalized: __('Issue'),
|
||||
mergeRequest: __('merge request'),
|
||||
mergeRequestCapitalized: __('Merge request'),
|
||||
locked: __('Locked'),
|
||||
unlocked: __('Unlocked'),
|
||||
lockingMergeRequest: __('Locking %{issuableDisplayName}'),
|
||||
unlockingMergeRequest: __('Unlocking %{issuableDisplayName}'),
|
||||
lockMergeRequest: __('Lock %{issuableDisplayName}'),
|
||||
|
|
@ -84,10 +82,7 @@ export default {
|
|||
return this.getNoteableData.discussion_locked;
|
||||
},
|
||||
lockStatus() {
|
||||
return this.isLocked ? this.$options.i18n.locked : this.$options.i18n.unlocked;
|
||||
},
|
||||
tooltipLabel() {
|
||||
return this.isLocked ? this.$options.i18n.locked : this.$options.i18n.unlocked;
|
||||
return this.isLocked ? this.$options.locked : this.$options.unlocked;
|
||||
},
|
||||
lockToggleInProgressText() {
|
||||
return this.isLocked ? this.unlockingMergeRequestText : this.lockingMergeRequestText;
|
||||
|
|
@ -205,7 +200,7 @@ export default {
|
|||
</gl-disclosure-dropdown-item>
|
||||
<div v-else class="block issuable-sidebar-item lock">
|
||||
<div
|
||||
v-gl-tooltip.left.viewport="{ title: tooltipLabel }"
|
||||
v-gl-tooltip.left.viewport="{ title: lockStatus.displayText }"
|
||||
class="sidebar-collapsed-icon"
|
||||
data-testid="sidebar-collapse-icon"
|
||||
@click="toggleForm"
|
||||
|
|
@ -239,7 +234,7 @@ export default {
|
|||
/>
|
||||
|
||||
<div data-testid="lock-status" class="sidebar-item-value" :class="lockStatus.class">
|
||||
{{ lockStatus }}
|
||||
{{ lockStatus.displayText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -122,14 +122,6 @@ export default {
|
|||
return null;
|
||||
}
|
||||
},
|
||||
showDeprecationAlert() {
|
||||
return (
|
||||
// create_runner_workflow_for_admin
|
||||
this.glFeatures.createRunnerWorkflowForAdmin ||
|
||||
// create_runner_workflow_for_namespace
|
||||
this.glFeatures.createRunnerWorkflowForNamespace
|
||||
);
|
||||
},
|
||||
},
|
||||
updated() {
|
||||
// Refocus on dom changes, after loading data
|
||||
|
|
@ -200,12 +192,7 @@ export default {
|
|||
v-on="$listeners"
|
||||
@shown="onShown"
|
||||
>
|
||||
<gl-alert
|
||||
v-if="showDeprecationAlert"
|
||||
:title="$options.i18n.deprecationAlertTitle"
|
||||
variant="warning"
|
||||
:dismissible="false"
|
||||
>
|
||||
<gl-alert :title="$options.i18n.deprecationAlertTitle" variant="warning" :dismissible="false">
|
||||
<gl-sprintf :message="$options.i18n.deprecationAlertContent">
|
||||
<template #link="{ content }">
|
||||
<gl-link target="_blank" :href="$options.LEGACY_REGISTER_HELP_URL"
|
||||
|
|
|
|||
|
|
@ -5,10 +5,6 @@ class Admin::RunnersController < Admin::ApplicationController
|
|||
|
||||
before_action :runner, except: [:index, :new, :tag_list, :runner_setup_scripts]
|
||||
|
||||
before_action only: [:index] do
|
||||
push_frontend_feature_flag(:create_runner_workflow_for_admin, current_user)
|
||||
end
|
||||
|
||||
feature_category :runner
|
||||
urgency :low
|
||||
|
||||
|
|
@ -23,12 +19,10 @@ class Admin::RunnersController < Admin::ApplicationController
|
|||
end
|
||||
|
||||
def new
|
||||
render_404 unless Feature.enabled?(:create_runner_workflow_for_admin, current_user)
|
||||
end
|
||||
|
||||
def register
|
||||
render_404 unless Feature.enabled?(:create_runner_workflow_for_admin, current_user) &&
|
||||
runner.registration_available?
|
||||
render_404 unless runner.registration_available?
|
||||
end
|
||||
|
||||
def update
|
||||
|
|
|
|||
|
|
@ -6,10 +6,6 @@ class Groups::RunnersController < Groups::ApplicationController
|
|||
before_action :authorize_update_runner!, only: [:edit, :update, :destroy, :pause, :resume]
|
||||
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show, :register]
|
||||
|
||||
before_action only: [:index] do
|
||||
push_frontend_feature_flag(:create_runner_workflow_for_namespace, group)
|
||||
end
|
||||
|
||||
feature_category :runner
|
||||
urgency :low
|
||||
|
||||
|
|
@ -35,11 +31,10 @@ class Groups::RunnersController < Groups::ApplicationController
|
|||
end
|
||||
|
||||
def new
|
||||
render_404 unless create_runner_workflow_for_namespace_enabled?
|
||||
end
|
||||
|
||||
def register
|
||||
render_404 unless create_runner_workflow_for_namespace_enabled? && runner.registration_available?
|
||||
render_404 unless runner.registration_available?
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -67,10 +62,6 @@ class Groups::RunnersController < Groups::ApplicationController
|
|||
|
||||
render_404
|
||||
end
|
||||
|
||||
def create_runner_workflow_for_namespace_enabled?
|
||||
Feature.enabled?(:create_runner_workflow_for_namespace, group)
|
||||
end
|
||||
end
|
||||
|
||||
Groups::RunnersController.prepend_mod
|
||||
|
|
|
|||
|
|
@ -24,11 +24,10 @@ class Projects::RunnersController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def new
|
||||
render_404 unless create_runner_workflow_for_namespace_enabled?
|
||||
end
|
||||
|
||||
def register
|
||||
render_404 unless create_runner_workflow_for_namespace_enabled? && runner.registration_available?
|
||||
render_404 unless runner.registration_available?
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
|
@ -84,8 +83,4 @@ class Projects::RunnersController < Projects::ApplicationController
|
|||
def runner_params
|
||||
params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
|
||||
end
|
||||
|
||||
def create_runner_workflow_for_namespace_enabled?
|
||||
Feature.enabled?(:create_runner_workflow_for_namespace, project.namespace)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ module Projects
|
|||
before_action do
|
||||
push_frontend_feature_flag(:ci_variables_pages, current_user)
|
||||
push_frontend_feature_flag(:ci_limit_environment_scope, @project)
|
||||
push_frontend_feature_flag(:create_runner_workflow_for_namespace, @project.namespace)
|
||||
push_frontend_feature_flag(:frozen_outbound_job_token_scopes, @project)
|
||||
push_frontend_feature_flag(:frozen_outbound_job_token_scopes_override, @project)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -37,8 +37,6 @@ module Mutations
|
|||
|
||||
parse_gid(**args)
|
||||
|
||||
check_feature_flag(**args)
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
|
|
@ -79,28 +77,6 @@ module Mutations
|
|||
GitlabSchema.parse_gid(args[:project_id], expected_type: ::Project)
|
||||
end
|
||||
end
|
||||
|
||||
def check_feature_flag(**args)
|
||||
case args[:runner_type]
|
||||
when 'instance_type'
|
||||
if Feature.disabled?(:create_runner_workflow_for_admin, current_user)
|
||||
raise Gitlab::Graphql::Errors::ResourceNotAvailable,
|
||||
'`create_runner_workflow_for_admin` feature flag is disabled.'
|
||||
end
|
||||
when 'group_type'
|
||||
namespace = find_object(**args).sync
|
||||
if Feature.disabled?(:create_runner_workflow_for_namespace, namespace)
|
||||
raise Gitlab::Graphql::Errors::ResourceNotAvailable,
|
||||
'`create_runner_workflow_for_namespace` feature flag is disabled.'
|
||||
end
|
||||
when 'project_type'
|
||||
project = find_object(**args).sync
|
||||
if project && Feature.disabled?(:create_runner_workflow_for_namespace, project.namespace)
|
||||
raise Gitlab::Graphql::Errors::ResourceNotAvailable,
|
||||
'`create_runner_workflow_for_namespace` feature flag is disabled.'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -22,10 +22,6 @@ class GlobalPolicy < BasePolicy
|
|||
condition(:project_bot, scope: :user) { @user&.project_bot? }
|
||||
condition(:migration_bot, scope: :user) { @user&.migration_bot? }
|
||||
|
||||
condition(:create_runner_workflow_enabled, scope: :user) do
|
||||
Feature.enabled?(:create_runner_workflow_for_admin, @user)
|
||||
end
|
||||
|
||||
condition(:service_account, scope: :user) { @user&.service_account? }
|
||||
|
||||
rule { anonymous }.policy do
|
||||
|
|
@ -128,10 +124,6 @@ class GlobalPolicy < BasePolicy
|
|||
enable :create_instance_runner
|
||||
end
|
||||
|
||||
rule { ~create_runner_workflow_enabled }.policy do
|
||||
prevent :create_instance_runner
|
||||
end
|
||||
|
||||
# We can't use `read_statistics` because the user may have different permissions for different projects
|
||||
rule { admin }.enable :use_project_statistics_filters
|
||||
|
||||
|
|
|
|||
|
|
@ -97,10 +97,6 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
|
|||
with_scope :subject
|
||||
condition(:crm_enabled, score: 0, scope: :subject) { @subject.crm_enabled? }
|
||||
|
||||
condition(:create_runner_workflow_enabled) do
|
||||
Feature.enabled?(:create_runner_workflow_for_namespace, group)
|
||||
end
|
||||
|
||||
condition(:achievements_enabled, scope: :subject) do
|
||||
Feature.enabled?(:achievements, @subject)
|
||||
end
|
||||
|
|
@ -375,10 +371,6 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
|
|||
enable :admin_observability
|
||||
end
|
||||
|
||||
rule { ~create_runner_workflow_enabled }.policy do
|
||||
prevent :create_runner
|
||||
end
|
||||
|
||||
# Should be matched with ProjectPolicy#read_internal_note
|
||||
rule { admin | reporter }.enable :read_internal_note
|
||||
|
||||
|
|
|
|||
|
|
@ -253,10 +253,6 @@ class ProjectPolicy < BasePolicy
|
|||
!Gitlab.config.terraform_state.enabled
|
||||
end
|
||||
|
||||
condition(:create_runner_workflow_enabled) do
|
||||
Feature.enabled?(:create_runner_workflow_for_namespace, project.namespace)
|
||||
end
|
||||
|
||||
condition(:namespace_catalog_available) { namespace_catalog_available? }
|
||||
|
||||
# `:read_project` may be prevented in EE, but `:read_project_for_iids` should
|
||||
|
|
@ -886,10 +882,6 @@ class ProjectPolicy < BasePolicy
|
|||
enable :read_code
|
||||
end
|
||||
|
||||
rule { ~create_runner_workflow_enabled }.policy do
|
||||
prevent :create_runner
|
||||
end
|
||||
|
||||
# Should be matched with GroupPolicy#read_internal_note
|
||||
rule { admin | can?(:reporter_access) }.enable :read_internal_note
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@
|
|||
- if Feature.enabled?(:edit_user_profile_vue, current_user)
|
||||
.js-user-profile
|
||||
- else
|
||||
= gitlab_ui_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user js-edit-user gl-mt-3 js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
|
||||
.row.js-search-settings-section
|
||||
.col-lg-4.profile-settings-sidebar
|
||||
%h4.gl-mt-0
|
||||
= gitlab_ui_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user js-edit-user js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
|
||||
.js-search-settings-section.gl-pb-6
|
||||
.profile-settings-sidebar
|
||||
%h4.gl-my-0
|
||||
= s_("Profiles|Public avatar")
|
||||
%p
|
||||
%p.gl-text-secondary
|
||||
- if @user.avatar?
|
||||
- if gravatar_enabled?
|
||||
= s_("Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}").html_safe % { gravatar_link: gravatar_link }
|
||||
|
|
@ -26,31 +26,29 @@
|
|||
- if current_appearance&.profile_image_guidelines?
|
||||
.md
|
||||
= brand_profile_image_guidelines
|
||||
.col-lg-8
|
||||
.avatar-image
|
||||
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
|
||||
= render Pajamas::AvatarComponent.new(@user, size: 96, alt: "", class: 'gl-float-left gl-mr-5')
|
||||
%h5.gl-mt-0= s_("Profiles|Upload new avatar")
|
||||
.gl-display-flex.gl-align-items-center.gl-my-3
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-choose-user-avatar-button' }) do
|
||||
= s_("Profiles|Choose file...")
|
||||
%span.gl-ml-3.js-avatar-filename= s_("Profiles|No file chosen.")
|
||||
= f.file_field :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*'
|
||||
.gl-text-gray-500= s_("Profiles|The maximum file size allowed is 200KB.")
|
||||
- if @user.avatar?
|
||||
= render Pajamas::ButtonComponent.new(variant: :danger,
|
||||
category: :secondary,
|
||||
href: profile_avatar_path,
|
||||
button_options: { class: 'gl-mt-3', data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") } },
|
||||
method: :delete) do
|
||||
= s_("Profiles|Remove avatar")
|
||||
.col-lg-12
|
||||
%hr
|
||||
.row.js-search-settings-section
|
||||
.col-lg-4.profile-settings-sidebar
|
||||
%h4.gl-mt-0= s_("Profiles|Current status")
|
||||
%p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
|
||||
.col-lg-8
|
||||
.avatar-image
|
||||
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
|
||||
= render Pajamas::AvatarComponent.new(@user, size: 96, alt: "", class: 'gl-float-left gl-mr-5')
|
||||
%h5.gl-mt-0= s_("Profiles|Upload new avatar")
|
||||
.gl-display-flex.gl-align-items-center.gl-my-3
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-choose-user-avatar-button' }) do
|
||||
= s_("Profiles|Choose file...")
|
||||
%span.gl-ml-3.js-avatar-filename= s_("Profiles|No file chosen.")
|
||||
= f.file_field :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*'
|
||||
.gl-text-gray-500= s_("Profiles|The maximum file size allowed is 200KB.")
|
||||
- if @user.avatar?
|
||||
= render Pajamas::ButtonComponent.new(variant: :danger,
|
||||
category: :secondary,
|
||||
href: profile_avatar_path,
|
||||
button_options: { class: 'gl-mt-3', data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") } },
|
||||
method: :delete) do
|
||||
= s_("Profiles|Remove avatar")
|
||||
|
||||
.js-search-settings-section.gl-border-t.gl-py-6
|
||||
.profile-settings-sidebar
|
||||
%h4.gl-my-0= s_("Profiles|Current status")
|
||||
%p.gl-text-secondary= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
|
||||
.gl-max-w-80
|
||||
#js-user-profile-set-status-form
|
||||
= f.fields_for :status, @user.status do |status_form|
|
||||
= status_form.hidden_field :emoji, data: { js_name: 'emoji' }
|
||||
|
|
@ -59,121 +57,114 @@
|
|||
= status_form.hidden_field :clear_status_after,
|
||||
value: user_clear_status_at(@user),
|
||||
data: { js_name: 'clearStatusAfter' }
|
||||
.col-lg-12
|
||||
%hr
|
||||
.row.user-time-preferences.js-search-settings-section
|
||||
.col-lg-4.profile-settings-sidebar
|
||||
%h4.gl-mt-0= s_("Profiles|Time settings")
|
||||
%p= s_("Profiles|Set your local time zone.")
|
||||
.col-lg-8
|
||||
= f.label :user_timezone, _("Time zone")
|
||||
.js-timezone-dropdown{ data: { timezone_data: timezone_data.to_json, initial_value: @user.timezone, name: 'user[timezone]' } }
|
||||
.col-lg-12
|
||||
%hr
|
||||
.row.js-search-settings-section
|
||||
.col-lg-4.profile-settings-sidebar
|
||||
%h4.gl-mt-0
|
||||
|
||||
.user-time-preferences.js-search-settings-section.gl-border-t.gl-py-6
|
||||
.profile-settings-sidebar
|
||||
%h4.gl-my-0= s_("Profiles|Time settings")
|
||||
%p.gl-text-secondary= s_("Profiles|Set your local time zone.")
|
||||
= f.label :user_timezone, _("Time zone")
|
||||
.js-timezone-dropdown{ data: { timezone_data: timezone_data.to_json, initial_value: @user.timezone, name: 'user[timezone]' } }
|
||||
|
||||
.js-search-settings-section.gl-border-t.gl-py-6
|
||||
.profile-settings-sidebar
|
||||
%h4.gl-my-0
|
||||
= s_("Profiles|Main settings")
|
||||
%p
|
||||
%p.gl-text-secondary
|
||||
= s_("Profiles|This information will appear on your profile.")
|
||||
- if current_user.ldap_user?
|
||||
= s_("Profiles|Some options are unavailable for LDAP accounts")
|
||||
.col-lg-8
|
||||
.row
|
||||
.form-group.gl-form-group.col-md-9.rspec-full-name
|
||||
= render 'profiles/name', form: f, user: @user
|
||||
.form-group.gl-form-group.col-md-3
|
||||
= f.label :id, s_('Profiles|User ID')
|
||||
= f.text_field :id, class: 'gl-form-input form-control', readonly: true
|
||||
.form-group.gl-form-group
|
||||
= f.label :pronouns, s_('Profiles|Pronouns')
|
||||
= f.text_field :pronouns, class: 'gl-form-input form-control gl-md-form-input-lg'
|
||||
%small.form-text.text-gl-muted
|
||||
= s_("Profiles|Enter your pronouns to let people know how to refer to you.")
|
||||
.form-group.gl-form-group
|
||||
= f.label :pronunciation, s_('Profiles|Pronunciation')
|
||||
= f.text_field :pronunciation, class: 'gl-form-input form-control gl-md-form-input-lg'
|
||||
%small.form-text.text-gl-muted
|
||||
= s_("Profiles|Enter how your name is pronounced to help people address you correctly.")
|
||||
= render_if_exists 'profiles/extra_settings', form: f
|
||||
= render_if_exists 'profiles/email_settings', form: f
|
||||
.form-group.gl-form-group
|
||||
= f.label :skype
|
||||
= f.text_field :skype, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|username")
|
||||
.form-group.gl-form-group
|
||||
= f.label :linkedin
|
||||
= f.text_field :linkedin, class: 'gl-form-input form-control gl-md-form-input-lg'
|
||||
%small.form-text.text-gl-muted
|
||||
= s_("Profiles|Your LinkedIn profile name from linkedin.com/in/profilename")
|
||||
.form-group.gl-form-group
|
||||
= f.label :twitter
|
||||
= f.text_field :twitter, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|@username")
|
||||
.form-group.gl-form-group
|
||||
- external_accounts_help_url = help_page_path('user/profile/index', anchor: 'add-external-accounts-to-your-user-profile-page')
|
||||
- external_accounts_link = link_to '', external_accounts_help_url, target: "_blank", rel: "noopener noreferrer"
|
||||
- external_accounts_docs_link = safe_format(s_('Profiles|Your Discord user ID. %{external_accounts_link_start}Learn more.%{external_accounts_link_end}'), tag_pair(external_accounts_link, :external_accounts_link_start, :external_accounts_link_end))
|
||||
- min_discord_length = 17
|
||||
- max_discord_length = 20
|
||||
= f.label :discord
|
||||
= f.text_field :discord,
|
||||
class: 'gl-form-input form-control gl-md-form-input-lg js-validate-length',
|
||||
placeholder: s_("Profiles|User ID"),
|
||||
data: { min_length: min_discord_length,
|
||||
min_length_message: s_('Profiles|Discord ID is too short (minimum is %{min_length} characters).') % { min_length: min_discord_length },
|
||||
max_length: max_discord_length,
|
||||
max_length_message: s_('Profiles|Discord ID is too long (maximum is %{max_length} characters).') % { max_length: max_discord_length },
|
||||
allow_empty: true}
|
||||
%small.form-text.text-gl-muted
|
||||
= external_accounts_docs_link
|
||||
.form-group.gl-form-group.rspec-full-name.gl-max-w-80
|
||||
= render 'profiles/name', form: f, user: @user
|
||||
.form-group.gl-form-group.gl-md-form-input-lg
|
||||
= f.label :id, s_('Profiles|User ID')
|
||||
= f.text_field :id, class: 'gl-form-input form-control', readonly: true
|
||||
.form-group.gl-form-group
|
||||
= f.label :pronouns, s_('Profiles|Pronouns')
|
||||
= f.text_field :pronouns, class: 'gl-form-input form-control gl-md-form-input-lg'
|
||||
%small.form-text.text-gl-muted
|
||||
= s_("Profiles|Enter your pronouns to let people know how to refer to you.")
|
||||
.form-group.gl-form-group
|
||||
= f.label :pronunciation, s_('Profiles|Pronunciation')
|
||||
= f.text_field :pronunciation, class: 'gl-form-input form-control gl-md-form-input-lg'
|
||||
%small.form-text.text-gl-muted
|
||||
= s_("Profiles|Enter how your name is pronounced to help people address you correctly.")
|
||||
= render_if_exists 'profiles/extra_settings', form: f
|
||||
= render_if_exists 'profiles/email_settings', form: f
|
||||
.form-group.gl-form-group
|
||||
= f.label :skype
|
||||
= f.text_field :skype, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|username")
|
||||
.form-group.gl-form-group
|
||||
= f.label :linkedin
|
||||
= f.text_field :linkedin, class: 'gl-form-input form-control gl-md-form-input-lg'
|
||||
%small.form-text.text-gl-muted
|
||||
= s_("Profiles|Your LinkedIn profile name from linkedin.com/in/profilename")
|
||||
.form-group.gl-form-group
|
||||
= f.label :twitter
|
||||
= f.text_field :twitter, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|@username")
|
||||
.form-group.gl-form-group
|
||||
- external_accounts_help_url = help_page_path('user/profile/index', anchor: 'add-external-accounts-to-your-user-profile-page')
|
||||
- external_accounts_link = link_to '', external_accounts_help_url, target: "_blank", rel: "noopener noreferrer"
|
||||
- external_accounts_docs_link = safe_format(s_('Profiles|Your Discord user ID. %{external_accounts_link_start}Learn more.%{external_accounts_link_end}'), tag_pair(external_accounts_link, :external_accounts_link_start, :external_accounts_link_end))
|
||||
- min_discord_length = 17
|
||||
- max_discord_length = 20
|
||||
= f.label :discord
|
||||
= f.text_field :discord,
|
||||
class: 'gl-form-input form-control gl-md-form-input-lg js-validate-length',
|
||||
placeholder: s_("Profiles|User ID"),
|
||||
data: { min_length: min_discord_length,
|
||||
min_length_message: s_('Profiles|Discord ID is too short (minimum is %{min_length} characters).') % { min_length: min_discord_length },
|
||||
max_length: max_discord_length,
|
||||
max_length_message: s_('Profiles|Discord ID is too long (maximum is %{max_length} characters).') % { max_length: max_discord_length },
|
||||
allow_empty: true}
|
||||
%small.form-text.text-gl-muted
|
||||
= external_accounts_docs_link
|
||||
|
||||
.form-group.gl-form-group
|
||||
= f.label :website_url, s_('Profiles|Website url')
|
||||
= f.text_field :website_url, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|https://website.com")
|
||||
.form-group.gl-form-group
|
||||
= f.label :location, s_('Profiles|Location')
|
||||
- if @user.read_only_attribute?(:location)
|
||||
= f.text_field :location, class: 'gl-form-input form-control gl-md-form-input-lg', readonly: true
|
||||
%small.form-text.text-gl-muted
|
||||
= s_("Profiles|Your location was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:location) }
|
||||
- else
|
||||
= f.text_field :location, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|City, country")
|
||||
.form-group.gl-form-group
|
||||
= f.label :job_title, s_('Profiles|Job title')
|
||||
= f.text_field :job_title, class: 'gl-form-input form-control gl-md-form-input-lg'
|
||||
.form-group.gl-form-group
|
||||
= f.label :organization, s_('Profiles|Organization')
|
||||
= f.text_field :organization, class: 'gl-form-input form-control gl-md-form-input-lg'
|
||||
.form-group.gl-form-group
|
||||
= f.label :website_url, s_('Profiles|Website url')
|
||||
= f.text_field :website_url, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|https://website.com")
|
||||
.form-group.gl-form-group
|
||||
= f.label :location, s_('Profiles|Location')
|
||||
- if @user.read_only_attribute?(:location)
|
||||
= f.text_field :location, class: 'gl-form-input form-control gl-md-form-input-lg', readonly: true
|
||||
%small.form-text.text-gl-muted
|
||||
= s_("Profiles|Who you represent or work for.")
|
||||
.form-group.gl-form-group
|
||||
= f.label :bio, s_('Profiles|Bio')
|
||||
= f.text_area :bio, class: 'gl-form-input gl-form-textarea form-control', rows: 4, maxlength: 250
|
||||
%small.form-text.text-gl-muted
|
||||
= s_("Profiles|Tell us about yourself in fewer than 250 characters.")
|
||||
%hr
|
||||
= s_("Profiles|Your location was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:location) }
|
||||
- else
|
||||
= f.text_field :location, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|City, country")
|
||||
.form-group.gl-form-group
|
||||
= f.label :job_title, s_('Profiles|Job title')
|
||||
= f.text_field :job_title, class: 'gl-form-input form-control gl-md-form-input-lg'
|
||||
.form-group.gl-form-group
|
||||
= f.label :organization, s_('Profiles|Organization')
|
||||
= f.text_field :organization, class: 'gl-form-input form-control gl-md-form-input-lg'
|
||||
%small.form-text.text-gl-muted
|
||||
= s_("Profiles|Who you represent or work for.")
|
||||
.form-group.gl-form-group.gl-mb-6.gl-max-w-80
|
||||
= f.label :bio, s_('Profiles|Bio')
|
||||
= f.text_area :bio, class: 'gl-form-input gl-form-textarea form-control', rows: 4, maxlength: 250
|
||||
%small.form-text.text-gl-muted
|
||||
= s_("Profiles|Tell us about yourself in fewer than 250 characters.")
|
||||
.gl-border-t.gl-pt-6
|
||||
%fieldset.form-group.gl-form-group
|
||||
%legend.col-form-label.col-form-label
|
||||
%legend.col-form-label
|
||||
= _('Private profile')
|
||||
- private_profile_label = s_("Profiles|Don't display activity-related personal information on your profile.")
|
||||
- private_profile_help_link = link_to sprite_icon('question-o'), help_page_path('user/profile/index.md', anchor: 'make-your-user-profile-page-private')
|
||||
= f.gitlab_ui_checkbox_component :private_profile, '%{private_profile_label} %{private_profile_help_link}'.html_safe % { private_profile_label: private_profile_label, private_profile_help_link: private_profile_help_link.html_safe }
|
||||
%fieldset.form-group.gl-form-group
|
||||
%legend.col-form-label.col-form-label
|
||||
%legend.col-form-label
|
||||
= s_("Profiles|Private contributions")
|
||||
= f.gitlab_ui_checkbox_component :include_private_contributions,
|
||||
s_('Profiles|Include private contributions on your profile'),
|
||||
help_text: s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information.")
|
||||
%fieldset.form-group.gl-form-group
|
||||
%legend.col-form-label.col-form-label
|
||||
%fieldset.form-group.gl-form-group.gl-mb-0
|
||||
%legend.col-form-label
|
||||
= s_("Profiles|Achievements")
|
||||
= f.gitlab_ui_checkbox_component :achievements_enabled,
|
||||
s_('Profiles|Display achievements on your profile')
|
||||
.row.js-hide-when-nothing-matches-search
|
||||
.col-lg-12
|
||||
%hr
|
||||
= f.submit s_("Profiles|Update profile settings"), class: 'gl-mr-3 js-password-prompt-btn', pajamas_button: true
|
||||
= render Pajamas::ButtonComponent.new(href: user_path(current_user)) do
|
||||
= s_('TagsPage|Cancel')
|
||||
.js-hide-when-nothing-matches-search.gl-border-t.gl-py-6
|
||||
= f.submit s_("Profiles|Update profile settings"), class: 'gl-mr-3 js-password-prompt-btn', pajamas_button: true
|
||||
= render Pajamas::ButtonComponent.new(href: user_path(current_user)) do
|
||||
= s_('TagsPage|Cancel')
|
||||
|
||||
#password-prompt-modal
|
||||
|
||||
|
|
|
|||
|
|
@ -3,26 +3,14 @@
|
|||
|
||||
.bs-callout.help-callout
|
||||
%p= s_('Runners|These runners are assigned to this project.')
|
||||
- if Feature.enabled?(:create_runner_workflow_for_namespace, @project.namespace)
|
||||
- if can?(current_user, :create_runner, @project)
|
||||
= render Pajamas::ButtonComponent.new(href: new_project_runner_path(@project), variant: :confirm) do
|
||||
= s_('Runners|New project runner')
|
||||
.gl-display-inline
|
||||
#js-project-runner-registration-dropdown{ data: { registration_token: @project.runners_token, project_id: @project.id } }
|
||||
- else
|
||||
= _('Please contact an admin to create runners.')
|
||||
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'restrict-runner-registration-by-all-users-in-an-instance'), target: '_blank', rel: 'noopener noreferrer'
|
||||
- if can?(current_user, :create_runner, @project)
|
||||
= render Pajamas::ButtonComponent.new(href: new_project_runner_path(@project), variant: :confirm) do
|
||||
= s_('Runners|New project runner')
|
||||
.gl-display-inline
|
||||
#js-project-runner-registration-dropdown{ data: { registration_token: @project.runners_token, project_id: @project.id } }
|
||||
- else
|
||||
- if can?(current_user, :register_project_runners, @project)
|
||||
= render partial: 'ci/runner/how_to_setup_runner',
|
||||
locals: { registration_token: @project.runners_token,
|
||||
type: _('project'),
|
||||
reset_token_url: reset_registration_token_namespace_project_settings_ci_cd_path,
|
||||
project_path: @project.path_with_namespace,
|
||||
group_path: '' }
|
||||
- else
|
||||
= _('Please contact an admin to register runners.')
|
||||
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'restrict-runner-registration-by-all-users-in-an-instance'), target: '_blank', rel: 'noopener noreferrer'
|
||||
= _('Please contact an admin to create runners.')
|
||||
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'restrict-runner-registration-by-all-users-in-an-instance'), target: '_blank', rel: 'noopener noreferrer'
|
||||
|
||||
%hr
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
data_category: optional
|
||||
key_path: usage_activity_by_stage_monthly.plan.service_desk_enabled_projects
|
||||
description: Count creator ids from projects with service desk enabled
|
||||
description: Count unique users who created projects (creator_id) with service desk enabled
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: respond
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
data_category: optional
|
||||
key_path: usage_activity_by_stage.plan.service_desk_enabled_projects
|
||||
description: Count creator ids from projects with service desk enabled
|
||||
description: Count unique users who created projects (creator_id) with service desk enabled
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: respond
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ development:
|
|||
cluster_cache:
|
||||
cluster:
|
||||
- redis://localhost:7001
|
||||
feature_flag:
|
||||
cluster:
|
||||
- redis://localhost:7001
|
||||
rate_limiting:
|
||||
cluster:
|
||||
- redis://localhost:7001
|
||||
|
|
@ -17,6 +20,9 @@ test:
|
|||
cluster_cache:
|
||||
cluster:
|
||||
- redis://localhost:7001
|
||||
feature_flag:
|
||||
cluster:
|
||||
- redis://localhost:7001
|
||||
rate_limiting:
|
||||
cluster:
|
||||
- redis://localhost:7001
|
||||
|
|
|
|||
|
|
@ -105,8 +105,6 @@ concurrency is set to:
|
|||
When `min_concurrency` is greater than `max_concurrency`, it is treated as
|
||||
being equal to `max_concurrency`.
|
||||
|
||||
You can find example values used by GitLab.com by searching for `concurrency:`
|
||||
in [the Helm charts](https://gitlab.com/gitlab-com/gl-infra/k8s-workloads/gitlab-com/-/blob/master/releases/gitlab/values/gprd.yaml.gotmpl).
|
||||
The values vary according to the work each specific deployment of Sidekiq does.
|
||||
Any other specialized deployments with processes dedicated to specific queues
|
||||
should have the concurrency tuned according to:
|
||||
|
|
|
|||
|
|
@ -38,10 +38,7 @@ If you are using GitLab.com:
|
|||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/383139) in GitLab 15.10. Deployed behind the `create_runner_workflow_for_admin` [flag](../../administration/feature_flags.md)
|
||||
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/389269) in GitLab 16.0.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature,
|
||||
ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `create_runner_workflow_for_admin`.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/415447) in GitLab 16.2. Feature flag `create_runner_workflow_for_admin` removed.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
|
|
@ -195,10 +192,7 @@ Group runners process jobs by using a first in, first out ([FIFO](https://en.wik
|
|||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/383143) in GitLab 15.10. Deployed behind the `create_runner_workflow_for_namespace` [flag](../../administration/feature_flags.md). Disabled by default.
|
||||
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/393919) in GitLab 16.0.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature,
|
||||
ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `create_runner_workflow_for_namespace`.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/415447) in GitLab 16.2. Feature flag `create_runner_workflow_for_admin` removed.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
|
|
@ -328,10 +322,7 @@ A fork *does* copy the CI/CD settings of the cloned repository.
|
|||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/383143) in GitLab 15.10. Deployed behind the `create_runner_workflow_for_namespace` [flag](../../administration/feature_flags.md). Disabled by default.
|
||||
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/393919) in GitLab 16.0.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature,
|
||||
ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `create_runner_workflow_for_namespace`.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/415447) in GitLab 16.2. Feature flag `create_runner_workflow_for_admin` removed.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
|
|
|
|||
|
|
@ -863,9 +863,9 @@ PUT /namespaces/:id/subscription_add_on_purchase/:add_on_name
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
|:------------|:--------|:---------|:------------|
|
||||
| `quantity` | integer | yes | Amount of units in the subscription add-on purchase (Example: Number of seats for a code suggestions add-on) |
|
||||
| `quantity` | integer | no | Amount of units in the subscription add-on purchase (Example: Number of seats for a code suggestions add-on) |
|
||||
| `expires_on` | date | yes | Expiration date of the subscription add-on purchase |
|
||||
| `purchase_xid` | string | yes | Identifier for the subscription add-on purchase (Example: Subscription name for a code suggestions add-on) |
|
||||
| `purchase_xid` | string | no | Identifier for the subscription add-on purchase (Example: Subscription name for a code suggestions add-on) |
|
||||
|
||||
Example request:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
stage: Manage
|
||||
group: Authentication and Authorization
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -347,12 +347,13 @@ If you have difficulty during the renewal process, contact the
|
|||
|
||||
Contacts can renew a subscription, cancel a subscription, or transfer the subscription to a different namespace.
|
||||
|
||||
To change the contacts:
|
||||
For information about how to transfer ownership of the Customers Portal account to another person, see
|
||||
[Change account owner information](../customers_portal.md#change-account-owner-information).
|
||||
|
||||
To add a secondary contact for your subscription:
|
||||
|
||||
1. Ensure an account exists in the
|
||||
[Customers Portal](https://customers.gitlab.com/customers/sign_in) for the user you want to add.
|
||||
1. Verify you have access to at least one of
|
||||
[these requirements](https://about.gitlab.com/handbook/support/license-and-renewals/workflows/customersdot/associating_purchases.html).
|
||||
1. [Create a ticket with the Support team](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=360000071293). Include any relevant material in your request.
|
||||
|
||||
## Compute
|
||||
|
|
|
|||
|
|
@ -428,12 +428,13 @@ The new tier takes effect when the new license is activated.
|
|||
|
||||
Contacts can renew a subscription, cancel a subscription, or transfer the subscription to a different namespace.
|
||||
|
||||
To change the contacts:
|
||||
For information about how to transfer ownership of the Customers Portal account to another person, see
|
||||
[Change account owner information](../customers_portal.md#change-account-owner-information).
|
||||
|
||||
To add a secondary contact for your subscription:
|
||||
|
||||
1. Ensure an account exists in the
|
||||
[Customers Portal](https://customers.gitlab.com/customers/sign_in) for the user you want to add.
|
||||
1. Verify you have access to at least one of
|
||||
[these requirements](https://about.gitlab.com/handbook/support/license-and-renewals/workflows/customersdot/associating_purchases.html).
|
||||
1. [Create a ticket with the Support team](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=360000071293). Include any relevant material in your request.
|
||||
|
||||
## Subscription expiry
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ You can use push options to skip a CI/CD pipeline, or pass CI/CD variables.
|
|||
| ------------------------------ | ------------------------------------------------------------------------------------------- |---------------------- |
|
||||
| `ci.skip` | Do not create a CI pipeline for the latest push. Only skips branch pipelines and not [merge request pipelines](../../ci/pipelines/merge_request_pipelines.md). This does not skip pipelines for CI integrations, such as Jenkins. | [11.7](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/15643) |
|
||||
| `ci.variable="<name>=<value>"` | Provide [CI/CD variables](../../ci/variables/index.md) to be used in a CI pipeline, if one is created due to the push. Only passes variables to branch pipelines and not [merge request pipelines](../../ci/pipelines/merge_request_pipelines.md). | [12.6](https://gitlab.com/gitlab-org/gitlab/-/issues/27983) |
|
||||
| `integrations.skip_ci` | Skip push events for CI integrations, such as Atlassian Bamboo, Buildkite, Drone, Jenkins, and JetBrains TeamCity. | [16.2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123837) |
|
||||
|
||||
An example of using `ci.skip`:
|
||||
|
||||
|
|
@ -50,6 +51,12 @@ An example of passing some CI/CD variables for a pipeline:
|
|||
git push -o ci.variable="MAX_RETRIES=10" -o ci.variable="MAX_TIME=600"
|
||||
```
|
||||
|
||||
An example of using `integrations.skip_ci`:
|
||||
|
||||
```shell
|
||||
git push -o integrations.skip_ci
|
||||
```
|
||||
|
||||
## Push options for merge requests
|
||||
|
||||
You can use Git push options to perform certain actions for merge requests at the same
|
||||
|
|
|
|||
|
|
@ -42,8 +42,10 @@ module Gitlab
|
|||
with_redis do |redis|
|
||||
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
|
||||
if Gitlab::Redis::ClusterUtil.cluster?(redis)
|
||||
Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
|
||||
keys.each { |key| pipeline.get(key) }
|
||||
redis.with_readonly_pipeline do
|
||||
Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
|
||||
keys.each { |key| pipeline.get(key) }
|
||||
end
|
||||
end
|
||||
else
|
||||
redis.mget(keys)
|
||||
|
|
|
|||
|
|
@ -15,12 +15,7 @@ module Gitlab
|
|||
#
|
||||
# @return [self]
|
||||
def self.build(project)
|
||||
strategy =
|
||||
if Feature.enabled?(:gitlab_error_tracking, project)
|
||||
OpenApiStrategy.new(project)
|
||||
else
|
||||
ActiveRecordStrategy.new(project)
|
||||
end
|
||||
strategy = OpenApiStrategy.new(project)
|
||||
|
||||
new(strategy)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -11,9 +11,11 @@ module Gitlab
|
|||
|
||||
Gitlab::Redis::Cache.with do |r|
|
||||
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
|
||||
Gitlab::Redis::CrossSlot::Pipeline.new(r).pipelined do |pipeline|
|
||||
subjects.each do |subject|
|
||||
results[subject.cache_key] = new(subject).read(pipeline)
|
||||
r.with_readonly_pipeline do
|
||||
Gitlab::Redis::CrossSlot::Pipeline.new(r).pipelined do |pipeline|
|
||||
subjects.each do |subject|
|
||||
results[subject.cache_key] = new(subject).read(pipeline)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -43,7 +43,13 @@ module Gitlab
|
|||
keys = names.map { |name| normalize_key(name, options) }
|
||||
|
||||
values = failsafe(:patched_read_multi_mget, returning: {}) do
|
||||
redis.with { |c| pipeline_mget(c, keys) }
|
||||
redis.with do |c|
|
||||
if c.is_a?(Gitlab::Redis::MultiStore)
|
||||
c.with_readonly_pipeline { pipeline_mget(c, keys) }
|
||||
else
|
||||
pipeline_mget(c, keys)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
names.zip(values).each_with_object({}) do |(name, value), results|
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
class NestedReadonlyPipelineError < StandardError
|
||||
def message
|
||||
'Nested use of with_readonly_pipeline is detected.'
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :primary_store, :secondary_store, :instance_name
|
||||
|
||||
FAILED_TO_READ_ERROR_MESSAGE = 'Failed to read from the redis default_store.'
|
||||
|
|
@ -100,6 +106,25 @@ module Gitlab
|
|||
validate_stores!
|
||||
end
|
||||
|
||||
# Pipelines are sent to both instances by default since
|
||||
# they could execute both read and write commands.
|
||||
#
|
||||
# But for pipelines that only consists of read commands, this method
|
||||
# can be used to scope the pipeline and send it only to the default store.
|
||||
def with_readonly_pipeline
|
||||
raise NestedReadonlyPipelineError if readonly_pipeline?
|
||||
|
||||
Thread.current[:readonly_pipeline] = true
|
||||
|
||||
yield
|
||||
ensure
|
||||
Thread.current[:readonly_pipeline] = false
|
||||
end
|
||||
|
||||
def readonly_pipeline?
|
||||
Thread.current[:readonly_pipeline].present?
|
||||
end
|
||||
|
||||
# rubocop:disable GitlabSecurity/PublicSend
|
||||
READ_COMMANDS.each do |name|
|
||||
define_method(name) do |*args, **kwargs, &block|
|
||||
|
|
@ -123,7 +148,7 @@ module Gitlab
|
|||
|
||||
PIPELINED_COMMANDS.each do |name|
|
||||
define_method(name) do |*args, **kwargs, &block|
|
||||
if use_primary_and_secondary_stores?
|
||||
if use_primary_and_secondary_stores? && !readonly_pipeline?
|
||||
pipelined_both(name, *args, **kwargs, &block)
|
||||
else
|
||||
send_command(default_store, name, *args, **kwargs, &block)
|
||||
|
|
|
|||
|
|
@ -34249,9 +34249,6 @@ msgstr ""
|
|||
msgid "Please contact an admin to create runners."
|
||||
msgstr ""
|
||||
|
||||
msgid "Please contact an admin to register runners."
|
||||
msgstr ""
|
||||
|
||||
msgid "Please contact your GitLab administrator if you think this is an error."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -39678,9 +39675,6 @@ msgstr ""
|
|||
msgid "Runners|Filter projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Get started with runners"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -41,18 +41,6 @@ RSpec.describe Admin::RunnersController, feature_category: :runner_fleet do
|
|||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template(:new)
|
||||
end
|
||||
|
||||
context 'when create_runner_workflow_for_admin is disabled' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_admin: false)
|
||||
end
|
||||
|
||||
it 'returns :not_found' do
|
||||
get :new
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#register' do
|
||||
|
|
@ -78,20 +66,6 @@ RSpec.describe Admin::RunnersController, feature_category: :runner_fleet do
|
|||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when create_runner_workflow_for_admin is disabled' do
|
||||
let_it_be(:new_runner) { create(:ci_runner, registration_type: :authenticated_user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_admin: false)
|
||||
end
|
||||
|
||||
it 'returns :not_found' do
|
||||
register
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#edit' do
|
||||
|
|
|
|||
|
|
@ -65,52 +65,28 @@ RSpec.describe Groups::RunnersController, feature_category: :runner_fleet do
|
|||
end
|
||||
|
||||
describe '#new' do
|
||||
context 'when create_runner_workflow_for_namespace is enabled' do
|
||||
context 'when user is owner' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: [group])
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
context 'when user is owner' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
it 'renders new with 200 status code' do
|
||||
get :new, params: { group_id: group }
|
||||
|
||||
it 'renders new with 200 status code' do
|
||||
get :new, params: { group_id: group }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template(:new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not owner' do
|
||||
before do
|
||||
group.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'renders a 404' do
|
||||
get :new, params: { group_id: group }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template(:new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when create_runner_workflow_for_namespace is disabled' do
|
||||
context 'when user is not owner' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: false)
|
||||
group.add_maintainer(user)
|
||||
end
|
||||
|
||||
context 'when user is owner' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
it 'renders a 404' do
|
||||
get :new, params: { group_id: group }
|
||||
|
||||
it 'renders a 404' do
|
||||
get :new, params: { group_id: group }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -118,66 +94,40 @@ RSpec.describe Groups::RunnersController, feature_category: :runner_fleet do
|
|||
describe '#register' do
|
||||
subject(:register) { get :register, params: { group_id: group, id: new_runner } }
|
||||
|
||||
context 'when create_runner_workflow_for_namespace is enabled' do
|
||||
context 'when user is owner' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: [group])
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
context 'when user is owner' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
context 'when runner can be registered after creation' do
|
||||
let_it_be(:new_runner) { create(:ci_runner, :group, groups: [group], registration_type: :authenticated_user) }
|
||||
|
||||
context 'when runner can be registered after creation' do
|
||||
let_it_be(:new_runner) { create(:ci_runner, :group, groups: [group], registration_type: :authenticated_user) }
|
||||
it 'renders a :register template' do
|
||||
register
|
||||
|
||||
it 'renders a :register template' do
|
||||
register
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template(:register)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when runner cannot be registered after creation' do
|
||||
let_it_be(:new_runner) { runner }
|
||||
|
||||
it 'returns :not_found' do
|
||||
register
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template(:register)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not owner' do
|
||||
before do
|
||||
group.add_maintainer(user)
|
||||
end
|
||||
context 'when runner cannot be registered after creation' do
|
||||
let_it_be(:new_runner) { runner }
|
||||
|
||||
context 'when runner can be registered after creation' do
|
||||
let_it_be(:new_runner) { create(:ci_runner, :group, groups: [group], registration_type: :authenticated_user) }
|
||||
it 'returns :not_found' do
|
||||
register
|
||||
|
||||
it 'returns :not_found' do
|
||||
register
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when create_runner_workflow_for_namespace is disabled' do
|
||||
let_it_be(:new_runner) { create(:ci_runner, :group, groups: [group], registration_type: :authenticated_user) }
|
||||
|
||||
context 'when user is not owner' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: false)
|
||||
group.add_maintainer(user)
|
||||
end
|
||||
|
||||
context 'when user is owner' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
context 'when runner can be registered after creation' do
|
||||
let_it_be(:new_runner) { create(:ci_runner, :group, groups: [group], registration_type: :authenticated_user) }
|
||||
|
||||
it 'returns :not_found' do
|
||||
register
|
||||
|
|
|
|||
|
|
@ -28,52 +28,28 @@ RSpec.describe Projects::RunnersController, feature_category: :runner_fleet do
|
|||
}
|
||||
end
|
||||
|
||||
context 'when create_runner_workflow_for_namespace is enabled' do
|
||||
context 'when user is maintainer' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
context 'when user is maintainer' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
it 'renders new with 200 status code' do
|
||||
get :new, params: params
|
||||
|
||||
it 'renders new with 200 status code' do
|
||||
get :new, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template(:new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not maintainer' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it 'renders a 404' do
|
||||
get :new, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template(:new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when create_runner_workflow_for_namespace is disabled' do
|
||||
context 'when user is not maintainer' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: false)
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
context 'when user is maintainer' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
it 'renders a 404' do
|
||||
get :new, params: params
|
||||
|
||||
it 'renders a 404' do
|
||||
get :new, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -81,66 +57,40 @@ RSpec.describe Projects::RunnersController, feature_category: :runner_fleet do
|
|||
describe '#register' do
|
||||
subject(:register) { get :register, params: { namespace_id: project.namespace, project_id: project, id: new_runner } }
|
||||
|
||||
context 'when create_runner_workflow_for_namespace is enabled' do
|
||||
context 'when user is maintainer' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
context 'when user is maintainer' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
context 'when runner can be registered after creation' do
|
||||
let_it_be(:new_runner) { create(:ci_runner, :project, projects: [project], registration_type: :authenticated_user) }
|
||||
|
||||
context 'when runner can be registered after creation' do
|
||||
let_it_be(:new_runner) { create(:ci_runner, :project, projects: [project], registration_type: :authenticated_user) }
|
||||
it 'renders a :register template' do
|
||||
register
|
||||
|
||||
it 'renders a :register template' do
|
||||
register
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template(:register)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when runner cannot be registered after creation' do
|
||||
let_it_be(:new_runner) { runner }
|
||||
|
||||
it 'returns :not_found' do
|
||||
register
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template(:register)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not maintainer' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
context 'when runner cannot be registered after creation' do
|
||||
let_it_be(:new_runner) { runner }
|
||||
|
||||
context 'when runner can be registered after creation' do
|
||||
let_it_be(:new_runner) { create(:ci_runner, :project, projects: [project], registration_type: :authenticated_user) }
|
||||
it 'returns :not_found' do
|
||||
register
|
||||
|
||||
it 'returns :not_found' do
|
||||
register
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when create_runner_workflow_for_namespace is disabled' do
|
||||
let_it_be(:new_runner) { create(:ci_runner, :project, projects: [project], registration_type: :authenticated_user) }
|
||||
|
||||
context 'when user is not maintainer' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: false)
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
context 'when user is maintainer' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
context 'when runner can be registered after creation' do
|
||||
let_it_be(:new_runner) { create(:ci_runner, :project, projects: [project], registration_type: :authenticated_user) }
|
||||
|
||||
it 'returns :not_found' do
|
||||
register
|
||||
|
|
|
|||
|
|
@ -32,30 +32,13 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
|
|||
end
|
||||
|
||||
describe "runners registration" do
|
||||
context 'when create_runner_workflow_for_namespace is enabled' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_admin: true)
|
||||
|
||||
visit admin_runners_path
|
||||
end
|
||||
|
||||
it_behaves_like "shows and resets runner registration token" do
|
||||
let(:dropdown_text) { s_('Runners|Register an instance runner') }
|
||||
let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token }
|
||||
end
|
||||
before do
|
||||
visit admin_runners_path
|
||||
end
|
||||
|
||||
context 'when create_runner_workflow_for_namespace is disabled' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_admin: false)
|
||||
|
||||
visit admin_runners_path
|
||||
end
|
||||
|
||||
it_behaves_like "shows and resets runner registration token" do
|
||||
let(:dropdown_text) { s_('Runners|Register an instance runner') }
|
||||
let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token }
|
||||
end
|
||||
it_behaves_like "shows and resets runner registration token" do
|
||||
let(:dropdown_text) { s_('Runners|Register an instance runner') }
|
||||
let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -16,21 +16,6 @@ RSpec.describe "Group Runners", feature_category: :runner_fleet do
|
|||
end
|
||||
|
||||
describe "Group runners page", :js do
|
||||
describe "legacy runners registration" do
|
||||
let_it_be(:group_registration_token) { group.runners_token }
|
||||
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: false)
|
||||
|
||||
visit group_runners_path(group)
|
||||
end
|
||||
|
||||
it_behaves_like "shows and resets runner registration token" do
|
||||
let(:dropdown_text) { 'Register a group runner' }
|
||||
let(:registration_token) { group_registration_token }
|
||||
end
|
||||
end
|
||||
|
||||
context "with no runners" do
|
||||
before do
|
||||
visit group_runners_path(group)
|
||||
|
|
|
|||
|
|
@ -14,17 +14,11 @@ RSpec.describe 'Runners', feature_category: :runner_fleet do
|
|||
stub_feature_flags(project_runners_vue_ui: false)
|
||||
end
|
||||
|
||||
context 'when user views runners page' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
context 'with user as project maintainer' do
|
||||
let_it_be(:project) { create(:project).tap { |project| project.add_maintainer(user) } }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
context 'when create_runner_workflow_for_namespace is enabled', :js do
|
||||
context 'when user views runners page', :js do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
|
||||
|
||||
visit project_runners_path(project)
|
||||
end
|
||||
|
||||
|
|
@ -38,58 +32,18 @@ RSpec.describe 'Runners', feature_category: :runner_fleet do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when user views new runner page' do
|
||||
context 'when create_runner_workflow_for_namespace is enabled', :js do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
|
||||
|
||||
visit new_project_runner_path(project)
|
||||
end
|
||||
|
||||
it_behaves_like 'creates runner and shows register page' do
|
||||
let(:register_path_pattern) { register_project_runner_path(project, '.*') }
|
||||
end
|
||||
|
||||
it 'shows the locked field' do
|
||||
expect(page).to have_selector('input[type="checkbox"][name="locked"]')
|
||||
expect(page).to have_content(_('Lock to current projects'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when create_runner_workflow_for_namespace is disabled' do
|
||||
context 'when user views new runner page', :js do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: false)
|
||||
visit new_project_runner_path(project)
|
||||
end
|
||||
|
||||
it 'user can see a link with instructions on how to install GitLab Runner' do
|
||||
visit project_runners_path(project)
|
||||
|
||||
expect(page).to have_link('Install GitLab Runner and ensure it\'s running.', href: "https://docs.gitlab.com/runner/install/")
|
||||
it_behaves_like 'creates runner and shows register page' do
|
||||
let(:register_path_pattern) { register_project_runner_path(project, '.*') }
|
||||
end
|
||||
|
||||
describe 'runners registration token' do
|
||||
let!(:token) { project.runners_token }
|
||||
|
||||
before do
|
||||
visit project_runners_path(project)
|
||||
end
|
||||
|
||||
it 'has a registration token' do
|
||||
expect(page.find('#registration_token')).to have_content(token)
|
||||
end
|
||||
|
||||
describe 'reload registration token' do
|
||||
let(:page_token) { find('#registration_token').text }
|
||||
|
||||
before do
|
||||
click_link 'Reset registration token'
|
||||
end
|
||||
|
||||
it 'changes registration token' do
|
||||
expect(page_token).not_to eq token
|
||||
end
|
||||
end
|
||||
it 'shows the locked field' do
|
||||
expect(page).to have_selector('input[type="checkbox"][name="locked"]')
|
||||
expect(page).to have_content(_('Lock to current projects'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -41,7 +41,9 @@ describe('RegistrationDropdown', () => {
|
|||
const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
|
||||
const findRegistrationToken = () => wrapper.findComponent(RegistrationToken);
|
||||
const findRegistrationTokenInput = () =>
|
||||
wrapper.findByLabelText(RegistrationToken.i18n.registrationToken);
|
||||
wrapper.findByLabelText(
|
||||
`${RegistrationToken.i18n.registrationToken} ${RegistrationDropdown.i18n.supportForRegistrationTokensDeprecated}`,
|
||||
);
|
||||
const findTokenResetDropdownItem = () =>
|
||||
wrapper.findComponent(RegistrationTokenResetDropdownItem);
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
|
|
@ -107,12 +109,12 @@ describe('RegistrationDropdown', () => {
|
|||
createComponent();
|
||||
|
||||
expect(findDropdown().props()).toMatchObject({
|
||||
category: 'primary',
|
||||
variant: 'confirm',
|
||||
category: 'tertiary',
|
||||
variant: 'default',
|
||||
});
|
||||
|
||||
expect(findDropdown().attributes()).toMatchObject({
|
||||
toggleclass: '',
|
||||
toggleclass: 'gl-px-3!',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -217,14 +219,9 @@ describe('RegistrationDropdown', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{ createRunnerWorkflowForAdmin: true },
|
||||
{ createRunnerWorkflowForNamespace: true },
|
||||
])('When showing a "deprecated" warning', (glFeatures) => {
|
||||
describe('When showing a "deprecated" warning', () => {
|
||||
it('passes deprecated variant props and attributes to dropdown', () => {
|
||||
createComponent({
|
||||
provide: { glFeatures },
|
||||
});
|
||||
createComponent();
|
||||
|
||||
expect(findDropdown().props()).toMatchObject({
|
||||
category: 'tertiary',
|
||||
|
|
@ -249,12 +246,7 @@ describe('RegistrationDropdown', () => {
|
|||
});
|
||||
|
||||
it('shows warning text', () => {
|
||||
createComponent(
|
||||
{
|
||||
provide: { glFeatures },
|
||||
},
|
||||
mountExtended,
|
||||
);
|
||||
createComponent({}, mountExtended);
|
||||
|
||||
const text = wrapper.findByText(s__('Runners|Support for registration tokens is deprecated'));
|
||||
|
||||
|
|
@ -262,12 +254,7 @@ describe('RegistrationDropdown', () => {
|
|||
});
|
||||
|
||||
it('button shows ellipsis icon', () => {
|
||||
createComponent(
|
||||
{
|
||||
provide: { glFeatures },
|
||||
},
|
||||
mountExtended,
|
||||
);
|
||||
createComponent({}, mountExtended);
|
||||
|
||||
expect(findDropdownBtn().findComponent(GlIcon).props('name')).toBe('ellipsis_v');
|
||||
expect(findDropdownBtn().findAllComponents(GlIcon)).toHaveLength(1);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {
|
|||
I18N_CREATE_RUNNER_LINK,
|
||||
I18N_STILL_USING_REGISTRATION_TOKENS,
|
||||
I18N_CONTACT_ADMIN_TO_REGISTER,
|
||||
I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
|
||||
I18N_NO_RESULTS,
|
||||
I18N_EDIT_YOUR_SEARCH,
|
||||
} from '~/ci/runner/constants';
|
||||
|
|
@ -59,137 +58,85 @@ describe('RunnerListEmptyState', () => {
|
|||
});
|
||||
|
||||
describe('when search is not filtered', () => {
|
||||
describe.each([
|
||||
{ createRunnerWorkflowForAdmin: true },
|
||||
{ createRunnerWorkflowForNamespace: true },
|
||||
])('when createRunnerWorkflow is enabled by %o', (currentGlFeatures) => {
|
||||
beforeEach(() => {
|
||||
glFeatures = currentGlFeatures;
|
||||
});
|
||||
|
||||
describe.each`
|
||||
newRunnerPath | registrationToken | expectedMessages
|
||||
${mockNewRunnerPath} | ${mockRegistrationToken} | ${[I18N_CREATE_RUNNER_LINK, I18N_STILL_USING_REGISTRATION_TOKENS]}
|
||||
${mockNewRunnerPath} | ${null} | ${[I18N_CREATE_RUNNER_LINK]}
|
||||
${null} | ${mockRegistrationToken} | ${[I18N_STILL_USING_REGISTRATION_TOKENS]}
|
||||
${null} | ${null} | ${[I18N_CONTACT_ADMIN_TO_REGISTER]}
|
||||
`(
|
||||
'when newRunnerPath is $newRunnerPath and registrationToken is $registrationToken',
|
||||
({ newRunnerPath, registrationToken, expectedMessages }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
newRunnerPath,
|
||||
registrationToken,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('shows title', () => {
|
||||
expectTitleToBe(I18N_GET_STARTED);
|
||||
});
|
||||
|
||||
it('renders an illustration', () => {
|
||||
expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
|
||||
});
|
||||
|
||||
it(`shows description: "${expectedMessages.join(' ')}"`, () => {
|
||||
expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, ...expectedMessages]);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe('with newRunnerPath and registration token', () => {
|
||||
describe.each`
|
||||
newRunnerPath | registrationToken | expectedMessages
|
||||
${mockNewRunnerPath} | ${mockRegistrationToken} | ${[I18N_CREATE_RUNNER_LINK, I18N_STILL_USING_REGISTRATION_TOKENS]}
|
||||
${mockNewRunnerPath} | ${null} | ${[I18N_CREATE_RUNNER_LINK]}
|
||||
${null} | ${mockRegistrationToken} | ${[I18N_STILL_USING_REGISTRATION_TOKENS]}
|
||||
${null} | ${null} | ${[I18N_CONTACT_ADMIN_TO_REGISTER]}
|
||||
`(
|
||||
'when newRunnerPath is $newRunnerPath and registrationToken is $registrationToken',
|
||||
({ newRunnerPath, registrationToken, expectedMessages }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
registrationToken: mockRegistrationToken,
|
||||
newRunnerPath: mockNewRunnerPath,
|
||||
newRunnerPath,
|
||||
registrationToken,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('shows links to the new runner page and registration instructions', () => {
|
||||
expect(findLinks().at(0).attributes('href')).toBe(mockNewRunnerPath);
|
||||
|
||||
const { value } = getBinding(findLinks().at(1).element, 'gl-modal');
|
||||
expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with newRunnerPath and no registration token', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
registrationToken: mockRegistrationToken,
|
||||
newRunnerPath: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('opens a runner registration instructions modal with a link', () => {
|
||||
const { value } = getBinding(findLink().element, 'gl-modal');
|
||||
expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with no newRunnerPath nor registration token', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
registrationToken: null,
|
||||
newRunnerPath: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('has no link', () => {
|
||||
expect(findLink().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when createRunnerWorkflow is disabled', () => {
|
||||
describe('when there is a registration token', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
registrationToken: mockRegistrationToken,
|
||||
},
|
||||
});
|
||||
it('shows title', () => {
|
||||
expectTitleToBe(I18N_GET_STARTED);
|
||||
});
|
||||
|
||||
it('renders an illustration', () => {
|
||||
expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
|
||||
});
|
||||
|
||||
it('opens a runner registration instructions modal with a link', () => {
|
||||
const { value } = getBinding(findLink().element, 'gl-modal');
|
||||
expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
|
||||
it(`shows description: "${expectedMessages.join(' ')}"`, () => {
|
||||
expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, ...expectedMessages]);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('displays text with registration instructions', () => {
|
||||
expectTitleToBe(I18N_GET_STARTED);
|
||||
|
||||
expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_FOLLOW_REGISTRATION_INSTRUCTIONS]);
|
||||
describe('with newRunnerPath and registration token', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
registrationToken: mockRegistrationToken,
|
||||
newRunnerPath: mockNewRunnerPath,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there is no registration token', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ props: { registrationToken: null } });
|
||||
});
|
||||
it('shows links to the new runner page and registration instructions', () => {
|
||||
expect(findLinks().at(0).attributes('href')).toBe(mockNewRunnerPath);
|
||||
|
||||
it('displays "contact admin" text', () => {
|
||||
expectTitleToBe(I18N_GET_STARTED);
|
||||
const { value } = getBinding(findLinks().at(1).element, 'gl-modal');
|
||||
expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
|
||||
});
|
||||
});
|
||||
|
||||
expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_CONTACT_ADMIN_TO_REGISTER]);
|
||||
describe('with newRunnerPath and no registration token', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
registrationToken: mockRegistrationToken,
|
||||
newRunnerPath: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('has no registration instructions link', () => {
|
||||
expect(findLink().exists()).toBe(false);
|
||||
it('opens a runner registration instructions modal with a link', () => {
|
||||
const { value } = getBinding(findLink().element, 'gl-modal');
|
||||
expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with no newRunnerPath nor registration token', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
registrationToken: null,
|
||||
newRunnerPath: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('has no link', () => {
|
||||
expect(findLink().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -483,35 +483,15 @@ describe('GroupRunnersApp', () => {
|
|||
expect(findRegistrationDropdown().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('when create_runner_workflow_for_namespace is enabled', () => {
|
||||
it('shows the create runner button', () => {
|
||||
createComponent({
|
||||
props: {
|
||||
newRunnerPath,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
createRunnerWorkflowForNamespace: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(findNewRunnerBtn().attributes('href')).toBe(newRunnerPath);
|
||||
});
|
||||
|
||||
it('when create_runner_workflow_for_namespace is disabled', () => {
|
||||
createComponent({
|
||||
props: {
|
||||
newRunnerPath,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
createRunnerWorkflowForNamespace: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(findNewRunnerBtn().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user has no permission to register group runner', () => {
|
||||
|
|
@ -524,16 +504,11 @@ describe('GroupRunnersApp', () => {
|
|||
expect(findRegistrationDropdown().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('when create_runner_workflow_for_namespace is enabled', () => {
|
||||
it('shows the create runner button', () => {
|
||||
createComponent({
|
||||
props: {
|
||||
newRunnerPath: null,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
createRunnerWorkflowForNamespace: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(findNewRunnerBtn().exists()).toBe(false);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { shallowMount } from '@vue/test-utils';
|
|||
import TaskListItemActions from '~/issues/show/components/task_list_item_actions.vue';
|
||||
import eventHub from '~/issues/show/event_hub';
|
||||
|
||||
jest.mock('~/issues/show/event_hub');
|
||||
|
||||
describe('TaskListItemActions component', () => {
|
||||
let wrapper;
|
||||
|
||||
|
|
@ -37,16 +39,12 @@ describe('TaskListItemActions component', () => {
|
|||
});
|
||||
|
||||
it('emits event when `Convert to task` dropdown item is clicked', () => {
|
||||
jest.spyOn(eventHub, '$emit');
|
||||
|
||||
findConvertToTaskItem().vm.$emit('action');
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10');
|
||||
});
|
||||
|
||||
it('emits event when `Delete` dropdown item is clicked', () => {
|
||||
jest.spyOn(eventHub, '$emit');
|
||||
|
||||
findDeleteItem().vm.$emit('action');
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10');
|
||||
|
|
|
|||
|
|
@ -1,38 +1,37 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import GfmAutoComplete from '~/gfm_auto_complete';
|
||||
import { TYPE_ISSUE } from '~/issues/constants';
|
||||
import RelatedIssuableInput from '~/related_issues/components/related_issuable_input.vue';
|
||||
import { PathIdSeparator } from '~/related_issues/constants';
|
||||
|
||||
jest.mock('ee_else_ce/gfm_auto_complete', () => {
|
||||
return function gfmAutoComplete() {
|
||||
return {
|
||||
constructor() {},
|
||||
setup() {},
|
||||
};
|
||||
};
|
||||
});
|
||||
jest.mock('~/gfm_auto_complete');
|
||||
|
||||
describe('RelatedIssuableInput', () => {
|
||||
let propsData;
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
inputValue: '',
|
||||
references: [],
|
||||
pathIdSeparator: PathIdSeparator.Issue,
|
||||
issuableType: TYPE_ISSUE,
|
||||
autoCompleteSources: {
|
||||
issues: `${TEST_HOST}/h5bp/html5-boilerplate/-/autocomplete_sources/issues`,
|
||||
const autoCompleteSources = {
|
||||
issues: `${TEST_HOST}/h5bp/html5-boilerplate/-/autocomplete_sources/issues`,
|
||||
};
|
||||
|
||||
const mountComponent = (props = {}) => {
|
||||
wrapper = shallowMount(RelatedIssuableInput, {
|
||||
propsData: {
|
||||
inputValue: '',
|
||||
references: [],
|
||||
pathIdSeparator: PathIdSeparator.Issue,
|
||||
issuableType: TYPE_ISSUE,
|
||||
autoCompleteSources,
|
||||
...props,
|
||||
},
|
||||
};
|
||||
});
|
||||
attachTo: document.body,
|
||||
});
|
||||
};
|
||||
|
||||
describe('autocomplete', () => {
|
||||
describe('with autoCompleteSources', () => {
|
||||
it('shows placeholder text', () => {
|
||||
const wrapper = shallowMount(RelatedIssuableInput, { propsData });
|
||||
mountComponent();
|
||||
|
||||
expect(wrapper.findComponent({ ref: 'input' }).element.placeholder).toBe(
|
||||
'Paste issue link or <#issue id>',
|
||||
|
|
@ -40,51 +39,32 @@ describe('RelatedIssuableInput', () => {
|
|||
});
|
||||
|
||||
it('has GfmAutoComplete', () => {
|
||||
const wrapper = shallowMount(RelatedIssuableInput, { propsData });
|
||||
mountComponent();
|
||||
|
||||
expect(wrapper.vm.gfmAutoComplete).toBeDefined();
|
||||
expect(GfmAutoComplete).toHaveBeenCalledWith(autoCompleteSources);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with no autoCompleteSources', () => {
|
||||
it('shows placeholder text', () => {
|
||||
const wrapper = shallowMount(RelatedIssuableInput, {
|
||||
propsData: {
|
||||
...propsData,
|
||||
references: ['!1', '!2'],
|
||||
},
|
||||
});
|
||||
mountComponent({ references: ['!1', '!2'] });
|
||||
|
||||
expect(wrapper.findComponent({ ref: 'input' }).element.value).toBe('');
|
||||
});
|
||||
|
||||
it('does not have GfmAutoComplete', () => {
|
||||
const wrapper = shallowMount(RelatedIssuableInput, {
|
||||
propsData: {
|
||||
...propsData,
|
||||
autoCompleteSources: {},
|
||||
},
|
||||
});
|
||||
mountComponent({ autoCompleteSources: {} });
|
||||
|
||||
expect(wrapper.vm.gfmAutoComplete).not.toBeDefined();
|
||||
expect(GfmAutoComplete).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('focus', () => {
|
||||
it('when clicking anywhere on the input wrapper it should focus the input', async () => {
|
||||
const wrapper = shallowMount(RelatedIssuableInput, {
|
||||
propsData: {
|
||||
...propsData,
|
||||
references: ['foo', 'bar'],
|
||||
},
|
||||
// We need to attach to document, so that `document.activeElement` is properly set in jsdom
|
||||
attachTo: document.body,
|
||||
});
|
||||
mountComponent({ references: ['foo', 'bar'] });
|
||||
|
||||
wrapper.find('li').trigger('click');
|
||||
|
||||
await nextTick();
|
||||
await wrapper.find('li').trigger('click');
|
||||
|
||||
expect(document.activeElement).toBe(wrapper.findComponent({ ref: 'input' }).element);
|
||||
});
|
||||
|
|
@ -92,11 +72,7 @@ describe('RelatedIssuableInput', () => {
|
|||
|
||||
describe('when filling in the input', () => {
|
||||
it('emits addIssuableFormInput with data', () => {
|
||||
const wrapper = shallowMount(RelatedIssuableInput, {
|
||||
propsData,
|
||||
});
|
||||
|
||||
wrapper.vm.$emit = jest.fn();
|
||||
mountComponent();
|
||||
|
||||
const newInputValue = 'filling in things';
|
||||
const untouchedRawReferences = newInputValue.trim().split(/\s/);
|
||||
|
|
@ -108,12 +84,16 @@ describe('RelatedIssuableInput', () => {
|
|||
input.element.selectionEnd = newInputValue.length;
|
||||
input.trigger('input');
|
||||
|
||||
expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormInput', {
|
||||
newValue: newInputValue,
|
||||
caretPos: newInputValue.length,
|
||||
untouchedRawReferences,
|
||||
touchedReference,
|
||||
});
|
||||
expect(wrapper.emitted('addIssuableFormInput')).toEqual([
|
||||
[
|
||||
{
|
||||
newValue: newInputValue,
|
||||
caretPos: newInputValue.length,
|
||||
untouchedRawReferences,
|
||||
touchedReference,
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { GlIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
|
|
@ -154,6 +155,13 @@ describe('IssuableLockForm', () => {
|
|||
expect(tooltip).toBeDefined();
|
||||
expect(tooltip.value.title).toBe(isLocked ? 'Locked' : 'Unlocked');
|
||||
});
|
||||
|
||||
it('renders lock icon', () => {
|
||||
const icon = findSidebarCollapseIcon().findComponent(GlIcon).props('name');
|
||||
const expected = isLocked ? 'lock' : 'lock-open';
|
||||
|
||||
expect(icon).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -148,3 +148,16 @@ export const getMrTimelogsQueryResponse = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const deleteTimelogMutationResponse = {
|
||||
data: {
|
||||
timelogDelete: {
|
||||
errors: [],
|
||||
timelog: {
|
||||
id: 'gid://gitlab/Issue/148',
|
||||
issue: {},
|
||||
mergeRequest: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import getIssueTimelogsQuery from '~/sidebar/queries/get_issue_timelogs.query.gr
|
|||
import getMrTimelogsQuery from '~/sidebar/queries/get_mr_timelogs.query.graphql';
|
||||
import deleteTimelogMutation from '~/sidebar/queries/delete_timelog.mutation.graphql';
|
||||
import {
|
||||
deleteTimelogMutationResponse,
|
||||
getIssueTimelogsQueryResponse,
|
||||
getMrTimelogsQueryResponse,
|
||||
timelogToRemoveId,
|
||||
|
|
@ -22,7 +23,7 @@ jest.mock('~/alert');
|
|||
describe('Issuable Time Tracking Report', () => {
|
||||
Vue.use(VueApollo);
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findDeleteButton = () => wrapper.findByTestId('deleteButton');
|
||||
const successIssueQueryHandler = jest.fn().mockResolvedValue(getIssueTimelogsQueryResponse);
|
||||
|
|
@ -30,30 +31,27 @@ describe('Issuable Time Tracking Report', () => {
|
|||
|
||||
const mountComponent = ({
|
||||
queryHandler = successIssueQueryHandler,
|
||||
mutationHandler,
|
||||
issuableType = 'issue',
|
||||
mountFunction = shallowMount,
|
||||
limitToHours = false,
|
||||
} = {}) => {
|
||||
fakeApollo = createMockApollo([
|
||||
[getIssueTimelogsQuery, queryHandler],
|
||||
[getMrTimelogsQuery, queryHandler],
|
||||
]);
|
||||
wrapper = extendedWrapper(
|
||||
mountFunction(Report, {
|
||||
apolloProvider: createMockApollo([
|
||||
[getIssueTimelogsQuery, queryHandler],
|
||||
[getMrTimelogsQuery, queryHandler],
|
||||
[deleteTimelogMutation, mutationHandler],
|
||||
]),
|
||||
provide: {
|
||||
issuableId: 1,
|
||||
issuableType,
|
||||
},
|
||||
propsData: { limitToHours, issuableId: '1' },
|
||||
apolloProvider: fakeApollo,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
fakeApollo = null;
|
||||
});
|
||||
|
||||
it('should render loading spinner', () => {
|
||||
mountComponent();
|
||||
|
||||
|
|
@ -135,50 +133,27 @@ describe('Issuable Time Tracking Report', () => {
|
|||
});
|
||||
|
||||
describe('when clicking on the delete timelog button', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent({ mountFunction: mount });
|
||||
});
|
||||
|
||||
it('calls `$apollo.mutate` with deleteTimelogMutation mutation and removes the row', async () => {
|
||||
const mutateSpy = jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
|
||||
data: {
|
||||
timelogDelete: {
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mutateSpy = jest.fn().mockResolvedValue(deleteTimelogMutationResponse);
|
||||
mountComponent({ mutationHandler: mutateSpy, mountFunction: mount });
|
||||
await waitForPromises();
|
||||
|
||||
await findDeleteButton().trigger('click');
|
||||
await waitForPromises();
|
||||
|
||||
expect(createAlert).not.toHaveBeenCalled();
|
||||
expect(mutateSpy).toHaveBeenCalledWith({
|
||||
mutation: deleteTimelogMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: timelogToRemoveId,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mutateSpy).toHaveBeenCalledWith({ input: { id: timelogToRemoveId } });
|
||||
});
|
||||
|
||||
it('calls `createAlert` with errorMessage and does not remove the row on promise reject', async () => {
|
||||
const mutateSpy = jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({});
|
||||
|
||||
const mutateSpy = jest.fn().mockRejectedValue({});
|
||||
mountComponent({ mutationHandler: mutateSpy, mountFunction: mount });
|
||||
await waitForPromises();
|
||||
|
||||
await findDeleteButton().trigger('click');
|
||||
await waitForPromises();
|
||||
|
||||
expect(mutateSpy).toHaveBeenCalledWith({
|
||||
mutation: deleteTimelogMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: timelogToRemoveId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mutateSpy).toHaveBeenCalledWith({ input: { id: timelogToRemoveId } });
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
message: 'An error occurred while removing the timelog.',
|
||||
captureError: true,
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ describe('CrmContactToken', () => {
|
|||
Vue.use(VueApollo);
|
||||
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
const getBaseToken = () => wrapper.findComponent(BaseToken);
|
||||
|
||||
|
|
@ -58,9 +57,8 @@ describe('CrmContactToken', () => {
|
|||
listeners = {},
|
||||
queryHandler = searchGroupCrmContactsQueryHandler,
|
||||
} = {}) => {
|
||||
fakeApollo = createMockApollo([[searchCrmContactsQuery, queryHandler]]);
|
||||
|
||||
wrapper = mount(CrmContactToken, {
|
||||
apolloProvider: createMockApollo([[searchCrmContactsQuery, queryHandler]]),
|
||||
propsData: {
|
||||
config,
|
||||
value,
|
||||
|
|
@ -75,14 +73,9 @@ describe('CrmContactToken', () => {
|
|||
},
|
||||
stubs,
|
||||
listeners,
|
||||
apolloProvider: fakeApollo,
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
fakeApollo = null;
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('fetchContacts', () => {
|
||||
describe('for groups', () => {
|
||||
|
|
@ -160,9 +153,7 @@ describe('CrmContactToken', () => {
|
|||
});
|
||||
|
||||
it('calls `createAlert` with alert error message when request fails', async () => {
|
||||
mountComponent();
|
||||
|
||||
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
|
||||
mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
|
||||
|
||||
getBaseToken().vm.$emit('fetch-suggestions');
|
||||
await waitForPromises();
|
||||
|
|
@ -173,12 +164,9 @@ describe('CrmContactToken', () => {
|
|||
});
|
||||
|
||||
it('sets `loading` to false when request completes', async () => {
|
||||
mountComponent();
|
||||
|
||||
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
|
||||
mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
|
||||
|
||||
getBaseToken().vm.$emit('fetch-suggestions');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(getBaseToken().props('suggestionsLoading')).toBe(false);
|
||||
|
|
@ -195,13 +183,7 @@ describe('CrmContactToken', () => {
|
|||
value: { data: '1' },
|
||||
});
|
||||
|
||||
const baseTokenEl = wrapper.findComponent(BaseToken);
|
||||
|
||||
expect(baseTokenEl.exists()).toBe(true);
|
||||
expect(baseTokenEl.props()).toMatchObject({
|
||||
suggestions: mockCrmContacts,
|
||||
getActiveTokenValue: wrapper.vm.getActiveContact,
|
||||
});
|
||||
expect(getBaseToken().props('suggestions')).toEqual(mockCrmContacts);
|
||||
});
|
||||
|
||||
it.each(mockCrmContacts)('renders token item when value is selected', (contact) => {
|
||||
|
|
@ -270,12 +252,9 @@ describe('CrmContactToken', () => {
|
|||
|
||||
it('emits listeners in the base-token', () => {
|
||||
const mockInput = jest.fn();
|
||||
mountComponent({
|
||||
listeners: {
|
||||
input: mockInput,
|
||||
},
|
||||
});
|
||||
wrapper.findComponent(BaseToken).vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
|
||||
mountComponent({ listeners: { input: mockInput } });
|
||||
|
||||
getBaseToken().vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
|
||||
|
||||
expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ describe('CrmOrganizationToken', () => {
|
|||
Vue.use(VueApollo);
|
||||
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
const getBaseToken = () => wrapper.findComponent(BaseToken);
|
||||
|
||||
|
|
@ -58,8 +57,8 @@ describe('CrmOrganizationToken', () => {
|
|||
listeners = {},
|
||||
queryHandler = searchGroupCrmOrganizationsQueryHandler,
|
||||
} = {}) => {
|
||||
fakeApollo = createMockApollo([[searchCrmOrganizationsQuery, queryHandler]]);
|
||||
wrapper = mount(CrmOrganizationToken, {
|
||||
apolloProvider: createMockApollo([[searchCrmOrganizationsQuery, queryHandler]]),
|
||||
propsData: {
|
||||
config,
|
||||
value,
|
||||
|
|
@ -74,14 +73,9 @@ describe('CrmOrganizationToken', () => {
|
|||
},
|
||||
stubs,
|
||||
listeners,
|
||||
apolloProvider: fakeApollo,
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
fakeApollo = null;
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('fetchOrganizations', () => {
|
||||
describe('for groups', () => {
|
||||
|
|
@ -159,9 +153,7 @@ describe('CrmOrganizationToken', () => {
|
|||
});
|
||||
|
||||
it('calls `createAlert` when request fails', async () => {
|
||||
mountComponent();
|
||||
|
||||
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
|
||||
mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
|
||||
|
||||
getBaseToken().vm.$emit('fetch-suggestions');
|
||||
await waitForPromises();
|
||||
|
|
@ -172,9 +164,7 @@ describe('CrmOrganizationToken', () => {
|
|||
});
|
||||
|
||||
it('sets `loading` to false when request completes', async () => {
|
||||
mountComponent();
|
||||
|
||||
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
|
||||
mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
|
||||
|
||||
getBaseToken().vm.$emit('fetch-suggestions');
|
||||
|
||||
|
|
@ -194,13 +184,7 @@ describe('CrmOrganizationToken', () => {
|
|||
value: { data: '1' },
|
||||
});
|
||||
|
||||
const baseTokenEl = wrapper.findComponent(BaseToken);
|
||||
|
||||
expect(baseTokenEl.exists()).toBe(true);
|
||||
expect(baseTokenEl.props()).toMatchObject({
|
||||
suggestions: mockCrmOrganizations,
|
||||
getActiveTokenValue: wrapper.vm.getActiveOrganization,
|
||||
});
|
||||
expect(getBaseToken().props('suggestions')).toEqual(mockCrmOrganizations);
|
||||
});
|
||||
|
||||
it.each(mockCrmOrganizations)('renders token item when value is selected', (organization) => {
|
||||
|
|
@ -269,12 +253,9 @@ describe('CrmOrganizationToken', () => {
|
|||
|
||||
it('emits listeners in the base-token', () => {
|
||||
const mockInput = jest.fn();
|
||||
mountComponent({
|
||||
listeners: {
|
||||
input: mockInput,
|
||||
},
|
||||
});
|
||||
wrapper.findComponent(BaseToken).vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
|
||||
mountComponent({ listeners: { input: mockInput } });
|
||||
|
||||
getBaseToken().vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
|
||||
|
||||
expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -90,14 +90,6 @@ describe('RunnerInstructionsModal component', () => {
|
|||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('should not show alert', () => {
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not show deprecation alert', () => {
|
||||
expect(findAlert('warning').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should contain a number of platforms buttons', () => {
|
||||
expect(runnerPlatformsHandler).toHaveBeenCalledWith({});
|
||||
|
||||
|
|
@ -112,19 +104,8 @@ describe('RunnerInstructionsModal component', () => {
|
|||
expect(architectures).toEqual(mockPlatformList[0].architectures.nodes);
|
||||
});
|
||||
|
||||
describe.each`
|
||||
glFeatures | deprecationAlertExists
|
||||
${{}} | ${false}
|
||||
${{ createRunnerWorkflowForAdmin: true }} | ${true}
|
||||
${{ createRunnerWorkflowForNamespace: true }} | ${true}
|
||||
`('with features $glFeatures', ({ glFeatures, deprecationAlertExists }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ provide: { glFeatures } });
|
||||
});
|
||||
|
||||
it(`alert is ${deprecationAlertExists ? 'shown' : 'not shown'}`, () => {
|
||||
expect(findAlert('warning').exists()).toBe(deprecationAlertExists);
|
||||
});
|
||||
it('alert is shown', () => {
|
||||
expect(findAlert('warning').exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when the modal resizes', () => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { GlSearchBoxByType } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql';
|
||||
import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
|
||||
import { TYPE_MERGE_REQUEST } from '~/issues/constants';
|
||||
|
|
@ -44,20 +44,20 @@ Vue.use(VueApollo);
|
|||
describe('User select dropdown', () => {
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
const hideDropdownMock = jest.fn();
|
||||
|
||||
const findSearchField = () => wrapper.findComponent(GlSearchBoxByType);
|
||||
const findParticipantsLoading = () => wrapper.find('[data-testid="loading-participants"]');
|
||||
const findSelectedParticipants = () => wrapper.findAll('[data-testid="selected-participant"]');
|
||||
const findParticipantsLoading = () => wrapper.findByTestId('loading-participants');
|
||||
const findSelectedParticipants = () => wrapper.findAllByTestId('selected-participant');
|
||||
const findSelectedParticipantByIndex = (index) =>
|
||||
findSelectedParticipants().at(index).findComponent(SidebarParticipant);
|
||||
const findUnselectedParticipants = () =>
|
||||
wrapper.findAll('[data-testid="unselected-participant"]');
|
||||
const findUnselectedParticipants = () => wrapper.findAllByTestId('unselected-participant');
|
||||
const findUnselectedParticipantByIndex = (index) =>
|
||||
findUnselectedParticipants().at(index).findComponent(SidebarParticipant);
|
||||
const findCurrentUser = () => wrapper.findAll('[data-testid="current-user"]');
|
||||
const findIssuableAuthor = () => wrapper.findAll('[data-testid="issuable-author"]');
|
||||
const findUnassignLink = () => wrapper.find('[data-testid="unassign"]');
|
||||
const findEmptySearchResults = () => wrapper.find('[data-testid="empty-results"]');
|
||||
const findCurrentUser = () => wrapper.findAllByTestId('current-user');
|
||||
const findIssuableAuthor = () => wrapper.findAllByTestId('issuable-author');
|
||||
const findUnassignLink = () => wrapper.findByTestId('unassign');
|
||||
const findEmptySearchResults = () => wrapper.findAllByTestId('empty-results');
|
||||
|
||||
const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(projectMembersResponse);
|
||||
const participantsQueryHandlerSuccess = jest.fn().mockResolvedValue(participantsQueryResponse);
|
||||
|
|
@ -72,7 +72,7 @@ describe('User select dropdown', () => {
|
|||
[searchUsersQueryOnMR, jest.fn().mockResolvedValue(searchResponseOnMR)],
|
||||
[getIssueParticipantsQuery, participantsQueryHandler],
|
||||
]);
|
||||
wrapper = shallowMount(UserSelect, {
|
||||
wrapper = shallowMountExtended(UserSelect, {
|
||||
apolloProvider: fakeApollo,
|
||||
propsData: {
|
||||
headerText: 'test',
|
||||
|
|
@ -97,7 +97,7 @@ describe('User select dropdown', () => {
|
|||
</div>
|
||||
`,
|
||||
methods: {
|
||||
hide: jest.fn(),
|
||||
hide: hideDropdownMock,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -106,6 +106,7 @@ describe('User select dropdown', () => {
|
|||
|
||||
afterEach(() => {
|
||||
fakeApollo = null;
|
||||
hideDropdownMock.mockClear();
|
||||
});
|
||||
|
||||
it('renders a loading spinner if participants are loading', () => {
|
||||
|
|
@ -290,12 +291,12 @@ describe('User select dropdown', () => {
|
|||
value: [assignee],
|
||||
},
|
||||
});
|
||||
wrapper.vm.$refs.dropdown.hide = jest.fn();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
findUnassignLink().trigger('click');
|
||||
|
||||
expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalledTimes(1);
|
||||
expect(hideDropdownMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('emits an empty array after unselecting the only selected assignee', async () => {
|
||||
|
|
|
|||
|
|
@ -65,9 +65,7 @@ RSpec.describe Gitlab::MarkdownCache::Redis::Extension, :clean_gitlab_redis_cach
|
|||
Gitlab::Redis::Cache.with do |redis|
|
||||
expect(redis).to receive(:pipelined).and_call_original
|
||||
|
||||
times = Gitlab::Redis::ClusterUtil.cluster?(redis) ? 2 : 1
|
||||
|
||||
expect_next_instances_of(Redis::PipelinedConnection, times) do |pipeline|
|
||||
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
|
||||
expect(pipeline).to receive(:mapped_hmget).once.and_call_original
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -832,6 +832,30 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
|
|||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'when with_readonly_pipeline is used' do
|
||||
it 'calls the default store only' do
|
||||
expect(primary_store).to receive(:send).and_call_original
|
||||
expect(secondary_store).not_to receive(:send).and_call_original
|
||||
|
||||
multi_store.with_readonly_pipeline { subject }
|
||||
end
|
||||
|
||||
context 'when used in a nested manner' do
|
||||
subject(:nested_subject) do
|
||||
multi_store.with_readonly_pipeline do
|
||||
multi_store.with_readonly_pipeline { subject }
|
||||
end
|
||||
end
|
||||
|
||||
it 'raises error' do
|
||||
expect { nested_subject }.to raise_error(Gitlab::Redis::MultiStore::NestedReadonlyPipelineError)
|
||||
expect { nested_subject }.to raise_error { |e|
|
||||
expect(e.message).to eq('Nested use of with_readonly_pipeline is detected.')
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -694,59 +694,5 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
|
|||
|
||||
it { is_expected.to be_disallowed(:create_instance_runner) }
|
||||
end
|
||||
|
||||
context 'create_runner_workflow_for_admin flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_admin: false)
|
||||
end
|
||||
|
||||
context 'admin' do
|
||||
let(:current_user) { admin_user }
|
||||
|
||||
context 'when admin mode is enabled', :enable_admin_mode do
|
||||
it { is_expected.to be_disallowed(:create_instance_runner) }
|
||||
end
|
||||
|
||||
context 'when admin mode is disabled' do
|
||||
it { is_expected.to be_disallowed(:create_instance_runner) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with project_bot' do
|
||||
let(:current_user) { project_bot }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_instance_runner) }
|
||||
end
|
||||
|
||||
context 'with migration_bot' do
|
||||
let(:current_user) { migration_bot }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_instance_runner) }
|
||||
end
|
||||
|
||||
context 'with security_bot' do
|
||||
let(:current_user) { security_bot }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_instance_runner) }
|
||||
end
|
||||
|
||||
context 'with llm_bot' do
|
||||
let(:current_user) { llm_bot }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_instance_runners) }
|
||||
end
|
||||
|
||||
context 'with regular user' do
|
||||
let(:current_user) { user }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_instance_runner) }
|
||||
end
|
||||
|
||||
context 'with anonymous' do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_instance_runner) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1483,155 +1483,81 @@ RSpec.describe GroupPolicy, feature_category: :system_access do
|
|||
end
|
||||
end
|
||||
|
||||
context 'create_runner_workflow_for_namespace flag enabled' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: [group])
|
||||
end
|
||||
context 'admin' do
|
||||
let(:current_user) { admin }
|
||||
|
||||
context 'admin' do
|
||||
let(:current_user) { admin }
|
||||
context 'when admin mode is enabled', :enable_admin_mode do
|
||||
it { is_expected.to be_allowed(:create_runner) }
|
||||
|
||||
context 'with specific group runner registration disabled' do
|
||||
before do
|
||||
group.runner_registration_enabled = false
|
||||
end
|
||||
|
||||
context 'when admin mode is enabled', :enable_admin_mode do
|
||||
it { is_expected.to be_allowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with specific group runner registration disabled' do
|
||||
before do
|
||||
group.runner_registration_enabled = false
|
||||
end
|
||||
context 'with group runner registration disabled' do
|
||||
before do
|
||||
stub_application_setting(valid_runner_registrars: ['project'])
|
||||
group.runner_registration_enabled = runner_registration_enabled
|
||||
end
|
||||
|
||||
context 'with specific group runner registration enabled' do
|
||||
let(:runner_registration_enabled) { true }
|
||||
|
||||
it { is_expected.to be_allowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with group runner registration disabled' do
|
||||
before do
|
||||
stub_application_setting(valid_runner_registrars: ['project'])
|
||||
group.runner_registration_enabled = runner_registration_enabled
|
||||
end
|
||||
context 'with specific group runner registration disabled' do
|
||||
let(:runner_registration_enabled) { false }
|
||||
|
||||
context 'with specific group runner registration enabled' do
|
||||
let(:runner_registration_enabled) { true }
|
||||
|
||||
it { is_expected.to be_allowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with specific group runner registration disabled' do
|
||||
let(:runner_registration_enabled) { false }
|
||||
|
||||
it { is_expected.to be_allowed(:create_runner) }
|
||||
end
|
||||
it { is_expected.to be_allowed(:create_runner) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when admin mode is disabled' do
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with owner' do
|
||||
let(:current_user) { owner }
|
||||
|
||||
it { is_expected.to be_allowed(:create_runner) }
|
||||
|
||||
it_behaves_like 'disallowed when group runner registration disabled'
|
||||
end
|
||||
|
||||
context 'with maintainer' do
|
||||
let(:current_user) { maintainer }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with reporter' do
|
||||
let(:current_user) { reporter }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with guest' do
|
||||
let(:current_user) { guest }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with developer' do
|
||||
let(:current_user) { developer }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with anonymous' do
|
||||
let(:current_user) { nil }
|
||||
|
||||
context 'when admin mode is disabled' do
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with create_runner_workflow_for_namespace flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: [other_group])
|
||||
end
|
||||
context 'with owner' do
|
||||
let(:current_user) { owner }
|
||||
|
||||
let_it_be(:other_group) { create(:group) }
|
||||
it { is_expected.to be_allowed(:create_runner) }
|
||||
|
||||
context 'admin' do
|
||||
let(:current_user) { admin }
|
||||
it_behaves_like 'disallowed when group runner registration disabled'
|
||||
end
|
||||
|
||||
context 'when admin mode is enabled', :enable_admin_mode do
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
context 'with maintainer' do
|
||||
let(:current_user) { maintainer }
|
||||
|
||||
context 'with specific group runner registration disabled' do
|
||||
before do
|
||||
group.runner_registration_enabled = false
|
||||
end
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
context 'with reporter' do
|
||||
let(:current_user) { reporter }
|
||||
|
||||
it_behaves_like 'disallowed when group runner registration disabled'
|
||||
end
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'when admin mode is disabled' do
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
end
|
||||
context 'with guest' do
|
||||
let(:current_user) { guest }
|
||||
|
||||
context 'with owner' do
|
||||
let(:current_user) { owner }
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
context 'with developer' do
|
||||
let(:current_user) { developer }
|
||||
|
||||
it_behaves_like 'disallowed when group runner registration disabled'
|
||||
end
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with maintainer' do
|
||||
let(:current_user) { maintainer }
|
||||
context 'with anonymous' do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with reporter' do
|
||||
let(:current_user) { reporter }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with guest' do
|
||||
let(:current_user) { guest }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with developer' do
|
||||
let(:current_user) { developer }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with anonymous' do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2879,42 +2879,10 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
|
|||
end
|
||||
|
||||
describe 'create_runner' do
|
||||
context 'create_runner_workflow_for_namespace flag enabled' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
|
||||
end
|
||||
|
||||
context 'admin' do
|
||||
let(:current_user) { admin }
|
||||
|
||||
context 'when admin mode is enabled', :enable_admin_mode do
|
||||
it { is_expected.to be_allowed(:create_runner) }
|
||||
|
||||
context 'with project runner registration disabled' do
|
||||
before do
|
||||
stub_application_setting(valid_runner_registrars: ['group'])
|
||||
end
|
||||
|
||||
it { is_expected.to be_allowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with specific project runner registration disabled' do
|
||||
before do
|
||||
project.update!(runner_registration_enabled: false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_allowed(:create_runner) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when admin mode is disabled' do
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with owner' do
|
||||
let(:current_user) { owner }
|
||||
context 'admin' do
|
||||
let(:current_user) { admin }
|
||||
|
||||
context 'when admin mode is enabled', :enable_admin_mode do
|
||||
it { is_expected.to be_allowed(:create_runner) }
|
||||
|
||||
context 'with project runner registration disabled' do
|
||||
|
|
@ -2922,7 +2890,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
|
|||
stub_application_setting(valid_runner_registrars: ['group'])
|
||||
end
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
it { is_expected.to be_allowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with specific project runner registration disabled' do
|
||||
|
|
@ -2930,125 +2898,65 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
|
|||
project.update!(runner_registration_enabled: false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
it { is_expected.to be_allowed(:create_runner) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with maintainer' do
|
||||
let(:current_user) { maintainer }
|
||||
|
||||
it { is_expected.to be_allowed(:create_runner) }
|
||||
context 'when admin mode is disabled' do
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with reporter' do
|
||||
let(:current_user) { reporter }
|
||||
context 'with owner' do
|
||||
let(:current_user) { owner }
|
||||
|
||||
it { is_expected.to be_allowed(:create_runner) }
|
||||
|
||||
context 'with project runner registration disabled' do
|
||||
before do
|
||||
stub_application_setting(valid_runner_registrars: ['group'])
|
||||
end
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with guest' do
|
||||
let(:current_user) { guest }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with developer' do
|
||||
let(:current_user) { developer }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with anonymous' do
|
||||
let(:current_user) { nil }
|
||||
context 'with specific project runner registration disabled' do
|
||||
before do
|
||||
project.update!(runner_registration_enabled: false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'create_runner_workflow_for_namespace flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: [group])
|
||||
end
|
||||
context 'with maintainer' do
|
||||
let(:current_user) { maintainer }
|
||||
|
||||
context 'admin' do
|
||||
let(:current_user) { admin }
|
||||
it { is_expected.to be_allowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'when admin mode is enabled', :enable_admin_mode do
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
context 'with reporter' do
|
||||
let(:current_user) { reporter }
|
||||
|
||||
context 'with project runner registration disabled' do
|
||||
before do
|
||||
stub_application_setting(valid_runner_registrars: ['group'])
|
||||
end
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
context 'with guest' do
|
||||
let(:current_user) { guest }
|
||||
|
||||
context 'with specific project runner registration disabled' do
|
||||
before do
|
||||
project.update!(runner_registration_enabled: false)
|
||||
end
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
end
|
||||
context 'with developer' do
|
||||
let(:current_user) { developer }
|
||||
|
||||
context 'when admin mode is disabled' do
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
end
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with owner' do
|
||||
let(:current_user) { owner }
|
||||
context 'with anonymous' do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
|
||||
context 'with project runner registration disabled' do
|
||||
before do
|
||||
stub_application_setting(valid_runner_registrars: ['group'])
|
||||
end
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with specific project runner registration disabled' do
|
||||
before do
|
||||
project.update!(runner_registration_enabled: false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with maintainer' do
|
||||
let(:current_user) { maintainer }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with reporter' do
|
||||
let(:current_user) { reporter }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with guest' do
|
||||
let(:current_user) { guest }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with developer' do
|
||||
let(:current_user) { developer }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
|
||||
context 'with anonymous' do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
it { is_expected.to be_disallowed(:create_runner) }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -95,18 +95,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
|
|||
end
|
||||
end
|
||||
|
||||
shared_context 'when :create_runner_workflow_for_namespace feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: [other_group])
|
||||
end
|
||||
|
||||
it 'returns an error' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect_graphql_errors_to_include('`create_runner_workflow_for_namespace` feature flag is disabled.')
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'when runner is created successfully' do
|
||||
it do
|
||||
expected_args = { user: current_user, params: anything }
|
||||
|
|
@ -139,18 +127,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
|
|||
context 'when user has permissions', :enable_admin_mode do
|
||||
let(:current_user) { admin }
|
||||
|
||||
context 'when :create_runner_workflow_for_admin feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_admin: false)
|
||||
end
|
||||
|
||||
it 'returns an error' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect_graphql_errors_to_include('`create_runner_workflow_for_admin` feature flag is disabled.')
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'when runner is created successfully'
|
||||
it_behaves_like 'when model is invalid returns error'
|
||||
end
|
||||
|
|
@ -164,17 +140,12 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
|
|||
}
|
||||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: [group])
|
||||
end
|
||||
|
||||
it_behaves_like 'when user does not have permissions'
|
||||
|
||||
context 'when user has permissions' do
|
||||
context 'when user is group owner' do
|
||||
let(:current_user) { group_owner }
|
||||
|
||||
it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
|
||||
it_behaves_like 'when runner is created successfully'
|
||||
it_behaves_like 'when model is invalid returns error'
|
||||
|
||||
|
|
@ -226,7 +197,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
|
|||
context 'when user is admin in admin mode', :enable_admin_mode do
|
||||
let(:current_user) { admin }
|
||||
|
||||
it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
|
||||
it_behaves_like 'when runner is created successfully'
|
||||
it_behaves_like 'when model is invalid returns error'
|
||||
end
|
||||
|
|
@ -249,7 +219,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
|
|||
context 'when user is group owner' do
|
||||
let(:current_user) { group_owner }
|
||||
|
||||
it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
|
||||
it_behaves_like 'when runner is created successfully'
|
||||
it_behaves_like 'when model is invalid returns error'
|
||||
|
||||
|
|
@ -304,7 +273,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
|
|||
context 'when user is admin in admin mode', :enable_admin_mode do
|
||||
let(:current_user) { admin }
|
||||
|
||||
it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
|
||||
it_behaves_like 'when runner is created successfully'
|
||||
it_behaves_like 'when model is invalid returns error'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe ErrorTracking::IssueDetailsService, feature_category: :error_tracking do
|
||||
include_context 'sentry error tracking context'
|
||||
|
||||
subject { described_class.new(project, user, params) }
|
||||
subject(:service) { described_class.new(project, user, params) }
|
||||
|
||||
describe '#execute' do
|
||||
context 'with authorized user' do
|
||||
|
|
@ -41,26 +41,41 @@ RSpec.describe ErrorTracking::IssueDetailsService, feature_category: :error_trac
|
|||
include_examples 'error tracking service http status handling', :issue_details
|
||||
|
||||
context 'with integrated error tracking' do
|
||||
let_it_be(:error) { create(:error_tracking_error, project: project) }
|
||||
|
||||
let(:params) { { issue_id: error.id } }
|
||||
let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
|
||||
let(:params) { { issue_id: issue_id } }
|
||||
|
||||
before do
|
||||
error_tracking_setting.update!(integrated: true)
|
||||
|
||||
allow(service).to receive(:error_repository).and_return(error_repository)
|
||||
end
|
||||
|
||||
it 'returns the error in detailed format' do
|
||||
expect(result[:status]).to eq(:success)
|
||||
expect(result[:issue].to_json).to eq(error.to_sentry_detailed_error.to_json)
|
||||
context 'when error is found' do
|
||||
let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
|
||||
let(:issue_id) { error.fingerprint }
|
||||
|
||||
before do
|
||||
allow(error_repository).to receive(:find_error).with(issue_id).and_return(error)
|
||||
end
|
||||
|
||||
it 'returns the error in detailed format' do
|
||||
expect(result[:status]).to eq(:success)
|
||||
expect(result[:issue]).to eq(error)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when error does not exist' do
|
||||
let(:params) { { issue_id: non_existing_record_id } }
|
||||
let(:issue_id) { non_existing_record_id }
|
||||
|
||||
before do
|
||||
allow(error_repository).to receive(:find_error).with(issue_id)
|
||||
.and_raise(Gitlab::ErrorTracking::ErrorRepository::DatabaseError.new('Error not found'))
|
||||
end
|
||||
|
||||
it 'returns the error in detailed format' do
|
||||
expect(result).to match(
|
||||
status: :error,
|
||||
message: /Couldn't find ErrorTracking::Error/,
|
||||
message: /Error not found/,
|
||||
http_status: :bad_request
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ RSpec.describe ErrorTracking::IssueLatestEventService, feature_category: :error_
|
|||
|
||||
let(:params) { {} }
|
||||
|
||||
subject { described_class.new(project, user, params) }
|
||||
subject(:service) { described_class.new(project, user, params) }
|
||||
|
||||
describe '#execute' do
|
||||
context 'with authorized user' do
|
||||
|
|
@ -29,27 +29,42 @@ RSpec.describe ErrorTracking::IssueLatestEventService, feature_category: :error_
|
|||
include_examples 'error tracking service http status handling', :issue_latest_event
|
||||
|
||||
context 'with integrated error tracking' do
|
||||
let_it_be(:error) { create(:error_tracking_error, project: project) }
|
||||
let_it_be(:event) { create(:error_tracking_error_event, error: error) }
|
||||
|
||||
let(:params) { { issue_id: error.id } }
|
||||
let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
|
||||
let(:params) { { issue_id: issue_id } }
|
||||
|
||||
before do
|
||||
error_tracking_setting.update!(integrated: true)
|
||||
|
||||
allow(service).to receive(:error_repository).and_return(error_repository)
|
||||
end
|
||||
|
||||
it 'returns the latest event in expected format' do
|
||||
expect(result[:status]).to eq(:success)
|
||||
expect(result[:latest_event].to_json).to eq(event.to_sentry_error_event.to_json)
|
||||
context 'when error is found' do
|
||||
let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
|
||||
let(:event) { build_stubbed(:error_tracking_open_api_error_event, fingerprint: error.fingerprint) }
|
||||
let(:issue_id) { error.fingerprint }
|
||||
|
||||
before do
|
||||
allow(error_repository).to receive(:last_event_for).with(issue_id).and_return(event)
|
||||
end
|
||||
|
||||
it 'returns the latest event in expected format' do
|
||||
expect(result[:status]).to eq(:success)
|
||||
expect(result[:latest_event]).to eq(event)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when error does not exist' do
|
||||
let(:params) { { issue_id: non_existing_record_id } }
|
||||
let(:issue_id) { non_existing_record_id }
|
||||
|
||||
before do
|
||||
allow(error_repository).to receive(:last_event_for).with(issue_id)
|
||||
.and_raise(Gitlab::ErrorTracking::ErrorRepository::DatabaseError.new('Error not found'))
|
||||
end
|
||||
|
||||
it 'returns the error in detailed format' do
|
||||
expect(result).to match(
|
||||
status: :error,
|
||||
message: /Couldn't find ErrorTracking::Error/,
|
||||
message: /Error not found/,
|
||||
http_status: :bad_request
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -113,17 +113,45 @@ RSpec.describe ErrorTracking::IssueUpdateService, feature_category: :error_track
|
|||
include_examples 'error tracking service sentry error handling', :update_issue
|
||||
|
||||
context 'with integrated error tracking' do
|
||||
let(:error) { create(:error_tracking_error, project: project) }
|
||||
let(:arguments) { { issue_id: error.id, status: 'resolved' } }
|
||||
let(:update_issue_response) { { updated: true, status: :success, closed_issue_iid: nil } }
|
||||
let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
|
||||
let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
|
||||
let(:issue_id) { error.fingerprint }
|
||||
let(:arguments) { { issue_id: issue_id, status: 'resolved' } }
|
||||
|
||||
before do
|
||||
error_tracking_setting.update!(integrated: true)
|
||||
|
||||
allow(update_service).to receive(:error_repository).and_return(error_repository)
|
||||
allow(error_repository).to receive(:update_error)
|
||||
.with(issue_id, status: 'resolved').and_return(updated)
|
||||
end
|
||||
|
||||
it 'resolves the error and responds with expected format' do
|
||||
expect(update_service.execute).to eq(update_issue_response)
|
||||
expect(error.reload.status).to eq('resolved')
|
||||
context 'when update succeeded' do
|
||||
let(:updated) { true }
|
||||
|
||||
it 'returns success with updated true' do
|
||||
expect(project.error_tracking_setting).to receive(:expire_issues_cache)
|
||||
|
||||
expect(update_service.execute).to eq(
|
||||
status: :success,
|
||||
updated: true,
|
||||
closed_issue_iid: nil
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when update failed' do
|
||||
let(:updated) { false }
|
||||
|
||||
it 'returns success with updated false' do
|
||||
expect(project.error_tracking_setting).to receive(:expire_issues_cache)
|
||||
|
||||
expect(update_service.execute).to eq(
|
||||
status: :success,
|
||||
updated: false,
|
||||
closed_issue_iid: nil
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ RSpec.describe ErrorTracking::ListIssuesService, feature_category: :error_tracki
|
|||
|
||||
let(:params) { {} }
|
||||
|
||||
subject { described_class.new(project, user, params) }
|
||||
subject(:service) { described_class.new(project, user, params) }
|
||||
|
||||
describe '#execute' do
|
||||
context 'Sentry backend' do
|
||||
context 'with Sentry backend' do
|
||||
let(:params) { { search_term: 'something', sort: 'last_seen', cursor: 'some-cursor' } }
|
||||
|
||||
let(:list_sentry_issues_args) do
|
||||
|
|
@ -42,7 +42,7 @@ RSpec.describe ErrorTracking::ListIssuesService, feature_category: :error_tracki
|
|||
expect(result).to eq(status: :success, pagination: {}, issues: issues)
|
||||
end
|
||||
|
||||
it 'returns bad request for an issue_status not on the whitelist' do
|
||||
it 'returns bad request with invalid issue_status' do
|
||||
params[:issue_status] = 'assigned'
|
||||
|
||||
expect(error_tracking_setting).not_to receive(:list_sentry_issues)
|
||||
|
|
@ -65,22 +65,84 @@ RSpec.describe ErrorTracking::ListIssuesService, feature_category: :error_tracki
|
|||
end
|
||||
end
|
||||
|
||||
context 'GitLab backend' do
|
||||
let_it_be(:error1) { create(:error_tracking_error, name: 'foo', project: project) }
|
||||
let_it_be(:error2) { create(:error_tracking_error, name: 'bar', project: project) }
|
||||
context 'with integrated error tracking' do
|
||||
let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
|
||||
let(:errors) { [] }
|
||||
let(:pagination) { Gitlab::ErrorTracking::ErrorRepository::Pagination.new(nil, nil) }
|
||||
let(:opts) { default_opts }
|
||||
|
||||
let(:params) { { limit: '1' } }
|
||||
let(:default_opts) do
|
||||
{
|
||||
filters: { status: described_class::DEFAULT_ISSUE_STATUS },
|
||||
query: nil,
|
||||
sort: described_class::DEFAULT_SORT,
|
||||
limit: described_class::DEFAULT_LIMIT,
|
||||
cursor: nil
|
||||
}
|
||||
end
|
||||
|
||||
let(:params) { {} }
|
||||
|
||||
before do
|
||||
error_tracking_setting.update!(integrated: true)
|
||||
|
||||
allow(service).to receive(:error_repository).and_return(error_repository)
|
||||
end
|
||||
|
||||
it 'returns the error in expected format' do
|
||||
expect(result[:status]).to eq(:success)
|
||||
expect(result[:issues].size).to eq(1)
|
||||
expect(result[:issues].first.to_json).to eq(error2.to_sentry_error.to_json)
|
||||
expect(result[:pagination][:next][:cursor]).to be_present
|
||||
expect(result[:pagination][:previous]).to be_nil
|
||||
context 'when errors are found' do
|
||||
let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
|
||||
let(:errors) { [error] }
|
||||
|
||||
before do
|
||||
allow(error_repository).to receive(:list_errors)
|
||||
.with(**opts)
|
||||
.and_return([errors, pagination])
|
||||
end
|
||||
|
||||
context 'without params' do
|
||||
it 'returns the errors without pagination' do
|
||||
expect(result[:status]).to eq(:success)
|
||||
expect(result[:issues]).to eq(errors)
|
||||
expect(result[:pagination]).to eq({})
|
||||
expect(error_repository).to have_received(:list_errors).with(**opts)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pagination' do
|
||||
context 'with next page' do
|
||||
before do
|
||||
pagination.next = 'next cursor'
|
||||
end
|
||||
|
||||
it 'has next cursor' do
|
||||
expect(result[:pagination]).to eq(next: { cursor: 'next cursor' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'with prev page' do
|
||||
before do
|
||||
pagination.prev = 'prev cursor'
|
||||
end
|
||||
|
||||
it 'has prev cursor' do
|
||||
expect(result[:pagination]).to eq(previous: { cursor: 'prev cursor' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'with next and prev page' do
|
||||
before do
|
||||
pagination.next = 'next cursor'
|
||||
pagination.prev = 'prev cursor'
|
||||
end
|
||||
|
||||
it 'has both cursors' do
|
||||
expect(result[:pagination]).to eq(
|
||||
next: { cursor: 'next cursor' },
|
||||
previous: { cursor: 'prev cursor' }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9123,10 +9123,6 @@
|
|||
- './spec/services/environments/schedule_to_delete_review_apps_service_spec.rb'
|
||||
- './spec/services/environments/stop_service_spec.rb'
|
||||
- './spec/services/error_tracking/base_service_spec.rb'
|
||||
- './spec/services/error_tracking/issue_details_service_spec.rb'
|
||||
- './spec/services/error_tracking/issue_latest_event_service_spec.rb'
|
||||
- './spec/services/error_tracking/issue_update_service_spec.rb'
|
||||
- './spec/services/error_tracking/list_issues_service_spec.rb'
|
||||
- './spec/services/error_tracking/list_projects_service_spec.rb'
|
||||
- './spec/services/event_create_service_spec.rb'
|
||||
- './spec/services/events/destroy_service_spec.rb'
|
||||
|
|
|
|||
|
|
@ -15,66 +15,27 @@ RSpec.describe 'projects/runners/_project_runners.html.haml', feature_category:
|
|||
allow(view).to receive(:reset_registration_token_namespace_project_settings_ci_cd_path).and_return('banana_url')
|
||||
end
|
||||
|
||||
context 'when create_runner_workflow_for_namespace is disabled' do
|
||||
context 'when user can create project runner' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: false)
|
||||
allow(view).to receive(:can?).with(user, :create_runner, project).and_return(true)
|
||||
end
|
||||
|
||||
context 'when project runner registration is allowed' do
|
||||
before do
|
||||
stub_application_setting(valid_runner_registrars: ['project'])
|
||||
allow(view).to receive(:can?).with(user, :register_project_runners, project).and_return(true)
|
||||
end
|
||||
it 'renders the New project runner button' do
|
||||
render 'projects/runners/project_runners', project: project
|
||||
|
||||
it 'enables the Remove project button for a project' do
|
||||
render 'projects/runners/project_runners', project: project
|
||||
|
||||
expect(rendered).to have_selector '#js-install-runner'
|
||||
expect(rendered).not_to have_content 'Please contact an admin to register runners.'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project runner registration is not allowed' do
|
||||
before do
|
||||
stub_application_setting(valid_runner_registrars: ['group'])
|
||||
end
|
||||
|
||||
it 'does not enable the Remove project button for a project' do
|
||||
render 'projects/runners/project_runners', project: project
|
||||
|
||||
expect(rendered).to have_content 'Please contact an admin to register runners.'
|
||||
expect(rendered).not_to have_selector '#js-install-runner'
|
||||
end
|
||||
expect(rendered).to have_link(s_('Runners|New project runner'), href: new_project_runner_path(project))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when create_runner_workflow_for_namespace is enabled' do
|
||||
context 'when user cannot create project runner' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow_for_namespace: project.namespace)
|
||||
allow(view).to receive(:can?).with(user, :create_runner, project).and_return(false)
|
||||
end
|
||||
|
||||
context 'when user can create project runner' do
|
||||
before do
|
||||
allow(view).to receive(:can?).with(user, :create_runner, project).and_return(true)
|
||||
end
|
||||
it 'does not render the New project runner button' do
|
||||
render 'projects/runners/project_runners', project: project
|
||||
|
||||
it 'renders the New project runner button' do
|
||||
render 'projects/runners/project_runners', project: project
|
||||
|
||||
expect(rendered).to have_link(s_('Runners|New project runner'), href: new_project_runner_path(project))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user cannot create project runner' do
|
||||
before do
|
||||
allow(view).to receive(:can?).with(user, :create_runner, project).and_return(false)
|
||||
end
|
||||
|
||||
it 'does not render the New project runner button' do
|
||||
render 'projects/runners/project_runners', project: project
|
||||
|
||||
expect(rendered).not_to have_link(s_('Runners|New project runner'))
|
||||
end
|
||||
expect(rendered).not_to have_link(s_('Runners|New project runner'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue