diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-04 12:09:00 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-04 12:09:00 +0000 |
commit | 88a0824944720b6edaaef56376713541b9a02118 (patch) | |
tree | f5fcc4f9755f249779cda9a8f02902d734af6e7e | |
parent | 7d19df2d34a9803d9f077c16315ba919b7ae2aa2 (diff) | |
download | gitlab-ce-88a0824944720b6edaaef56376713541b9a02118.tar.gz |
Add latest changes from gitlab-org/gitlab@master
81 files changed, 1627 insertions, 840 deletions
diff --git a/.gitlab/ci/cache-repo.gitlab-ci.yml b/.gitlab/ci/cache-repo.gitlab-ci.yml index 1162e98e246..9dcb6f40589 100644 --- a/.gitlab/ci/cache-repo.gitlab-ci.yml +++ b/.gitlab/ci/cache-repo.gitlab-ci.yml @@ -1,3 +1,7 @@ +# 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 diff --git a/.gitlab/ci/cng.gitlab-ci.yml b/.gitlab/ci/cng.gitlab-ci.yml index bd11042eb11..96ffd3941ab 100644 --- a/.gitlab/ci/cng.gitlab-ci.yml +++ b/.gitlab/ci/cng.gitlab-ci.yml @@ -1,15 +1,17 @@ +# 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 diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue index 3d389cf3db5..59c5586edcd 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue +++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue @@ -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">{{ diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue index 49a5d4657af..0cfe47dafaf 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue +++ b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue @@ -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__( diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index 809b3d5f57e..0ca13e897f3 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -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" /> diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js index 9c35534523e..024a13ce102 100644 --- a/app/assets/javascripts/jobs/index.js +++ b/app/assets/javascripts/jobs/index.js @@ -1,11 +1,18 @@ 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, }, diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js index 41cc5a181dc..f4030939f2c 100644 --- a/app/assets/javascripts/jobs/store/actions.js +++ b/app/assets/javascripts/jobs/store/actions.js @@ -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.')); }; /** diff --git a/app/assets/javascripts/jobs/store/mutation_types.js b/app/assets/javascripts/jobs/store/mutation_types.js index 858fa3b73ab..6c4f1b5a191 100644 --- a/app/assets/javascripts/jobs/store/mutation_types.js +++ b/app/assets/javascripts/jobs/store/mutation_types.js @@ -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'; diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js index 77c68cac4a6..6193d8d34ab 100644 --- a/app/assets/javascripts/jobs/store/mutations.js +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -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; }, diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js index cdc1780f3d6..5a61828ec6d 100644 --- a/app/assets/javascripts/jobs/store/state.js +++ b/app/assets/javascripts/jobs/store/state.js @@ -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, diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/registry/settings/components/settings_form.vue index ad2fdb4fd40..cab3c7fff85 100644 --- a/app/assets/javascripts/registry/settings/components/settings_form.vue +++ b/app/assets/javascripts/registry/settings/components/settings_form.vue @@ -1,16 +1,20 @@ <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> diff --git a/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue b/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue new file mode 100644 index 00000000000..84d1c5ccc6a --- /dev/null +++ b/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue @@ -0,0 +1,197 @@ +<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> diff --git a/app/assets/javascripts/registry/shared/components/expiration_policy_form.vue b/app/assets/javascripts/registry/shared/components/expiration_policy_form.vue deleted file mode 100644 index c044add3759..00000000000 --- a/app/assets/javascripts/registry/shared/components/expiration_policy_form.vue +++ /dev/null @@ -1,247 +0,0 @@ -<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> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 4f5f3ee5cf9..e30876813c2 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -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; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index fa88ca91170..7cb629dee21 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 30589694e3f..b40264bfdf4 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -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! diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index f55acad8517..80bf765f3a4 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -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? diff --git a/app/models/issue.rb b/app/models/issue.rb index 3823b5e0fba..fd4a8c90386 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -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 diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb index 25eab6e4e03..94992adfd1e 100644 --- a/app/models/pool_repository.rb +++ b/app/models/pool_repository.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index 064c647ac59..54bed41e9e7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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 diff --git a/app/models/storage/hashed_project.rb b/app/models/storage/hashed.rb index 9a38b06b2f9..898e75194db 100644 --- a/app/models/storage/hashed_project.rb +++ b/app/models/storage/hashed.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Storage - class HashedProject + class Hashed attr_accessor :project delegate :gitlab_shell, :repository_storage, to: :project diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index b326b266017..0fc71d2e3f3 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -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 diff --git a/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml b/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml index 56d46580b9e..c10983a5405 100644 --- a/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml +++ b/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml @@ -1,10 +1,12 @@ - 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 diff --git a/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml index 91925f5f96f..aee355bbf71 100644 --- a/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml +++ b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml @@ -1,8 +1,8 @@ - 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', diff --git a/app/views/clusters/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml index 629585d82cd..fae78fbb7f4 100644 --- a/app/views/clusters/clusters/new.html.haml +++ b/app/views/clusters/clusters/new.html.haml @@ -1,6 +1,7 @@ - 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' diff --git a/changelogs/unreleased/198009-ms-teams-notification-message-improved.yml b/changelogs/unreleased/198009-ms-teams-notification-message-improved.yml new file mode 100644 index 00000000000..0545578f0fe --- /dev/null +++ b/changelogs/unreleased/198009-ms-teams-notification-message-improved.yml @@ -0,0 +1,5 @@ +--- +title: Improve message UI on Microsoft Teams notification +merge_request: 23385 +author: Takuya Noguchi +type: fixed diff --git a/changelogs/unreleased/201761-design-discussion-note-preview-is-broken.yml b/changelogs/unreleased/201761-design-discussion-note-preview-is-broken.yml new file mode 100644 index 00000000000..f399ec25afc --- /dev/null +++ b/changelogs/unreleased/201761-design-discussion-note-preview-is-broken.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Design discussion note preview is broken +merge_request: 24288 +author: +type: fixed diff --git a/changelogs/unreleased/34802-switch-cloud-providers.yml b/changelogs/unreleased/34802-switch-cloud-providers.yml new file mode 100644 index 00000000000..54beba6e85d --- /dev/null +++ b/changelogs/unreleased/34802-switch-cloud-providers.yml @@ -0,0 +1,5 @@ +--- +title: Allow to switch between cloud providers in cluster creation screen +merge_request: 23362 +author: +type: changed diff --git a/changelogs/unreleased/nicolasdular-use-recaptcha-on-signup.yml b/changelogs/unreleased/nicolasdular-use-recaptcha-on-signup.yml new file mode 100644 index 00000000000..a0360b399d7 --- /dev/null +++ b/changelogs/unreleased/nicolasdular-use-recaptcha-on-signup.yml @@ -0,0 +1,5 @@ +--- +title: Enable recaptcha check on sign up +merge_request: 24274 +author: +type: fixed diff --git a/changelogs/unreleased/refactor-admin-mode-in-sidekiq-jobs.yml b/changelogs/unreleased/refactor-admin-mode-in-sidekiq-jobs.yml new file mode 100644 index 00000000000..37e34b3174c --- /dev/null +++ b/changelogs/unreleased/refactor-admin-mode-in-sidekiq-jobs.yml @@ -0,0 +1,5 @@ +--- +title: Admin mode support in sidekiq jobs +merge_request: 21792 +author: Diego Louzán +type: changed diff --git a/changelogs/unreleased/sh-add-due-date-simple-sort.yml b/changelogs/unreleased/sh-add-due-date-simple-sort.yml new file mode 100644 index 00000000000..9d45ab28e4c --- /dev/null +++ b/changelogs/unreleased/sh-add-due-date-simple-sort.yml @@ -0,0 +1,5 @@ +--- +title: Optimize issue search when sorting by due date and position +merge_request: 24217 +author: +type: performance diff --git a/db/migrate/20200109030418_add_sorting_index_to_packages.rb b/db/migrate/20200109030418_add_sorting_index_to_packages.rb new file mode 100644 index 00000000000..989f3de3c04 --- /dev/null +++ b/db/migrate/20200109030418_add_sorting_index_to_packages.rb @@ -0,0 +1,21 @@ +# 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 diff --git a/db/migrate/20200109233938_remove_project_id_index_from_packages.rb b/db/migrate/20200109233938_remove_project_id_index_from_packages.rb new file mode 100644 index 00000000000..e806c5d5a18 --- /dev/null +++ b/db/migrate/20200109233938_remove_project_id_index_from_packages.rb @@ -0,0 +1,17 @@ +# 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 diff --git a/db/schema.rb b/db/schema.rb index 662115945cf..e7a1ace68f4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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| diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md index 663cfb11910..849e782da42 100644 --- a/doc/administration/packages/container_registry.md +++ b/doc/administration/packages/container_registry.md @@ -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 diff --git a/doc/api/container_registry.md b/doc/api/container_registry.md index 4baeec3653c..f9c6ec4e572 100644 --- a/doc/api/container_registry.md +++ b/doc/api/container_registry.md @@ -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`. | diff --git a/doc/development/contributing/style_guides.md b/doc/development/contributing/style_guides.md index 24c8de80e51..022c40a9d1b 100644 --- a/doc/development/contributing/style_guides.md +++ b/doc/development/contributing/style_guides.md @@ -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). diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md index 5b74d74bbb0..21cecd84e75 100644 --- a/doc/install/aws/index.md +++ b/doc/install/aws/index.md @@ -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. diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md index 9678ff4de5a..5d7bba32ead 100644 --- a/doc/user/application_security/dast/index.md +++ b/doc/user/application_security/dast/index.md @@ -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 diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md index 9a6f61c9c8c..24f28e547b3 100644 --- a/doc/user/application_security/sast/index.md +++ b/doc/user/application_security/sast/index.md @@ -240,7 +240,8 @@ build: - target/ spotbugs-sast: - dependencies: build + dependencies: + - build script: - /analyzer run -compile=false variables: diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md index f65042d2684..27c6a580797 100644 --- a/doc/user/packages/maven_repository/index.md +++ b/doc/user/packages/maven_repository/index.md @@ -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, diff --git a/lib/gitlab/auth/current_user_mode.rb b/lib/gitlab/auth/current_user_mode.rb index cb39baaa6cc..1ef95c03cfc 100644 --- a/lib/gitlab/auth/current_user_mode.rb +++ b/lib/gitlab/auth/current_user_mode.rb @@ -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 diff --git a/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb b/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb index 3c142327e94..2a079060380 100644 --- a/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb +++ b/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb @@ -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 diff --git a/lib/gitlab/background_migration/backfill_project_repositories.rb b/lib/gitlab/background_migration/backfill_project_repositories.rb index 1d9aa050041..263546bd132 100644 --- a/lib/gitlab/background_migration/backfill_project_repositories.rb +++ b/lib/gitlab/background_migration/backfill_project_repositories.rb @@ -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 diff --git a/lib/gitlab/sidekiq_middleware.rb b/lib/gitlab/sidekiq_middleware.rb index 439d45b7a14..6c27213df49 100644 --- a/lib/gitlab/sidekiq_middleware.rb +++ b/lib/gitlab/sidekiq_middleware.rb @@ -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 diff --git a/lib/gitlab/sidekiq_middleware/admin_mode/client.rb b/lib/gitlab/sidekiq_middleware/admin_mode/client.rb new file mode 100644 index 00000000000..e227ee654ee --- /dev/null +++ b/lib/gitlab/sidekiq_middleware/admin_mode/client.rb @@ -0,0 +1,34 @@ +# 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 diff --git a/lib/gitlab/sidekiq_middleware/admin_mode/server.rb b/lib/gitlab/sidekiq_middleware/admin_mode/server.rb new file mode 100644 index 00000000000..6366867a0fa --- /dev/null +++ b/lib/gitlab/sidekiq_middleware/admin_mode/server.rb @@ -0,0 +1,24 @@ +# 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 diff --git a/lib/microsoft_teams/notifier.rb b/lib/microsoft_teams/notifier.rb index 340bf709f5e..096e1e2ee96 100644 --- a/lib/microsoft_teams/notifier.rb +++ b/lib/microsoft_teams/notifier.rb @@ -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 diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index a252b7809b8..0a52b01af03 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -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! } diff --git a/scripts/trigger-build b/scripts/trigger-build index 6e50d8907d8..889dcc01043 100755 --- a/scripts/trigger-build +++ b/scripts/trigger-build @@ -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 diff --git a/spec/features/admin/admin_mode/workers_spec.rb b/spec/features/admin/admin_mode/workers_spec.rb new file mode 100644 index 00000000000..e33c9d7e64c --- /dev/null +++ b/spec/features/admin/admin_mode/workers_spec.rb @@ -0,0 +1,77 @@ +# 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 diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb index 3e8197588ed..954773e766d 100644 --- a/spec/features/admin/admin_uses_repository_checks_spec.rb +++ b/spec/features/admin/admin_uses_repository_checks_spec.rb @@ -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) diff --git a/spec/features/projects/clusters/eks_spec.rb b/spec/features/projects/clusters/eks_spec.rb index bb0072fc8dd..a856376cb4b 100644 --- a/spec/features/projects/clusters/eks_spec.rb +++ b/spec/features/projects/clusters/eks_spec.rb @@ -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 diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 09cd1c6a765..0143461eadb 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -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 } diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb index 3c82ae59cfa..daa987ea389 100644 --- a/spec/features/users/signup_spec.rb +++ b/spec/features/users/signup_spec.rb @@ -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 diff --git a/spec/frontend/jobs/store/mutations_spec.js b/spec/frontend/jobs/store/mutations_spec.js index d1ab152330e..d77690ffac0 100644 --- a/spec/frontend/jobs/store/mutations_spec.js +++ b/spec/frontend/jobs/store/mutations_spec.js @@ -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); }); diff --git a/spec/frontend/registry/settings/components/settings_form_spec.js b/spec/frontend/registry/settings/components/settings_form_spec.js index eefb0313a0b..2b3e529b283 100644 --- a/spec/frontend/registry/settings/components/settings_form_spec.js +++ b/spec/frontend/registry/settings/components/settings_form_spec.js @@ -1,7 +1,7 @@ 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); + }); + }); + }); }); diff --git a/spec/frontend/registry/shared/components/__snapshots__/expiration_policy_fields_spec.js.snap b/spec/frontend/registry/shared/components/__snapshots__/expiration_policy_fields_spec.js.snap new file mode 100644 index 00000000000..c5f5ea68d9e --- /dev/null +++ b/spec/frontend/registry/shared/components/__snapshots__/expiration_policy_fields_spec.js.snap @@ -0,0 +1,133 @@ +// 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> +`; diff --git a/spec/frontend/registry/shared/components/__snapshots__/expiration_policy_form_spec.js.snap b/spec/frontend/registry/shared/components/__snapshots__/expiration_policy_form_spec.js.snap deleted file mode 100644 index b53736951e1..00000000000 --- a/spec/frontend/registry/shared/components/__snapshots__/expiration_policy_form_spec.js.snap +++ /dev/null @@ -1,186 +0,0 @@ -// 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> -`; diff --git a/spec/frontend/registry/shared/components/expiration_policy_form_spec.js b/spec/frontend/registry/shared/components/expiration_policy_fields_spec.js index b51519925f1..b384fd62406 100644 --- a/spec/frontend/registry/shared/components/expiration_policy_form_spec.js +++ b/spec/frontend/registry/shared/components/expiration_policy_fields_spec.js @@ -1,6 +1,6 @@ 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(); }); }); diff --git a/spec/helpers/clusters_helper_spec.rb b/spec/helpers/clusters_helper_spec.rb index ff8394b9475..5651b899ed0 100644 --- a/spec/helpers/clusters_helper_spec.rb +++ b/spec/helpers/clusters_helper_spec.rb @@ -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 diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index 0fcd6080106..31b49c45908 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -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 => { diff --git a/spec/javascripts/jobs/store/actions_spec.js b/spec/javascripts/jobs/store/actions_spec.js index c0e8dbf9b22..47257688bd5 100644 --- a/spec/javascripts/jobs/store/actions_spec.js +++ b/spec/javascripts/jobs/store/actions_spec.js @@ -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); }); }); diff --git a/spec/lib/gitlab/auth/current_user_mode_spec.rb b/spec/lib/gitlab/auth/current_user_mode_spec.rb index 3b3db0f7315..7c2fdac6c25 100644 --- a/spec/lib/gitlab/auth/current_user_mode_spec.rb +++ b/spec/lib/gitlab/auth/current_user_mode_spec.rb @@ -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( diff --git a/spec/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config_spec.rb index 5cad479ff05..4714712f733 100644 --- a/spec/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config_spec.rb @@ -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) } diff --git a/spec/lib/gitlab/sidekiq_middleware/admin_mode/client_spec.rb b/spec/lib/gitlab/sidekiq_middleware/admin_mode/client_spec.rb new file mode 100644 index 00000000000..f6449bae8c3 --- /dev/null +++ b/spec/lib/gitlab/sidekiq_middleware/admin_mode/client_spec.rb @@ -0,0 +1,94 @@ +# 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 diff --git a/spec/lib/gitlab/sidekiq_middleware/admin_mode/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/admin_mode/server_spec.rb new file mode 100644 index 00000000000..60475f0e403 --- /dev/null +++ b/spec/lib/gitlab/sidekiq_middleware/admin_mode/server_spec.rb @@ -0,0 +1,72 @@ +# 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 diff --git a/spec/lib/gitlab/sidekiq_middleware_spec.rb b/spec/lib/gitlab/sidekiq_middleware_spec.rb index e8dcbbd2ee1..19242d25e27 100644 --- a/spec/lib/gitlab/sidekiq_middleware_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware_spec.rb @@ -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 diff --git a/spec/lib/microsoft_teams/notifier_spec.rb b/spec/lib/microsoft_teams/notifier_spec.rb index 64ab8d85807..25538db159e 100644 --- a/spec/lib/microsoft_teams/notifier_spec.rb +++ b/spec/lib/microsoft_teams/notifier_spec.rb @@ -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 diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 5c3f7c09e22..5aa74134692 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -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) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 438ed6d83fa..42151d86ac0 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -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 diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb index 81aee4cfcac..ae5af9e0f29 100644 --- a/spec/policies/base_policy_spec.rb +++ b/spec/policies/base_policy_spec.rb @@ -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) diff --git a/spec/services/error_tracking/issue_details_service_spec.rb b/spec/services/error_tracking/issue_details_service_spec.rb index 60d6172ef64..9f217deda21 100644 --- a/spec/services/error_tracking/issue_details_service_spec.rb +++ b/spec/services/error_tracking/issue_details_service_spec.rb @@ -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 diff --git a/spec/services/error_tracking/issue_latest_event_service_spec.rb b/spec/services/error_tracking/issue_latest_event_service_spec.rb index 7f53eabd717..078d7511850 100644 --- a/spec/services/error_tracking/issue_latest_event_service_spec.rb +++ b/spec/services/error_tracking/issue_latest_event_service_spec.rb @@ -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 diff --git a/spec/services/error_tracking/issue_update_service_spec.rb b/spec/services/error_tracking/issue_update_service_spec.rb index ad1dafe6ccc..78388328a22 100644 --- a/spec/services/error_tracking/issue_update_service_spec.rb +++ b/spec/services/error_tracking/issue_update_service_spec.rb @@ -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 diff --git a/spec/services/projects/after_rename_service_spec.rb b/spec/services/projects/after_rename_service_spec.rb index bf637b70aaf..b81dd3d7e3f 100644 --- a/spec/services/projects/after_rename_service_spec.rb +++ b/spec/services/projects/after_rename_service_spec.rb @@ -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" } diff --git a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb index ab9d2bdba8f..b0827f6a2ee 100644 --- a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb +++ b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb @@ -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) } diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb index 132b895fc35..71be335c11d 100644 --- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb +++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb @@ -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) } diff --git a/spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb b/spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb index c2ba9626f41..98b343371df 100644 --- a/spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb +++ b/spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb @@ -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) } diff --git a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb index 97c7c0af946..6dcd2ff4555 100644 --- a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb +++ b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb @@ -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) } diff --git a/spec/support/shared_contexts/sentry_error_tracking_shared_context.rb b/spec/support/shared_contexts/sentry_error_tracking_shared_context.rb index ee678580fb9..f06de53f0c1 100644 --- a/spec/support/shared_contexts/sentry_error_tracking_shared_context.rb +++ b/spec/support/shared_contexts/sentry_error_tracking_shared_context.rb @@ -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 |