Commit 1a0d6dbd authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent b11f7057
...@@ -217,7 +217,6 @@ Gitlab/DuplicateSpecLocation: ...@@ -217,7 +217,6 @@ Gitlab/DuplicateSpecLocation:
- ee/spec/helpers/auth_helper_spec.rb - ee/spec/helpers/auth_helper_spec.rb
- ee/spec/lib/gitlab/gl_repository_spec.rb - ee/spec/lib/gitlab/gl_repository_spec.rb
- ee/spec/models/namespace_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/issues/create_service_spec.rb
- ee/spec/services/merge_requests/create_service_spec.rb - ee/spec/services/merge_requests/create_service_spec.rb
- ee/spec/services/merge_requests/refresh_service_spec.rb - ee/spec/services/merge_requests/refresh_service_spec.rb
...@@ -225,7 +224,6 @@ Gitlab/DuplicateSpecLocation: ...@@ -225,7 +224,6 @@ Gitlab/DuplicateSpecLocation:
- ee/spec/services/system_hooks_service_spec.rb - ee/spec/services/system_hooks_service_spec.rb
- ee/spec/helpers/ee/auth_helper_spec.rb - ee/spec/helpers/ee/auth_helper_spec.rb
- ee/spec/models/ee/namespace_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/issues/create_service_spec.rb
- ee/spec/services/ee/merge_requests/create_service_spec.rb - ee/spec/services/ee/merge_requests/create_service_spec.rb
- ee/spec/services/ee/merge_requests/refresh_service_spec.rb - ee/spec/services/ee/merge_requests/refresh_service_spec.rb
...@@ -414,15 +412,12 @@ RSpec/RepeatedExample: ...@@ -414,15 +412,12 @@ RSpec/RepeatedExample:
- 'spec/models/concerns/issuable_spec.rb' - 'spec/models/concerns/issuable_spec.rb'
- 'spec/models/member_spec.rb' - 'spec/models/member_spec.rb'
- 'spec/models/project_services/chat_message/pipeline_message_spec.rb' - 'spec/models/project_services/chat_message/pipeline_message_spec.rb'
- 'spec/models/user_spec.rb'
- 'spec/models/wiki_page_spec.rb' - 'spec/models/wiki_page_spec.rb'
- 'spec/requests/api/merge_requests_spec.rb'
- 'spec/routing/admin_routing_spec.rb' - 'spec/routing/admin_routing_spec.rb'
- 'spec/rubocop/cop/migration/update_large_table_spec.rb' - 'spec/rubocop/cop/migration/update_large_table_spec.rb'
- 'spec/services/notification_service_spec.rb' - 'spec/services/notification_service_spec.rb'
- 'spec/services/web_hook_service_spec.rb' - 'spec/services/web_hook_service_spec.rb'
- 'ee/spec/models/group_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/boards/lists/update_service_spec.rb'
- 'ee/spec/services/geo/repository_verification_primary_service_spec.rb' - 'ee/spec/services/geo/repository_verification_primary_service_spec.rb'
- 'ee/spec/workers/geo/file_download_dispatch_worker_spec.rb' - 'ee/spec/workers/geo/file_download_dispatch_worker_spec.rb'
...@@ -41,6 +41,8 @@ const Api = { ...@@ -41,6 +41,8 @@ const Api = {
createBranchPath: '/api/:version/projects/:id/repository/branches', createBranchPath: '/api/:version/projects/:id/repository/branches',
releasesPath: '/api/:version/projects/:id/releases', releasesPath: '/api/:version/projects/:id/releases',
releasePath: '/api/:version/projects/:id/releases/:tag_name', 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', mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
adminStatisticsPath: '/api/:version/application/statistics', adminStatisticsPath: '/api/:version/application/statistics',
pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id', pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id',
...@@ -460,6 +462,23 @@ const Api = { ...@@ -460,6 +462,23 @@ const Api = {
return axios.put(url, release); 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() { adminStatistics() {
const url = Api.buildUrl(this.adminStatisticsPath); const url = Api.buildUrl(this.adminStatisticsPath);
return axios.get(url); return axios.get(url);
......
...@@ -7,6 +7,8 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue'; ...@@ -7,6 +7,8 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { BACK_URL_PARAM } from '~/releases/constants'; import { BACK_URL_PARAM } from '~/releases/constants';
import { getParameterByName } from '~/lib/utils/common_utils'; 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 { export default {
name: 'ReleaseEditApp', name: 'ReleaseEditApp',
...@@ -16,10 +18,12 @@ export default { ...@@ -16,10 +18,12 @@ export default {
GlButton, GlButton,
GlLink, GlLink,
MarkdownField, MarkdownField,
AssetLinksForm,
}, },
directives: { directives: {
autofocusonshow, autofocusonshow,
}, },
mixins: [glFeatureFlagsMixin()],
computed: { computed: {
...mapState('detail', [ ...mapState('detail', [
'isFetchingRelease', 'isFetchingRelease',
...@@ -80,6 +84,9 @@ export default { ...@@ -80,6 +84,9 @@ export default {
cancelPath() { cancelPath() {
return getParameterByName(BACK_URL_PARAM) || this.releasesPagePath; return getParameterByName(BACK_URL_PARAM) || this.releasesPagePath;
}, },
showAssetLinksForm() {
return this.glFeatures.releaseAssetLinkEditing;
},
}, },
created() { created() {
this.fetchRelease(); this.fetchRelease();
...@@ -153,6 +160,8 @@ export default { ...@@ -153,6 +160,8 @@ export default {
</div> </div>
</gl-form-group> </gl-form-group>
<asset-links-form v-if="showAssetLinksForm" />
<div class="d-flex pt-3"> <div class="d-flex pt-3">
<gl-button <gl-button
class="mr-auto js-submit-button" class="mr-auto js-submit-button"
......
<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>
...@@ -41,20 +41,74 @@ export const receiveUpdateReleaseError = ({ commit }, error) => { ...@@ -41,20 +41,74 @@ export const receiveUpdateReleaseError = ({ commit }, error) => {
createFlash(s__('Release|Something went wrong while saving the release details')); createFlash(s__('Release|Something went wrong while saving the release details'));
}; };
export const updateRelease = ({ dispatch, state }) => { export const updateRelease = ({ dispatch, state, getters }) => {
dispatch('requestUpdateRelease'); dispatch('requestUpdateRelease');
return api const { release } = state;
.updateRelease(state.projectId, state.tagName, {
name: state.release.name, return (
description: state.release.description, api
}) .updateRelease(state.projectId, state.tagName, {
.then(() => dispatch('receiveUpdateReleaseSuccess')) name: release.name,
.catch(error => { description: release.description,
dispatch('receiveUpdateReleaseError', error); })
});
/**
* 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 }) => { export const navigateToReleasesPage = ({ state }) => {
redirectTo(state.releasesPagePath); 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);
};
/**
* @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;
};
import * as actions from './actions'; import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
import createState from './state'; import createState from './state';
export default initialState => ({ export default initialState => ({
namespaced: true, namespaced: true,
actions, actions,
getters,
mutations, mutations,
state: createState(initialState), state: createState(initialState),
}); });
...@@ -8,3 +8,8 @@ export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES'; ...@@ -8,3 +8,8 @@ export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES';
export const REQUEST_UPDATE_RELEASE = 'REQUEST_UPDATE_RELEASE'; export const REQUEST_UPDATE_RELEASE = 'REQUEST_UPDATE_RELEASE';
export const RECEIVE_UPDATE_RELEASE_SUCCESS = 'RECEIVE_UPDATE_RELEASE_SUCCESS'; export const RECEIVE_UPDATE_RELEASE_SUCCESS = 'RECEIVE_UPDATE_RELEASE_SUCCESS';
export const RECEIVE_UPDATE_RELEASE_ERROR = 'RECEIVE_UPDATE_RELEASE_ERROR'; 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';
import * as types from './mutation_types'; 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 { export default {
[types.REQUEST_RELEASE](state) { [types.REQUEST_RELEASE](state) {
...@@ -8,6 +13,7 @@ export default { ...@@ -8,6 +13,7 @@ export default {
state.fetchError = undefined; state.fetchError = undefined;
state.isFetchingRelease = false; state.isFetchingRelease = false;
state.release = data; state.release = data;
state.originalRelease = Object.freeze(cloneDeep(state.release));
}, },
[types.RECEIVE_RELEASE_ERROR](state, error) { [types.RECEIVE_RELEASE_ERROR](state, error) {
state.fetchError = error; state.fetchError = error;
...@@ -33,4 +39,26 @@ export default { ...@@ -33,4 +39,26 @@ export default {
state.updateError = error; state.updateError = error;
state.isUpdatingRelease = false; 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);
},
}; };
...@@ -5,6 +5,7 @@ export default ({ ...@@ -5,6 +5,7 @@ export default ({
markdownDocsPath, markdownDocsPath,
markdownPreviewPath, markdownPreviewPath,
updateReleaseApiDocsPath, updateReleaseApiDocsPath,
releaseAssetsDocsPath,
}) => ({ }) => ({
projectId, projectId,
tagName, tagName,
...@@ -12,9 +13,18 @@ export default ({ ...@@ -12,9 +13,18 @@ export default ({
markdownDocsPath, markdownDocsPath,
markdownPreviewPath, markdownPreviewPath,
updateReleaseApiDocsPath, updateReleaseApiDocsPath,
releaseAssetsDocsPath,
/** The Release object */
release: null, 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, isFetchingRelease: false,
fetchError: null, fetchError: null,
......
...@@ -8,6 +8,7 @@ module Groups ...@@ -8,6 +8,7 @@ module Groups
before_action :authorize_update_max_artifacts_size!, only: [:update] before_action :authorize_update_max_artifacts_size!, only: [:update]
before_action do before_action do
push_frontend_feature_flag(:new_variables_ui, @group) push_frontend_feature_flag(:new_variables_ui, @group)
push_frontend_feature_flag(:ajax_new_deploy_token, @group)
end end
before_action :define_variables, only: [:show, :create_deploy_token] before_action :define_variables, only: [:show, :create_deploy_token]
...@@ -42,13 +43,30 @@ module Groups ...@@ -42,13 +43,30 @@ module Groups
end end
def create_deploy_token def create_deploy_token
@new_deploy_token = Groups::DeployTokens::CreateService.new(@group, current_user, deploy_token_params).execute result = Projects::DeployTokens::CreateService.new(@group, current_user, deploy_token_params).execute
@new_deploy_token = result[:deploy_token]
if @new_deploy_token.persisted?
flash.now[:notice] = s_('DeployTokens|Your new group 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 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 end
render 'show'
end end
private private
......
...@@ -9,6 +9,7 @@ class Projects::ReleasesController < Projects::ApplicationController ...@@ -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_issue_summary, project, default_enabled: true)
push_frontend_feature_flag(:release_evidence_collection, 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_show_page, project, default_enabled: true)
push_frontend_feature_flag(:release_asset_link_editing, project)
end end
before_action :authorize_update_release!, only: %i[edit update] before_action :authorize_update_release!, only: %i[edit update]
......
...@@ -7,6 +7,7 @@ module Projects ...@@ -7,6 +7,7 @@ module Projects
before_action :define_variables before_action :define_variables
before_action do before_action do
push_frontend_feature_flag(:new_variables_ui, @project) push_frontend_feature_flag(:new_variables_ui, @project)
push_frontend_feature_flag(:ajax_new_deploy_token, @project)
end end
def show def show
...@@ -47,13 +48,30 @@ module Projects ...@@ -47,13 +48,30 @@ module Projects
end end
def create_deploy_token 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? if result[:status] == :success
flash.now[:notice] = s_('DeployTokens|Your new project deploy token has been created.') 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 end
render 'show'
end end
private private
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
# milestone_title: string # milestone_title: string
# release_tag: string # release_tag: string
# author_id: integer # author_id: integer
# author_username: string
# assignee_id: integer # assignee_id: integer
# search: string # search: string
# in: 'title', 'description', or a string joining them with comma # in: 'title', 'description', or a string joining them with comma
......
...@@ -8,8 +8,8 @@ module ReleasesHelper ...@@ -8,8 +8,8 @@ module ReleasesHelper
image_path(IMAGE_PATH) image_path(IMAGE_PATH)
end end
def help_page def help_page(anchor: nil)
help_page_path(DOCUMENTATION_PATH) help_page_path(DOCUMENTATION_PATH, anchor: anchor)
end end
def data_for_releases_page def data_for_releases_page
...@@ -29,7 +29,8 @@ module ReleasesHelper ...@@ -29,7 +29,8 @@ module ReleasesHelper
markdown_preview_path: preview_markdown_path(@project), markdown_preview_path: preview_markdown_path(@project),
markdown_docs_path: help_page_path('user/markdown'), markdown_docs_path: help_page_path('user/markdown'),
releases_page_path: project_releases_path(@project, anchor: @release.tag), 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
end end
...@@ -318,6 +318,7 @@ class ProjectPolicy < BasePolicy ...@@ -318,6 +318,7 @@ class ProjectPolicy < BasePolicy
enable :read_pod_logs enable :read_pod_logs
enable :destroy_deploy_token enable :destroy_deploy_token
enable :read_prometheus_alerts enable :read_prometheus_alerts
enable :admin_terraform_state
end end
rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
......
...@@ -6,7 +6,13 @@ module Groups ...@@ -6,7 +6,13 @@ module Groups
include DeployTokenMethods include DeployTokenMethods
def execute 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 end
end end
......
...@@ -6,7 +6,13 @@ module Projects ...@@ -6,7 +6,13 @@ module Projects
include DeployTokenMethods include DeployTokenMethods
def execute 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 end
end end
......
%p.profile-settings-content %p.profile-settings-content
= s_("DeployTokens|Pick a name for the application, and we'll give you a unique deploy token.") = 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_errors(token)
.form-group .form-group
......
---
title: Accept `author_username` as a param in Merge Requests API
merge_request: 28100
author:
type: changed
---
title: Conditional mocking of admin mode in specs by directory
merge_request: 28420
author: Diego Louzán
type: other
---
title: Remove repeated examples in user model specs
merge_request: 28450
author: Rajendra Kadam
type: changed
# 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
...@@ -8520,6 +8520,8 @@ CREATE INDEX index_analytics_ca_project_stages_on_relative_position ON public.an ...@@ -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_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_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); 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; ...@@ -12909,5 +12911,6 @@ COPY "schema_migrations" (version) FROM STDIN;
20200326135443 20200326135443
20200326144443 20200326144443
20200326145443 20200326145443
20200330074719
\. \.
...@@ -541,6 +541,12 @@ Particular attention should be shown to: ...@@ -541,6 +541,12 @@ Particular attention should be shown to:
gitlab-ctl reconfigure 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: 1. Verify that GitLab can reach Praefect:
```shell ```shell
......
...@@ -30,6 +30,7 @@ GET /merge_requests?state=all ...@@ -30,6 +30,7 @@ GET /merge_requests?state=all
GET /merge_requests?milestone=release GET /merge_requests?milestone=release
GET /merge_requests?labels=bug,reproduced GET /merge_requests?labels=bug,reproduced
GET /merge_requests?author_id=5 GET /merge_requests?author_id=5
GET /merge_requests?author_username=gitlab-bot
GET /merge_requests?my_reaction_emoji=star GET /merge_requests?my_reaction_emoji=star
GET /merge_requests?scope=assigned_to_me GET /merge_requests?scope=assigned_to_me
GET /merge_requests?search=foo&in=title GET /merge_requests?search=foo&in=title
...@@ -51,7 +52,8 @@ Parameters: ...@@ -51,7 +52,8 @@ Parameters:
| `updated_after` | datetime | no | Return merge requests updated on or after the given time | | `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 | | `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. | | `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. | | `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. | | `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. | | `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: ...@@ -230,7 +232,8 @@ Parameters:
| `updated_after` | datetime | no | Return merge requests updated on or after the given time | | `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 | | `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)_ | | `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)_ | | `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. | | `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. | | `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: ...@@ -392,7 +395,8 @@ Parameters:
| `updated_after` | datetime | no | Return merge requests updated on or after the given time | | `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 | | `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> | | `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)_ | | `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. | | `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. | | `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. |
......
...@@ -243,12 +243,35 @@ Do not include the same information in multiple places. [Link to a SSOT instead. ...@@ -243,12 +243,35 @@ Do not include the same information in multiple places. [Link to a SSOT instead.
## Language ## Language
- Use inclusive language and avoid jargon, as well as uncommon GitLab documentation should be clear and easy to understand.
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 documentation.
- Be clear, concise, and stick to the goal of the doc.
- Write in US English with US grammar. - 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. - 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: - Use title case when referring to:
- [GitLab Features](https://about.gitlab.com/features/). For example, Issue Board, - [GitLab Features](https://about.gitlab.com/features/). For example, Issue Board,
Geo, and Runner. Geo, and Runner.
...@@ -335,8 +358,6 @@ as even native users of English might misunderstand them. ...@@ -335,8 +358,6 @@ as even native users of English might misunderstand them.
- [Write in Markdown](#markdown). - [Write in Markdown](#markdown).
- Splitting long lines (preferably up to 100 characters) can make it easier to provide feedback on small chunks of text. - 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. - 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: - Insert an empty line between different markups (for example, after every paragraph, header, list, and so on). Example:
```md ```md
...@@ -572,13 +593,10 @@ For other punctuation rules, please refer to the ...@@ -572,13 +593,10 @@ For other punctuation rules, please refer to the
- Leave exactly one blank line before and after a heading. - Leave exactly one blank line before and after a heading.
- Do not use links in headings. - Do not use links in headings.
- Add the corresponding [product badge](#product-badges) according to the tier the feature belongs. - 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 - Our docs site search engine prioritizes words used in headings and subheadings.
it refers to a product feature. For example, capitalizing "issues" is acceptable in Make you subheading titles clear, descriptive, and complete to help users find the
`## What you can do with GitLab Issues`, but not in `## Closing multiple issues`. right example, as shown in the section on [heading titles](#heading-titles).
- Our docs site search engine prioritizes headings, therefore, make sure to write - See [Capitalization](#capitalization) for guidelines on capitalizing headings.
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.
### Heading titles ### Heading titles
...@@ -589,6 +607,9 @@ Keep heading titles clear and direct. Make every word count. To accommodate sear ...@@ -589,6 +607,9 @@ Keep heading titles clear and direct. Make every word count. To accommodate sear
| Configure GDK | Configuring GDK | | Configure GDK | Configuring GDK |
| GitLab Release and Maintenance Policy | This section covers GitLab's Release and Maintenance Policy | | GitLab Release and Maintenance Policy | This section covers GitLab's Release and Maintenance Policy |
| Backport to older releases | Backporting to older releases | | 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:** NOTE: **Note:**
If you change an existing title, be careful. Any such changes may affect not only [links](#anchor-links) If you change an existing title, be careful. Any such changes may affect not only [links](#anchor-links)
......
...@@ -38,11 +38,9 @@ The current stages are: ...@@ -38,11 +38,9 @@ The current stages are:
## Default image ## Default image
The default image is currently The default image is defined in <https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab-ci.yml>.
`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`.
It includes Ruby 2.6.5, Go 1.12, Git 2.24, Git LFS 2.9, Chrome 73, Node 12, Yarn 1.21, It includes Ruby, Go, Git, Git LFS, Chrome, Node, Yarn, PostgreSQL, and Graphics Magick.
PostgreSQL 9.6, and Graphics Magick 1.3.34.
The images used in our pipelines are configured in the The images used in our pipelines are configured in the
[`gitlab-org/gitlab-build-images`](https://gitlab.com/gitlab-org/gitlab-build-images) [`gitlab-org/gitlab-build-images`](https://gitlab.com/gitlab-org/gitlab-build-images)
......
...@@ -68,7 +68,10 @@ A self-managed subscription uses a hybrid model. You pay for a subscription acco ...@@ -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: 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. - Members with Guest permissions on an Ultimate subscription.
- GitLab-created service accounts: `Ghost User` and `Support Bot`. - GitLab-created service accounts: `Ghost User` and `Support Bot`.
......
...@@ -230,7 +230,7 @@ Container Scanning can be executed on an offline GitLab Ultimate installation by ...@@ -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 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`. `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: 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:
......
...@@ -9,25 +9,28 @@ type: reference ...@@ -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. > - 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** 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. 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. 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 When you hover over an epic bar, a popover appears with its title, start and due dates, and weight
completed. 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) ![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 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** - Created date
- **Last updated** - Last updated
- **Start date** - Start date
- **Due 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, 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). including the [epics list view](../epics/index.md).
......
...@@ -637,6 +637,15 @@ clicking on the context menu in the upper-right corner. ...@@ -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. 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 ### Downloading data as CSV
Data from Prometheus charts on the metrics dashboard can be downloaded as CSV. Data from Prometheus charts on the metrics dashboard can be downloaded as CSV.
......
...@@ -172,6 +172,7 @@ module API ...@@ -172,6 +172,7 @@ module API
mount ::API::ProjectSnippets mount ::API::ProjectSnippets
mount ::API::ProjectStatistics mount ::API::ProjectStatistics
mount ::API::ProjectTemplates mount ::API::ProjectTemplates
mount ::API::Terraform::State
mount ::API::ProtectedBranches mount ::API::ProtectedBranches
mount ::API::ProtectedTags mount ::API::ProtectedTags
mount ::API::Releases mount ::API::Releases
......
...@@ -46,6 +46,10 @@ module API ...@@ -46,6 +46,10 @@ module API
prepend_if_ee('EE::API::APIGuard::HelperMethods') # rubocop: disable Cop/InjectEnterpriseEditionModule prepend_if_ee('EE::API::APIGuard::HelperMethods') # rubocop: disable Cop/InjectEnterpriseEditionModule
include Gitlab::Auth::AuthFinders include Gitlab::Auth::AuthFinders
def access_token
super || find_personal_access_token_from_http_basic_auth
end
def find_current_user! def find_current_user!
user = find_user_from_sources user = find_user_from_sources
return unless user return unless user
......
...@@ -65,11 +65,15 @@ module API ...@@ -65,11 +65,15 @@ module API
post ':id/deploy_tokens' do post ':id/deploy_tokens' do
authorize!(:create_deploy_token, user_project) 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)) user_project, current_user, scope_params.merge(declared(params, include_missing: false, include_parent_namespaces: false))
).execute ).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 end
desc 'Delete a project deploy token' do desc 'Delete a project deploy token' do
...@@ -126,11 +130,15 @@ module API ...@@ -126,11 +130,15 @@ module API
post ':id/deploy_tokens' do post ':id/deploy_tokens' do
authorize!(:create_deploy_token, user_group) 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)) user_group, current_user, scope_params.merge(declared(params, include_missing: false, include_parent_namespaces: false))
).execute ).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 end
desc 'Delete a group deploy token' do desc 'Delete a group deploy token' do
......
...@@ -36,7 +36,11 @@ module API ...@@ -36,7 +36,11 @@ module API
type: String, type: String,
values: %w[simple], values: %w[simple],
desc: 'If simple, returns the `iid`, URL, title, description, and basic state of merge request' 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_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, optional :assignee_id,
types: [Integer, String], types: [Integer, String],
integer_none_any: true, integer_none_any: true,
......
...@@ -207,10 +207,7 @@ module API ...@@ -207,10 +207,7 @@ module API
status 202 status 202
header 'Job-Status', job.status header 'Job-Status', job.status
header 'Range', "0-#{stream_size}" header 'Range', "0-#{stream_size}"
header 'X-GitLab-Trace-Update-Interval', job.trace.update_interval.to_s
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
end end
desc 'Authorize artifacts uploading for job' do desc 'Authorize artifacts uploading for job' do
......
# 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
...@@ -167,6 +167,14 @@ module Gitlab ...@@ -167,6 +167,14 @@ module Gitlab
oauth_token oauth_token
end 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 def parsed_oauth_token
Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods) Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods)
end end
......
...@@ -76,6 +76,11 @@ msgid_plural "%d changed files" ...@@ -76,6 +76,11 @@ msgid_plural "%d changed files"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%d child epic"
msgid_plural "%d child epics"
msgstr[0] ""
msgstr[1] ""
msgid "%d code quality issue" msgid "%d code quality issue"
msgid_plural "%d code quality issues" msgid_plural "%d code quality issues"
msgstr[0] "" msgstr[0] ""
...@@ -390,6 +395,9 @@ msgstr "" ...@@ -390,6 +395,9 @@ msgstr ""
msgid "%{openedIssues} open, %{closedIssues} closed" msgid "%{openedIssues} open, %{closedIssues} closed"
msgstr "" msgstr ""
msgid "%{percentage}%% weight completed"
msgstr ""
msgid "%{percent}%% complete" msgid "%{percent}%% complete"
msgstr "" msgstr ""
...@@ -1060,6 +1068,9 @@ msgid_plural "Add %d issues" ...@@ -1060,6 +1068,9 @@ msgid_plural "Add %d issues"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" 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" msgid "Add CHANGELOG"
msgstr "" msgstr ""
...@@ -1138,6 +1149,9 @@ msgstr "" ...@@ -1138,6 +1149,9 @@ msgstr ""
msgid "Add an issue" msgid "Add an issue"
msgstr "" msgstr ""
msgid "Add another link"
msgstr ""
msgid "Add approval rule" msgid "Add approval rule"
msgstr "" msgstr ""
...@@ -4986,6 +5000,9 @@ msgstr "" ...@@ -4986,6 +5000,9 @@ msgstr ""
msgid "Collapse approvers" msgid "Collapse approvers"
msgstr "" msgstr ""
msgid "Collapse child epics"
msgstr ""
msgid "Collapse sidebar" msgid "Collapse sidebar"
msgstr "" msgstr ""
...@@ -8307,6 +8324,9 @@ msgstr "" ...@@ -8307,6 +8324,9 @@ msgstr ""
msgid "Expand approvers" msgid "Expand approvers"
msgstr "" msgstr ""
msgid "Expand child epics"
msgstr ""
msgid "Expand down" msgid "Expand down"
msgstr "" msgstr ""
...@@ -11994,6 +12014,9 @@ msgstr "" ...@@ -11994,6 +12014,9 @@ msgstr ""
msgid "Link copied" msgid "Link copied"
msgstr "" msgstr ""
msgid "Link title"
msgstr ""
msgid "Linked emails (%{email_count})" msgid "Linked emails (%{email_count})"
msgstr "" msgstr ""
...@@ -14795,6 +14818,9 @@ msgstr "" ...@@ -14795,6 +14818,9 @@ msgstr ""
msgid "Pods in use" msgid "Pods in use"
msgstr "" 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" msgid "Preferences"
msgstr "" msgstr ""
...@@ -16539,6 +16565,12 @@ msgid_plural "Releases" ...@@ -16539,6 +16565,12 @@ msgid_plural "Releases"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "Release assets"
msgstr ""
msgid "Release assets documentation"
msgstr ""
msgid "Release does not have the same project as the milestone" msgid "Release does not have the same project as the milestone"
msgstr "" msgstr ""
...@@ -16608,6 +16640,9 @@ msgstr "" ...@@ -16608,6 +16640,9 @@ msgstr ""
msgid "Remove approvers?" msgid "Remove approvers?"
msgstr "" msgstr ""
msgid "Remove asset link"
msgstr ""
msgid "Remove assignee" msgid "Remove assignee"
msgstr "" msgstr ""
...@@ -20745,19 +20780,25 @@ msgstr "" ...@@ -20745,19 +20780,25 @@ msgstr ""
msgid "ThreatMonitoring|Anomalous Requests" msgid "ThreatMonitoring|Anomalous Requests"
msgstr "" msgstr ""
msgid "ThreatMonitoring|At this time, threat monitoring only supports WAF data." msgid "ThreatMonitoring|Application firewall not detected"
msgstr "" msgstr ""
msgid "ThreatMonitoring|Container Network Policy" msgid "ThreatMonitoring|Container Network Policy"
msgstr "" 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" msgid "ThreatMonitoring|Dropped Packets"
msgstr "" msgstr ""
msgid "ThreatMonitoring|Environment" msgid "ThreatMonitoring|Environment"
msgstr "" msgstr ""
msgid "ThreatMonitoring|No traffic to display" msgid "ThreatMonitoring|No environments detected"
msgstr "" msgstr ""
msgid "ThreatMonitoring|Operations Per Second" msgid "ThreatMonitoring|Operations Per Second"
...@@ -20778,6 +20819,9 @@ msgstr "" ...@@ -20778,6 +20819,9 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics" msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr "" 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." 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 "" msgstr ""
...@@ -20787,13 +20831,10 @@ msgstr "" ...@@ -20787,13 +20831,10 @@ msgstr ""
msgid "ThreatMonitoring|Threat Monitoring help page link" msgid "ThreatMonitoring|Threat Monitoring help page link"
msgstr "" msgstr ""
msgid "ThreatMonitoring|Threat monitoring is not enabled" msgid "ThreatMonitoring|Time"
msgstr ""
msgid "ThreatMonitoring|Threat monitoring provides security monitoring and rules to protect production applications."
msgstr "" 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 "" msgstr ""
msgid "ThreatMonitoring|Total Packets" msgid "ThreatMonitoring|Total Packets"
...@@ -20802,10 +20843,10 @@ msgstr "" ...@@ -20802,10 +20843,10 @@ msgstr ""
msgid "ThreatMonitoring|Total Requests" msgid "ThreatMonitoring|Total Requests"
msgstr "" msgstr ""
msgid "ThreatMonitoring|Web Application Firewall" msgid "ThreatMonitoring|View documentation"
msgstr "" 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 "" 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." 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."
......
...@@ -212,14 +212,86 @@ describe Groups::Settings::CiCdController do ...@@ -212,14 +212,86 @@ describe Groups::Settings::CiCdController do
end end
describe 'POST create_deploy_token' do describe 'POST create_deploy_token' do
it_behaves_like 'a created deploy token' do context 'when ajax_new_deploy_token feature flag is disabled for the project' do
let(:entity) { group }
let(:create_entity_params) { { group_id: group } }
let(:deploy_token_type) { DeployToken.deploy_token_types[:group_type] }
before do before do
stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: group })
entity.add_owner(user) entity.add_owner(user)
end 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 end
end end
...@@ -249,10 +249,82 @@ describe Projects::Settings::CiCdController do ...@@ -249,10 +249,82 @@ describe Projects::Settings::CiCdController do
end end
describe 'POST create_deploy_token' do describe 'POST create_deploy_token' do
it_behaves_like 'a created deploy token' do context 'when ajax_new_deploy_token feature flag is disabled for the project' do
let(:entity) { project } before do
let(:create_entity_params) { { namespace_id: project.namespace, project_id: project } } stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project })
let(:deploy_token_type) { DeployToken.deploy_token_types[:project_type] } 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 end
end end
...@@ -14,6 +14,7 @@ describe 'Projects > Settings > CI / CD settings' do ...@@ -14,6 +14,7 @@ describe 'Projects > Settings > CI / CD settings' do
project.add_role(user, role) project.add_role(user, role)
sign_in(user) sign_in(user)
stub_container_registry_config(enabled: true) stub_container_registry_config(enabled: true)
stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project })
visit project_settings_ci_cd_path(project) visit project_settings_ci_cd_path(project)
end end
......
...@@ -11,6 +11,7 @@ describe 'Repository Settings > User sees revoke deploy token modal', :js do ...@@ -11,6 +11,7 @@ describe 'Repository Settings > User sees revoke deploy token modal', :js do
before do before do
project.add_role(user, role) project.add_role(user, role)
sign_in(user) sign_in(user)
stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project })
visit(project_settings_ci_cd_path(project)) visit(project_settings_ci_cd_path(project))
click_link('Revoke') click_link('Revoke')
end end
......
...@@ -570,4 +570,65 @@ describe('Api', () => { ...@@ -570,4 +570,65 @@ describe('Api', () => {
.catch(done.fail); .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);
});
});
});
});
}); });
...@@ -4,6 +4,7 @@ import ReleaseEditApp from '~/releases/components/app_edit.vue'; ...@@ -4,6 +4,7 @@ import ReleaseEditApp from '~/releases/components/app_edit.vue';
import { release as originalRelease } from '../mock_data'; import { release as originalRelease } from '../mock_data';
import * as commonUtils from '~/lib/utils/common_utils'; import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants'; import { BACK_URL_PARAM } from '~/releases/constants';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
describe('Release edit component', () => { describe('Release edit component', () => {
let wrapper; let wrapper;
...@@ -11,7 +12,7 @@ describe('Release edit component', () => { ...@@ -11,7 +12,7 @@ describe('Release edit component', () => {
let actions; let actions;
let state; let state;
const factory = () => { const factory = (featureFlags = {}) => {
state = { state = {
release, release,
markdownDocsPath: 'path/to/markdown/docs', markdownDocsPath: 'path/to/markdown/docs',
...@@ -22,6 +23,7 @@ describe('Release edit component', () => { ...@@ -22,6 +23,7 @@ describe('Release edit component', () => {
actions = { actions = {
fetchRelease: jest.fn(), fetchRelease: jest.fn(),
updateRelease: jest.fn(), updateRelease: jest.fn(),
addEmptyAssetLink: jest.fn(),
}; };
const store = new Vuex.Store({ const store = new Vuex.Store({
...@@ -36,6 +38,9 @@ describe('Release edit component', () => { ...@@ -36,6 +38,9 @@ describe('Release edit component', () => {
wrapper = mount(ReleaseEditApp, { wrapper = mount(ReleaseEditApp, {
store, store,
provide: {
glFeatures: featureFlags,
},
}); });
}; };
...@@ -132,4 +137,28 @@ describe('Release edit component', () => { ...@@ -132,4 +137,28 @@ describe('Release edit component', () => {
expect(cancelButton.attributes().href).toBe(backUrl); 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);
});
});
});
}); });
...@@ -9,6 +9,7 @@ import createState from '~/releases/stores/modules/detail/state'; ...@@ -9,6 +9,7 @@ import createState from '~/releases/stores/modules/detail/state';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import api from '~/api';
jest.mock('~/flash', () => jest.fn()); jest.mock('~/flash', () => jest.fn());
...@@ -179,40 +180,92 @@ describe('Release detail actions', () => { ...@@ -179,40 +180,92 @@ describe('Release detail actions', () => {
}); });
describe('updateRelease', () => { describe('updateRelease', () => {
let getReleaseUrl; let getters;
let dispatch;
let callOrder;
beforeEach(() => { beforeEach(() => {
state.release = release; state.release = convertObjectPropsToCamelCase(release);
state.projectId = '18'; state.projectId = '18';
state.tagName = 'v1.3'; state.tagName = state.release.tagName;
getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`;
});
it(`dispatches requestUpdateRelease and receiveUpdateReleaseSuccess`, () => { getters = {
mock.onPut(getReleaseUrl).replyOnce(200); releaseLinksToDelete: [{ id: '1' }, { id: '2' }],
releaseLinksToCreate: [{ id: 'new-link-1' }, { id: 'new-link-2' }],
};
return testAction( dispatch = jest.fn();
actions.updateRelease,
undefined, callOrder = [];
state, jest.spyOn(api, 'updateRelease').mockImplementation(() => {
[], callOrder.push('updateRelease');
[{ type: 'requestUpdateRelease' }, { type: 'receiveUpdateReleaseSuccess' }], 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`, () => { it('dispatches requestUpdateRelease and receiveUpdateReleaseSuccess', () => {
mock.onPut(getReleaseUrl).replyOnce(500); return actions.updateRelease({ dispatch, state, getters }).then(() => {
expect(dispatch.mock.calls).toEqual([
['requestUpdateRelease'],
['receiveUpdateReleaseSuccess'],
]);
});
});
return testAction( it('dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object', () => {
actions.updateRelease, jest.spyOn(api, 'updateRelease').mockRejectedValue(error);
undefined,
state, return actions.updateRelease({ dispatch, state, getters }).then(() => {
[], expect(dispatch.mock.calls).toEqual([
[ ['requestUpdateRelease'],
{ type: 'requestUpdateRelease' }, ['receiveUpdateReleaseError', error],
{ type: 'receiveUpdateReleaseError', payload: expect.anything() }, ]);
], });
); });
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);
});
});
}); });
}); });
}); });
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);
});
});
});
...@@ -8,11 +8,12 @@ ...@@ -8,11 +8,12 @@
import createState from '~/releases/stores/modules/detail/state'; import createState from '~/releases/stores/modules/detail/state';
import mutations from '~/releases/stores/modules/detail/mutations'; import mutations from '~/releases/stores/modules/detail/mutations';
import * as types from '~/releases/stores/modules/detail/mutation_types'; 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', () => { describe('Release detail mutations', () => {
let state; let state;
let releaseClone; let release;
beforeEach(() => { beforeEach(() => {
state = createState({ state = createState({
...@@ -23,7 +24,7 @@ describe('Release detail mutations', () => { ...@@ -23,7 +24,7 @@ describe('Release detail mutations', () => {
markdownPreviewPath: 'path/to/markdown/preview', markdownPreviewPath: 'path/to/markdown/preview',
updateReleaseApiDocsPath: 'path/to/api/docs', updateReleaseApiDocsPath: 'path/to/api/docs',
}); });
releaseClone = JSON.parse(JSON.stringify(release)); release = convertObjectPropsToCamelCase(originalRelease);
}); });
describe(types.REQUEST_RELEASE, () => { describe(types.REQUEST_RELEASE, () => {
...@@ -36,13 +37,15 @@ describe('Release detail mutations', () => { ...@@ -36,13 +37,15 @@ describe('Release detail mutations', () => {
describe(types.RECEIVE_RELEASE_SUCCESS, () => { describe(types.RECEIVE_RELEASE_SUCCESS, () => {
it('handles a successful response from the server', () => { 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.fetchError).toEqual(undefined);
expect(state.isFetchingRelease).toEqual(false); 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', () => { ...@@ -61,7 +64,7 @@ describe('Release detail mutations', () => {
describe(types.UPDATE_RELEASE_TITLE, () => { describe(types.UPDATE_RELEASE_TITLE, () => {
it("updates the release's title", () => { it("updates the release's title", () => {
state.release = releaseClone; state.release = release;
const newTitle = 'The new release title'; const newTitle = 'The new release title';
mutations[types.UPDATE_RELEASE_TITLE](state, newTitle); mutations[types.UPDATE_RELEASE_TITLE](state, newTitle);
...@@ -71,7 +74,7 @@ describe('Release detail mutations', () => { ...@@ -71,7 +74,7 @@ describe('Release detail mutations', () => {
describe(types.UPDATE_RELEASE_NOTES, () => { describe(types.UPDATE_RELEASE_NOTES, () => {
it("updates the release's notes", () => { it("updates the release's notes", () => {
state.release = releaseClone; state.release = release;
const newNotes = 'The new release notes'; const newNotes = 'The new release notes';
mutations[types.UPDATE_RELEASE_NOTES](state, newNotes); mutations[types.UPDATE_RELEASE_NOTES](state, newNotes);
...@@ -89,7 +92,7 @@ describe('Release detail mutations', () => { ...@@ -89,7 +92,7 @@ describe('Release detail mutations', () => {
describe(types.RECEIVE_UPDATE_RELEASE_SUCCESS, () => { describe(types.RECEIVE_UPDATE_RELEASE_SUCCESS, () => {
it('handles a successful response from the server', () => { 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); expect(state.updateError).toEqual(undefined);
...@@ -107,4 +110,65 @@ describe('Release detail mutations', () => { ...@@ -107,4 +110,65 @@ describe('Release detail mutations', () => {
expect(state.updateError).toEqual(error); 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);
});
});
}); });
...@@ -53,7 +53,8 @@ describe ReleasesHelper do ...@@ -53,7 +53,8 @@ describe ReleasesHelper do
markdown_preview_path markdown_preview_path
markdown_docs_path markdown_docs_path
releases_page_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) expect(helper.data_for_edit_release_page.keys).to eq(keys)
end end
end end
......
...@@ -335,6 +335,54 @@ describe Gitlab::Auth::AuthFinders do ...@@ -335,6 +335,54 @@ describe Gitlab::Auth::AuthFinders do
end end
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 describe '#find_user_from_basic_auth_job' do
def basic_http_auth(username, password) def basic_http_auth(username, password)
ActionController::HttpAuthentication::Basic.encode_credentials(username, password) ActionController::HttpAuthentication::Basic.encode_credentials(username, password)
......
...@@ -3681,15 +3681,15 @@ describe User, :do_not_mock_admin_mode do ...@@ -3681,15 +3681,15 @@ describe User, :do_not_mock_admin_mode do
end end
it 'returns false if email can not be synced' do 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 end
it 'returns false if location can not be synced' do 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 end
it 'returns true for all syncable attributes if all syncable attributes can be synced' do it 'returns true for all syncable attributes if all syncable attributes can be synced' do
......
...@@ -53,6 +53,7 @@ describe ProjectPolicy do ...@@ -53,6 +53,7 @@ describe ProjectPolicy do
admin_commit_status admin_build admin_container_image admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster admin_pipeline admin_environment admin_deployment destroy_release add_cluster
daily_statistics read_deploy_token create_deploy_token destroy_deploy_token daily_statistics read_deploy_token create_deploy_token destroy_deploy_token
admin_terraform_state
] ]
end end
......
...@@ -526,12 +526,48 @@ describe API::MergeRequests do ...@@ -526,12 +526,48 @@ describe API::MergeRequests do
expect_response_contain_exactly(merge_request3.id) expect_response_contain_exactly(merge_request3.id)
end end
it 'returns an array of merge requests authored by the given user' do context 'filter by author' do
merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch') 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 end
it 'returns an array of merge requests assigned to the given user' do it 'returns an array of merge requests assigned to the given user' do
...@@ -1525,7 +1561,7 @@ describe API::MergeRequests do ...@@ -1525,7 +1561,7 @@ describe API::MergeRequests do
it "returns 400 when target_branch is missing" do it "returns 400 when target_branch is missing" do
post api("/projects/#{forked_project.id}/merge_requests", user2), 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) expect(response).to have_gitlab_http_status(:bad_request)
end end
......
...@@ -1349,19 +1349,6 @@ describe API::Runner, :clean_gitlab_redis_shared_state do ...@@ -1349,19 +1349,6 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(response.header['X-GitLab-Trace-Update-Interval']).to eq('30') expect(response.header['X-GitLab-Trace-Update-Interval']).to eq('30')
end end
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 end
context 'when Runner makes a force-patch' do context 'when Runner makes a force-patch' do
......
# 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
...@@ -214,15 +214,62 @@ RSpec.configure do |config| ...@@ -214,15 +214,62 @@ RSpec.configure do |config|
# modifying a significant number of specs to test both states for admin # modifying a significant number of specs to test both states for admin
# mode enabled / disabled. # mode enabled / disabled.
# #
# See https://gitlab.com/gitlab-org/gitlab/issues/31511 # This will only be applied to specs below dirs in `admin_mode_mock_dirs`
# See gitlab/spec/support/helpers/admin_mode_helpers.rb
# #
# 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 # 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: # 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 # context 'some test in mocked dir', :do_not_mock_admin_mode do ... end
unless example.metadata[:do_not_mock_admin_mode] 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| allow_any_instance_of(Gitlab::Auth::CurrentUserMode).to receive(:admin_mode?) do |current_user_mode|
current_user_mode.send(:user)&.admin? current_user_mode.send(:user)&.admin?
end end
......
...@@ -17,7 +17,7 @@ RSpec.shared_examples 'a deploy token creation service' do ...@@ -17,7 +17,7 @@ RSpec.shared_examples 'a deploy token creation service' do
end end
it 'returns a DeployToken' do 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
end end
...@@ -25,7 +25,7 @@ RSpec.shared_examples 'a deploy token creation service' do ...@@ -25,7 +25,7 @@ RSpec.shared_examples 'a deploy token creation service' do
let(:deploy_token_params) { attributes_for(:deploy_token, expires_at: '') } let(:deploy_token_params) { attributes_for(:deploy_token, expires_at: '') }
it 'sets Forever.date' do 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
end end
...@@ -33,7 +33,7 @@ RSpec.shared_examples 'a deploy token creation service' do ...@@ -33,7 +33,7 @@ RSpec.shared_examples 'a deploy token creation service' do
let(:deploy_token_params) { attributes_for(:deploy_token, username: '') } let(:deploy_token_params) { attributes_for(:deploy_token, username: '') }
it 'converts it to nil' do 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
end end
...@@ -41,7 +41,7 @@ RSpec.shared_examples 'a deploy token creation service' do ...@@ -41,7 +41,7 @@ RSpec.shared_examples 'a deploy token creation service' do
let(:deploy_token_params) { attributes_for(:deploy_token, username: 'deployer') } let(:deploy_token_params) { attributes_for(:deploy_token, username: 'deployer') }
it 'keeps the provided username' do 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
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment