summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop.yml5
-rw-r--r--app/assets/javascripts/api.js19
-rw-r--r--app/assets/javascripts/releases/components/app_edit.vue9
-rw-r--r--app/assets/javascripts/releases/components/asset_links_form.vue124
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/actions.js74
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/getters.js24
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/index.js2
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/mutation_types.js5
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/mutations.js28
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/state.js10
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb30
-rw-r--r--app/controllers/projects/releases_controller.rb1
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb28
-rw-r--r--app/finders/merge_requests_finder.rb1
-rw-r--r--app/helpers/releases_helper.rb7
-rw-r--r--app/policies/project_policy.rb1
-rw-r--r--app/services/groups/deploy_tokens/create_service.rb8
-rw-r--r--app/services/projects/deploy_tokens/create_service.rb8
-rw-r--r--app/views/shared/deploy_tokens/_form.html.haml2
-rw-r--r--changelogs/unreleased/ag-add-author-username-to-mr-api.yml5
-rw-r--r--changelogs/unreleased/chore-conditional-mocking-admin-mode-specs.yml5
-rw-r--r--changelogs/unreleased/refactor-user-specs.yml5
-rw-r--r--db/migrate/20200330074719_add_index_for_group_vsm_usage_ping.rb18
-rw-r--r--db/structure.sql3
-rw-r--r--doc/administration/gitaly/praefect.md6
-rw-r--r--doc/api/merge_requests.md10
-rw-r--r--doc/development/documentation/styleguide.md47
-rw-r--r--doc/development/pipelines.md6
-rw-r--r--doc/subscriptions/index.md5
-rw-r--r--doc/user/application_security/container_scanning/index.md2
-rw-r--r--doc/user/group/roadmap/index.md17
-rw-r--r--doc/user/project/integrations/prometheus.md9
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/api_guard.rb4
-rw-r--r--lib/api/deploy_tokens.rb16
-rw-r--r--lib/api/helpers/merge_requests_helpers.rb4
-rw-r--r--lib/api/runner.rb5
-rw-r--r--lib/api/terraform/state.rb44
-rw-r--r--lib/gitlab/auth/auth_finders.rb8
-rw-r--r--locale/gitlab.pot59
-rw-r--r--spec/controllers/groups/settings/ci_cd_controller_spec.rb82
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb80
-rw-r--r--spec/features/projects/settings/ci_cd_settings_spec.rb1
-rw-r--r--spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb1
-rw-r--r--spec/frontend/api_spec.js61
-rw-r--r--spec/frontend/releases/components/app_edit_spec.js31
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js105
-rw-r--r--spec/frontend/releases/stores/modules/detail/getters_spec.js59
-rw-r--r--spec/frontend/releases/stores/modules/detail/mutations_spec.js80
-rw-r--r--spec/helpers/releases_helper_spec.rb3
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb48
-rw-r--r--spec/models/user_spec.rb8
-rw-r--r--spec/policies/project_policy_spec.rb1
-rw-r--r--spec/requests/api/merge_requests_spec.rb46
-rw-r--r--spec/requests/api/runner_spec.rb13
-rw-r--r--spec/requests/api/terraform/state_spec.rb97
-rw-r--r--spec/spec_helper.rb57
-rw-r--r--spec/support/services/deploy_token_shared_examples.rb8
58 files changed, 1292 insertions, 154 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 5e2cde935b2..3935832bc6c 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -217,7 +217,6 @@ Gitlab/DuplicateSpecLocation:
- ee/spec/helpers/auth_helper_spec.rb
- ee/spec/lib/gitlab/gl_repository_spec.rb
- ee/spec/models/namespace_spec.rb
- - ee/spec/serializers/environment_entity_spec.rb
- ee/spec/services/issues/create_service_spec.rb
- ee/spec/services/merge_requests/create_service_spec.rb
- ee/spec/services/merge_requests/refresh_service_spec.rb
@@ -225,7 +224,6 @@ Gitlab/DuplicateSpecLocation:
- ee/spec/services/system_hooks_service_spec.rb
- ee/spec/helpers/ee/auth_helper_spec.rb
- ee/spec/models/ee/namespace_spec.rb
- - ee/spec/serializers/ee/environment_entity_spec.rb
- ee/spec/services/ee/issues/create_service_spec.rb
- ee/spec/services/ee/merge_requests/create_service_spec.rb
- ee/spec/services/ee/merge_requests/refresh_service_spec.rb
@@ -414,15 +412,12 @@ RSpec/RepeatedExample:
- 'spec/models/concerns/issuable_spec.rb'
- 'spec/models/member_spec.rb'
- 'spec/models/project_services/chat_message/pipeline_message_spec.rb'
- - 'spec/models/user_spec.rb'
- 'spec/models/wiki_page_spec.rb'
- - 'spec/requests/api/merge_requests_spec.rb'
- 'spec/routing/admin_routing_spec.rb'
- 'spec/rubocop/cop/migration/update_large_table_spec.rb'
- 'spec/services/notification_service_spec.rb'
- 'spec/services/web_hook_service_spec.rb'
- 'ee/spec/models/group_spec.rb'
- - 'ee/spec/requests/api/merge_request_approvals_spec.rb'
- 'ee/spec/services/boards/lists/update_service_spec.rb'
- 'ee/spec/services/geo/repository_verification_primary_service_spec.rb'
- 'ee/spec/workers/geo/file_download_dispatch_worker_spec.rb'
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 14381f63e4b..797eaf629bf 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -41,6 +41,8 @@ const Api = {
createBranchPath: '/api/:version/projects/:id/repository/branches',
releasesPath: '/api/:version/projects/:id/releases',
releasePath: '/api/:version/projects/:id/releases/:tag_name',
+ releaseLinksPath: '/api/:version/projects/:id/releases/:tag_name/assets/links',
+ releaseLinkPath: '/api/:version/projects/:id/releases/:tag_name/assets/links/:link_id',
mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
adminStatisticsPath: '/api/:version/application/statistics',
pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id',
@@ -460,6 +462,23 @@ const Api = {
return axios.put(url, release);
},
+ createReleaseLink(projectPath, tagName, link) {
+ const url = Api.buildUrl(this.releaseLinksPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':tag_name', encodeURIComponent(tagName));
+
+ return axios.post(url, link);
+ },
+
+ deleteReleaseLink(projectPath, tagName, linkId) {
+ const url = Api.buildUrl(this.releaseLinkPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':tag_name', encodeURIComponent(tagName))
+ .replace(':link_id', encodeURIComponent(linkId));
+
+ return axios.delete(url);
+ },
+
adminStatistics() {
const url = Api.buildUrl(this.adminStatisticsPath);
return axios.get(url);
diff --git a/app/assets/javascripts/releases/components/app_edit.vue b/app/assets/javascripts/releases/components/app_edit.vue
index 6f4baaa5d74..4fa0e96217a 100644
--- a/app/assets/javascripts/releases/components/app_edit.vue
+++ b/app/assets/javascripts/releases/components/app_edit.vue
@@ -7,6 +7,8 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { BACK_URL_PARAM } from '~/releases/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
+import AssetLinksForm from './asset_links_form.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'ReleaseEditApp',
@@ -16,10 +18,12 @@ export default {
GlButton,
GlLink,
MarkdownField,
+ AssetLinksForm,
},
directives: {
autofocusonshow,
},
+ mixins: [glFeatureFlagsMixin()],
computed: {
...mapState('detail', [
'isFetchingRelease',
@@ -80,6 +84,9 @@ export default {
cancelPath() {
return getParameterByName(BACK_URL_PARAM) || this.releasesPagePath;
},
+ showAssetLinksForm() {
+ return this.glFeatures.releaseAssetLinkEditing;
+ },
},
created() {
this.fetchRelease();
@@ -153,6 +160,8 @@ export default {
</div>
</gl-form-group>
+ <asset-links-form v-if="showAssetLinksForm" />
+
<div class="d-flex pt-3">
<gl-button
class="mr-auto js-submit-button"
diff --git a/app/assets/javascripts/releases/components/asset_links_form.vue b/app/assets/javascripts/releases/components/asset_links_form.vue
new file mode 100644
index 00000000000..f4c92477775
--- /dev/null
+++ b/app/assets/javascripts/releases/components/asset_links_form.vue
@@ -0,0 +1,124 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import {
+ GlSprintf,
+ GlLink,
+ GlFormGroup,
+ GlButton,
+ GlIcon,
+ GlTooltipDirective,
+ GlFormInput,
+} from '@gitlab/ui';
+
+export default {
+ name: 'AssetLinksForm',
+ components: { GlSprintf, GlLink, GlFormGroup, GlButton, GlIcon, GlFormInput },
+ directives: { GlTooltip: GlTooltipDirective },
+ computed: {
+ ...mapState('detail', ['release', 'releaseAssetsDocsPath']),
+ },
+ created() {
+ this.addEmptyAssetLink();
+ },
+ methods: {
+ ...mapActions('detail', [
+ 'addEmptyAssetLink',
+ 'updateAssetLinkUrl',
+ 'updateAssetLinkName',
+ 'removeAssetLink',
+ ]),
+ onAddAnotherClicked() {
+ this.addEmptyAssetLink();
+ },
+ onRemoveClicked(linkId) {
+ this.removeAssetLink(linkId);
+ },
+ onUrlInput(linkIdToUpdate, newUrl) {
+ this.updateAssetLinkUrl({ linkIdToUpdate, newUrl });
+ },
+ onLinkTitleInput(linkIdToUpdate, newName) {
+ this.updateAssetLinkName({ linkIdToUpdate, newName });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="d-flex flex-column release-assets-links-form">
+ <h2 class="text-4">{{ __('Release assets') }}</h2>
+ <p class="m-0">
+ <gl-sprintf
+ :message="
+ __(
+ 'Add %{linkStart}assets%{linkEnd} to your Release. GitLab automatically includes read-only assets, like source code and release evidence.',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link
+ :href="releaseAssetsDocsPath"
+ target="_blank"
+ :aria-label="__('Release assets documentation')"
+ >
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <h3 class="text-3">{{ __('Links') }}</h3>
+ <p>
+ {{
+ __(
+ 'Point to any links you like: documentation, built binaries, or other related materials. These can be internal or external links from your GitLab instance.',
+ )
+ }}
+ </p>
+ <div
+ v-for="(link, index) in release.assets.links"
+ :key="link.id"
+ class="d-flex flex-column flex-sm-row align-items-stretch align-items-sm-end"
+ >
+ <gl-form-group
+ class="url-field form-group flex-grow-1 mr-sm-4"
+ :label="__('URL')"
+ :label-for="`asset-url-${index}`"
+ >
+ <gl-form-input
+ :id="`asset-url-${index}`"
+ :value="link.url"
+ type="text"
+ class="form-control"
+ @change="onUrlInput(link.id, $event)"
+ />
+ </gl-form-group>
+
+ <gl-form-group
+ class="link-title-field flex-grow-1 mr-sm-4"
+ :label="__('Link title')"
+ :label-for="`asset-link-name-${index}`"
+ >
+ <gl-form-input
+ :id="`asset-link-name-${index}`"
+ :value="link.name"
+ type="text"
+ class="form-control"
+ @change="onLinkTitleInput(link.id, $event)"
+ />
+ </gl-form-group>
+
+ <gl-button
+ v-gl-tooltip
+ class="mb-5 mb-sm-3 flex-grow-0 flex-shrink-0 remove-button"
+ :aria-label="__('Remove asset link')"
+ :title="__('Remove asset link')"
+ @click="onRemoveClicked(link.id)"
+ >
+ <gl-icon class="m-0" name="remove" />
+ <span class="d-inline d-sm-none">{{ __('Remove asset link') }}</span>
+ </gl-button>
+ </div>
+ <gl-button variant="link" class="align-self-end mb-5 mb-sm-0" @click="onAddAnotherClicked">
+ {{ __('Add another link') }}
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/releases/stores/modules/detail/actions.js b/app/assets/javascripts/releases/stores/modules/detail/actions.js
index 1b77f01368e..7b84c18242c 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/actions.js
@@ -41,20 +41,74 @@ export const receiveUpdateReleaseError = ({ commit }, error) => {
createFlash(s__('Release|Something went wrong while saving the release details'));
};
-export const updateRelease = ({ dispatch, state }) => {
+export const updateRelease = ({ dispatch, state, getters }) => {
dispatch('requestUpdateRelease');
- return api
- .updateRelease(state.projectId, state.tagName, {
- name: state.release.name,
- description: state.release.description,
- })
- .then(() => dispatch('receiveUpdateReleaseSuccess'))
- .catch(error => {
- dispatch('receiveUpdateReleaseError', error);
- });
+ const { release } = state;
+
+ return (
+ api
+ .updateRelease(state.projectId, state.tagName, {
+ name: release.name,
+ description: release.description,
+ })
+
+ /**
+ * Currently, we delete all existing links and then
+ * recreate new ones on each edit. This is because the
+ * REST API doesn't support bulk updating of Release links,
+ * and updating individual links can lead to validation
+ * race conditions (in particular, the "URLs must be unique")
+ * constraint.
+ *
+ * This isn't ideal since this is no longer an atomic
+ * operation - parts of it can fail while others succeed,
+ * leaving the Release in an inconsistent state.
+ *
+ * This logic should be refactored to use GraphQL once
+ * https://gitlab.com/gitlab-org/gitlab/-/issues/208702
+ * is closed.
+ */
+
+ .then(() => {
+ // Delete all links currently associated with this Release
+ return Promise.all(
+ getters.releaseLinksToDelete.map(l =>
+ api.deleteReleaseLink(state.projectId, release.tagName, l.id),
+ ),
+ );
+ })
+ .then(() => {
+ // Create a new link for each link in the form
+ return Promise.all(
+ getters.releaseLinksToCreate.map(l =>
+ api.createReleaseLink(state.projectId, release.tagName, l),
+ ),
+ );
+ })
+ .then(() => dispatch('receiveUpdateReleaseSuccess'))
+ .catch(error => {
+ dispatch('receiveUpdateReleaseError', error);
+ })
+ );
};
export const navigateToReleasesPage = ({ state }) => {
redirectTo(state.releasesPagePath);
};
+
+export const addEmptyAssetLink = ({ commit }) => {
+ commit(types.ADD_EMPTY_ASSET_LINK);
+};
+
+export const updateAssetLinkUrl = ({ commit }, { linkIdToUpdate, newUrl }) => {
+ commit(types.UPDATE_ASSET_LINK_URL, { linkIdToUpdate, newUrl });
+};
+
+export const updateAssetLinkName = ({ commit }, { linkIdToUpdate, newName }) => {
+ commit(types.UPDATE_ASSET_LINK_NAME, { linkIdToUpdate, newName });
+};
+
+export const removeAssetLink = ({ commit }, linkIdToRemove) => {
+ commit(types.REMOVE_ASSET_LINK, linkIdToRemove);
+};
diff --git a/app/assets/javascripts/releases/stores/modules/detail/getters.js b/app/assets/javascripts/releases/stores/modules/detail/getters.js
new file mode 100644
index 00000000000..562284dc48d
--- /dev/null
+++ b/app/assets/javascripts/releases/stores/modules/detail/getters.js
@@ -0,0 +1,24 @@
+/**
+ * @returns {Boolean} `true` if the release link is empty, i.e. it has
+ * empty (or whitespace-only) values for both `url` and `name`.
+ * Otherwise, `false`.
+ */
+const isEmptyReleaseLink = l => !/\S/.test(l.url) && !/\S/.test(l.name);
+
+/** Returns all release links that aren't empty */
+export const releaseLinksToCreate = state => {
+ if (!state.release) {
+ return [];
+ }
+
+ return state.release.assets.links.filter(l => !isEmptyReleaseLink(l));
+};
+
+/** Returns all release links that should be deleted */
+export const releaseLinksToDelete = state => {
+ if (!state.originalRelease) {
+ return [];
+ }
+
+ return state.originalRelease.assets.links;
+};
diff --git a/app/assets/javascripts/releases/stores/modules/detail/index.js b/app/assets/javascripts/releases/stores/modules/detail/index.js
index b4430cff2ab..40fdb04f2eb 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/index.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/index.js
@@ -1,10 +1,12 @@
import * as actions from './actions';
+import * as getters from './getters';
import mutations from './mutations';
import createState from './state';
export default initialState => ({
namespaced: true,
actions,
+ getters,
mutations,
state: createState(initialState),
});
diff --git a/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js b/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js
index 51c0590012a..04944b76e42 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js
@@ -8,3 +8,8 @@ export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES';
export const REQUEST_UPDATE_RELEASE = 'REQUEST_UPDATE_RELEASE';
export const RECEIVE_UPDATE_RELEASE_SUCCESS = 'RECEIVE_UPDATE_RELEASE_SUCCESS';
export const RECEIVE_UPDATE_RELEASE_ERROR = 'RECEIVE_UPDATE_RELEASE_ERROR';
+
+export const ADD_EMPTY_ASSET_LINK = 'ADD_EMPTY_ASSET_LINK';
+export const UPDATE_ASSET_LINK_URL = 'UPDATE_ASSET_LINK_URL';
+export const UPDATE_ASSET_LINK_NAME = 'UPDATE_ASSET_LINK_NAME';
+export const REMOVE_ASSET_LINK = 'REMOVE_ASSET_LINK';
diff --git a/app/assets/javascripts/releases/stores/modules/detail/mutations.js b/app/assets/javascripts/releases/stores/modules/detail/mutations.js
index 913db6c2b2a..3d97e3a75c2 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/mutations.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/mutations.js
@@ -1,4 +1,9 @@
import * as types from './mutation_types';
+import { uniqueId, cloneDeep } from 'lodash';
+
+const findReleaseLink = (release, id) => {
+ return release.assets.links.find(l => l.id === id);
+};
export default {
[types.REQUEST_RELEASE](state) {
@@ -8,6 +13,7 @@ export default {
state.fetchError = undefined;
state.isFetchingRelease = false;
state.release = data;
+ state.originalRelease = Object.freeze(cloneDeep(state.release));
},
[types.RECEIVE_RELEASE_ERROR](state, error) {
state.fetchError = error;
@@ -33,4 +39,26 @@ export default {
state.updateError = error;
state.isUpdatingRelease = false;
},
+
+ [types.ADD_EMPTY_ASSET_LINK](state) {
+ state.release.assets.links.push({
+ id: uniqueId('new-link-'),
+ url: '',
+ name: '',
+ });
+ },
+
+ [types.UPDATE_ASSET_LINK_URL](state, { linkIdToUpdate, newUrl }) {
+ const linkToUpdate = findReleaseLink(state.release, linkIdToUpdate);
+ linkToUpdate.url = newUrl;
+ },
+
+ [types.UPDATE_ASSET_LINK_NAME](state, { linkIdToUpdate, newName }) {
+ const linkToUpdate = findReleaseLink(state.release, linkIdToUpdate);
+ linkToUpdate.name = newName;
+ },
+
+ [types.REMOVE_ASSET_LINK](state, linkIdToRemove) {
+ state.release.assets.links = state.release.assets.links.filter(l => l.id !== linkIdToRemove);
+ },
};
diff --git a/app/assets/javascripts/releases/stores/modules/detail/state.js b/app/assets/javascripts/releases/stores/modules/detail/state.js
index a19e8d044e2..b513e1bed79 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/state.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/state.js
@@ -5,6 +5,7 @@ export default ({
markdownDocsPath,
markdownPreviewPath,
updateReleaseApiDocsPath,
+ releaseAssetsDocsPath,
}) => ({
projectId,
tagName,
@@ -12,9 +13,18 @@ export default ({
markdownDocsPath,
markdownPreviewPath,
updateReleaseApiDocsPath,
+ releaseAssetsDocsPath,
+ /** The Release object */
release: null,
+ /**
+ * A deep clone of the Release object above.
+ * Used when editing this Release so that
+ * changes can be computed.
+ */
+ originalRelease: null,
+
isFetchingRelease: false,
fetchError: null,
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index ffa3f2c3364..989013df8d4 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -8,6 +8,7 @@ module Groups
before_action :authorize_update_max_artifacts_size!, only: [:update]
before_action do
push_frontend_feature_flag(:new_variables_ui, @group)
+ push_frontend_feature_flag(:ajax_new_deploy_token, @group)
end
before_action :define_variables, only: [:show, :create_deploy_token]
@@ -42,13 +43,30 @@ module Groups
end
def create_deploy_token
- @new_deploy_token = Groups::DeployTokens::CreateService.new(@group, current_user, deploy_token_params).execute
-
- if @new_deploy_token.persisted?
- flash.now[:notice] = s_('DeployTokens|Your new group deploy token has been created.')
+ result = Projects::DeployTokens::CreateService.new(@group, current_user, deploy_token_params).execute
+ @new_deploy_token = result[:deploy_token]
+
+ if result[:status] == :success
+ respond_to do |format|
+ format.json do
+ # IMPORTANT: It's a security risk to expose the token value more than just once here!
+ json = API::Entities::DeployTokenWithToken.represent(@new_deploy_token).as_json
+ render json: json, status: result[:http_status]
+ end
+ format.html do
+ flash.now[:notice] = s_('DeployTokens|Your new group deploy token has been created.')
+ render :show
+ end
+ end
+ else
+ respond_to do |format|
+ format.json { render json: { message: result[:message] }, status: result[:http_status] }
+ format.html do
+ flash.now[:alert] = result[:message]
+ render :show
+ end
+ end
end
-
- render 'show'
end
private
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index fc60f42095c..010e9411c7d 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -9,6 +9,7 @@ class Projects::ReleasesController < Projects::ApplicationController
push_frontend_feature_flag(:release_issue_summary, project, default_enabled: true)
push_frontend_feature_flag(:release_evidence_collection, project, default_enabled: true)
push_frontend_feature_flag(:release_show_page, project, default_enabled: true)
+ push_frontend_feature_flag(:release_asset_link_editing, project)
end
before_action :authorize_update_release!, only: %i[edit update]
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index aac6ecb07e4..5feb3e019c2 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -7,6 +7,7 @@ module Projects
before_action :define_variables
before_action do
push_frontend_feature_flag(:new_variables_ui, @project)
+ push_frontend_feature_flag(:ajax_new_deploy_token, @project)
end
def show
@@ -47,13 +48,30 @@ module Projects
end
def create_deploy_token
- @new_deploy_token = Projects::DeployTokens::CreateService.new(@project, current_user, deploy_token_params).execute
+ result = Projects::DeployTokens::CreateService.new(@project, current_user, deploy_token_params).execute
+ @new_deploy_token = result[:deploy_token]
- if @new_deploy_token.persisted?
- flash.now[:notice] = s_('DeployTokens|Your new project deploy token has been created.')
+ if result[:status] == :success
+ respond_to do |format|
+ format.json do
+ # IMPORTANT: It's a security risk to expose the token value more than just once here!
+ json = API::Entities::DeployTokenWithToken.represent(@new_deploy_token).as_json
+ render json: json, status: result[:http_status]
+ end
+ format.html do
+ flash.now[:notice] = s_('DeployTokens|Your new project deploy token has been created.')
+ render :show
+ end
+ end
+ else
+ respond_to do |format|
+ format.json { render json: { message: result[:message] }, status: result[:http_status] }
+ format.html do
+ flash.now[:alert] = result[:message]
+ render :show
+ end
+ end
end
-
- render 'show'
end
private
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 410ad645cd9..d5e6c4783c1 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -14,6 +14,7 @@
# milestone_title: string
# release_tag: string
# author_id: integer
+# author_username: string
# assignee_id: integer
# search: string
# in: 'title', 'description', or a string joining them with comma
diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb
index 6fbef800faa..af51427dc91 100644
--- a/app/helpers/releases_helper.rb
+++ b/app/helpers/releases_helper.rb
@@ -8,8 +8,8 @@ module ReleasesHelper
image_path(IMAGE_PATH)
end
- def help_page
- help_page_path(DOCUMENTATION_PATH)
+ def help_page(anchor: nil)
+ help_page_path(DOCUMENTATION_PATH, anchor: anchor)
end
def data_for_releases_page
@@ -29,7 +29,8 @@ module ReleasesHelper
markdown_preview_path: preview_markdown_path(@project),
markdown_docs_path: help_page_path('user/markdown'),
releases_page_path: project_releases_path(@project, anchor: @release.tag),
- update_release_api_docs_path: help_page_path('api/releases/index.md', anchor: 'update-a-release')
+ update_release_api_docs_path: help_page_path('api/releases/index.md', anchor: 'update-a-release'),
+ release_assets_docs_path: help_page(anchor: 'release-assets')
}
end
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 99aeca17699..e694963eac0 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -318,6 +318,7 @@ class ProjectPolicy < BasePolicy
enable :read_pod_logs
enable :destroy_deploy_token
enable :read_prometheus_alerts
+ enable :admin_terraform_state
end
rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
diff --git a/app/services/groups/deploy_tokens/create_service.rb b/app/services/groups/deploy_tokens/create_service.rb
index 8c42b56ebb0..d747dabcd3c 100644
--- a/app/services/groups/deploy_tokens/create_service.rb
+++ b/app/services/groups/deploy_tokens/create_service.rb
@@ -6,7 +6,13 @@ module Groups
include DeployTokenMethods
def execute
- create_deploy_token_for(@group, params)
+ deploy_token = create_deploy_token_for(@group, params)
+
+ if deploy_token.persisted?
+ success(deploy_token: deploy_token, http_status: :ok)
+ else
+ error(deploy_token.errors.full_messages.to_sentence, :bad_request)
+ end
end
end
end
diff --git a/app/services/projects/deploy_tokens/create_service.rb b/app/services/projects/deploy_tokens/create_service.rb
index 51cb68dfb10..e943b2489ac 100644
--- a/app/services/projects/deploy_tokens/create_service.rb
+++ b/app/services/projects/deploy_tokens/create_service.rb
@@ -6,7 +6,13 @@ module Projects
include DeployTokenMethods
def execute
- create_deploy_token_for(@project, params)
+ deploy_token = create_deploy_token_for(@project, params)
+
+ if deploy_token.persisted?
+ success(deploy_token: deploy_token, http_status: :ok)
+ else
+ error(deploy_token.errors.full_messages.to_sentence, :bad_request)
+ end
end
end
end
diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml
index 99e259ba944..c4e82d8e157 100644
--- a/app/views/shared/deploy_tokens/_form.html.haml
+++ b/app/views/shared/deploy_tokens/_form.html.haml
@@ -1,7 +1,7 @@
%p.profile-settings-content
= s_("DeployTokens|Pick a name for the application, and we'll give you a unique deploy token.")
-= form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post do |f|
+= form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post, remote: Feature.enabled?(:ajax_new_deploy_token, group_or_project) do |f|
= form_errors(token)
.form-group
diff --git a/changelogs/unreleased/ag-add-author-username-to-mr-api.yml b/changelogs/unreleased/ag-add-author-username-to-mr-api.yml
new file mode 100644
index 00000000000..dd01deebd6b
--- /dev/null
+++ b/changelogs/unreleased/ag-add-author-username-to-mr-api.yml
@@ -0,0 +1,5 @@
+---
+title: Accept `author_username` as a param in Merge Requests API
+merge_request: 28100
+author:
+type: changed
diff --git a/changelogs/unreleased/chore-conditional-mocking-admin-mode-specs.yml b/changelogs/unreleased/chore-conditional-mocking-admin-mode-specs.yml
new file mode 100644
index 00000000000..d26ecc6c4f6
--- /dev/null
+++ b/changelogs/unreleased/chore-conditional-mocking-admin-mode-specs.yml
@@ -0,0 +1,5 @@
+---
+title: Conditional mocking of admin mode in specs by directory
+merge_request: 28420
+author: Diego Louzán
+type: other
diff --git a/changelogs/unreleased/refactor-user-specs.yml b/changelogs/unreleased/refactor-user-specs.yml
new file mode 100644
index 00000000000..7e473dbe7c2
--- /dev/null
+++ b/changelogs/unreleased/refactor-user-specs.yml
@@ -0,0 +1,5 @@
+---
+title: Remove repeated examples in user model specs
+merge_request: 28450
+author: Rajendra Kadam
+type: changed
diff --git a/db/migrate/20200330074719_add_index_for_group_vsm_usage_ping.rb b/db/migrate/20200330074719_add_index_for_group_vsm_usage_ping.rb
new file mode 100644
index 00000000000..55ae2383d71
--- /dev/null
+++ b/db/migrate/20200330074719_add_index_for_group_vsm_usage_ping.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddIndexForGroupVsmUsagePing < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'index_analytics_cycle_analytics_group_stages_custom_only'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :analytics_cycle_analytics_group_stages, :id, where: 'custom = true', name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :analytics_cycle_analytics_group_stages, INDEX_NAME
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 1a4b8582ab0..aa43d49b6dc 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -8520,6 +8520,8 @@ CREATE INDEX index_analytics_ca_project_stages_on_relative_position ON public.an
CREATE INDEX index_analytics_ca_project_stages_on_start_event_label_id ON public.analytics_cycle_analytics_project_stages USING btree (start_event_label_id);
+CREATE INDEX index_analytics_cycle_analytics_group_stages_custom_only ON public.analytics_cycle_analytics_group_stages USING btree (id) WHERE (custom = true);
+
CREATE INDEX index_application_settings_on_custom_project_templates_group_id ON public.application_settings USING btree (custom_project_templates_group_id);
CREATE INDEX index_application_settings_on_file_template_project_id ON public.application_settings USING btree (file_template_project_id);
@@ -12909,5 +12911,6 @@ COPY "schema_migrations" (version) FROM STDIN;
20200326135443
20200326144443
20200326145443
+20200330074719
\.
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index b010a6425c8..a34c6c0b336 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -541,6 +541,12 @@ Particular attention should be shown to:
gitlab-ctl reconfigure
```
+1. Verify each `gitlab-shell` on each Gitaly instance can reach GitLab. On each Gitaly instance run:
+
+ ```shell
+ /opt/gitlab/embedded/service/gitlab-shell/bin/check -config /opt/gitlab/embedded/service/gitlab-shell/config.yml
+ ```
+
1. Verify that GitLab can reach Praefect:
```shell
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 881fc27feec..454d4a2b35b 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -30,6 +30,7 @@ GET /merge_requests?state=all
GET /merge_requests?milestone=release
GET /merge_requests?labels=bug,reproduced
GET /merge_requests?author_id=5
+GET /merge_requests?author_username=gitlab-bot
GET /merge_requests?my_reaction_emoji=star
GET /merge_requests?scope=assigned_to_me
GET /merge_requests?search=foo&in=title
@@ -51,7 +52,8 @@ Parameters:
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead. |
-| `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me` |
+| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`
+| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. _([Introduced][ce-13060] in GitLab 12.10)_ | |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. |
| `approver_ids` **(STARTER)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
| `approved_by_ids` **(STARTER)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
@@ -230,7 +232,8 @@ Parameters:
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13060] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
-| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
+| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. _([Introduced][ce-13060] in GitLab 9.5)_
+| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. _([Introduced][ce-13060] in GitLab 12.10)_ | |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ |
| `approver_ids` **(STARTER)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
| `approved_by_ids` **(STARTER)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
@@ -392,7 +395,8 @@ Parameters:
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> |
-| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
+| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. _([Introduced][ce-13060] in GitLab 9.5)_
+| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. _([Introduced][ce-13060] in GitLab 12.10)_ | |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ |
| `approver_ids` **(STARTER)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
| `approved_by_ids` **(STARTER)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index b231fa45f6d..940c660dc35 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -243,12 +243,35 @@ Do not include the same information in multiple places. [Link to a SSOT instead.
## Language
-- Use inclusive language and avoid jargon, as well as uncommon
- words. The docs should be clear and easy to understand.
-- Do not write in the first person singular. Instead of "I" or "me," use "we," "you," "us," or "one."
-- Be clear, concise, and stick to the goal of the doc.
+GitLab documentation should be clear and easy to understand.
+
+- Be clear, concise, and stick to the goal of the documentation.
- Write in US English with US grammar.
+- Use inclusive language.
+- Avoid jargon.
+- Avoid uncommon words.
+- Don't write in the first person singular.
+ - Instead of "I" or "me," use "we," "you," "us," or "one."
+
+### Point of view
+
+In most cases, it’s appropriate to use the second-person (you, yours) point of view,
+because it’s friendly and easy to understand.
+
+<!-- How do we harmonize the second person in Pajamas with our first person plural in our doc guide? -->
+
+### Capitalization
+
- Capitalize "G" and "L" in GitLab.
+- Use sentence case for:
+ - Titles.
+ - Labels.
+ - Menu items.
+ - Buttons.
+ - Headings. Don't capitalize other words in the title, unless
+ it refers to a product feature. For example:
+ - Capitalizing "issues" is acceptable in
+ `## What you can do with GitLab Issues`, but not in `## Closing multiple issues`.
- Use title case when referring to:
- [GitLab Features](https://about.gitlab.com/features/). For example, Issue Board,
Geo, and Runner.
@@ -335,8 +358,6 @@ as even native users of English might misunderstand them.
- [Write in Markdown](#markdown).
- Splitting long lines (preferably up to 100 characters) can make it easier to provide feedback on small chunks of text.
- Insert an empty line for new paragraphs.
-- Add a new line by ending a line with two spaces. [Using a backslash](../../user/markdown.md#newlines) doesn't work in the docs site.
-- Use sentence case for titles, headings, labels, menu items, and buttons.
- Insert an empty line between different markups (for example, after every paragraph, header, list, and so on). Example:
```md
@@ -572,13 +593,10 @@ For other punctuation rules, please refer to the
- Leave exactly one blank line before and after a heading.
- Do not use links in headings.
- Add the corresponding [product badge](#product-badges) according to the tier the feature belongs.
-- Use sentence case in headings. Do not capitalize the words of the title, unless
- it refers to a product feature. For example, capitalizing "issues" is acceptable in
- `## What you can do with GitLab Issues`, but not in `## Closing multiple issues`.
-- Our docs site search engine prioritizes headings, therefore, make sure to write
- headings that contextualize the subject and help to take the user to the right
- document. For example, `## Examples` is a bad heading; `## GitLab Pages examples`
- is a better one. It's not an exact science, but please consider this carefully.
+- Our docs site search engine prioritizes words used in headings and subheadings.
+ Make you subheading titles clear, descriptive, and complete to help users find the
+ right example, as shown in the section on [heading titles](#heading-titles).
+- See [Capitalization](#capitalization) for guidelines on capitalizing headings.
### Heading titles
@@ -589,6 +607,9 @@ Keep heading titles clear and direct. Make every word count. To accommodate sear
| Configure GDK | Configuring GDK |
| GitLab Release and Maintenance Policy | This section covers GitLab's Release and Maintenance Policy |
| Backport to older releases | Backporting to older releases |
+| GitLab Pages examples | Examples |
+
+For guidelines on capitalizing headings, see the section on [capitalization](#capitalization).
NOTE: **Note:**
If you change an existing title, be careful. Any such changes may affect not only [links](#anchor-links)
diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md
index bfd184d7d5a..8e13c5e7efc 100644
--- a/doc/development/pipelines.md
+++ b/doc/development/pipelines.md
@@ -38,11 +38,9 @@ The current stages are:
## Default image
-The default image is currently
-`registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34`.
+The default image is defined in <https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab-ci.yml>.
-It includes Ruby 2.6.5, Go 1.12, Git 2.24, Git LFS 2.9, Chrome 73, Node 12, Yarn 1.21,
-PostgreSQL 9.6, and Graphics Magick 1.3.34.
+It includes Ruby, Go, Git, Git LFS, Chrome, Node, Yarn, PostgreSQL, and Graphics Magick.
The images used in our pipelines are configured in the
[`gitlab-org/gitlab-build-images`](https://gitlab.com/gitlab-org/gitlab-build-images)
diff --git a/doc/subscriptions/index.md b/doc/subscriptions/index.md
index e3dd46af6bf..1c8ecec139d 100644
--- a/doc/subscriptions/index.md
+++ b/doc/subscriptions/index.md
@@ -68,7 +68,10 @@ A self-managed subscription uses a hybrid model. You pay for a subscription acco
Every occupied seat, whether by person, job, or bot is counted in the subscription, with the following exceptions:
-- Blocked users who are blocked prior to the renewal of a subscription won't be counted as active users for the renewal subscription. They may count as active users in the subscription period in which they were originally added.
+- [Deactivated](../user/admin_area/activating_deactivating_users.md#deactivating-a-user) and
+[blocked](../user/admin_area/blocking_unblocking_users.md) users who are restricted prior to the
+renewal of a subscription won't be counted as active users for the renewal subscription. They may
+count as active users in the subscription period in which they were originally added.
- Members with Guest permissions on an Ultimate subscription.
- GitLab-created service accounts: `Ghost User` and `Support Bot`.
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index d1ded52585e..86b18d4012b 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -230,7 +230,7 @@ Container Scanning can be executed on an offline GitLab Ultimate installation by
```
1. If your local Docker container registry is running securely over `HTTPS`, but you're using a
- self-signed certificate, then you must set `DOCKER_INSECURE: true` in the above
+ self-signed certificate, then you must set `DOCKER_INSECURE: "true"` in the above
`container_scanning` section of your `.gitlab-ci.yml`.
It may be worthwhile to set up a [scheduled pipeline](../../../ci/pipelines/schedules.md) to automatically build a new version of the vulnerabilities database on a preset schedule. You can use the following `.gitlab-yml.ci` as a template:
diff --git a/doc/user/group/roadmap/index.md b/doc/user/group/roadmap/index.md
index 08f1e4af6b0..043a37d735b 100644
--- a/doc/user/group/roadmap/index.md
+++ b/doc/user/group/roadmap/index.md
@@ -9,25 +9,28 @@ type: reference
> - In [GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/issues/5164) and later, the epic bars show their title, progress, and completed weight percentage.
An Epic within a group containing **Start date** and/or **Due date**
-can be visualized in a form of a timeline (e.g. a Gantt chart). The Epics Roadmap page
+can be visualized in a form of a timeline (a Gantt chart). The Epics Roadmap page
shows such a visualization for all the epics which are under a group and/or its subgroups.
On the epic bars, you can see their title, progress, and completed weight percentage.
When you hover over an epic bar, a popover appears with its title, start and due dates, and weight
completed.
+You can expand epics that contain child epics to show their child epics in the roadmap.
+You can click the chevron **{chevron-down}** next to the epic title to expand and collapse the child epics.
+
![roadmap view](img/roadmap_view_v12_9.png)
-A dropdown allows you to show only open or closed epics. By default, all epics are shown.
+A dropdown menu allows you to show only open or closed epics. By default, all epics are shown.
![epics state dropdown](img/epics_state_dropdown.png)
-Epics in the view can be sorted by:
+You can sort epics in the Roadmap view by:
-- **Created date**
-- **Last updated**
-- **Start date**
-- **Due date**
+- Created date
+- Last updated
+- Start date
+- Due date
Each option contains a button that toggles the sort order between **ascending** and **descending**. The sort option and order will be persisted when browsing Epics,
including the [epics list view](../epics/index.md).
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index 7968f5d1463..5a070db9439 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -637,6 +637,15 @@ clicking on the context menu in the upper-right corner.
If you use the **Timeline zoom** function at the bottom of the chart, logs will narrow down to the time range you selected.
+### Timeline zoom and URL sharing
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/198910) in GitLab 12.8.
+
+You can use the **Timeline zoom** function at the bottom of a chart to zoom in
+on a date and time of your choice. When you click and drag the sliders to select
+a different beginning or end date of data to display, GitLab adds your selected start
+and end times to the URL, enabling you to share specific timeframes more easily.
+
### Downloading data as CSV
Data from Prometheus charts on the metrics dashboard can be downloaded as CSV.
diff --git a/lib/api/api.rb b/lib/api/api.rb
index bc333880bbd..51fc006ec08 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -172,6 +172,7 @@ module API
mount ::API::ProjectSnippets
mount ::API::ProjectStatistics
mount ::API::ProjectTemplates
+ mount ::API::Terraform::State
mount ::API::ProtectedBranches
mount ::API::ProtectedTags
mount ::API::Releases
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index 5cab13f001e..9dd2de5c7ba 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -46,6 +46,10 @@ module API
prepend_if_ee('EE::API::APIGuard::HelperMethods') # rubocop: disable Cop/InjectEnterpriseEditionModule
include Gitlab::Auth::AuthFinders
+ def access_token
+ super || find_personal_access_token_from_http_basic_auth
+ end
+
def find_current_user!
user = find_user_from_sources
return unless user
diff --git a/lib/api/deploy_tokens.rb b/lib/api/deploy_tokens.rb
index a637bfcb180..fb4c4265aef 100644
--- a/lib/api/deploy_tokens.rb
+++ b/lib/api/deploy_tokens.rb
@@ -65,11 +65,15 @@ module API
post ':id/deploy_tokens' do
authorize!(:create_deploy_token, user_project)
- deploy_token = ::Projects::DeployTokens::CreateService.new(
+ result = ::Projects::DeployTokens::CreateService.new(
user_project, current_user, scope_params.merge(declared(params, include_missing: false, include_parent_namespaces: false))
).execute
- present deploy_token, with: Entities::DeployTokenWithToken
+ if result[:status] == :success
+ present result[:deploy_token], with: Entities::DeployTokenWithToken
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
end
desc 'Delete a project deploy token' do
@@ -126,11 +130,15 @@ module API
post ':id/deploy_tokens' do
authorize!(:create_deploy_token, user_group)
- deploy_token = ::Groups::DeployTokens::CreateService.new(
+ result = ::Groups::DeployTokens::CreateService.new(
user_group, current_user, scope_params.merge(declared(params, include_missing: false, include_parent_namespaces: false))
).execute
- present deploy_token, with: Entities::DeployTokenWithToken
+ if result[:status] == :success
+ present result[:deploy_token], with: Entities::DeployTokenWithToken
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
end
desc 'Delete a group deploy token' do
diff --git a/lib/api/helpers/merge_requests_helpers.rb b/lib/api/helpers/merge_requests_helpers.rb
index 0126d7a3756..e0753254002 100644
--- a/lib/api/helpers/merge_requests_helpers.rb
+++ b/lib/api/helpers/merge_requests_helpers.rb
@@ -36,7 +36,11 @@ module API
type: String,
values: %w[simple],
desc: 'If simple, returns the `iid`, URL, title, description, and basic state of merge request'
+
optional :author_id, type: Integer, desc: 'Return merge requests which are authored by the user with the given ID'
+ optional :author_username, type: String, desc: 'Return merge requests which are authored by the user with the given username'
+ mutually_exclusive :author_id, :author_username
+
optional :assignee_id,
types: [Integer, String],
integer_none_any: true,
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 0b6bad6708b..f97e28de628 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -207,10 +207,7 @@ module API
status 202
header 'Job-Status', job.status
header 'Range', "0-#{stream_size}"
-
- if Feature.enabled?(:runner_job_trace_update_interval_header, default_enabled: true)
- header 'X-GitLab-Trace-Update-Interval', job.trace.update_interval.to_s
- end
+ header 'X-GitLab-Trace-Update-Interval', job.trace.update_interval.to_s
end
desc 'Authorize artifacts uploading for job' do
diff --git a/lib/api/terraform/state.rb b/lib/api/terraform/state.rb
new file mode 100644
index 00000000000..7e55dfedfeb
--- /dev/null
+++ b/lib/api/terraform/state.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module API
+ module Terraform
+ class State < Grape::API
+ before { authenticate! }
+ before { authorize! :admin_terraform_state, user_project }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ params do
+ requires :name, type: String, desc: 'The name of a terraform state'
+ end
+ namespace ':id/terraform/state/:name' do
+ desc 'Get a terraform state by its name'
+ route_setting :authentication, basic_auth_personal_access_token: true
+ get do
+ status 501
+ content_type 'text/plain'
+ body 'not implemented'
+ end
+
+ desc 'Add a new terraform state or update an existing one'
+ route_setting :authentication, basic_auth_personal_access_token: true
+ post do
+ status 501
+ content_type 'text/plain'
+ body 'not implemented'
+ end
+
+ desc 'Delete a terraform state of certain name'
+ route_setting :authentication, basic_auth_personal_access_token: true
+ delete do
+ status 501
+ content_type 'text/plain'
+ body 'not implemented'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/auth_finders.rb b/lib/gitlab/auth/auth_finders.rb
index fe61d9fe8ca..f0ca6491bd0 100644
--- a/lib/gitlab/auth/auth_finders.rb
+++ b/lib/gitlab/auth/auth_finders.rb
@@ -167,6 +167,14 @@ module Gitlab
oauth_token
end
+ def find_personal_access_token_from_http_basic_auth
+ return unless route_authentication_setting[:basic_auth_personal_access_token]
+ return unless has_basic_credentials?(current_request)
+
+ _username, password = user_name_and_password(current_request)
+ PersonalAccessToken.find_by_token(password)
+ end
+
def parsed_oauth_token
Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods)
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4484260b70e..b2d55f35a62 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -76,6 +76,11 @@ msgid_plural "%d changed files"
msgstr[0] ""
msgstr[1] ""
+msgid "%d child epic"
+msgid_plural "%d child epics"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d code quality issue"
msgid_plural "%d code quality issues"
msgstr[0] ""
@@ -390,6 +395,9 @@ msgstr ""
msgid "%{openedIssues} open, %{closedIssues} closed"
msgstr ""
+msgid "%{percentage}%% weight completed"
+msgstr ""
+
msgid "%{percent}%% complete"
msgstr ""
@@ -1060,6 +1068,9 @@ msgid_plural "Add %d issues"
msgstr[0] ""
msgstr[1] ""
+msgid "Add %{linkStart}assets%{linkEnd} to your Release. GitLab automatically includes read-only assets, like source code and release evidence."
+msgstr ""
+
msgid "Add CHANGELOG"
msgstr ""
@@ -1138,6 +1149,9 @@ msgstr ""
msgid "Add an issue"
msgstr ""
+msgid "Add another link"
+msgstr ""
+
msgid "Add approval rule"
msgstr ""
@@ -4986,6 +5000,9 @@ msgstr ""
msgid "Collapse approvers"
msgstr ""
+msgid "Collapse child epics"
+msgstr ""
+
msgid "Collapse sidebar"
msgstr ""
@@ -8307,6 +8324,9 @@ msgstr ""
msgid "Expand approvers"
msgstr ""
+msgid "Expand child epics"
+msgstr ""
+
msgid "Expand down"
msgstr ""
@@ -11994,6 +12014,9 @@ msgstr ""
msgid "Link copied"
msgstr ""
+msgid "Link title"
+msgstr ""
+
msgid "Linked emails (%{email_count})"
msgstr ""
@@ -14795,6 +14818,9 @@ msgstr ""
msgid "Pods in use"
msgstr ""
+msgid "Point to any links you like: documentation, built binaries, or other related materials. These can be internal or external links from your GitLab instance."
+msgstr ""
+
msgid "Preferences"
msgstr ""
@@ -16539,6 +16565,12 @@ msgid_plural "Releases"
msgstr[0] ""
msgstr[1] ""
+msgid "Release assets"
+msgstr ""
+
+msgid "Release assets documentation"
+msgstr ""
+
msgid "Release does not have the same project as the milestone"
msgstr ""
@@ -16608,6 +16640,9 @@ msgstr ""
msgid "Remove approvers?"
msgstr ""
+msgid "Remove asset link"
+msgstr ""
+
msgid "Remove assignee"
msgstr ""
@@ -20745,19 +20780,25 @@ msgstr ""
msgid "ThreatMonitoring|Anomalous Requests"
msgstr ""
-msgid "ThreatMonitoring|At this time, threat monitoring only supports WAF data."
+msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
+msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgstr ""
+
+msgid "ThreatMonitoring|Container NetworkPolicies not detected"
+msgstr ""
+
msgid "ThreatMonitoring|Dropped Packets"
msgstr ""
msgid "ThreatMonitoring|Environment"
msgstr ""
-msgid "ThreatMonitoring|No traffic to display"
+msgid "ThreatMonitoring|No environments detected"
msgstr ""
msgid "ThreatMonitoring|Operations Per Second"
@@ -20778,6 +20819,9 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgstr ""
+
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
msgstr ""
@@ -20787,13 +20831,10 @@ msgstr ""
msgid "ThreatMonitoring|Threat Monitoring help page link"
msgstr ""
-msgid "ThreatMonitoring|Threat monitoring is not enabled"
-msgstr ""
-
-msgid "ThreatMonitoring|Threat monitoring provides security monitoring and rules to protect production applications."
+msgid "ThreatMonitoring|Time"
msgstr ""
-msgid "ThreatMonitoring|Time"
+msgid "ThreatMonitoring|To view this data, ensure you have configured an environment for this project and that at least one threat monitoring feature is enabled."
msgstr ""
msgid "ThreatMonitoring|Total Packets"
@@ -20802,10 +20843,10 @@ msgstr ""
msgid "ThreatMonitoring|Total Requests"
msgstr ""
-msgid "ThreatMonitoring|Web Application Firewall"
+msgid "ThreatMonitoring|View documentation"
msgstr ""
-msgid "ThreatMonitoring|While it's rare to have no traffic coming to your application, it can happen. In any event, we ask that you double check your settings to make sure you've set up the Network Policies correctly."
+msgid "ThreatMonitoring|Web Application Firewall"
msgstr ""
msgid "ThreatMonitoring|While it's rare to have no traffic coming to your application, it can happen. In any event, we ask that you double check your settings to make sure you've set up the WAF correctly."
diff --git a/spec/controllers/groups/settings/ci_cd_controller_spec.rb b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
index fbf88a01eb3..8ba7a96e1ee 100644
--- a/spec/controllers/groups/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
@@ -212,14 +212,86 @@ describe Groups::Settings::CiCdController do
end
describe 'POST create_deploy_token' do
- it_behaves_like 'a created deploy token' do
- let(:entity) { group }
- let(:create_entity_params) { { group_id: group } }
- let(:deploy_token_type) { DeployToken.deploy_token_types[:group_type] }
-
+ context 'when ajax_new_deploy_token feature flag is disabled for the project' do
before do
+ stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: group })
entity.add_owner(user)
end
+
+ it_behaves_like 'a created deploy token' do
+ let(:entity) { group }
+ let(:create_entity_params) { { group_id: group } }
+ let(:deploy_token_type) { DeployToken.deploy_token_types[:group_type] }
+ end
+ end
+
+ context 'when ajax_new_deploy_token feature flag is enabled for the project' do
+ let(:good_deploy_token_params) do
+ {
+ name: 'name',
+ expires_at: 1.day.from_now.to_s,
+ username: 'deployer',
+ read_repository: '1',
+ deploy_token_type: DeployToken.deploy_token_types[:group_type]
+ }
+ end
+ let(:request_params) do
+ {
+ group_id: group.to_param,
+ deploy_token: deploy_token_params
+ }
+ end
+
+ before do
+ group.add_owner(user)
+ end
+
+ subject { post :create_deploy_token, params: request_params, format: :json }
+
+ context('a good request') do
+ let(:deploy_token_params) { good_deploy_token_params }
+ let(:expected_response) do
+ {
+ 'id' => be_a(Integer),
+ 'name' => deploy_token_params[:name],
+ 'username' => deploy_token_params[:username],
+ 'expires_at' => Time.parse(deploy_token_params[:expires_at]),
+ 'token' => be_a(String),
+ 'scopes' => deploy_token_params.inject([]) do |scopes, kv|
+ key, value = kv
+ key.to_s.start_with?('read_') && !value.to_i.zero? ? scopes << key.to_s : scopes
+ end
+ }
+ end
+
+ it 'creates the deploy token' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/deploy_token')
+ expect(json_response).to match(expected_response)
+ end
+ end
+
+ context('a bad request') do
+ let(:deploy_token_params) { good_deploy_token_params.except(:read_repository) }
+ let(:expected_response) { { 'message' => "Scopes can't be blank" } }
+
+ it 'does not create the deploy token' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response).to match(expected_response)
+ end
+ end
+
+ context('an invalid request') do
+ let(:deploy_token_params) { good_deploy_token_params.except(:name) }
+
+ it 'raises a validation error' do
+ expect { subject }.to raise_error(ActiveRecord::StatementInvalid)
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index a8631389e17..87b40a02567 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -249,10 +249,82 @@ describe Projects::Settings::CiCdController do
end
describe 'POST create_deploy_token' do
- it_behaves_like 'a created deploy token' do
- let(:entity) { project }
- let(:create_entity_params) { { namespace_id: project.namespace, project_id: project } }
- let(:deploy_token_type) { DeployToken.deploy_token_types[:project_type] }
+ context 'when ajax_new_deploy_token feature flag is disabled for the project' do
+ before do
+ stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project })
+ end
+
+ it_behaves_like 'a created deploy token' do
+ let(:entity) { project }
+ let(:create_entity_params) { { namespace_id: project.namespace, project_id: project } }
+ let(:deploy_token_type) { DeployToken.deploy_token_types[:project_type] }
+ end
+ end
+
+ context 'when ajax_new_deploy_token feature flag is enabled for the project' do
+ let(:good_deploy_token_params) do
+ {
+ name: 'name',
+ expires_at: 1.day.from_now.to_s,
+ username: 'deployer',
+ read_repository: '1',
+ deploy_token_type: DeployToken.deploy_token_types[:project_type]
+ }
+ end
+ let(:request_params) do
+ {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ deploy_token: deploy_token_params
+ }
+ end
+
+ subject { post :create_deploy_token, params: request_params, format: :json }
+
+ context('a good request') do
+ let(:deploy_token_params) { good_deploy_token_params }
+ let(:expected_response) do
+ {
+ 'id' => be_a(Integer),
+ 'name' => deploy_token_params[:name],
+ 'username' => deploy_token_params[:username],
+ 'expires_at' => Time.parse(deploy_token_params[:expires_at]),
+ 'token' => be_a(String),
+ 'scopes' => deploy_token_params.inject([]) do |scopes, kv|
+ key, value = kv
+ key.to_s.start_with?('read_') && !value.to_i.zero? ? scopes << key.to_s : scopes
+ end
+ }
+ end
+
+ it 'creates the deploy token' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/deploy_token')
+ expect(json_response).to match(expected_response)
+ end
+ end
+
+ context('a bad request') do
+ let(:deploy_token_params) { good_deploy_token_params.except(:read_repository) }
+ let(:expected_response) { { 'message' => "Scopes can't be blank" } }
+
+ it 'does not create the deploy token' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response).to match(expected_response)
+ end
+ end
+
+ context('an invalid request') do
+ let(:deploy_token_params) { good_deploy_token_params.except(:name) }
+
+ it 'raises a validation error' do
+ expect { subject }.to raise_error(ActiveRecord::StatementInvalid)
+ end
+ end
end
end
end
diff --git a/spec/features/projects/settings/ci_cd_settings_spec.rb b/spec/features/projects/settings/ci_cd_settings_spec.rb
index 8b9b1ac00c3..d8208a93bb1 100644
--- a/spec/features/projects/settings/ci_cd_settings_spec.rb
+++ b/spec/features/projects/settings/ci_cd_settings_spec.rb
@@ -14,6 +14,7 @@ describe 'Projects > Settings > CI / CD settings' do
project.add_role(user, role)
sign_in(user)
stub_container_registry_config(enabled: true)
+ stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project })
visit project_settings_ci_cd_path(project)
end
diff --git a/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb b/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb
index a9253c20896..11b06604f27 100644
--- a/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb
+++ b/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb
@@ -11,6 +11,7 @@ describe 'Repository Settings > User sees revoke deploy token modal', :js do
before do
project.add_role(user, role)
sign_in(user)
+ stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project })
visit(project_settings_ci_cd_path(project))
click_link('Revoke')
end
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index c0126b2330d..179aa3a49d7 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -570,4 +570,65 @@ describe('Api', () => {
.catch(done.fail);
});
});
+
+ describe('createReleaseLink', () => {
+ const dummyProjectPath = 'gitlab-org/gitlab';
+ const dummyReleaseTag = 'v1.3';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodeURIComponent(
+ dummyProjectPath,
+ )}/releases/${dummyReleaseTag}/assets/links`;
+ const expectedLink = {
+ url: 'https://example.com',
+ name: 'An example link',
+ };
+
+ describe('when the Release is successfully created', () => {
+ it('resolves the Promise', () => {
+ mock.onPost(expectedUrl, expectedLink).replyOnce(201);
+
+ return Api.createReleaseLink(dummyProjectPath, dummyReleaseTag, expectedLink).then(() => {
+ expect(mock.history.post).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('when an error occurs while creating the Release', () => {
+ it('rejects the Promise', () => {
+ mock.onPost(expectedUrl, expectedLink).replyOnce(500);
+
+ return Api.createReleaseLink(dummyProjectPath, dummyReleaseTag, expectedLink).catch(() => {
+ expect(mock.history.post).toHaveLength(1);
+ });
+ });
+ });
+ });
+
+ describe('deleteReleaseLink', () => {
+ const dummyProjectPath = 'gitlab-org/gitlab';
+ const dummyReleaseTag = 'v1.3';
+ const dummyLinkId = '4';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodeURIComponent(
+ dummyProjectPath,
+ )}/releases/${dummyReleaseTag}/assets/links/${dummyLinkId}`;
+
+ describe('when the Release is successfully deleted', () => {
+ it('resolves the Promise', () => {
+ mock.onDelete(expectedUrl).replyOnce(200);
+
+ return Api.deleteReleaseLink(dummyProjectPath, dummyReleaseTag, dummyLinkId).then(() => {
+ expect(mock.history.delete).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('when an error occurs while deleting the Release', () => {
+ it('rejects the Promise', () => {
+ mock.onDelete(expectedUrl).replyOnce(500);
+
+ return Api.deleteReleaseLink(dummyProjectPath, dummyReleaseTag, dummyLinkId).catch(() => {
+ expect(mock.history.delete).toHaveLength(1);
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/releases/components/app_edit_spec.js b/spec/frontend/releases/components/app_edit_spec.js
index ac4b2b9124f..e27c27b292a 100644
--- a/spec/frontend/releases/components/app_edit_spec.js
+++ b/spec/frontend/releases/components/app_edit_spec.js
@@ -4,6 +4,7 @@ import ReleaseEditApp from '~/releases/components/app_edit.vue';
import { release as originalRelease } from '../mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants';
+import AssetLinksForm from '~/releases/components/asset_links_form.vue';
describe('Release edit component', () => {
let wrapper;
@@ -11,7 +12,7 @@ describe('Release edit component', () => {
let actions;
let state;
- const factory = () => {
+ const factory = (featureFlags = {}) => {
state = {
release,
markdownDocsPath: 'path/to/markdown/docs',
@@ -22,6 +23,7 @@ describe('Release edit component', () => {
actions = {
fetchRelease: jest.fn(),
updateRelease: jest.fn(),
+ addEmptyAssetLink: jest.fn(),
};
const store = new Vuex.Store({
@@ -36,6 +38,9 @@ describe('Release edit component', () => {
wrapper = mount(ReleaseEditApp, {
store,
+ provide: {
+ glFeatures: featureFlags,
+ },
});
};
@@ -132,4 +137,28 @@ describe('Release edit component', () => {
expect(cancelButton.attributes().href).toBe(backUrl);
});
});
+
+ describe('asset links form', () => {
+ const findAssetLinksForm = () => wrapper.find(AssetLinksForm);
+
+ describe('when the release_asset_link_editing feature flag is disabled', () => {
+ beforeEach(() => {
+ factory({ releaseAssetLinkEditing: false });
+ });
+
+ it('does not render the asset links portion of the form', () => {
+ expect(findAssetLinksForm().exists()).toBe(false);
+ });
+ });
+
+ describe('when the release_asset_link_editing feature flag is enabled', () => {
+ beforeEach(() => {
+ factory({ releaseAssetLinkEditing: true });
+ });
+
+ it('renders the asset links portion of the form', () => {
+ expect(findAssetLinksForm().exists()).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index 70f7432c65d..4a1790adb09 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -9,6 +9,7 @@ import createState from '~/releases/stores/modules/detail/state';
import createFlash from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { redirectTo } from '~/lib/utils/url_utility';
+import api from '~/api';
jest.mock('~/flash', () => jest.fn());
@@ -179,40 +180,92 @@ describe('Release detail actions', () => {
});
describe('updateRelease', () => {
- let getReleaseUrl;
+ let getters;
+ let dispatch;
+ let callOrder;
beforeEach(() => {
- state.release = release;
+ state.release = convertObjectPropsToCamelCase(release);
state.projectId = '18';
- state.tagName = 'v1.3';
- getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`;
- });
+ state.tagName = state.release.tagName;
- it(`dispatches requestUpdateRelease and receiveUpdateReleaseSuccess`, () => {
- mock.onPut(getReleaseUrl).replyOnce(200);
+ getters = {
+ releaseLinksToDelete: [{ id: '1' }, { id: '2' }],
+ releaseLinksToCreate: [{ id: 'new-link-1' }, { id: 'new-link-2' }],
+ };
- return testAction(
- actions.updateRelease,
- undefined,
- state,
- [],
- [{ type: 'requestUpdateRelease' }, { type: 'receiveUpdateReleaseSuccess' }],
- );
+ dispatch = jest.fn();
+
+ callOrder = [];
+ jest.spyOn(api, 'updateRelease').mockImplementation(() => {
+ callOrder.push('updateRelease');
+ return Promise.resolve();
+ });
+ jest.spyOn(api, 'deleteReleaseLink').mockImplementation(() => {
+ callOrder.push('deleteReleaseLink');
+ return Promise.resolve();
+ });
+ jest.spyOn(api, 'createReleaseLink').mockImplementation(() => {
+ callOrder.push('createReleaseLink');
+ return Promise.resolve();
+ });
});
- it(`dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object`, () => {
- mock.onPut(getReleaseUrl).replyOnce(500);
+ it('dispatches requestUpdateRelease and receiveUpdateReleaseSuccess', () => {
+ return actions.updateRelease({ dispatch, state, getters }).then(() => {
+ expect(dispatch.mock.calls).toEqual([
+ ['requestUpdateRelease'],
+ ['receiveUpdateReleaseSuccess'],
+ ]);
+ });
+ });
- return testAction(
- actions.updateRelease,
- undefined,
- state,
- [],
- [
- { type: 'requestUpdateRelease' },
- { type: 'receiveUpdateReleaseError', payload: expect.anything() },
- ],
- );
+ it('dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object', () => {
+ jest.spyOn(api, 'updateRelease').mockRejectedValue(error);
+
+ return actions.updateRelease({ dispatch, state, getters }).then(() => {
+ expect(dispatch.mock.calls).toEqual([
+ ['requestUpdateRelease'],
+ ['receiveUpdateReleaseError', error],
+ ]);
+ });
+ });
+
+ it('updates the Release, then deletes all existing links, and then recreates new links', () => {
+ return actions.updateRelease({ dispatch, state, getters }).then(() => {
+ expect(callOrder).toEqual([
+ 'updateRelease',
+ 'deleteReleaseLink',
+ 'deleteReleaseLink',
+ 'createReleaseLink',
+ 'createReleaseLink',
+ ]);
+
+ expect(api.updateRelease.mock.calls).toEqual([
+ [
+ state.projectId,
+ state.tagName,
+ {
+ name: state.release.name,
+ description: state.release.description,
+ },
+ ],
+ ]);
+
+ expect(api.deleteReleaseLink).toHaveBeenCalledTimes(getters.releaseLinksToDelete.length);
+ getters.releaseLinksToDelete.forEach(link => {
+ expect(api.deleteReleaseLink).toHaveBeenCalledWith(
+ state.projectId,
+ state.tagName,
+ link.id,
+ );
+ });
+
+ expect(api.createReleaseLink).toHaveBeenCalledTimes(getters.releaseLinksToCreate.length);
+ getters.releaseLinksToCreate.forEach(link => {
+ expect(api.createReleaseLink).toHaveBeenCalledWith(state.projectId, state.tagName, link);
+ });
+ });
});
});
});
diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js
new file mode 100644
index 00000000000..7dc95c24055
--- /dev/null
+++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js
@@ -0,0 +1,59 @@
+import * as getters from '~/releases/stores/modules/detail/getters';
+
+describe('Release detail getters', () => {
+ describe('releaseLinksToCreate', () => {
+ it("returns an empty array if state.release doesn't exist", () => {
+ const state = {};
+ expect(getters.releaseLinksToCreate(state)).toEqual([]);
+ });
+
+ it("returns all release links that aren't empty", () => {
+ const emptyLinks = [
+ { url: '', name: '' },
+ { url: ' ', name: '' },
+ { url: ' ', name: ' ' },
+ { url: '\r\n', name: '\t' },
+ ];
+
+ const nonEmptyLinks = [
+ { url: 'https://example.com/1', name: 'Example 1' },
+ { url: '', name: 'Example 2' },
+ { url: 'https://example.com/3', name: '' },
+ ];
+
+ const state = {
+ release: {
+ assets: {
+ links: [...emptyLinks, ...nonEmptyLinks],
+ },
+ },
+ };
+
+ expect(getters.releaseLinksToCreate(state)).toEqual(nonEmptyLinks);
+ });
+ });
+
+ describe('releaseLinksToDelete', () => {
+ it("returns an empty array if state.originalRelease doesn't exist", () => {
+ const state = {};
+ expect(getters.releaseLinksToDelete(state)).toEqual([]);
+ });
+
+ it('returns all links associated with the original release', () => {
+ const originalLinks = [
+ { url: 'https://example.com/1', name: 'Example 1' },
+ { url: 'https://example.com/2', name: 'Example 2' },
+ ];
+
+ const state = {
+ originalRelease: {
+ assets: {
+ links: originalLinks,
+ },
+ },
+ };
+
+ expect(getters.releaseLinksToDelete(state)).toEqual(originalLinks);
+ });
+ });
+});
diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
index d49c8854ca2..cb5a1880b0c 100644
--- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
@@ -8,11 +8,12 @@
import createState from '~/releases/stores/modules/detail/state';
import mutations from '~/releases/stores/modules/detail/mutations';
import * as types from '~/releases/stores/modules/detail/mutation_types';
-import { release } from '../../../mock_data';
+import { release as originalRelease } from '../../../mock_data';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
describe('Release detail mutations', () => {
let state;
- let releaseClone;
+ let release;
beforeEach(() => {
state = createState({
@@ -23,7 +24,7 @@ describe('Release detail mutations', () => {
markdownPreviewPath: 'path/to/markdown/preview',
updateReleaseApiDocsPath: 'path/to/api/docs',
});
- releaseClone = JSON.parse(JSON.stringify(release));
+ release = convertObjectPropsToCamelCase(originalRelease);
});
describe(types.REQUEST_RELEASE, () => {
@@ -36,13 +37,15 @@ describe('Release detail mutations', () => {
describe(types.RECEIVE_RELEASE_SUCCESS, () => {
it('handles a successful response from the server', () => {
- mutations[types.RECEIVE_RELEASE_SUCCESS](state, releaseClone);
+ mutations[types.RECEIVE_RELEASE_SUCCESS](state, release);
expect(state.fetchError).toEqual(undefined);
expect(state.isFetchingRelease).toEqual(false);
- expect(state.release).toEqual(releaseClone);
+ expect(state.release).toEqual(release);
+
+ expect(state.originalRelease).toEqual(release);
});
});
@@ -61,7 +64,7 @@ describe('Release detail mutations', () => {
describe(types.UPDATE_RELEASE_TITLE, () => {
it("updates the release's title", () => {
- state.release = releaseClone;
+ state.release = release;
const newTitle = 'The new release title';
mutations[types.UPDATE_RELEASE_TITLE](state, newTitle);
@@ -71,7 +74,7 @@ describe('Release detail mutations', () => {
describe(types.UPDATE_RELEASE_NOTES, () => {
it("updates the release's notes", () => {
- state.release = releaseClone;
+ state.release = release;
const newNotes = 'The new release notes';
mutations[types.UPDATE_RELEASE_NOTES](state, newNotes);
@@ -89,7 +92,7 @@ describe('Release detail mutations', () => {
describe(types.RECEIVE_UPDATE_RELEASE_SUCCESS, () => {
it('handles a successful response from the server', () => {
- mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state, releaseClone);
+ mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state, release);
expect(state.updateError).toEqual(undefined);
@@ -107,4 +110,65 @@ describe('Release detail mutations', () => {
expect(state.updateError).toEqual(error);
});
});
+
+ describe(types.ADD_EMPTY_ASSET_LINK, () => {
+ it('adds a new, empty link object to the release', () => {
+ state.release = release;
+
+ const linksBefore = [...state.release.assets.links];
+
+ mutations[types.ADD_EMPTY_ASSET_LINK](state);
+
+ expect(state.release.assets.links).toEqual([
+ ...linksBefore,
+ {
+ id: expect.stringMatching(/^new-link-/),
+ url: '',
+ name: '',
+ },
+ ]);
+ });
+ });
+
+ describe(types.UPDATE_ASSET_LINK_URL, () => {
+ it('updates an asset link with a new URL', () => {
+ state.release = release;
+
+ const newUrl = 'https://example.com/updated/url';
+
+ mutations[types.UPDATE_ASSET_LINK_URL](state, {
+ linkIdToUpdate: state.release.assets.links[0].id,
+ newUrl,
+ });
+
+ expect(state.release.assets.links[0].url).toEqual(newUrl);
+ });
+ });
+
+ describe(types.UPDATE_ASSET_LINK_NAME, () => {
+ it('updates an asset link with a new name', () => {
+ state.release = release;
+
+ const newName = 'Updated Link';
+
+ mutations[types.UPDATE_ASSET_LINK_NAME](state, {
+ linkIdToUpdate: state.release.assets.links[0].id,
+ newName,
+ });
+
+ expect(state.release.assets.links[0].name).toEqual(newName);
+ });
+ });
+
+ describe(types.REMOVE_ASSET_LINK, () => {
+ it('removes an asset link from the release', () => {
+ state.release = release;
+
+ const linkToRemove = state.release.assets.links[0];
+
+ mutations[types.REMOVE_ASSET_LINK](state, linkToRemove.id);
+
+ expect(state.release.assets.links).not.toContainEqual(linkToRemove);
+ });
+ });
});
diff --git a/spec/helpers/releases_helper_spec.rb b/spec/helpers/releases_helper_spec.rb
index d9d6a324f09..282758679cb 100644
--- a/spec/helpers/releases_helper_spec.rb
+++ b/spec/helpers/releases_helper_spec.rb
@@ -53,7 +53,8 @@ describe ReleasesHelper do
markdown_preview_path
markdown_docs_path
releases_page_path
- update_release_api_docs_path)
+ update_release_api_docs_path
+ release_assets_docs_path)
expect(helper.data_for_edit_release_page.keys).to eq(keys)
end
end
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index bffaaef4ed4..0b6fda31d7b 100644
--- a/spec/lib/gitlab/auth/auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -335,6 +335,54 @@ describe Gitlab::Auth::AuthFinders do
end
end
+ describe '#find_personal_access_token_from_http_basic_auth' do
+ def auth_header_with(token)
+ env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials('username', token)
+ end
+
+ context 'access token is valid' do
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+ let(:route_authentication_setting) { { basic_auth_personal_access_token: true } }
+
+ it 'finds the token from basic auth' do
+ auth_header_with(personal_access_token.token)
+
+ expect(find_personal_access_token_from_http_basic_auth).to eq personal_access_token
+ end
+ end
+
+ context 'access token is not valid' do
+ let(:route_authentication_setting) { { basic_auth_personal_access_token: true } }
+
+ it 'returns nil' do
+ auth_header_with('failing_token')
+
+ expect(find_personal_access_token_from_http_basic_auth).to be_nil
+ end
+ end
+
+ context 'route_setting is not set' do
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ it 'returns nil' do
+ auth_header_with(personal_access_token.token)
+
+ expect(find_personal_access_token_from_http_basic_auth).to be_nil
+ end
+ end
+
+ context 'route_setting is not correct' do
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+ let(:route_authentication_setting) { { basic_auth_personal_access_token: false } }
+
+ it 'returns nil' do
+ auth_header_with(personal_access_token.token)
+
+ expect(find_personal_access_token_from_http_basic_auth).to be_nil
+ end
+ end
+ end
+
describe '#find_user_from_basic_auth_job' do
def basic_http_auth(username, password)
ActionController::HttpAuthentication::Basic.encode_credentials(username, password)
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 608e3a6e938..f16b1fb136d 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -3681,15 +3681,15 @@ describe User, :do_not_mock_admin_mode do
end
it 'returns false if email can not be synced' do
- stub_omniauth_setting(sync_profile_attributes: %w(location email))
+ stub_omniauth_setting(sync_profile_attributes: %w(location name))
- expect(user.sync_attribute?(:name)).to be_falsey
+ expect(user.sync_attribute?(:email)).to be_falsey
end
it 'returns false if location can not be synced' do
- stub_omniauth_setting(sync_profile_attributes: %w(location email))
+ stub_omniauth_setting(sync_profile_attributes: %w(name email))
- expect(user.sync_attribute?(:name)).to be_falsey
+ expect(user.sync_attribute?(:location)).to be_falsey
end
it 'returns true for all syncable attributes if all syncable attributes can be synced' do
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index a729da5afad..cce84c4f357 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -53,6 +53,7 @@ describe ProjectPolicy do
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
daily_statistics read_deploy_token create_deploy_token destroy_deploy_token
+ admin_terraform_state
]
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 5440e187ba9..403e182ad99 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -526,12 +526,48 @@ describe API::MergeRequests do
expect_response_contain_exactly(merge_request3.id)
end
- it 'returns an array of merge requests authored by the given user' do
- merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch')
+ context 'filter by author' do
+ let(:user3) { create(:user) }
+ let(:project) { create(:project, :public, :repository, creator: user3, namespace: user3.namespace, only_allow_merge_if_pipeline_succeeds: false) }
+ let!(:merge_request3) do
+ create(:merge_request, :simple, author: user3, assignees: [user3], source_project: project, target_project: project, source_branch: 'other-branch')
+ end
- get api('/merge_requests', user), params: { author_id: user2.id, scope: :all }
+ context 'when only `author_id` is passed' do
+ it 'returns an array of merge requests authored by the given user' do
+ get api('/merge_requests', user), params: {
+ author_id: user3.id,
+ scope: :all
+ }
- expect_response_contain_exactly(merge_request3.id)
+ expect_response_contain_exactly(merge_request3.id)
+ end
+ end
+
+ context 'when only `author_username` is passed' do
+ it 'returns an array of merge requests authored by the given user(by `author_username`)' do
+ get api('/merge_requests', user), params: {
+ author_username: user3.username,
+ scope: :all
+ }
+
+ expect_response_contain_exactly(merge_request3.id)
+ end
+ end
+
+ context 'when both `author_id` and `author_username` are passed' do
+ it 'returns a 400' do
+ get api('/merge_requests', user), params: {
+ author_id: user.id,
+ author_username: user2.username,
+ scope: :all
+ }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq(
+ 'author_id, author_username are mutually exclusive')
+ end
+ end
end
it 'returns an array of merge requests assigned to the given user' do
@@ -1525,7 +1561,7 @@ describe API::MergeRequests do
it "returns 400 when target_branch is missing" do
post api("/projects/#{forked_project.id}/merge_requests", user2),
- params: { title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id }
+ params: { title: 'Test merge_request', source_branch: "master", author: user2, target_project_id: project.id }
expect(response).to have_gitlab_http_status(:bad_request)
end
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index d05d886bf85..a67c1a48ad4 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -1349,19 +1349,6 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(response.header['X-GitLab-Trace-Update-Interval']).to eq('30')
end
end
-
- context 'when feature flag runner_job_trace_update_interval_header is disabled' do
- before do
- stub_feature_flags(runner_job_trace_update_interval_header: { enabled: false })
- end
-
- it 'does not return X-GitLab-Trace-Update-Interval header' do
- patch_the_trace
-
- expect(response).to have_gitlab_http_status(:accepted)
- expect(response.header).not_to have_key 'X-GitLab-Trace-Update-Interval'
- end
- end
end
context 'when Runner makes a force-patch' do
diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb
new file mode 100644
index 00000000000..b0a963db684
--- /dev/null
+++ b/spec/requests/api/terraform/state_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Terraform::State do
+ def auth_header_for(user)
+ auth_header = ActionController::HttpAuthentication::Basic.encode_credentials(
+ user.username,
+ create(:personal_access_token, user: user).token
+ )
+ { 'HTTP_AUTHORIZATION' => auth_header }
+ end
+
+ let!(:project) { create(:project) }
+ let(:developer) { create(:user) }
+ let(:maintainer) { create(:user) }
+ let(:state_name) { 'state' }
+
+ before do
+ project.add_maintainer(maintainer)
+ end
+
+ describe 'GET /projects/:id/terraform/state/:name' do
+ it 'returns 401 if user is not authenticated' do
+ headers = { 'HTTP_AUTHORIZATION' => 'failing_token' }
+ get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: headers
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'returns terraform state belonging to a project of given state name' do
+ get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(maintainer)
+
+ expect(response).to have_gitlab_http_status(:not_implemented)
+ expect(response.body).to eq('not implemented')
+ end
+
+ it 'returns not found if the project does not exists' do
+ get api("/projects/0000/terraform/state/#{state_name}"), headers: auth_header_for(maintainer)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns forbidden if the user cannot access the state' do
+ project.add_developer(developer)
+ get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(developer)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ describe 'POST /projects/:id/terraform/state/:name' do
+ context 'when terraform state with a given name is already present' do
+ it 'updates the state' do
+ post api("/projects/#{project.id}/terraform/state/#{state_name}"),
+ params: '{ "instance": "example-instance" }',
+ headers: { 'Content-Type' => 'text/plain' }.merge(auth_header_for(maintainer))
+
+ expect(response).to have_gitlab_http_status(:not_implemented)
+ expect(response.body).to eq('not implemented')
+ end
+
+ it 'returns forbidden if the user cannot access the state' do
+ project.add_developer(developer)
+ get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(developer)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when there is no terraform state of a given name' do
+ it 'creates a new state' do
+ post api("/projects/#{project.id}/terraform/state/example2"),
+ headers: auth_header_for(maintainer),
+ params: '{ "database": "example-database" }'
+
+ expect(response).to have_gitlab_http_status(:not_implemented)
+ expect(response.body).to eq('not implemented')
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/terraform/state/:name' do
+ it 'deletes the state' do
+ delete api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(maintainer)
+
+ expect(response).to have_gitlab_http_status(:not_implemented)
+ end
+
+ it 'returns forbidden if the user cannot access the state' do
+ project.add_developer(developer)
+ get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(developer)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 30524e4bbae..08320934ba1 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -214,15 +214,62 @@ RSpec.configure do |config|
# modifying a significant number of specs to test both states for admin
# mode enabled / disabled.
#
- # See https://gitlab.com/gitlab-org/gitlab/issues/31511
- # See gitlab/spec/support/helpers/admin_mode_helpers.rb
+ # This will only be applied to specs below dirs in `admin_mode_mock_dirs`
#
- # If it is required to have the real behaviour that an admin is signed in
+ # See ongoing migration: https://gitlab.com/gitlab-org/gitlab/-/issues/31511
+ #
+ # Until the migration is finished, if it is required to have the real
+ # behaviour in any of the mocked dirs specs that an admin is signed in
# with normal user mode and needs to switch to admin mode, it is possible to
# mark such tests with the `do_not_mock_admin_mode` metadata tag, e.g:
#
- # context 'some test with normal user mode', :do_not_mock_admin_mode do ... end
- unless example.metadata[:do_not_mock_admin_mode]
+ # context 'some test in mocked dir', :do_not_mock_admin_mode do ... end
+ admin_mode_mock_dirs = %w(
+ ./ee/spec/controllers
+ ./ee/spec/elastic_integration
+ ./ee/spec/features
+ ./ee/spec/finders
+ ./ee/spec/lib
+ ./ee/spec/models
+ ./ee/spec/policies
+ ./ee/spec/requests/admin
+ ./ee/spec/serializers
+ ./ee/spec/services
+ ./ee/spec/support/protected_tags
+ ./ee/spec/support/shared_examples
+ ./spec/controllers
+ ./spec/features
+ ./spec/finders
+ ./spec/frontend
+ ./spec/helpers
+ ./spec/lib
+ ./spec/models
+ ./spec/policies
+ ./spec/requests
+ ./spec/serializers
+ ./spec/services
+ ./spec/support/cycle_analytics_helpers
+ ./spec/support/protected_tags
+ ./spec/support/shared_examples
+ ./spec/views
+ ./spec/workers
+ )
+
+ if !example.metadata[:do_not_mock_admin_mode] && example.metadata[:file_path].start_with?(*admin_mode_mock_dirs)
+ allow_any_instance_of(Gitlab::Auth::CurrentUserMode).to receive(:admin_mode?) do |current_user_mode|
+ current_user_mode.send(:user)&.admin?
+ end
+ end
+
+ # Administrators have to re-authenticate in order to access administrative
+ # functionality when feature flag :user_mode_in_session is active. Any spec
+ # that requires administrative access can use the tag :enable_admin_mode
+ # to avoid the second auth step (provided the user is already an admin):
+ #
+ # context 'some test that requires admin mode', :enable_admin_mode do ... end
+ #
+ # See also spec/support/helpers/admin_mode_helpers.rb
+ if example.metadata[:enable_admin_mode]
allow_any_instance_of(Gitlab::Auth::CurrentUserMode).to receive(:admin_mode?) do |current_user_mode|
current_user_mode.send(:user)&.admin?
end
diff --git a/spec/support/services/deploy_token_shared_examples.rb b/spec/support/services/deploy_token_shared_examples.rb
index b49f4743f7d..70efd1fcd0c 100644
--- a/spec/support/services/deploy_token_shared_examples.rb
+++ b/spec/support/services/deploy_token_shared_examples.rb
@@ -17,7 +17,7 @@ RSpec.shared_examples 'a deploy token creation service' do
end
it 'returns a DeployToken' do
- expect(subject).to be_an_instance_of DeployToken
+ expect(subject[:deploy_token]).to be_an_instance_of DeployToken
end
end
@@ -25,7 +25,7 @@ RSpec.shared_examples 'a deploy token creation service' do
let(:deploy_token_params) { attributes_for(:deploy_token, expires_at: '') }
it 'sets Forever.date' do
- expect(subject.read_attribute(:expires_at)).to eq(Forever.date)
+ expect(subject[:deploy_token].read_attribute(:expires_at)).to eq(Forever.date)
end
end
@@ -33,7 +33,7 @@ RSpec.shared_examples 'a deploy token creation service' do
let(:deploy_token_params) { attributes_for(:deploy_token, username: '') }
it 'converts it to nil' do
- expect(subject.read_attribute(:username)).to be_nil
+ expect(subject[:deploy_token].read_attribute(:username)).to be_nil
end
end
@@ -41,7 +41,7 @@ RSpec.shared_examples 'a deploy token creation service' do
let(:deploy_token_params) { attributes_for(:deploy_token, username: 'deployer') }
it 'keeps the provided username' do
- expect(subject.read_attribute(:username)).to eq('deployer')
+ expect(subject[:deploy_token].read_attribute(:username)).to eq('deployer')
end
end