Commit 88a08249 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 7d19df2d
# Make sure to update all the similar conditions in other CI config files if you modify these conditions
.if-cache-credentials-schedule: &if-cache-credentials-schedule
if: '$CI_REPO_CACHE_CREDENTIALS && $CI_PIPELINE_SOURCE == "schedule"'
# Builds a cached .tar.gz of the master branch with full history and
# uploads it to Google Cloud Storage. This archive is downloaded by a
# script defined by a CI/CD variable named CI_PRE_CLONE_SCRIPT. This has
......@@ -33,8 +37,6 @@ cache-repo:
- tar cf $TAR_FILENAME .
- gzip $TAR_FILENAME
- gsutil cp $TAR_FILENAME.gz gs://gitlab-ci-git-repo-cache/project-$CI_PROJECT_ID/gitlab-master.tar.gz
only:
variables:
- $CI_REPO_CACHE_CREDENTIALS
refs:
- schedules
rules:
- <<: *if-cache-credentials-schedule
when: on_success
# Make sure to update all the similar conditions in other CI config files if you modify these conditions
.if-canonical-gitlab-tag: &if-canonical-gitlab-tag
if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE =~ /^gitlab-org($|\/)/ && $CI_COMMIT_TAG'
cloud-native-image:
extends: .only:variables-canonical-dot-com
image: ruby:2.6-alpine
dependencies: []
stage: post-test
allow_failure: true
variables:
GIT_DEPTH: "1"
when: manual
script:
- install_gitlab_gem
- CNG_PROJECT_PATH="gitlab-org/build/CNG" BUILD_TRIGGER_TOKEN=$CI_JOB_TOKEN ./scripts/trigger-build cng
only:
refs:
- tags
rules:
- <<: *if-canonical-gitlab-tag
when: manual
......@@ -306,9 +306,9 @@ export default {
</script>
<template>
<form name="eks-cluster-configuration-form">
<h2>
<h4>
{{ s__('ClusterIntegration|Enter the details for your Amazon EKS Kubernetes cluster') }}
</h2>
</h4>
<div class="mb-3" v-html="kubernetesIntegrationHelpText"></div>
<div class="form-group">
<label class="label-bold" for="eks-cluster-name">{{
......
......@@ -83,7 +83,7 @@ export default {
</script>
<template>
<form name="service-credentials-form">
<h2>{{ s__('ClusterIntegration|Authenticate with Amazon Web Services') }}</h2>
<h4>{{ s__('ClusterIntegration|Authenticate with Amazon Web Services') }}</h4>
<p>
{{
s__(
......
......@@ -8,7 +8,6 @@ import { polyfillSticky } from '~/lib/utils/sticky';
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import Callout from '~/vue_shared/components/callout.vue';
import Icon from '~/vue_shared/components/icon.vue';
import createStore from '../store';
import EmptyState from './empty_state.vue';
import EnvironmentsBlock from './environments_block.vue';
import ErasedBlock from './erased_block.vue';
......@@ -22,7 +21,6 @@ import { isNewJobLogActive } from '../store/utils';
export default {
name: 'JobPageApp',
store: createStore(),
components: {
CiHeader,
Callout,
......@@ -60,27 +58,15 @@ export default {
required: false,
default: null,
},
endpoint: {
type: String,
required: true,
},
terminalPath: {
type: String,
required: false,
default: null,
},
pagePath: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
logState: {
type: String,
required: true,
},
subscriptionsMoreMinutesUrl: {
type: String,
required: false,
......@@ -161,37 +147,28 @@ export default {
created() {
this.throttled = _.throttle(this.toggleScrollButtons, 100);
this.setJobEndpoint(this.endpoint);
this.setTraceOptions({
logState: this.logState,
pagePath: this.pagePath,
});
this.fetchJob();
this.fetchTrace();
window.addEventListener('resize', this.onResize);
window.addEventListener('scroll', this.updateScroll);
},
mounted() {
this.updateSidebar();
},
destroyed() {
beforeDestroy() {
this.stopPollingTrace();
this.stopPolling();
window.removeEventListener('resize', this.onResize);
window.removeEventListener('scroll', this.updateScroll);
},
methods: {
...mapActions([
'setJobEndpoint',
'setTraceOptions',
'fetchJob',
'fetchJobsForStage',
'hideSidebar',
'showSidebar',
'toggleSidebar',
'fetchTrace',
'scrollBottom',
'scrollTop',
'stopPollingTrace',
'stopPolling',
'toggleScrollButtons',
'toggleScrollAnimation',
]),
......@@ -223,7 +200,7 @@ export default {
<div>
<gl-loading-icon
v-if="isLoading"
:size="2"
size="lg"
class="js-job-loading qa-loading-animation prepend-top-20"
/>
......
import Vue from 'vue';
import JobApp from './components/job_app.vue';
import createStore from './store';
export default () => {
const element = document.getElementById('js-job-vue-app');
const store = createStore();
// Let's start initializing the store (i.e. fetching data) right away
store.dispatch('init', element.dataset);
return new Vue({
el: element,
store,
components: {
JobApp,
},
......
......@@ -14,6 +14,16 @@ import {
scrollUp,
} from '~/lib/utils/scroll_utils';
export const init = ({ dispatch }, { endpoint, logState, pagePath }) => {
dispatch('setJobEndpoint', endpoint);
dispatch('setTraceOptions', {
logState,
pagePath,
});
return Promise.all([dispatch('fetchJob'), dispatch('fetchTrace')]);
};
export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint);
export const setTraceOptions = ({ commit }, options) => commit(types.SET_TRACE_OPTIONS, options);
......@@ -147,7 +157,6 @@ export const toggleScrollisInBottom = ({ commit }, toggle) => {
export const requestTrace = ({ commit }) => commit(types.REQUEST_TRACE);
let traceTimeout;
export const fetchTrace = ({ dispatch, state }) =>
axios
.get(`${state.traceEndpoint}/trace.json`, {
......@@ -157,24 +166,32 @@ export const fetchTrace = ({ dispatch, state }) =>
dispatch('toggleScrollisInBottom', isScrolledToBottom());
dispatch('receiveTraceSuccess', data);
if (!data.complete) {
traceTimeout = setTimeout(() => {
dispatch('fetchTrace');
}, 4000);
} else {
if (data.complete) {
dispatch('stopPollingTrace');
} else if (!state.traceTimeout) {
dispatch('startPollingTrace');
}
})
.catch(() => dispatch('receiveTraceError'));
export const stopPollingTrace = ({ commit }) => {
export const startPollingTrace = ({ dispatch, commit }) => {
const traceTimeout = setTimeout(() => {
commit(types.SET_TRACE_TIMEOUT, 0);
dispatch('fetchTrace');
}, 4000);
commit(types.SET_TRACE_TIMEOUT, traceTimeout);
};
export const stopPollingTrace = ({ state, commit }) => {
clearTimeout(state.traceTimeout);
commit(types.SET_TRACE_TIMEOUT, 0);
commit(types.STOP_POLLING_TRACE);
clearTimeout(traceTimeout);
};
export const receiveTraceSuccess = ({ commit }, log) => commit(types.RECEIVE_TRACE_SUCCESS, log);
export const receiveTraceError = ({ commit }) => {
commit(types.RECEIVE_TRACE_ERROR);
clearTimeout(traceTimeout);
export const receiveTraceError = ({ dispatch }) => {
dispatch('stopPollingTrace');
flash(__('An error occurred while fetching the job log.'));
};
/**
......
......@@ -10,7 +10,6 @@ export const DISABLE_SCROLL_BOTTOM = 'DISABLE_SCROLL_BOTTOM';
export const DISABLE_SCROLL_TOP = 'DISABLE_SCROLL_TOP';
export const ENABLE_SCROLL_BOTTOM = 'ENABLE_SCROLL_BOTTOM';
export const ENABLE_SCROLL_TOP = 'ENABLE_SCROLL_TOP';
// TODO
export const TOGGLE_SCROLL_ANIMATION = 'TOGGLE_SCROLL_ANIMATION';
export const TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_TRACE = 'TOGGLE_IS_SCROLL_IN_BOTTOM';
......@@ -20,6 +19,7 @@ export const RECEIVE_JOB_SUCCESS = 'RECEIVE_JOB_SUCCESS';
export const RECEIVE_JOB_ERROR = 'RECEIVE_JOB_ERROR';
export const REQUEST_TRACE = 'REQUEST_TRACE';
export const SET_TRACE_TIMEOUT = 'SET_TRACE_TIMEOUT';
export const STOP_POLLING_TRACE = 'STOP_POLLING_TRACE';
export const RECEIVE_TRACE_SUCCESS = 'RECEIVE_TRACE_SUCCESS';
export const RECEIVE_TRACE_ERROR = 'RECEIVE_TRACE_ERROR';
......
......@@ -53,17 +53,14 @@ export default {
state.isTraceComplete = log.complete || state.isTraceComplete;
},
/**
* Will remove loading animation
*/
[types.STOP_POLLING_TRACE](state) {
state.isTraceComplete = true;
[types.SET_TRACE_TIMEOUT](state, id) {
state.traceTimeout = id;
},
/**
* Will remove loading animation
*/
[types.RECEIVE_TRACE_ERROR](state) {
[types.STOP_POLLING_TRACE](state) {
state.isTraceComplete = true;
},
......
......@@ -22,6 +22,7 @@ export default () => ({
isTraceComplete: false,
traceSize: 0,
isTraceSizeVisible: false,
traceTimeout: 0,
// used as a query parameter to fetch the trace
traceState: null,
......
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlCard, GlButton, GlLoadingIcon } from '@gitlab/ui';
import Tracking from '~/tracking';
import {
UPDATE_SETTINGS_ERROR_MESSAGE,
UPDATE_SETTINGS_SUCCESS_MESSAGE,
} from '../../shared/constants';
import { mapComputed } from '~/vuex_shared/bindings';
import ExpirationPolicyForm from '../../shared/components/expiration_policy_form.vue';
import ExpirationPolicyFields from '../../shared/components/expiration_policy_fields.vue';
export default {
components: {
ExpirationPolicyForm,
GlCard,
GlButton,
GlLoadingIcon,
ExpirationPolicyFields,
},
mixins: [Tracking.mixin()],
labelsConfig: {
......@@ -22,12 +26,19 @@ export default {
tracking: {
label: 'docker_container_retention_and_expiration_policies',
},
formIsValid: true,
};
},
computed: {
...mapState(['formOptions', 'isLoading']),
...mapGetters({ isEdited: 'getIsEdited' }),
...mapComputed([{ key: 'settings', getter: 'getSettings' }], 'updateSettings'),
isSubmitButtonDisabled() {
return !this.formIsValid || this.isLoading;
},
isCancelButtonDisabled() {
return !this.isEdited || this.isLoading;
},
},
methods: {
...mapActions(['resetSettings', 'saveSettings']),
......@@ -46,12 +57,42 @@ export default {
</script>
<template>
<expiration-policy-form
v-model="settings"
:form-options="formOptions"
:is-loading="isLoading"
:disable-cancel-button="!isEdited"
@submit="submit"
@reset="reset"
/>
<form ref="form-element" @submit.prevent="submit" @reset.prevent="reset">
<gl-card>
<template #header>
{{ s__('ContainerRegistry|Tag expiration policy') }}
</template>
<template #default>
<expiration-policy-fields
v-model="settings"
:form-options="formOptions"
:is-loading="isLoading"
@validated="formIsValid = true"
@invalidated="formIsValid = false"
/>
</template>
<template #footer>
<div class="d-flex justify-content-end">
<gl-button
ref="cancel-button"
type="reset"
class="mr-2 d-block"
:disabled="isCancelButtonDisabled"
>
{{ __('Cancel') }}
</gl-button>
<gl-button
ref="save-button"
type="submit"
:disabled="isSubmitButtonDisabled"
variant="success"
class="d-flex justify-content-center align-items-center js-no-auto-disable"
>
{{ __('Save expiration policy') }}
<gl-loading-icon v-if="isLoading" class="ml-2" />
</gl-button>
</div>
</template>
</gl-card>
</form>
</template>
<script>
import { uniqueId } from 'lodash';
import { GlFormGroup, GlToggle, GlFormSelect, GlFormTextarea } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import { NAME_REGEX_LENGTH } from '../constants';
import { mapComputedToEvent } from '../utils';
export default {
components: {
GlFormGroup,
GlToggle,
GlFormSelect,
GlFormTextarea,
},
props: {
formOptions: {
type: Object,
required: false,
default: () => ({}),
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
value: {
type: Object,
required: false,
default: () => ({}),
},
labelCols: {
type: [Number, String],
required: false,
default: 3,
},
labelAlign: {
type: String,
required: false,
default: 'right',
},
},
nameRegexPlaceholder: '.*',
selectList: [
{
name: 'expiration-policy-interval',
label: s__('ContainerRegistry|Expiration interval:'),
model: 'older_than',
optionKey: 'olderThan',
},
{
name: 'expiration-policy-schedule',
label: s__('ContainerRegistry|Expiration schedule:'),
model: 'cadence',
optionKey: 'cadence',
},
{
name: 'expiration-policy-latest',
label: s__('ContainerRegistry|Number of tags to retain:'),
model: 'keep_n',
optionKey: 'keepN',
},
],
data() {
return {
uniqueId: uniqueId(),
};
},
computed: {
...mapComputedToEvent(['enabled', 'cadence', 'older_than', 'keep_n', 'name_regex'], 'value'),
policyEnabledText() {
return this.enabled ? __('enabled') : __('disabled');
},
toggleDescriptionText() {
return sprintf(
s__('ContainerRegistry|Docker tag expiration policy is %{toggleStatus}'),
{
toggleStatus: `<strong>${this.policyEnabledText}</strong>`,
},
false,
);
},
regexHelpText() {
return sprintf(
s__(
'ContainerRegistry|Wildcards such as %{codeStart}*-stable%{codeEnd} or %{codeStart}production/*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}',
),
{
codeStart: '<code>',
codeEnd: '</code>',
},
false,
);
},
nameRegexState() {
return this.name_regex ? this.name_regex.length <= NAME_REGEX_LENGTH : null;
},
fieldsValidity() {
return this.nameRegexState !== false;
},
isFormElementDisabled() {
return !this.enabled || this.isLoading;
},
},
watch: {
fieldsValidity: {
immediate: true,
handler(valid) {
if (valid) {
this.$emit('validated');
} else {
this.$emit('invalidated');
}
},
},
},
methods: {
idGenerator(id) {
return `${id}_${this.uniqueId}`;
},
updateModel(value, key) {
this[key] = value;
},
},
};
</script>
<template>
<div ref="form-elements" class="lh-2">
<gl-form-group
:id="idGenerator('expiration-policy-toggle-group')"
:label-cols="labelCols"
:label-align="labelAlign"
:label-for="idGenerator('expiration-policy-toggle')"
:label="s__('ContainerRegistry|Expiration policy:')"
>
<div class="d-flex align-items-start">
<gl-toggle
:id="idGenerator('expiration-policy-toggle')"
v-model="enabled"
:disabled="isLoading"
/>
<span class="mb-2 ml-1 lh-2" v-html="toggleDescriptionText"></span>
</div>
</gl-form-group>
<gl-form-group
v-for="select in $options.selectList"
:id="idGenerator(`${select.name}-group`)"
:key="select.name"
:label-cols="labelCols"
:label-align="labelAlign"
:label-for="idGenerator(select.name)"
:label="select.label"
>
<gl-form-select
:id="idGenerator(select.name)"
:value="value[select.model]"
:disabled="isFormElementDisabled"
@input="updateModel($event, select.model)"
>
<option
v-for="option in formOptions[select.optionKey]"
:key="option.key"
:value="option.key"
>
{{ option.label }}
</option>
</gl-form-select>
</gl-form-group>
<gl-form-group
:id="idGenerator('expiration-policy-name-matching-group')"
:label-cols="labelCols"
:label-align="labelAlign"
:label-for="idGenerator('expiration-policy-name-matching')"
:label="
s__('ContainerRegistry|Docker tags with names matching this regex pattern will expire:')
"
:state="nameRegexState"
:invalid-feedback="
s__('ContainerRegistry|The value of this input should be less than 255 characters')
"
>
<gl-form-textarea
:id="idGenerator('expiration-policy-name-matching')"
v-model="name_regex"
:placeholder="$options.nameRegexPlaceholder"
:state="nameRegexState"
:disabled="isFormElementDisabled"
trim
/>
<template #description>
<span ref="regex-description" v-html="regexHelpText"></span>
</template>
</gl-form-group>
</div>
</template>
<script>
import { uniqueId } from 'lodash';
import {
GlFormGroup,
GlToggle,
GlFormSelect,
GlFormTextarea,
GlButton,
GlCard,
GlLoadingIcon,
} from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import { NAME_REGEX_LENGTH } from '../constants';
import { mapComputedToEvent } from '../utils';
export default {
components: {
GlFormGroup,
GlToggle,
GlFormSelect,
GlFormTextarea,
GlButton,
GlCard,
GlLoadingIcon,
},
props: {
formOptions: {
type: Object,
required: false,
default: () => ({}),
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
value: {
type: Object,
required: false,
default: () => ({}),
},
labelCols: {
type: [Number, String],
required: false,
default: 3,
},
labelAlign: {
type: String,
required: false,
default: 'right',
},
disableCancelButton: {
type: Boolean,
required: false,
default: false,
},
},
nameRegexPlaceholder: '.*',
data() {
return {
uniqueId: uniqueId(),
};
},
computed: {
...mapComputedToEvent(['enabled', 'cadence', 'older_than', 'keep_n', 'name_regex'], 'value'),
policyEnabledText() {
return this.enabled ? __('enabled') : __('disabled');
},
toggleDescriptionText() {
return sprintf(
s__('ContainerRegistry|Docker tag expiration policy is %{toggleStatus}'),
{
toggleStatus: `<strong>${this.policyEnabledText}</strong>`,
},
false,
);
},
regexHelpText() {
return sprintf(
s__(
'ContainerRegistry|Wildcards such as %{codeStart}*-stable%{codeEnd} or %{codeStart}production/*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}',
),
{
codeStart: '<code>',
codeEnd: '</code>',
},
false,
);
},
nameRegexState() {
return this.name_regex ? this.name_regex.length <= NAME_REGEX_LENGTH : null;
},
formIsInvalid() {
return this.nameRegexState === false;
},
isFormElementDisabled() {
return !this.enabled || this.isLoading;
},
isSubmitButtonDisabled() {
return this.formIsInvalid || this.isLoading;
},
isCancelButtonDisabled() {
return this.disableCancelButton || this.isLoading;
},
},
methods: {
idGenerator(id) {
return `${id}_${this.uniqueId}`;
},
},
};
</script>
<template>
<form
ref="form-element"
class="lh-2"
@submit.prevent="$emit('submit')"
@reset.prevent="$emit('reset')"
>
<gl-card>
<template #header>
{{ s__('ContainerRegistry|Tag expiration policy') }}
</template>
<template>
<gl-form-group
:id="idGenerator('expiration-policy-toggle-group')"
:label-cols="labelCols"
:label-align="labelAlign"
:label-for="idGenerator('expiration-policy-toggle')"
:label="s__('ContainerRegistry|Expiration policy:')"
>
<div class="d-flex align-items-start">
<gl-toggle
:id="idGenerator('expiration-policy-toggle')"
v-model="enabled"
:disabled="isLoading"
/>
<span class="mb-2 ml-1 lh-2" v-html="toggleDescriptionText"></span>
</div>
</gl-form-group>
<gl-form-group
:id="idGenerator('expiration-policy-interval-group')"
:label-cols="labelCols"
:label-align="labelAlign"
:label-for="idGenerator('expiration-policy-interval')"
:label="s__('ContainerRegistry|Expiration interval:')"
>
<gl-form-select
:id="idGenerator('expiration-policy-interval')"
v-model="older_than"
:disabled="isFormElementDisabled"
>
<option v-for="option in formOptions.olderThan" :key="option.key" :value="option.key">
{{ option.label }}
</option>
</gl-form-select>
</gl-form-group>
<gl-form-group
:id="idGenerator('expiration-policy-schedule-group')"
:label-cols="labelCols"
:label-align="labelAlign"
:label-for="idGenerator('expiration-policy-schedule')"
:label="s__('ContainerRegistry|Expiration schedule:')"
>
<gl-form-select
:id="idGenerator('expiration-policy-schedule')"
v-model="cadence"
:disabled="isFormElementDisabled"
>
<option v-for="option in formOptions.cadence" :key="option.key" :value="option.key">
{{ option.label }}
</option>
</gl-form-select>
</gl-form-group>
<gl-form-group
:id="idGenerator('expiration-policy-latest-group')"
:label-cols="labelCols"
:label-align="labelAlign"
:label-for="idGenerator('expiration-policy-latest')"
:label="s__('ContainerRegistry|Number of tags to retain:')"
>
<gl-form-select
:id="idGenerator('expiration-policy-latest')"
v-model="keep_n"
:disabled="isFormElementDisabled"
>
<option v-for="option in formOptions.keepN" :key="option.key" :value="option.key">
{{ option.label }}
</option>
</gl-form-select>
</gl-form-group>
<gl-form-group
:id="idGenerator('expiration-policy-name-matching-group')"
:label-cols="labelCols"
:label-align="labelAlign"
:label-for="idGenerator('expiration-policy-name-matching')"
:label="
s__('ContainerRegistry|Docker tags with names matching this regex pattern will expire:')
"
:state="nameRegexState"
:invalid-feedback="
s__('ContainerRegistry|The value of this input should be less than 255 characters')
"
>
<gl-form-textarea
:id="idGenerator('expiration-policy-name-matching')"
v-model="name_regex"
:placeholder="$options.nameRegexPlaceholder"
:state="nameRegexState"
:disabled="isFormElementDisabled"
trim
/>
<template #description>
<span ref="regex-description" v-html="regexHelpText"></span>
</template>
</gl-form-group>
</template>
<template #footer>
<div class="d-flex justify-content-end">
<gl-button
ref="cancel-button"
type="reset"
class="mr-2 d-block"
:disabled="isCancelButtonDisabled"
>
{{ __('Cancel') }}
</gl-button>
<gl-button
ref="save-button"
type="submit"
:disabled="isSubmitButtonDisabled"
variant="success"
class="d-flex justify-content-center align-items-center js-no-auto-disable"
>
{{ __('Save expiration policy') }}
<gl-loading-icon v-if="isLoading" class="ml-2" />
</gl-button>
</div>
</template>
</gl-card>
</form>
</template>
......@@ -79,6 +79,12 @@ export default {
required: false,
default: false,
},
// This prop is used as a fallback in case if textarea.elm is undefined
textareaValue: {
type: String,
required: false,
default: '',
},
},
data() {
return {
......@@ -183,7 +189,7 @@ export default {
Can't use `$refs` as the component is technically in the parent component
so we access the VNode & then get the element
*/
const text = this.$slots.textarea[0].elm.value;
const text = this.$slots.textarea[0]?.elm?.value || this.textareaValue;
if (text) {
this.markdownPreviewLoading = true;
......
......@@ -37,6 +37,7 @@ class ApplicationController < ActionController::Base
around_action :set_current_context
around_action :set_locale
around_action :set_session_storage
around_action :set_current_admin
after_action :set_page_title_header, if: :json_request?
after_action :limit_session_time, if: -> { !current_user }
......@@ -473,6 +474,13 @@ class ApplicationController < ActionController::Base
response.headers['Page-Title'] = URI.escape(page_title('GitLab'))
end
def set_current_admin(&block)
return yield unless Feature.enabled?(:user_mode_in_session)
return yield unless current_user
Gitlab::Auth::CurrentUserMode.with_current_admin(current_user, &block)
end
def html_request?
request.format.html?
end
......
......@@ -139,7 +139,6 @@ class RegistrationsController < Devise::RegistrationsController
ensure_correct_params!
return unless Feature.enabled?(:registrations_recaptcha, default_enabled: true) # reCAPTCHA on the UI will still display however
return if experiment_enabled?(:signup_flow) # when the experimental signup flow is enabled for the current user, disable the reCAPTCHA check
return unless show_recaptcha_sign_up?
return unless Gitlab::Recaptcha.load_configurations!
......
......@@ -17,17 +17,6 @@ module ClustersHelper
end
end
def new_cluster_partial(provider: nil)
case provider
when 'aws'
'clusters/clusters/aws/new'
when 'gcp'
'clusters/clusters/gcp/new'
else
'clusters/clusters/cloud_providers/cloud_provider_selector'
end
end
def render_gcp_signup_offer
return if Gitlab::CurrentSettings.current_application_settings.hide_third_party_offers?
return unless show_gcp_signup_offer?
......
......@@ -147,6 +147,20 @@ class Issue < ApplicationRecord
'project_id'
end
def self.simple_sorts
super.merge(
{
'closest_future_date' => -> { order_closest_future_date },
'closest_future_date_asc' => -> { order_closest_future_date },
'due_date' => -> { order_due_date_asc.with_order_id_desc },
'due_date_asc' => -> { order_due_date_asc.with_order_id_desc },
'due_date_desc' => -> { order_due_date_desc.with_order_id_desc },
'relative_position' => -> { order_relative_position_asc.with_order_id_desc },
'relative_position_asc' => -> { order_relative_position_asc.with_order_id_desc }
}
)
end
def self.sort_by_attribute(method, excluded_labels: [])
case method.to_s
when 'closest_future_date', 'closest_future_date_asc' then order_closest_future_date
......
......@@ -110,8 +110,8 @@ class PoolRepository < ApplicationRecord
end
def storage
Storage::HashedProject
.new(self, prefix: Storage::HashedProject::POOL_PATH_PREFIX)
Storage::Hashed
.new(self, prefix: Storage::Hashed::POOL_PATH_PREFIX)
end
end
......
......@@ -2288,7 +2288,7 @@ class Project < ApplicationRecord
def storage
@storage ||=
if hashed_storage?(:repository)
Storage::HashedProject.new(self)
Storage::Hashed.new(self)
else
Storage::LegacyProject.new(self)
end
......
# frozen_string_literal: true
module Storage
class HashedProject
class Hashed
attr_accessor :project
delegate :gitlab_shell, :repository_storage, to: :project
......
......@@ -36,7 +36,7 @@ class FileUploader < GitlabUploader
def self.base_dir(model, store = Store::LOCAL)
decorated_model = model
decorated_model = Storage::HashedProject.new(model) if store == Store::REMOTE
decorated_model = Storage::Hashed.new(model) if store == Store::REMOTE
model_path_segment(decorated_model)
end
......@@ -57,7 +57,7 @@ class FileUploader < GitlabUploader
# Returns a String without a trailing slash
def self.model_path_segment(model)
case model
when Storage::HashedProject then model.disk_path
when Storage::Hashed then model.disk_path
else
model.hashed_storage?(:attachments) ? model.disk_path : model.full_path
end
......
- provider = local_assigns.fetch(:provider)
- is_current_provider = provider == params[:provider]
- logo_path = local_assigns.fetch(:logo_path)
- label = local_assigns.fetch(:label)
- last = local_assigns.fetch(:last, false)
- classes = ['btn btn-light btn-outline flex-fill d-inline-flex flex-column justify-content-center align-items-center', ('mr-3' unless last)]
- classes = ["btn btn-light btn-outline flex-fill d-inline-flex flex-column justify-content-center align-items-center w-50 js-create-#{provider}-cluster-button"]
- conditional_classes = [('mr-3' unless last), ('active' if is_current_provider)]
= link_to clusterable.new_path(provider: provider), class: classes do
= link_to clusterable.new_path(provider: provider), class: classes + conditional_classes do
.svg-content.p-2= image_tag logo_path, alt: label, class: 'gl-w-64 gl-h-64'
%span
= label
- gke_label = s_('ClusterIntegration|Google GKE')
- eks_label = s_('ClusterIntegration|Amazon EKS')
- create_cluster_label = s_('ClusterIntegration|Create cluster on')
.d-flex.flex-column
%h5.mb-3
.d-flex.flex-column.p-3
%h4.mb-3
= create_cluster_label
.d-flex
= render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
......
- breadcrumb_title _('Kubernetes')
- page_title _('Kubernetes Cluster')
- active_tab = local_assigns.fetch(:active_tab, 'create')
- provider = params[:provider]
= javascript_include_tag 'https://apis.google.com/js/api.js'
= render_gcp_signup_offer
......@@ -19,8 +20,12 @@
%span Add existing cluster
.tab-content.gitlab-tab-content
.tab-pane{ id: 'create-cluster-pane', class: active_when(active_tab == 'create'), role: 'tabpanel' }
= render new_cluster_partial(provider: params[:provider])
.tab-pane.p-0{ id: 'create-cluster-pane', class: active_when(active_tab == 'create'), role: 'tabpanel' }
= render 'clusters/clusters/cloud_providers/cloud_provider_selector'
- if ['aws', 'gcp'].include?(provider)
.p-3.border-top
= render "clusters/clusters/#{provider}/new"
.tab-pane{ id: 'add-cluster-pane', class: active_when(active_tab == 'add'), role: 'tabpanel' }
= render 'clusters/clusters/user/header'
......
---
title: Improve message UI on Microsoft Teams notification
merge_request: 23385
author: Takuya Noguchi
type: fixed
---
title: Resolve Design discussion note preview is broken
merge_request: 24288
author:
type: fixed
---
title: Allow to switch between cloud providers in cluster creation screen
merge_request: 23362
author:
type: changed
---
title: Enable recaptcha check on sign up
merge_request: 24274
author:
type: fixed
---
title: Admin mode support in sidekiq jobs
merge_request: 21792
author: Diego Louzán
type: changed
---
title: Optimize issue search when sorting by due date and position
merge_request: 24217
author:
type: performance
# frozen_string_literal: true
class AddSortingIndexToPackages < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :packages_packages, [:project_id, :created_at]
add_concurrent_index :packages_packages, [:project_id, :version]
add_concurrent_index :packages_packages, [:project_id, :package_type]
end
def down
remove_concurrent_index :packages_packages, [:project_id, :created_at]
remove_concurrent_index :packages_packages, [:project_id, :version]
remove_concurrent_index :packages_packages, [:project_id, :package_type]
end
end
# frozen_string_literal: true
class RemoveProjectIdIndexFromPackages < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
remove_concurrent_index :packages_packages, [:project_id]
end
def down
add_concurrent_index :packages_packages, [:project_id]
end
end
......@@ -2977,8 +2977,10 @@ ActiveRecord::Schema.define(version: 2020_01_30_161817) do
t.string "version"
t.integer "package_type", limit: 2, null: false
t.index ["name"], name: "index_packages_packages_on_name_trigram", opclass: :gin_trgm_ops, using: :gin
t.index ["project_id", "created_at"], name: "index_packages_packages_on_project_id_and_created_at"
t.index ["project_id", "name", "version", "package_type"], name: "idx_packages_packages_on_project_id_name_version_package_type"
t.index ["project_id"], name: "index_packages_packages_on_project_id"
t.index ["project_id", "package_type"], name: "index_packages_packages_on_project_id_and_package_type"
t.index ["project_id", "version"], name: "index_packages_packages_on_project_id_and_version"
end
create_table "packages_tags", force: :cascade do |t|
......
......@@ -609,7 +609,7 @@ of removing unused tags. Currently, this is exposed using the API, but in the fu
these controls will be migrated to the GitLab interface.
Project maintainers can
[delete Container Registry tags in bulk](../../api/container_registry.md#delete-repository-tags-in-bulk)
[delete Container Registry tags in bulk](../../api/container_registry.md#delete-registry-repository-tags-in-bulk)
periodically based on their own criteria, however, this alone does not recycle data,
it only unlinks tags from manifests and image blobs. To recycle the Container
Registry data in the whole GitLab instance, you can use the built-in command
......
......@@ -129,7 +129,7 @@ DELETE /projects/:id/registry/repositories/:repository_id
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2"
```
## List repository tags
## List registry repository tags
### Within a project
......@@ -165,7 +165,7 @@ Example response:
]
```
## Get details of a repository tag
## Get details of a registry repository tag
Get details of a registry repository tag.
......@@ -198,7 +198,7 @@ Example response:
}
```
## Delete a repository tag
## Delete a registry repository tag
Delete a registry repository tag.
......@@ -219,9 +219,9 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
This action does not delete blobs. In order to delete them and recycle disk space,
[run the garbage collection](https://docs.gitlab.com/omnibus/maintenance/README.html#removing-unused-layers-not-referenced-by-manifests).
## Delete repository tags in bulk
## Delete registry repository tags in bulk
Delete repository tags in bulk based on given criteria.
Delete registry repository tags in bulk based on given criteria.
```
DELETE /projects/:id/registry/repositories/:repository_id/tags
......@@ -231,7 +231,7 @@ DELETE /projects/:id/registry/repositories/:repository_id/tags
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `repository_id` | integer | yes | The ID of registry repository. |
| `name_regex` | string | yes | The regex of the name to delete. To delete all tags specify `.*`. |
| `name_regex` | string | yes | The [re2](https://github.com/google/re2/wiki/Syntax) regex of the name to delete. To delete all tags specify `.*`.|
| `keep_n` | integer | no | The amount of latest tags of given name to keep. |
| `older_than` | string | no | Tags to delete that are older than the given time, written in human readable form `1h`, `1d`, `1month`. |
......
......@@ -18,6 +18,11 @@ RuboCop (and other checks) offenses on every modified file.
This saves you time as you don't have to wait for the same errors to be detected
by the CI.
Overcommit relies on a pre-commit hook to prevent commits that violate its ruleset.
If you wish to override this behavior, it can be done by passing the ENV variable
`OVERCOMMIT_DISABLE`; i.e. `OVERCOMMIT_DISABLE=1 git rebase master` to rebase while
disabling the Git hook.
## Ruby, Rails, RSpec
Our codebase style is defined and enforced by [RuboCop](https://github.com/rubocop-hq/rubocop).
......
......@@ -4,6 +4,8 @@ type: howto
# Installing GitLab HA on Amazon Web Services (AWS)
DANGER: **Danger:** This guide is under review and the steps below will be revised and updated in due time. For more detail, please see [this epic](https://gitlab.com/groups/gitlab-org/-/epics/912).
This page offers a walkthrough of a common HA (Highly Available) configuration
for GitLab on AWS. You should customize it to accommodate your needs.
......
......@@ -48,8 +48,6 @@ However, DAST can be [configured](#full-scan)
to also perform a so-called "active scan". That is, attack your application and produce a more extensive security report.
It can be very useful combined with [Review Apps](../../../ci/review_apps/index.md).
The [`dast`](https://gitlab.com/gitlab-org/security-products/dast/container_registry) Docker image in GitLab container registry is updated on a weekly basis to have all [`owasp2docker-weekly`](https://hub.docker.com/r/owasp/zap2docker-weekly/) updates in it.
## Use cases
It helps you automatically find security vulnerabilities in your running web
......
......@@ -240,7 +240,8 @@ build:
- target/
spotbugs-sast:
dependencies: build
dependencies:
- build
script:
- /analyzer run -compile=false
variables:
......
......@@ -25,11 +25,78 @@ You should then be able to see the **Packages** section on the left sidebar.
Next, you must configure your project to authorize with the GitLab Maven
repository.
## Authenticating to the GitLab Maven Repository
## Getting Started
If a project is private or you want to upload Maven artifacts to GitLab,
credentials will need to be provided for authorization. Support is available for
[personal access tokens](#authenticating-with-a-personal-access-token) and
This section will cover installing Maven and building a package. This is a
quickstart to help if you're new to building Maven packages. If you're already
using Maven and understand how to build your own packages, move onto the
[next section](#adding-the-gitlab-package-registry-as-a-maven-remote).
### Installing Maven
Follow the instructions at [maven.apache.org](https://maven.apache.org/install.html)
to download and install Maven for your local development environment. Once
installation is complete, verify you can use Maven in your terminal by running:
```shell
mvn --version
```
You should see something similar to the below printed in the output:
```shell
Apache Maven 3.6.1 (d66c9c0b3152b2e69ee9bac180bb8fcc8e6af555; 2019-04-04T20:00:29+01:00)
Maven home: /Users/<your_user>/apache-maven-3.6.1
Java version: 12.0.2, vendor: Oracle Corporation, runtime: /Library/Java/JavaVirtualMachines/jdk-12.0.2.jdk/Contents/Home
Default locale: en_GB, platform encoding: UTF-8
OS name: "mac os x", version: "10.15.2", arch: "x86_64", family: "mac"
```
### Creating a project
Understanding how to create a full Java project is outside the scope of this
guide but you can follow the steps below to create a new project that can be
published to the GitLab Package Registry.
Start by opening your terminal and creating a directory where you would like to
store the project in your environment. From inside the directory, you can run
the following Maven command to initalize a new package:
```shell
mvn archetype:generate -DgroupId=com.mycompany.mydepartment -DartifactId=my-project -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
```
The arguments are as follows:
- `DgroupId`: A unique string that identifies your package. You should follow
the [Maven naming conventions](https://maven.apache.org/guides/mini/guide-naming-conventions.html).
- `DartifactId`: The name of the JAR, appended to the end of the `DgroupId`.
- `DarchetypeArtifactId`: The archetype used to create the intial structure of
the project.
- `DinteractiveMode`: Create the project using batch mode (optional).
After running the command, you should see the following message, indicating that
your project has been set up successfully:
```shell
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.429 s
[INFO] Finished at: 2020-01-28T11:47:04Z
[INFO] ------------------------------------------------------------------------
```
You should see a new directory where you ran this command matching your
`DartifactId` parameter (in this case it should be `my-project`).
## Adding the GitLab Package Registry as a Maven remote
The next step is to add the GitLab Package Registry as a Maven remote. If a
project is private or you want to upload Maven artifacts to GitLab,
credentials will need to be provided for authorization too. Support is available
for [personal access tokens](#authenticating-with-a-personal-access-token) and
[CI job tokens](#authenticating-with-a-ci-job-token) only.
[Deploy tokens](../../project/deploy_tokens/index.md) and regular username/password
credentials do not work.
......@@ -92,7 +159,9 @@ You can read more on
## Configuring your project to use the GitLab Maven repository URL
To download and upload packages from GitLab, you need a `repository` and
`distributionManagement` section in your `pom.xml` file.
`distributionManagement` section in your `pom.xml` file. If you're following the
steps from above, then you'll need to add the following information to your
`my-project/pom.xml` file.
Depending on your workflow and the amount of Maven packages you have, there are
3 ways you can configure your project to use the GitLab endpoint for Maven packages:
......@@ -133,7 +202,7 @@ would look like:
```
The `id` must be the same with what you
[defined in `settings.xml`](#authenticating-to-the-gitlab-maven-repository).
[defined in `settings.xml`](#adding-the-gitlab-package-registry-as-a-maven-remote).
Replace `PROJECT_ID` with your project ID which can be found on the home page
of your project.
......@@ -186,7 +255,7 @@ the `distributionManagement` section:
```
The `id` must be the same with what you
[defined in `settings.xml`](#authenticating-to-the-gitlab-maven-repository).
[defined in `settings.xml`](#adding-the-gitlab-package-registry-as-a-maven-remote).
Replace `my-group` with your group name and `PROJECT_ID` with your project ID
which can be found on the home page of your project.
......@@ -241,7 +310,7 @@ the `distributionManagement` section:
```
The `id` must be the same with what you
[defined in `settings.xml`](#authenticating-to-the-gitlab-maven-repository).
[defined in `settings.xml`](#adding-the-gitlab-package-registry-as-a-maven-remote).
Replace `PROJECT_ID` with your project ID which can be found on the home page
of your project.
......@@ -257,17 +326,85 @@ project's ID can be used for uploading.
## Uploading packages
Once you have set up the [authentication](#authenticating-to-the-gitlab-maven-repository)
and [configuration](#configuring-your-project-to-use-the-gitlab-maven-repository-url),
Once you have set up the [remote and authentication](#adding-the-gitlab-package-registry-as-a-maven-remote)
and [configured your project](#configuring-your-project-to-use-the-gitlab-maven-repository-url),
test to upload a Maven artifact from a project of yours:
```shell
mvn deploy
```
If the deploy is successful, you should see the build success message again:
```shell
...
[INFO] BUILD SUCCESS
...
```
You should also see that the upload was uploaded to the correct registry:
```shell
Uploading to gitlab-maven: https://gitlab.com/api/v4/projects/PROJECT_ID/packages/maven/com/mycompany/mydepartment/my-project/1.0-SNAPSHOT/my-project-1.0-20200128.120857-1.jar
```
You can then navigate to your project's **Packages** page and see the uploaded
artifacts or even delete them.
## Installing a package
Installing a package from the GitLab Package Registry requires that you set up
the [remote and authentication](#adding-the-gitlab-package-registry-as-a-maven-remote)
as above. Once this is completed, there are two ways for installaing a package.
### Install with `mvn install`
Add the dependency manually to your project `pom.xml` file. To add the example
created above, the XML would look like:
```xml
<dependency>
<groupId>com.mycompany.mydepartment</groupId>
<artifactId>my-project</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
```
Then, inside your project, run the following:
```shell
mvn install
```
Provided everything is set up correctly, you should see the dependency
downloaded from the GitLab Package Registry:
```shell
Downloading from gitlab-maven: http://gitlab.com/api/v4/projects/PROJECT_ID/packages/maven/com/mycompany/mydepartment/my-project/1.0-SNAPSHOT/my-project-1.0-20200128.120857-1.pom
```
### Install with `mvn dependency:get`
The second way to install packages is to use the Maven commands directly.
Inside your project directory, run:
```shell
mvn dependency:get -Dartifact=com.nickkipling.app:nick-test-app:1.1-SNAPSHOT
```
You should see the same downloading message confirming that the project was
retrieved from the GitLab Package Registry.
TIP: **Tip:**
Both the XML block and Maven command are readily copy and pastable from the
Package details page, allowing for quick and easy installation.
## Removing a package
In the packages view of your project page, you can delete packages by clicking
the red trash icons or by clicking the **Delete** button on the package details
page.
## Creating Maven packages with GitLab CI/CD
Once you have your repository configured to use the GitLab Maven Repository,
......
......@@ -10,12 +10,54 @@ module Gitlab
class CurrentUserMode
NotRequestedError = Class.new(StandardError)
# RequestStore entries
CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY = { res: :current_user_mode, data: :bypass_session_admin_id }.freeze
CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY = { res: :current_user_mode, data: :current_admin }.freeze
# SessionStore entries
SESSION_STORE_KEY = :current_user_mode
ADMIN_MODE_START_TIME_KEY = 'admin_mode'
ADMIN_MODE_REQUESTED_TIME_KEY = 'admin_mode_requested'
ADMIN_MODE_START_TIME_KEY = :admin_mode
ADMIN_MODE_REQUESTED_TIME_KEY = :admin_mode_requested
MAX_ADMIN_MODE_TIME = 6.hours
ADMIN_MODE_REQUESTED_GRACE_PERIOD = 5.minutes
class << self
# Admin mode activation requires storing a flag in the user session. Using this
# method when scheduling jobs in Sidekiq will bypass the session check for a
# user that was already in admin mode
def bypass_session!(admin_id)
Gitlab::SafeRequestStore[CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY] = admin_id
Gitlab::AppLogger.debug("Bypassing session in admin mode for: #{admin_id}")
yield
ensure
Gitlab::SafeRequestStore.delete(CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY)
end
def bypass_session_admin_id
Gitlab::SafeRequestStore[CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY]
end
# Store in the current request the provided user model (only if in admin mode)
# and yield
def with_current_admin(admin)
return yield unless self.new(admin).admin_mode?
Gitlab::SafeRequestStore[CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY] = admin
Gitlab::AppLogger.debug("Admin mode active for: #{admin.username}")
yield
ensure
Gitlab::SafeRequestStore.delete(CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY)
end
def current_admin
Gitlab::SafeRequestStore[CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY]
end
end
def initialize(user)
@user = user
end
......@@ -42,7 +84,7 @@ module Gitlab
raise NotRequestedError unless admin_mode_requested?
reset_request_store
reset_request_store_cache_entries
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
current_session_data[ADMIN_MODE_START_TIME_KEY] = Time.now
......@@ -55,7 +97,7 @@ module Gitlab
def disable_admin_mode!
return unless user&.admin?
reset_request_store
reset_request_store_cache_entries
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
current_session_data[ADMIN_MODE_START_TIME_KEY] = nil
......@@ -64,7 +106,7 @@ module Gitlab
def request_admin_mode!
return unless user&.admin?
reset_request_store
reset_request_store_cache_entries
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = Time.now
end
......@@ -73,10 +115,12 @@ module Gitlab
attr_reader :user
# RequestStore entry to cache #admin_mode? result
def admin_mode_rs_key
@admin_mode_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode? }
end
# RequestStore entry to cache #admin_mode_requested? result
def admin_mode_requested_rs_key
@admin_mode_requested_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode_requested? }
end
......@@ -86,6 +130,7 @@ module Gitlab
end
def any_session_with_admin_mode?
return true if bypass_session?
return true if current_session_data.initiated? && current_session_data[ADMIN_MODE_START_TIME_KEY].to_i > MAX_ADMIN_MODE_TIME.ago.to_i
all_sessions.any? do |session|
......@@ -103,7 +148,11 @@ module Gitlab
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY].to_i > ADMIN_MODE_REQUESTED_GRACE_PERIOD.ago.to_i
end
def reset_request_store
def bypass_session?
user&.id && user.id == self.class.bypass_session_admin_id
end
def reset_request_store_cache_entries
Gitlab::SafeRequestStore.delete(admin_mode_rs_key)
Gitlab::SafeRequestStore.delete(admin_mode_requested_rs_key)
end
......
......@@ -11,7 +11,7 @@ module Gitlab
module Storage
# Class that returns the disk path for a project using hashed storage
class HashedProject
class Hashed
attr_accessor :project
ROOT_PATH_PREFIX = '@hashed'
......@@ -121,7 +121,7 @@ module Gitlab
def storage
@storage ||=
if hashed_storage?
Storage::HashedProject.new(self)
Storage::Hashed.new(self)
else
Storage::LegacyProject.new(self)
end
......
......@@ -46,7 +46,7 @@ module Gitlab
module Storage
# Class that returns the disk path for a project using hashed storage
class HashedProject
class Hashed
attr_accessor :project
ROOT_PATH_PREFIX = '@hashed'
......@@ -176,7 +176,7 @@ module Gitlab
def storage
@storage ||=
if hashed_storage?
Storage::HashedProject.new(self)
Storage::Hashed.new(self)
else
Storage::LegacyProject.new(self)
end
......
......@@ -17,6 +17,7 @@ module Gitlab
chain.add Gitlab::SidekiqMiddleware::BatchLoader
chain.add Labkit::Middleware::Sidekiq::Server
chain.add Gitlab::SidekiqMiddleware::InstrumentationLogger
chain.add Gitlab::SidekiqMiddleware::AdminMode::Server
chain.add Gitlab::SidekiqStatus::ServerMiddleware
chain.add Gitlab::SidekiqMiddleware::WorkerContext::Server
end
......@@ -31,6 +32,7 @@ module Gitlab
chain.add Gitlab::SidekiqMiddleware::ClientMetrics
chain.add Gitlab::SidekiqMiddleware::WorkerContext::Client # needs to be before the Labkit middleware
chain.add Labkit::Middleware::Sidekiq::Client
chain.add Gitlab::SidekiqMiddleware::AdminMode::Client
end
end
end
......
# frozen_string_literal: true
module Gitlab
module SidekiqMiddleware
module AdminMode
# Checks if admin mode is enabled for the request creating the sidekiq job
# by examining if admin mode has been enabled for the user
# If enabled then it injects a job field that persists through the job execution
class Client
def call(_worker_class, job, _queue, _redis_pool)
return yield unless Feature.enabled?(:user_mode_in_session)
# Admin mode enabled in the original request or in a nested sidekiq job
admin_mode_user_id = find_admin_user_id
if admin_mode_user_id
job['admin_mode_user_id'] ||= admin_mode_user_id
Gitlab::AppLogger.debug("AdminMode::Client injected admin mode for job: #{job.inspect}")
end
yield
end
private
def find_admin_user_id
Gitlab::Auth::CurrentUserMode.current_admin&.id ||
Gitlab::Auth::CurrentUserMode.bypass_session_admin_id
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module SidekiqMiddleware
module AdminMode
class Server
def call(_worker, job, _queue)
return yield unless Feature.enabled?(:user_mode_in_session)
admin_mode_user_id = job['admin_mode_user_id']
# Do not bypass session if this job was not enabled with admin mode on
return yield unless admin_mode_user_id
Gitlab::Auth::CurrentUserMode.bypass_session!(admin_mode_user_id) do
Gitlab::AppLogger.debug("AdminMode::Server bypasses session for admin mode in job: #{job.inspect}")
yield
end
end
end
end
end
end
......@@ -36,10 +36,7 @@ module MicrosoftTeams
attachments = options[:attachments]
unless attachments.blank?
result['sections'] << {
'title' => 'Details',
'facts' => [{ 'name' => 'Attachments', 'value' => attachments }]
}
result['sections'] << { text: attachments }
end
result.to_json
......
......@@ -15,7 +15,7 @@ module QA
disable_optional_jobs(project)
end
describe 'Auto DevOps support', :orchestrated, :kubernetes, quarantine: 'https://gitlab.com/gitlab-org/gitlab/issues/118481' do
describe 'Auto DevOps support', :orchestrated, :kubernetes do
context 'when rbac is enabled' do
let(:cluster) { Service::KubernetesCluster.new.create! }
......
......@@ -91,7 +91,9 @@ module Trigger
'TOP_UPSTREAM_SOURCE_PROJECT' => ENV['CI_PROJECT_PATH'],
'TOP_UPSTREAM_SOURCE_JOB' => ENV['CI_JOB_URL'],
'TOP_UPSTREAM_SOURCE_SHA' => ENV['CI_COMMIT_SHA'],
'TOP_UPSTREAM_SOURCE_REF' => ENV['CI_COMMIT_REF_NAME']
'TOP_UPSTREAM_SOURCE_REF' => ENV['CI_COMMIT_REF_NAME'],
'TOP_UPSTREAM_MERGE_REQUEST_PROJECT_ID' => ENV['CI_MERGE_REQUEST_PROJECT_ID'],
'TOP_UPSTREAM_MERGE_REQUEST_IID' => ENV['CI_MERGE_REQUEST_IID']
}
end
......
# frozen_string_literal: true
require 'spec_helper'
# Test an operation that triggers background jobs requiring administrative rights
describe 'Admin mode for workers', :do_not_mock_admin_mode, :request_store, :clean_gitlab_redis_shared_state do
let(:user) { create(:user) }
let(:user_to_delete) { create(:user) }
before do
add_sidekiq_middleware
sign_in(user)
end
context 'as a regular user' do
it 'cannot delete user' do
visit admin_user_path(user_to_delete)
expect(page).to have_gitlab_http_status(:not_found)
end
end
context 'as an admin user' do
let(:user) { create(:admin) }
context 'when admin mode disabled' do
it 'cannot delete user', :js do
visit admin_user_path(user_to_delete)
expect(page).to have_content('Re-authentication required')
end
end
context 'when admin mode enabled', :delete do
before do
gitlab_enable_admin_mode_sign_in(user)
end
it 'can delete user', :js do
visit admin_user_path(user_to_delete)
click_button 'Delete user'
page.within '.modal-dialog' do
find("input[name='username']").send_keys(user_to_delete.name)
click_button 'Delete user'
wait_for_requests
end
expect(page).to have_content('The user is being deleted.')
# Perform jobs while logged out so that admin mode is only enabled in job metadata
execute_jobs_signed_out(user)
visit admin_user_path(user_to_delete)
expect(page).to have_title('Not Found')
end
end
end
def add_sidekiq_middleware
Sidekiq::Testing.server_middleware do |chain|
chain.add Gitlab::SidekiqMiddleware::AdminMode::Server
end
end
def execute_jobs_signed_out(user)
gitlab_sign_out
Sidekiq::Worker.drain_all
sign_in(user)
gitlab_enable_admin_mode_sign_in(user)
end
end
......@@ -2,46 +2,64 @@
require 'spec_helper'
describe 'Admin uses repository checks' do
describe 'Admin uses repository checks', :request_store, :clean_gitlab_redis_shared_state, :do_not_mock_admin_mode do
include StubENV
let(:admin) { create(:admin) }
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
sign_in(create(:admin))
sign_in(admin)
end
it 'to trigger a single check' do
project = create(:project)
visit_admin_project_page(project)
context 'when admin mode is disabled' do
it 'admin project page requires admin mode' do
project = create(:project)
visit_admin_project_page(project)
page.within('.repository-check') do
click_button 'Trigger repository check'
expect(page).not_to have_css('.repository-check')
expect(page).to have_content('Enter Admin Mode')
end
expect(page).to have_content('Repository check was triggered')
end
it 'to see a single failed repository check', :js do
project = create(:project)
project.update_columns(
last_repository_check_failed: true,
last_repository_check_at: Time.now
)
visit_admin_project_page(project)
context 'when admin mode is enabled' do
before do
gitlab_enable_admin_mode_sign_in(admin)
end
it 'to trigger a single check', :js do
project = create(:project)
visit_admin_project_page(project)
page.within('.repository-check') do
click_button 'Trigger repository check'
end
page.within('.alert') do
expect(page.text).to match(/Last repository check \(just now\) failed/)
expect(page).to have_content('Repository check was triggered')
end
end
it 'to clear all repository checks', :js do
visit repository_admin_application_settings_path
it 'to see a single failed repository check', :js do
project = create(:project)
project.update_columns(
last_repository_check_failed: true,
last_repository_check_at: Time.now
)
visit_admin_project_page(project)
page.within('.alert') do
expect(page.text).to match(/Last repository check \(just now\) failed/)
end
end
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
it 'to clear all repository checks', :js do
visit repository_admin_application_settings_path
accept_confirm { find(:link, 'Clear all repository checks').send_keys(:return) }
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
expect(page).to have_content('Started asynchronous removal of all repository check states.')
accept_confirm { find(:link, 'Clear all repository checks').send_keys(:return) }
expect(page).to have_content('Started asynchronous removal of all repository check states.')
end
end
def visit_admin_project_page(project)
......
......@@ -30,6 +30,10 @@ describe 'AWS EKS Cluster', :js do
it 'user sees a form to create an EKS cluster' do
expect(page).to have_content('Create new cluster on EKS')
end
it 'highlights Amazon EKS logo' do
expect(page).to have_css('.js-create-aws-cluster-button.active')
end
end
end
end
......@@ -38,6 +38,10 @@ describe 'Gcp Cluster', :js, :do_not_mock_admin_mode do
click_link 'Google GKE'
end
it 'highlights Google GKE logo' do
expect(page).to have_css('.js-create-gcp-cluster-button.active')
end
context 'when user filled form with valid parameters' do
subject { submit_form }
......
......@@ -360,7 +360,7 @@ shared_examples 'Signup' do
InvisibleCaptcha.timestamp_enabled = true
stub_application_setting(recaptcha_enabled: true)
allow_next_instance_of(RegistrationsController) do |instance|
allow(instance).to receive(:verify_recaptcha).and_return(false)
allow(instance).to receive(:verify_recaptcha).and_return(true)
end
end
......@@ -368,28 +368,53 @@ shared_examples 'Signup' do
InvisibleCaptcha.timestamp_enabled = false
end
it 'prevents from signing up' do
visit new_user_registration_path
context 'when reCAPTCHA detects malicious behaviour' do
before do
allow_next_instance_of(RegistrationsController) do |instance|
allow(instance).to receive(:verify_recaptcha).and_return(false)
end
end
fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: new_user.email
it 'prevents from signing up' do
visit new_user_registration_path
if Gitlab::Experimentation.enabled?(:signup_flow)
fill_in 'new_user_first_name', with: new_user.first_name
fill_in 'new_user_last_name', with: new_user.last_name
else
fill_in 'new_user_name', with: new_user.name
fill_in 'new_user_email_confirmation', with: new_user.email
fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: new_user.email
if Gitlab::Experimentation.enabled?(:signup_flow)
fill_in 'new_user_first_name', with: new_user.first_name
fill_in 'new_user_last_name', with: new_user.last_name
else
fill_in 'new_user_name', with: new_user.name
fill_in 'new_user_email_confirmation', with: new_user.email
end
fill_in 'new_user_password', with: new_user.password
expect { click_button 'Register' }.not_to change { User.count }
expect(page).to have_content('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
end
end
fill_in 'new_user_password', with: new_user.password
context 'when invisible captcha detects malicious behaviour' do
it 'prevents from signing up' do
visit new_user_registration_path
expect { click_button 'Register' }.not_to change { User.count }
fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: new_user.email
if Gitlab::Experimentation.enabled?(:signup_flow)
if Gitlab::Experimentation.enabled?(:signup_flow)
fill_in 'new_user_first_name', with: new_user.first_name
fill_in 'new_user_last_name', with: new_user.last_name
else
fill_in 'new_user_name', with: new_user.name
fill_in 'new_user_email_confirmation', with: new_user.email
end
fill_in 'new_user_password', with: new_user.password
expect { click_button 'Register' }.not_to change { User.count }
expect(page).to have_content('That was a bit too quick! Please resubmit.')
else
expect(page).to have_content('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
end
end
end
......
......@@ -157,17 +157,21 @@ describe('Jobs Store Mutations', () => {
});
});
describe('STOP_POLLING_TRACE', () => {
it('sets isTraceComplete to true', () => {
mutations[types.STOP_POLLING_TRACE](stateCopy);
describe('SET_TRACE_TIMEOUT', () => {
it('sets the traceTimeout id', () => {
const id = 7;
expect(stateCopy.isTraceComplete).toEqual(true);
expect(stateCopy.traceTimeout).not.toEqual(id);
mutations[types.SET_TRACE_TIMEOUT](stateCopy, id);
expect(stateCopy.traceTimeout).toEqual(id);
});
});
describe('RECEIVE_TRACE_ERROR', () => {
it('resets trace state and sets error to true', () => {
mutations[types.RECEIVE_TRACE_ERROR](stateCopy);
describe('STOP_POLLING_TRACE', () => {
it('sets isTraceComplete to true', () => {
mutations[types.STOP_POLLING_TRACE](stateCopy);
expect(stateCopy.isTraceComplete).toEqual(true);
});
......
import { shallowMount } from '@vue/test-utils';
import Tracking from '~/tracking';
import component from '~/registry/settings/components/settings_form.vue';
import expirationPolicyForm from '~/registry/shared/components/expiration_policy_form.vue';
import expirationPolicyFields from '~/registry/shared/components/expiration_policy_fields.vue';
import { createStore } from '~/registry/settings/store/';
import {
UPDATE_SETTINGS_ERROR_MESSAGE,
......@@ -14,14 +14,34 @@ describe('Settings Form', () => {
let store;
let dispatchSpy;
const GlLoadingIcon = { name: 'gl-loading-icon-stub', template: '<svg></svg>' };
const GlCard = {
name: 'gl-card-stub',
template: `
<div>
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
`,
};
const trackingPayload = {
label: 'docker_container_retention_and_expiration_policies',
};
const findForm = () => wrapper.find(expirationPolicyForm);
const findForm = () => wrapper.find({ ref: 'form-element' });
const findFields = () => wrapper.find(expirationPolicyFields);
const findCancelButton = () => wrapper.find({ ref: 'cancel-button' });
const findSaveButton = () => wrapper.find({ ref: 'save-button' });
const findLoadingIcon = (parent = wrapper) => parent.find(GlLoadingIcon);
const mountComponent = () => {
wrapper = shallowMount(component, {
stubs: {
GlCard,
GlLoadingIcon,
},
mocks: {
$toast: {
show: jest.fn(),
......@@ -47,46 +67,50 @@ describe('Settings Form', () => {
let form;
beforeEach(() => {
form = findForm();
dispatchSpy.mockReturnValue();
});
describe('data binding', () => {
it('v-model change update the settings property', () => {
dispatchSpy.mockReturnValue();
form.vm.$emit('input', 'foo');
findFields().vm.$emit('input', 'foo');
expect(dispatchSpy).toHaveBeenCalledWith('updateSettings', { settings: 'foo' });
});
});
describe('form reset event', () => {
beforeEach(() => {
form.trigger('reset');
});
it('calls the appropriate function', () => {
dispatchSpy.mockReturnValue();
form.vm.$emit('reset');
expect(dispatchSpy).toHaveBeenCalledWith('resetSettings');
});
it('tracks the reset event', () => {
dispatchSpy.mockReturnValue();
form.vm.$emit('reset');
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'reset_form', trackingPayload);
});
});
describe('form submit event ', () => {
it('save has type submit', () => {
mountComponent();
expect(findSaveButton().attributes('type')).toBe('submit');
});
it('dispatches the saveSettings action', () => {
dispatchSpy.mockResolvedValue();
form.vm.$emit('submit');
form.trigger('submit');
expect(dispatchSpy).toHaveBeenCalledWith('saveSettings');
});
it('tracks the submit event', () => {
dispatchSpy.mockResolvedValue();
form.vm.$emit('submit');
form.trigger('submit');
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'submit_form', trackingPayload);
});
it('show a success toast when submit succeed', () => {
dispatchSpy.mockResolvedValue();
form.vm.$emit('submit');
form.trigger('submit');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE, {
type: 'success',
......@@ -96,7 +120,7 @@ describe('Settings Form', () => {
it('show an error toast when submit fails', () => {
dispatchSpy.mockRejectedValue();
form.vm.$emit('submit');
form.trigger('submit');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE, {
type: 'error',
......@@ -105,4 +129,52 @@ describe('Settings Form', () => {
});
});
});
describe('form actions', () => {
describe('cancel button', () => {
beforeEach(() => {
store.commit('SET_SETTINGS', { foo: 'bar' });
});
it('has type reset', () => {
expect(findCancelButton().attributes('type')).toBe('reset');
});
it('is disabled when isEdited is false', () =>
wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe('true');
}));
it('is disabled isLoading is true', () => {
store.commit('TOGGLE_LOADING');
store.commit('UPDATE_SETTINGS', { settings: { foo: 'baz' } });
return wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe('true');
store.commit('TOGGLE_LOADING');
});
});
it('is enabled when isLoading is false and isEdited is true', () => {
store.commit('UPDATE_SETTINGS', { settings: { foo: 'baz' } });
return wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe(undefined);
});
});
});
describe('when isLoading is true', () => {
beforeEach(() => {
store.commit('TOGGLE_LOADING');
});
afterEach(() => {
store.commit('TOGGLE_LOADING');
});
it('submit button is disabled and shows a spinner', () => {
const button = findSaveButton();
expect(button.attributes('disabled')).toBeTruthy();
expect(findLoadingIcon(button).exists()).toBe(true);
});
});
});
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Expiration Policy Form renders 1`] = `
<div
class="lh-2"
>
<glformgroup-stub
id="expiration-policy-toggle-group"
label="Expiration policy:"
label-align="right"
label-cols="3"
label-for="expiration-policy-toggle"
>
<div
class="d-flex align-items-start"
>
<gltoggle-stub
id="expiration-policy-toggle"
labeloff="Toggle Status: OFF"
labelon="Toggle Status: ON"
/>
<span
class="mb-2 ml-1 lh-2"
>
Docker tag expiration policy is
<strong>
disabled
</strong>
</span>
</div>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-interval-group"
label="Expiration interval:"
label-align="right"
label-cols="3"
label-for="expiration-policy-interval"
>
<glformselect-stub
disabled="true"
id="expiration-policy-interval"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-schedule-group"
label="Expiration schedule:"
label-align="right"
label-cols="3"
label-for="expiration-policy-schedule"
>
<glformselect-stub
disabled="true"
id="expiration-policy-schedule"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-latest-group"
label="Number of tags to retain:"
label-align="right"
label-cols="3"
label-for="expiration-policy-latest"
>
<glformselect-stub
disabled="true"
id="expiration-policy-latest"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-name-matching-group"
invalid-feedback="The value of this input should be less than 255 characters"
label="Docker tags with names matching this regex pattern will expire:"
label-align="right"
label-cols="3"
label-for="expiration-policy-name-matching"
>
<glformtextarea-stub
disabled="true"
id="expiration-policy-name-matching"
placeholder=".*"
trim=""
value=""
/>
</glformgroup-stub>
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Expiration Policy Form renders 1`] = `
<form
class="lh-2"
>
<div
class="card"
>
<!---->
<div
class="card-header"
>
Tag expiration policy
</div>
<div
class="card-body"
>
<!---->
<!---->
<glformgroup-stub
id="expiration-policy-toggle-group"
label="Expiration policy:"
label-align="right"
label-cols="3"
label-for="expiration-policy-toggle"
>
<div
class="d-flex align-items-start"
>
<gltoggle-stub
id="expiration-policy-toggle"
labeloff="Toggle Status: OFF"
labelon="Toggle Status: ON"
/>
<span
class="mb-2 ml-1 lh-2"
>
Docker tag expiration policy is
<strong>
disabled
</strong>
</span>
</div>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-interval-group"
label="Expiration interval:"
label-align="right"
label-cols="3"
label-for="expiration-policy-interval"
>
<glformselect-stub
disabled="true"
id="expiration-policy-interval"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-schedule-group"
label="Expiration schedule:"
label-align="right"
label-cols="3"
label-for="expiration-policy-schedule"
>
<glformselect-stub
disabled="true"
id="expiration-policy-schedule"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-latest-group"
label="Number of tags to retain:"
label-align="right"
label-cols="3"
label-for="expiration-policy-latest"
>
<glformselect-stub
disabled="true"
id="expiration-policy-latest"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-name-matching-group"
invalid-feedback="The value of this input should be less than 255 characters"
label="Docker tags with names matching this regex pattern will expire:"
label-align="right"
label-cols="3"
label-for="expiration-policy-name-matching"
>
<glformtextarea-stub
disabled="true"
id="expiration-policy-name-matching"
placeholder=".*"
trim=""
value=""
/>
</glformgroup-stub>
</div>
<div
class="card-footer"
>
<div
class="d-flex justify-content-end"
>
<glbutton-stub
class="mr-2 d-block"
size="md"
type="reset"
variant="secondary"
>
Cancel
</glbutton-stub>
<glbutton-stub
class="d-flex justify-content-center align-items-center js-no-auto-disable"
size="md"
type="submit"
variant="success"
>
Save expiration policy
<!---->
</glbutton-stub>
</div>
</div>
<!---->
</div>
</form>
`;
import { mount } from '@vue/test-utils';
import stubChildren from 'helpers/stub_children';
import component from '~/registry/shared/components/expiration_policy_form.vue';
import component from '~/registry/shared/components/expiration_policy_fields.vue';
import { NAME_REGEX_LENGTH } from '~/registry/shared/constants';
import { formOptions } from '../mock_data';
......@@ -10,22 +10,14 @@ describe('Expiration Policy Form', () => {
const FORM_ELEMENTS_ID_PREFIX = '#expiration-policy';
const GlLoadingIcon = { name: 'gl-loading-icon-stub', template: '<svg></svg>' };
const findFormGroup = name => wrapper.find(`${FORM_ELEMENTS_ID_PREFIX}-${name}-group`);
const findFormElements = (name, parent = wrapper) =>
parent.find(`${FORM_ELEMENTS_ID_PREFIX}-${name}`);
const findCancelButton = () => wrapper.find({ ref: 'cancel-button' });
const findSaveButton = () => wrapper.find({ ref: 'save-button' });
const findForm = () => wrapper.find({ ref: 'form-element' });
const findLoadingIcon = (parent = wrapper) => parent.find(GlLoadingIcon);
const mountComponent = props => {
wrapper = mount(component, {
stubs: {
...stubChildren(component),
GlCard: false,
GlLoadingIcon,
},
propsData: {
formOptions,
......@@ -114,77 +106,20 @@ describe('Expiration Policy Form', () => {
},
);
describe('form actions', () => {
describe('cancel button', () => {
it('has type reset', () => {
mountComponent();
expect(findCancelButton().attributes('type')).toBe('reset');
});
it('is disabled when disableCancelButton is true', () => {
mountComponent({ disableCancelButton: true });
return wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe('true');
});
});
it('is disabled isLoading is true', () => {
mountComponent({ isLoading: true });
return wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe('true');
});
});
it('is enabled when isLoading and disableCancelButton are false', () => {
mountComponent({ disableCancelButton: false, isLoading: false });
return wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe(undefined);
});
});
});
describe('form cancel event', () => {
it('calls the appropriate function', () => {
mountComponent();
findForm().trigger('reset');
expect(wrapper.emitted('reset')).toBeTruthy();
});
});
it('save has type submit', () => {
mountComponent();
expect(findSaveButton().attributes('type')).toBe('submit');
});
describe('when isLoading is true', () => {
beforeEach(() => {
mountComponent({ isLoading: true });
});
it.each`
elementName
${'toggle'}
${'interval'}
${'schedule'}
${'latest'}
${'name-matching'}
`(`${FORM_ELEMENTS_ID_PREFIX}-$elementName is disabled`, ({ elementName }) => {
expect(findFormElements(elementName).attributes('disabled')).toBe('true');
});
it('submit button is disabled and shows a spinner', () => {
const button = findSaveButton();
expect(button.attributes('disabled')).toBeTruthy();
expect(findLoadingIcon(button)).toExist();
});
describe('when isLoading is true', () => {
beforeEach(() => {
mountComponent({ isLoading: true });
});
describe('form submit event ', () => {
it('calls the appropriate function', () => {
mountComponent();
findForm().trigger('submit');
expect(wrapper.emitted('submit')).toBeTruthy();
});
it.each`
elementName
${'toggle'}
${'interval'}
${'schedule'}
${'latest'}
${'name-matching'}
`(`${FORM_ELEMENTS_ID_PREFIX}-$elementName is disabled`, ({ elementName }) => {
expect(findFormElements(elementName).attributes('disabled')).toBe('true');
});
});
......@@ -196,20 +131,20 @@ describe('Expiration Policy Form', () => {
mountComponent({ value: { name_regex: invalidString } });
});
it('save btn is disabled', () => {
expect(findSaveButton().attributes('disabled')).toBeTruthy();
});
it('nameRegexState is false', () => {
expect(wrapper.vm.nameRegexState).toBe(false);
});
it('emit the @invalidated event', () => {
expect(wrapper.emitted('invalidated')).toBeTruthy();
});
});
it('if the user did not type validation is null', () => {
mountComponent({ value: { name_regex: '' } });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.nameRegexState).toBe(null);
expect(findSaveButton().attributes('disabled')).toBeFalsy();
expect(wrapper.emitted('validated')).toBeTruthy();
});
});
......
......@@ -58,32 +58,4 @@ describe ClustersHelper do
it { is_expected.to eq('Create new cluster') }
end
end
describe '#render_new_provider_form' do
subject { helper.new_cluster_partial(provider: provider) }
context 'GCP provider' do
let(:provider) { 'gcp' }
it { is_expected.to eq('clusters/clusters/gcp/new') }
end
context 'AWS provider' do
let(:provider) { 'aws' }
it { is_expected.to eq('clusters/clusters/aws/new') }
end
context 'other provider' do
let(:provider) { 'other' }
it { is_expected.to eq('clusters/clusters/cloud_providers/cloud_provider_selector') }
end
context 'no provider' do
let(:provider) { nil }
it { is_expected.to eq('clusters/clusters/cloud_providers/cloud_provider_selector') }
end
end
end
......@@ -6,7 +6,6 @@ import axios from '~/lib/utils/axios_utils';
import jobApp from '~/jobs/components/job_app.vue';
import createStore from '~/jobs/store';
import * as types from '~/jobs/store/mutation_types';
import { resetStore } from '../store/helpers';
import job from '../mock_data';
describe('Job App ', () => {
......@@ -16,24 +15,29 @@ describe('Job App ', () => {
let vm;
let mock;
const props = {
const initSettings = {
endpoint: `${gl.TEST_HOST}jobs/123.json`,
pagePath: `${gl.TEST_HOST}jobs/123`,
logState:
'eyJvZmZzZXQiOjE3NDUxLCJuX29wZW5fdGFncyI6MCwiZmdfY29sb3IiOm51bGwsImJnX2NvbG9yIjpudWxsLCJzdHlsZV9tYXNrIjowfQ%3D%3D',
};
const props = {
runnerHelpUrl: 'help/runner',
deploymentHelpUrl: 'help/deployment',
runnerSettingsUrl: 'settings/ci-cd/runners',
variablesSettingsUrl: 'settings/ci-cd/variables',
terminalPath: 'jobs/123/terminal',
pagePath: `${gl.TEST_HOST}jobs/123`,
projectPath: 'user-name/project-name',
subscriptionsMoreMinutesUrl: 'https://customers.gitlab.com/buy_pipeline_minutes',
logState:
'eyJvZmZzZXQiOjE3NDUxLCJuX29wZW5fdGFncyI6MCwiZmdfY29sb3IiOm51bGwsImJnX2NvbG9yIjpudWxsLCJzdHlsZV9tYXNrIjowfQ%3D%3D',
};
const waitForJobReceived = () => waitForMutation(store, types.RECEIVE_JOB_SUCCESS);
const setupAndMount = ({ jobData = {}, traceData = {} } = {}) => {
mock.onGet(props.endpoint).replyOnce(200, { ...job, ...jobData });
mock.onGet(`${props.pagePath}/trace.json`).reply(200, traceData);
mock.onGet(initSettings.endpoint).replyOnce(200, { ...job, ...jobData });
mock.onGet(`${initSettings.pagePath}/trace.json`).reply(200, traceData);
store.dispatch('init', initSettings);
vm = mountComponentWithStore(Component, { props, store });
......@@ -46,7 +50,6 @@ describe('Job App ', () => {
});
afterEach(() => {
resetStore(store);
vm.$destroy();
mock.restore();
});
......@@ -384,7 +387,6 @@ describe('Job App ', () => {
})
.then(done)
.catch(done.fail);
done();
});
it('displays remaining time for a delayed job', done => {
......
......@@ -15,6 +15,7 @@ import {
scrollBottom,
requestTrace,
fetchTrace,
startPollingTrace,
stopPollingTrace,
receiveTraceSuccess,
receiveTraceError,
......@@ -241,6 +242,50 @@ describe('Job State actions', () => {
done,
);
});
describe('when job is incomplete', () => {
let tracePayload;
beforeEach(() => {
tracePayload = {
html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
complete: false,
};
mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, tracePayload);
});
it('dispatches startPollingTrace', done => {
testAction(
fetchTrace,
null,
mockedState,
[],
[
{ type: 'toggleScrollisInBottom', payload: true },
{ type: 'receiveTraceSuccess', payload: tracePayload },
{ type: 'startPollingTrace' },
],
done,
);
});
it('does not dispatch startPollingTrace when timeout is non-empty', done => {
mockedState.traceTimeout = 1;
testAction(
fetchTrace,
null,
mockedState,
[],
[
{ type: 'toggleScrollisInBottom', payload: true },
{ type: 'receiveTraceSuccess', payload: tracePayload },
],
done,
);
});
});
});
describe('error', () => {
......@@ -265,16 +310,69 @@ describe('Job State actions', () => {
});
});
describe('startPollingTrace', () => {
let dispatch;
let commit;
beforeEach(() => {
jasmine.clock().install();
dispatch = jasmine.createSpy();
commit = jasmine.createSpy();
startPollingTrace({ dispatch, commit });
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should save the timeout id but not call fetchTrace', () => {
expect(commit).toHaveBeenCalledWith(types.SET_TRACE_TIMEOUT, 1);
expect(dispatch).not.toHaveBeenCalledWith('fetchTrace');
});
describe('after timeout has passed', () => {
beforeEach(() => {
jasmine.clock().tick(4000);
});
it('should clear the timeout id and fetchTrace', () => {
expect(commit).toHaveBeenCalledWith(types.SET_TRACE_TIMEOUT, 0);
expect(dispatch).toHaveBeenCalledWith('fetchTrace');
});
});
});
describe('stopPollingTrace', () => {
let origTimeout;
beforeEach(() => {
// Can't use spyOn(window, 'clearTimeout') because this caused unrelated specs to timeout
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23838#note_280277727
origTimeout = window.clearTimeout;
window.clearTimeout = jasmine.createSpy();
});
afterEach(() => {
window.clearTimeout = origTimeout;
});
it('should commit STOP_POLLING_TRACE mutation ', done => {
const traceTimeout = 7;
testAction(
stopPollingTrace,
null,
mockedState,
[{ type: types.STOP_POLLING_TRACE }],
{ ...mockedState, traceTimeout },
[{ type: types.SET_TRACE_TIMEOUT, payload: 0 }, { type: types.STOP_POLLING_TRACE }],
[],
done,
);
)
.then(() => {
expect(window.clearTimeout).toHaveBeenCalledWith(traceTimeout);
})
.then(done)
.catch(done.fail);
});
});
......@@ -292,15 +390,8 @@ describe('Job State actions', () => {
});
describe('receiveTraceError', () => {
it('should commit RECEIVE_TRACE_ERROR mutation ', done => {
testAction(
receiveTraceError,
null,
mockedState,
[{ type: types.RECEIVE_TRACE_ERROR }],
[],
done,
);
it('should commit stop polling trace', done => {
testAction(receiveTraceError, null, mockedState, [], [{ type: 'stopPollingTrace' }], done);
});
});
......
......@@ -2,10 +2,10 @@
require 'spec_helper'
describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode do
describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_store do
include_context 'custom session'
let(:user) { build(:user) }
let(:user) { build_stubbed(:user) }
subject { described_class.new(user) }
......@@ -13,54 +13,66 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode do
allow(ActiveSession).to receive(:list_sessions).with(user).and_return([session])
end
describe '#admin_mode?', :request_store do
context 'when the user is a regular user' do
it 'is false by default' do
expect(subject.admin_mode?).to be(false)
end
shared_examples 'admin mode cannot be enabled' do
it 'is false by default' do
expect(subject.admin_mode?).to be(false)
end
it 'cannot be enabled with a valid password' do
subject.enable_admin_mode!(password: user.password)
it 'cannot be enabled with a valid password' do
subject.enable_admin_mode!(password: user.password)
expect(subject.admin_mode?).to be(false)
end
expect(subject.admin_mode?).to be(false)
end
it 'cannot be enabled with an invalid password' do
subject.enable_admin_mode!(password: nil)
it 'cannot be enabled with an invalid password' do
subject.enable_admin_mode!(password: nil)
expect(subject.admin_mode?).to be(false)
end
expect(subject.admin_mode?).to be(false)
end
it 'cannot be enabled with empty params' do
subject.enable_admin_mode!
it 'cannot be enabled with empty params' do
subject.enable_admin_mode!
expect(subject.admin_mode?).to be(false)
end
expect(subject.admin_mode?).to be(false)
end
it 'disable has no effect' do
subject.enable_admin_mode!
subject.disable_admin_mode!
it 'disable has no effect' do
subject.enable_admin_mode!
subject.disable_admin_mode!
expect(subject.admin_mode?).to be(false)
end
context 'skipping password validation' do
it 'cannot be enabled with a valid password' do
subject.enable_admin_mode!(password: user.password, skip_password_validation: true)
expect(subject.admin_mode?).to be(false)
end
context 'skipping password validation' do
it 'cannot be enabled with a valid password' do
subject.enable_admin_mode!(password: user.password, skip_password_validation: true)
it 'cannot be enabled with an invalid password' do
subject.enable_admin_mode!(skip_password_validation: true)
expect(subject.admin_mode?).to be(false)
end
expect(subject.admin_mode?).to be(false)
end
end
end
it 'cannot be enabled with an invalid password' do
subject.enable_admin_mode!(skip_password_validation: true)
describe '#admin_mode?' do
context 'when the user is a regular user' do
it_behaves_like 'admin mode cannot be enabled'
expect(subject.admin_mode?).to be(false)
context 'bypassing session' do
it_behaves_like 'admin mode cannot be enabled' do
around do |example|
described_class.bypass_session!(user.id) { example.run }
end
end
end
end
context 'when the user is an admin' do
let(:user) { build(:user, :admin) }
let(:user) { build_stubbed(:user, :admin) }
context 'when admin mode not requested' do
it 'is false by default' do
......@@ -148,11 +160,36 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode do
end
end
end
context 'bypassing session' do
it 'is active by default' do
described_class.bypass_session!(user.id) do
expect(subject.admin_mode?).to be(true)
end
end
it 'enable has no effect' do
described_class.bypass_session!(user.id) do
subject.request_admin_mode!
subject.enable_admin_mode!(password: user.password)
expect(subject.admin_mode?).to be(true)
end
end
it 'disable has no effect' do
described_class.bypass_session!(user.id) do
subject.disable_admin_mode!
expect(subject.admin_mode?).to be(true)
end
end
end
end
end
describe '#enable_admin_mode!' do
let(:user) { build(:user, :admin) }
let(:user) { build_stubbed(:user, :admin) }
it 'creates a timestamp in the session' do
subject.request_admin_mode!
......@@ -163,7 +200,7 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode do
end
describe '#enable_sessionless_admin_mode!' do
let(:user) { build(:user, :admin) }
let(:user) { build_stubbed(:user, :admin) }
it 'enabled admin mode without password' do
subject.enable_sessionless_admin_mode!
......@@ -173,7 +210,7 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode do
end
describe '#disable_admin_mode!' do
let(:user) { build(:user, :admin) }
let(:user) { build_stubbed(:user, :admin) }
it 'sets the session timestamp to nil' do
subject.request_admin_mode!
......@@ -183,6 +220,73 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode do
end
end
describe '.bypass_session!' do
context 'with a regular user' do
it 'admin mode is false' do
described_class.bypass_session!(user.id) do
expect(subject.admin_mode?).to be(false)
expect(described_class.bypass_session_admin_id).to be(user.id)
end
expect(described_class.bypass_session_admin_id).to be_nil
end
end
context 'with an admin user' do
let(:user) { build_stubbed(:user, :admin) }
it 'admin mode is true' do
described_class.bypass_session!(user.id) do
expect(subject.admin_mode?).to be(true)
expect(described_class.bypass_session_admin_id).to be(user.id)
end
expect(described_class.bypass_session_admin_id).to be_nil
end
end
end
describe '.with_current_request_admin_mode' do
context 'with a regular user' do
it 'user is not available inside nor outside the yielded block' do
described_class.with_current_admin(user) do
expect(described_class.current_admin).to be_nil
end
expect(described_class.bypass_session_admin_id).to be_nil
end
end
context 'with an admin user' do
let(:user) { build_stubbed(:user, :admin) }
context 'admin mode is disabled' do
it 'user is not available inside nor outside the yielded block' do
described_class.with_current_admin(user) do
expect(described_class.current_admin).to be_nil
end
expect(described_class.bypass_session_admin_id).to be_nil
end
end
context 'admin mode is enabled' do
before do
subject.request_admin_mode!
subject.enable_admin_mode!(password: user.password)
end
it 'user is available only inside the yielded block' do
described_class.with_current_admin(user) do
expect(described_class.current_admin).to be(user)
end
expect(described_class.current_admin).to be_nil
end
end
end
end
def expected_session_entry(value_matcher)
{
Gitlab::Auth::CurrentUserMode::SESSION_STORE_KEY => a_hash_including(
......
......@@ -8,7 +8,7 @@ describe Gitlab::BackgroundMigration::BackfillProjectFullpathInRepoConfig, :migr
let(:group) { namespaces.create!(name: 'foo', path: 'foo') }
let(:subgroup) { namespaces.create!(name: 'bar', path: 'bar', parent_id: group.id) }
describe described_class::Storage::HashedProject do
describe described_class::Storage::Hashed do
let(:project) { double(id: 555) }
subject(:project_storage) { described_class.new(project) }
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::SidekiqMiddleware::AdminMode::Client, :do_not_mock_admin_mode, :request_store do
include AdminModeHelper
let(:worker) do
Class.new do
def perform; end
end
end
let(:job) { {} }
let(:queue) { :test }
it 'yields block' do
expect do |b|
subject.call(worker, job, queue, nil, &b)
end.to yield_control.once
end
context 'user is a regular user' do
it 'no admin mode field in payload' do
subject.call(worker, job, queue, nil) { nil }
expect(job).not_to include('admin_mode_user_id')
end
end
context 'user is an administrator' do
let(:admin) { create(:admin) }
context 'admin mode disabled' do
it 'no admin mode field in payload' do
subject.call(worker, job, queue, nil) { nil }
expect(job).not_to include('admin_mode_user_id')
end
end
context 'admin mode enabled' do
before do
enable_admin_mode!(admin)
end
context 'when sidekiq required context not set' do
it 'no admin mode field in payload' do
subject.call(worker, job, queue, nil) { nil }
expect(job).not_to include('admin_mode_user_id')
end
end
context 'when user stored in current request' do
it 'has admin mode field in payload' do
Gitlab::Auth::CurrentUserMode.with_current_admin(admin) do
subject.call(worker, job, queue, nil) { nil }
expect(job).to include('admin_mode_user_id' => admin.id)
end
end
end
context 'when bypassing session' do
it 'has admin mode field in payload' do
Gitlab::Auth::CurrentUserMode.bypass_session!(admin.id) do
subject.call(worker, job, queue, nil) { nil }
expect(job).to include('admin_mode_user_id' => admin.id)
end
end
end
end
end
context 'admin mode feature disabled' do
before do
stub_feature_flags(user_mode_in_session: false)
end
it 'yields block' do
expect do |b|
subject.call(worker, job, queue, nil, &b)
end.to yield_control.once
end
it 'no admin mode field in payload' do
subject.call(worker, job, queue, nil) { nil }
expect(job).not_to include('admin_mode_user_id')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::SidekiqMiddleware::AdminMode::Server, :do_not_mock_admin_mode, :request_store do
include AdminModeHelper
let(:worker) do
Class.new do
def perform; end
end
end
let(:job) { {} }
let(:queue) { :test }
it 'yields block' do
expect do |b|
subject.call(worker, job, queue, &b)
end.to yield_control.once
end
context 'job has no admin mode field' do
it 'session is not bypassed' do
subject.call(worker, job, queue) do
expect(Gitlab::Auth::CurrentUserMode.bypass_session_admin_id).to be_nil
end
end
end
context 'job has admin mode field' do
let(:admin) { create(:admin) }
context 'nil admin mode id' do
let(:job) { { 'admin_mode_user_id' => nil } }
it 'session is not bypassed' do
subject.call(worker, job, queue) do
expect(Gitlab::Auth::CurrentUserMode.bypass_session_admin_id).to be_nil
end
end
end
context 'valid admin mode id' do
let(:job) { { 'admin_mode_user_id' => admin.id } }
it 'session is bypassed' do
subject.call(worker, job, queue) do
expect(Gitlab::Auth::CurrentUserMode.bypass_session_admin_id).to be(admin.id)
end
end
end
end
context 'admin mode feature disabled' do
before do
stub_feature_flags(user_mode_in_session: false)
end
it 'yields block' do
expect do |b|
subject.call(worker, job, queue, &b)
end.to yield_control.once
end
it 'session is not bypassed' do
subject.call(worker, job, queue) do
expect(Gitlab::Auth::CurrentUserMode.bypass_session_admin_id).to be_nil
end
end
end
end
......@@ -45,7 +45,8 @@ describe Gitlab::SidekiqMiddleware do
Gitlab::SidekiqMiddleware::ArgumentsLogger,
Gitlab::SidekiqMiddleware::MemoryKiller,
Gitlab::SidekiqMiddleware::RequestStoreMiddleware,
Gitlab::SidekiqMiddleware::WorkerContext::Server
Gitlab::SidekiqMiddleware::WorkerContext::Server,
Gitlab::SidekiqMiddleware::AdminMode::Server
]
end
let(:enabled_sidekiq_middlewares) { all_sidekiq_middlewares - disabled_sidekiq_middlewares }
......@@ -115,7 +116,8 @@ describe Gitlab::SidekiqMiddleware do
Gitlab::SidekiqStatus::ClientMiddleware,
Gitlab::SidekiqMiddleware::ClientMetrics,
Gitlab::SidekiqMiddleware::WorkerContext::Client,
Labkit::Middleware::Sidekiq::Client
Labkit::Middleware::Sidekiq::Client,
Gitlab::SidekiqMiddleware::AdminMode::Client
]
end
......
......@@ -17,7 +17,7 @@ describe MicrosoftTeams::Notifier do
text: '[#1 Awesome issue](http://localhost/namespace2/gitlabhq/issues/1)',
image: 'http://someimage.com'
},
attachments: 'please fix'
attachments: "[GitLab](https://gitlab.com)\n\n- _Ruby_\n- **Go**\n"
}
end
......@@ -31,13 +31,7 @@ describe MicrosoftTeams::Notifier do
'activityImage' => 'http://someimage.com'
},
{
'title' => 'Details',
'facts' => [
{
'name' => 'Attachments',
'value' => 'please fix'
}
]
text: "[GitLab](https://gitlab.com)\n\n- _Ruby_\n- **Go**\n"
}
],
'title' => 'JohnDoe4/project2',
......@@ -54,4 +48,14 @@ describe MicrosoftTeams::Notifier do
expect(subject.ping(options)).to be true
end
end
describe '#body' do
it 'returns Markdown-based body when HTML was passed' do
expect(subject.send(:body, options)).to eq(body.to_json)
end
it 'fails when empty Hash was passed' do
expect { subject.send(:body, {}) }.to raise_error(ArgumentError)
end
end
end
......@@ -84,6 +84,16 @@ describe Issue do
end
end
describe '.simple_sorts' do
it 'includes all keys' do
expect(described_class.simple_sorts.keys).to include(
*%w(created_asc created_at_asc created_date created_desc created_at_desc
closest_future_date closest_future_date_asc due_date due_date_asc due_date_desc
id_asc id_desc relative_position relative_position_asc
updated_desc updated_asc updated_at_asc updated_at_desc))
end
end
describe '#order_by_position_and_priority' do
let(:project) { create :project }
let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
......
......@@ -2985,9 +2985,9 @@ describe User, :do_not_mock_admin_mode do
end
end
describe '#can_read_all_resources?' do
describe '#can_read_all_resources?', :request_store do
it 'returns false for regular user' do
user = build(:user)
user = build_stubbed(:user)
expect(user.can_read_all_resources?).to be_falsy
end
......@@ -2995,7 +2995,7 @@ describe User, :do_not_mock_admin_mode do
context 'for admin user' do
include_context 'custom session'
let(:user) { build(:user, :admin) }
let(:user) { build_stubbed(:user, :admin) }
context 'when admin mode is disabled' do
it 'returns false' do
......
......@@ -23,8 +23,8 @@ describe BasePolicy, :do_not_mock_admin_mode do
end
describe 'read cross project' do
let(:current_user) { create(:user) }
let(:user) { create(:user) }
let(:current_user) { build_stubbed(:user) }
let(:user) { build_stubbed(:user) }
subject { described_class.new(current_user, [user]) }
......@@ -38,7 +38,7 @@ describe BasePolicy, :do_not_mock_admin_mode do
it { is_expected.not_to be_allowed(:read_cross_project) }
context 'for admins' do
let(:current_user) { build(:admin) }
let(:current_user) { build_stubbed(:admin) }
subject { described_class.new(current_user, nil) }
......@@ -56,14 +56,14 @@ describe BasePolicy, :do_not_mock_admin_mode do
end
describe 'full private access' do
let(:current_user) { create(:user) }
let(:current_user) { build_stubbed(:user) }
subject { described_class.new(current_user, nil) }
it { is_expected.not_to be_allowed(:read_all_resources) }
context 'for admins' do
let(:current_user) { build(:admin) }
let(:current_user) { build_stubbed(:admin) }
it 'allowed when in admin mode' do
enable_admin_mode!(current_user)
......
......@@ -5,6 +5,8 @@ require 'spec_helper'
describe ErrorTracking::IssueDetailsService do
include_context 'sentry error tracking context'
subject { described_class.new(project, user, params) }
describe '#execute' do
context 'with authorized user' do
context 'when issue_details returns a detailed error' do
......
......@@ -5,6 +5,8 @@ require 'spec_helper'
describe ErrorTracking::IssueLatestEventService do
include_context 'sentry error tracking context'
subject { described_class.new(project, user) }
describe '#execute' do
context 'with authorized user' do
context 'when issue_latest_event returns an error event' do
......
......@@ -7,16 +7,19 @@ describe ErrorTracking::IssueUpdateService do
let(:arguments) { { issue_id: 1234, status: 'resolved' } }
subject { described_class.new(project, user, arguments) }
subject(:update_service) { described_class.new(project, user, arguments) }
shared_examples 'does not perform close issue flow' do
it 'does not call the close issue service' do
update_service.execute
expect(issue_close_service)
.not_to receive(:execute)
.not_to have_received(:execute)
end
it 'does not create system note' do
expect(SystemNoteService).not_to receive(:close_after_error_tracking_resolve)
update_service.execute
end
end
......@@ -31,13 +34,13 @@ describe ErrorTracking::IssueUpdateService do
end
it 'returns the response' do
expect(result).to eq(update_issue_response.merge(status: :success, closed_issue_iid: nil))
expect(update_service.execute).to eq(update_issue_response.merge(status: :success, closed_issue_iid: nil))
end
it 'updates any related issue' do
expect(subject).to receive(:update_related_issue)
expect(update_service).to receive(:update_related_issue)
result
update_service.execute
end
context 'related issue and resolving' do
......@@ -48,39 +51,46 @@ describe ErrorTracking::IssueUpdateService do
let(:issue_close_service) { spy(:issue_close_service) }
before do
allow_any_instance_of(SentryIssueFinder)
.to receive(:execute)
.and_return(sentry_issue)
allow_next_instance_of(SentryIssueFinder) do |finder|
allow(finder).to receive(:execute).and_return(sentry_issue)
end
allow(Issues::CloseService)
.to receive(:new)
.and_return(issue_close_service)
end
after do
result
allow(issue_close_service)
.to receive(:execute)
.and_return(issue)
end
it 'closes the issue' do
update_service.execute
expect(issue_close_service)
.to receive(:execute)
.to have_received(:execute)
.with(issue, system_note: false)
.and_return(issue)
end
it 'creates a system note' do
expect(SystemNoteService).to receive(:close_after_error_tracking_resolve)
end
context 'issues gets closed' do
let(:closed_issue) { create(:issue, :closed, project: project) }
it 'returns a response with closed issue' do
closed_issue = create(:issue, :closed, project: project)
before do
expect(issue_close_service)
.to receive(:execute)
.with(issue, system_note: false)
.and_return(closed_issue)
end
expect(issue_close_service)
.to receive(:execute)
.with(issue, system_note: false)
.and_return(closed_issue)
it 'creates a system note' do
expect(SystemNoteService).to receive(:close_after_error_tracking_resolve)
update_service.execute
end
expect(result).to eq(status: :success, updated: true, closed_issue_iid: closed_issue.iid)
it 'returns a response with closed issue' do
expect(update_service.execute).to eq(status: :success, updated: true, closed_issue_iid: closed_issue.iid)
end
end
context 'issue is already closed' do
......
......@@ -5,7 +5,7 @@ require 'spec_helper'
describe Projects::AfterRenameService do
let(:rugged_config) { rugged_repo(project.repository).config }
let(:legacy_storage) { Storage::LegacyProject.new(project) }
let(:hashed_storage) { Storage::HashedProject.new(project) }
let(:hashed_storage) { Storage::Hashed.new(project) }
let!(:path_before_rename) { project.path }
let!(:full_path_before_rename) { project.full_path }
let!(:path_after_rename) { "#{project.path}-renamed" }
......
......@@ -7,7 +7,7 @@ describe Projects::HashedStorage::MigrateAttachmentsService do
let(:project) { create(:project, :repository, storage_version: 1, skip_disk_validation: true) }
let(:legacy_storage) { Storage::LegacyProject.new(project) }
let(:hashed_storage) { Storage::HashedProject.new(project) }
let(:hashed_storage) { Storage::Hashed.new(project) }
let!(:upload) { Upload.find_by(path: file_uploader.upload_path) }
let(:file_uploader) { build(:file_uploader, project: project) }
......
......@@ -8,7 +8,7 @@ describe Projects::HashedStorage::MigrateRepositoryService do
let(:gitlab_shell) { Gitlab::Shell.new }
let(:project) { create(:project, :legacy_storage, :repository, :wiki_repo) }
let(:legacy_storage) { Storage::LegacyProject.new(project) }
let(:hashed_storage) { Storage::HashedProject.new(project) }
let(:hashed_storage) { Storage::Hashed.new(project) }
subject(:service) { described_class.new(project: project, old_disk_path: project.disk_path) }
......
......@@ -7,7 +7,7 @@ describe Projects::HashedStorage::RollbackAttachmentsService do
let(:project) { create(:project, :repository, skip_disk_validation: true) }
let(:legacy_storage) { Storage::LegacyProject.new(project) }
let(:hashed_storage) { Storage::HashedProject.new(project) }
let(:hashed_storage) { Storage::Hashed.new(project) }
let!(:upload) { Upload.find_by(path: file_uploader.upload_path) }
let(:file_uploader) { build(:file_uploader, project: project) }
......
......@@ -8,7 +8,7 @@ describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab_redis
let(:gitlab_shell) { Gitlab::Shell.new }
let(:project) { create(:project, :repository, :wiki_repo, storage_version: ::Project::HASHED_STORAGE_FEATURES[:repository]) }
let(:legacy_storage) { Storage::LegacyProject.new(project) }
let(:hashed_storage) { Storage::HashedProject.new(project) }
let(:hashed_storage) { Storage::Hashed.new(project) }
subject(:service) { described_class.new(project: project, old_disk_path: project.disk_path) }
......
......@@ -9,8 +9,6 @@ shared_context 'sentry error tracking context' do
let(:params) { {} }
let(:result) { subject.execute }
subject { described_class.new(project, user, params) }
let(:error_tracking_setting) do
create(:project_error_tracking_setting, api_url: sentry_url, token: token, project: project)
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment