Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
0076bbc673
commit
71d6b9014b
2
Gemfile
2
Gemfile
|
@ -239,7 +239,7 @@ gem 'ruby-progressbar', '~> 1.10'
|
||||||
gem 'settingslogic', '~> 2.0.9'
|
gem 'settingslogic', '~> 2.0.9'
|
||||||
|
|
||||||
# Linear-time regex library for untrusted regular expressions
|
# Linear-time regex library for untrusted regular expressions
|
||||||
gem 're2', '~> 1.5.0'
|
gem 're2', '~> 1.6.0'
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
|
|
||||||
|
|
|
@ -455,7 +455,7 @@
|
||||||
{"name":"rbtree","version":"0.4.4","platform":"ruby","checksum":"c1277a502a96fe8fd8656cb619db1ac87145df809ea4db35f7242b50bb161d5c"},
|
{"name":"rbtree","version":"0.4.4","platform":"ruby","checksum":"c1277a502a96fe8fd8656cb619db1ac87145df809ea4db35f7242b50bb161d5c"},
|
||||||
{"name":"rchardet","version":"1.8.0","platform":"ruby","checksum":"693acd5253d5ade81a51940697955f6dd4bb2f0d245bda76a8e23deec70a52c7"},
|
{"name":"rchardet","version":"1.8.0","platform":"ruby","checksum":"693acd5253d5ade81a51940697955f6dd4bb2f0d245bda76a8e23deec70a52c7"},
|
||||||
{"name":"rdoc","version":"6.3.2","platform":"ruby","checksum":"def4a720235c27d56c176ae73555e647eb04ea58a8bbaa927f8f9f79de7805a6"},
|
{"name":"rdoc","version":"6.3.2","platform":"ruby","checksum":"def4a720235c27d56c176ae73555e647eb04ea58a8bbaa927f8f9f79de7805a6"},
|
||||||
{"name":"re2","version":"1.5.0","platform":"ruby","checksum":"35fe8b408de9f1ef609b1e54e01ea1e55413ca3e9daf1e4b20756d9a02f630cc"},
|
{"name":"re2","version":"1.6.0","platform":"ruby","checksum":"2e37f27971f6a76223eac688c04f3e48aea374f34b302ec22d75b4635cd64bc1"},
|
||||||
{"name":"recaptcha","version":"4.13.1","platform":"ruby","checksum":"dc6c2cb78afa87034358b7ba1c6f7175972b5709fdf7500e2550623e119e3788"},
|
{"name":"recaptcha","version":"4.13.1","platform":"ruby","checksum":"dc6c2cb78afa87034358b7ba1c6f7175972b5709fdf7500e2550623e119e3788"},
|
||||||
{"name":"recursive-open-struct","version":"1.1.3","platform":"ruby","checksum":"a3538a72552fcebcd0ada657bdff313641a4a5fbc482c08cfb9a65acb1c9de5a"},
|
{"name":"recursive-open-struct","version":"1.1.3","platform":"ruby","checksum":"a3538a72552fcebcd0ada657bdff313641a4a5fbc482c08cfb9a65acb1c9de5a"},
|
||||||
{"name":"redcarpet","version":"3.5.1","platform":"ruby","checksum":"717f64cb6ec11c8d9ec9b521ed26ca2eeda68b4fe1fc3388a641176dbd47732f"},
|
{"name":"redcarpet","version":"3.5.1","platform":"ruby","checksum":"717f64cb6ec11c8d9ec9b521ed26ca2eeda68b4fe1fc3388a641176dbd47732f"},
|
||||||
|
|
|
@ -1139,7 +1139,7 @@ GEM
|
||||||
rbtree (0.4.4)
|
rbtree (0.4.4)
|
||||||
rchardet (1.8.0)
|
rchardet (1.8.0)
|
||||||
rdoc (6.3.2)
|
rdoc (6.3.2)
|
||||||
re2 (1.5.0)
|
re2 (1.6.0)
|
||||||
recaptcha (4.13.1)
|
recaptcha (4.13.1)
|
||||||
json
|
json
|
||||||
recursive-open-struct (1.1.3)
|
recursive-open-struct (1.1.3)
|
||||||
|
@ -1753,7 +1753,7 @@ DEPENDENCIES
|
||||||
rainbow (~> 3.0)
|
rainbow (~> 3.0)
|
||||||
rbtrace (~> 0.4)
|
rbtrace (~> 0.4)
|
||||||
rdoc (~> 6.3.2)
|
rdoc (~> 6.3.2)
|
||||||
re2 (~> 1.5.0)
|
re2 (~> 1.6.0)
|
||||||
recaptcha (~> 4.11)
|
recaptcha (~> 4.11)
|
||||||
redis (~> 4.7.0)
|
redis (~> 4.7.0)
|
||||||
redis-actionpack (~> 5.3.0)
|
redis-actionpack (~> 5.3.0)
|
||||||
|
|
|
@ -7,15 +7,12 @@ import RemoveMemberButton from './remove_member_button.vue';
|
||||||
export default {
|
export default {
|
||||||
name: 'AccessRequestActionButtons',
|
name: 'AccessRequestActionButtons',
|
||||||
components: { ActionButtonGroup, RemoveMemberButton, ApproveAccessRequestButton },
|
components: { ActionButtonGroup, RemoveMemberButton, ApproveAccessRequestButton },
|
||||||
|
inheritAttrs: false,
|
||||||
props: {
|
props: {
|
||||||
member: {
|
member: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
permissions: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isCurrentUser: {
|
isCurrentUser: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -43,10 +40,10 @@ export default {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<action-button-group>
|
<action-button-group>
|
||||||
<div v-if="permissions.canUpdate" class="gl-px-1">
|
<div class="gl-px-1">
|
||||||
<approve-access-request-button :member-id="member.id" />
|
<approve-access-request-button :member-id="member.id" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="permissions.canRemove" class="gl-px-1">
|
<div class="gl-px-1">
|
||||||
<remove-member-button
|
<remove-member-button
|
||||||
:member-id="member.id"
|
:member-id="member.id"
|
||||||
:message="message"
|
:message="message"
|
||||||
|
|
|
@ -27,13 +27,13 @@ export const TABS = [
|
||||||
{
|
{
|
||||||
namespace: MEMBER_TYPES.invite,
|
namespace: MEMBER_TYPES.invite,
|
||||||
title: __('Invited'),
|
title: __('Invited'),
|
||||||
canManageMembersPermissionsRequired: true,
|
requiredPermissions: ['canManageMembers'],
|
||||||
queryParamValue: TAB_QUERY_PARAM_VALUES.invite,
|
queryParamValue: TAB_QUERY_PARAM_VALUES.invite,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
namespace: MEMBER_TYPES.accessRequest,
|
namespace: MEMBER_TYPES.accessRequest,
|
||||||
title: __('Access requests'),
|
title: __('Access requests'),
|
||||||
canManageMembersPermissionsRequired: true,
|
requiredPermissions: ['canManageAccessRequests'],
|
||||||
queryParamValue: TAB_QUERY_PARAM_VALUES.accessRequest,
|
queryParamValue: TAB_QUERY_PARAM_VALUES.accessRequest,
|
||||||
},
|
},
|
||||||
...EE_TABS,
|
...EE_TABS,
|
||||||
|
@ -44,7 +44,7 @@ export default {
|
||||||
ACTIVE_TAB_QUERY_PARAM_NAME,
|
ACTIVE_TAB_QUERY_PARAM_NAME,
|
||||||
TABS,
|
TABS,
|
||||||
components: { MembersApp, GlTabs, GlTab, GlBadge, GlButton },
|
components: { MembersApp, GlTabs, GlTab, GlBadge, GlButton },
|
||||||
inject: ['canManageMembers', 'canExportMembers', 'exportCsvPath'],
|
inject: ['canManageMembers', 'canManageAccessRequests', 'canExportMembers', 'exportCsvPath'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedTabIndex: 0,
|
selectedTabIndex: 0,
|
||||||
|
@ -96,15 +96,13 @@ export default {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { canManageMembersPermissionsRequired = false } = tab;
|
const { requiredPermissions = [] } = tab;
|
||||||
const tabCanBeShown =
|
const tabCanBeShown =
|
||||||
this.getTabCount(tab) > 0 || this.activeTabIndexCalculatedFromUrlParams === index;
|
this.getTabCount(tab) > 0 || this.activeTabIndexCalculatedFromUrlParams === index;
|
||||||
|
|
||||||
if (canManageMembersPermissionsRequired) {
|
return (
|
||||||
return this.canManageMembers && tabCanBeShown;
|
tabCanBeShown && requiredPermissions.every((requiredPermission) => this[requiredPermission])
|
||||||
}
|
);
|
||||||
|
|
||||||
return tabCanBeShown;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const initMembersApp = (el, options) => {
|
||||||
const {
|
const {
|
||||||
sourceId,
|
sourceId,
|
||||||
canManageMembers,
|
canManageMembers,
|
||||||
|
canManageAccessRequests,
|
||||||
canExportMembers,
|
canExportMembers,
|
||||||
canFilterByEnterprise,
|
canFilterByEnterprise,
|
||||||
exportCsvPath,
|
exportCsvPath,
|
||||||
|
@ -61,6 +62,7 @@ export const initMembersApp = (el, options) => {
|
||||||
currentUserId: gon.current_user_id || null,
|
currentUserId: gon.current_user_id || null,
|
||||||
sourceId,
|
sourceId,
|
||||||
canManageMembers,
|
canManageMembers,
|
||||||
|
canManageAccessRequests,
|
||||||
canFilterByEnterprise,
|
canFilterByEnterprise,
|
||||||
canExportMembers,
|
canExportMembers,
|
||||||
exportCsvPath,
|
exportCsvPath,
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
import { GlButton } from '@gitlab/ui';
|
|
||||||
import { MOCK_HTML } from '../../../../../../spec/frontend/vue_shared/components/markdown_drawer/mock_data';
|
|
||||||
import MarkdownDrawer from './markdown_drawer.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
component: MarkdownDrawer,
|
|
||||||
title: 'vue_shared/markdown_drawer',
|
|
||||||
parameters: {
|
|
||||||
mirage: {
|
|
||||||
timing: 1000,
|
|
||||||
handlers: {
|
|
||||||
get: {
|
|
||||||
'/help/user/search/global_search/advanced_search_syntax.json': [
|
|
||||||
200,
|
|
||||||
{},
|
|
||||||
{ html: MOCK_HTML },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const createStory = ({ ...options }) => (_, { argTypes }) => ({
|
|
||||||
components: { MarkdownDrawer, GlButton },
|
|
||||||
props: Object.keys(argTypes),
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
render: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggleDrawer() {
|
|
||||||
this.$refs.drawer.toggleDrawer();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
this.render = true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
template: `
|
|
||||||
<div v-if="render">
|
|
||||||
<gl-button @click="toggleDrawer">Open Drawer</gl-button>
|
|
||||||
<markdown-drawer
|
|
||||||
:documentPath="'user/search/global_search/advanced_search_syntax.json'"
|
|
||||||
ref="drawer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Default = createStory({});
|
|
|
@ -1,117 +0,0 @@
|
||||||
<script>
|
|
||||||
import { GlSafeHtmlDirective as SafeHtml, GlDrawer, GlAlert, GlSkeletonLoader } from '@gitlab/ui';
|
|
||||||
import $ from 'jquery';
|
|
||||||
import '~/behaviors/markdown/render_gfm';
|
|
||||||
import { s__ } from '~/locale';
|
|
||||||
import { contentTop } from '~/lib/utils/common_utils';
|
|
||||||
import { getRenderedMarkdown } from './utils/fetch';
|
|
||||||
|
|
||||||
export const cache = {};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'MarkdownDrawer',
|
|
||||||
components: {
|
|
||||||
GlDrawer,
|
|
||||||
GlAlert,
|
|
||||||
GlSkeletonLoader,
|
|
||||||
},
|
|
||||||
directives: {
|
|
||||||
SafeHtml,
|
|
||||||
},
|
|
||||||
i18n: {
|
|
||||||
alert: s__('MardownDrawer|Could not fetch help contents.'),
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
documentPath: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
loading: false,
|
|
||||||
hasFetchError: false,
|
|
||||||
title: '',
|
|
||||||
body: null,
|
|
||||||
open: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
drawerOffsetTop() {
|
|
||||||
return `${contentTop()}px`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
documentPath: {
|
|
||||||
immediate: true,
|
|
||||||
handler: 'fetchMarkdown',
|
|
||||||
},
|
|
||||||
open(open) {
|
|
||||||
if (open && this.body) {
|
|
||||||
this.renderGLFM();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async fetchMarkdown() {
|
|
||||||
const cached = cache[this.documentPath];
|
|
||||||
this.hasFetchError = false;
|
|
||||||
this.title = '';
|
|
||||||
if (cached) {
|
|
||||||
this.title = cached.title;
|
|
||||||
this.body = cached.body;
|
|
||||||
if (this.open) {
|
|
||||||
this.renderGLFM();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.loading = true;
|
|
||||||
const { body, title, hasFetchError } = await getRenderedMarkdown(this.documentPath);
|
|
||||||
this.title = title;
|
|
||||||
this.body = body;
|
|
||||||
this.loading = false;
|
|
||||||
this.hasFetchError = hasFetchError;
|
|
||||||
if (this.open) {
|
|
||||||
this.renderGLFM();
|
|
||||||
}
|
|
||||||
cache[this.documentPath] = { title, body };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renderGLFM() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
$(this.$refs['content-element']).renderGFM();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
closeDrawer() {
|
|
||||||
this.open = false;
|
|
||||||
},
|
|
||||||
toggleDrawer() {
|
|
||||||
this.open = !this.open;
|
|
||||||
},
|
|
||||||
openDrawer() {
|
|
||||||
this.open = true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
safeHtmlConfig: {
|
|
||||||
ADD_TAGS: ['copy-code'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<gl-drawer :header-height="drawerOffsetTop" :open="open" header-sticky @close="closeDrawer">
|
|
||||||
<template #title>
|
|
||||||
<h4 data-testid="title-element" class="gl-m-0">{{ title }}</h4>
|
|
||||||
</template>
|
|
||||||
<template #default>
|
|
||||||
<div v-if="hasFetchError">
|
|
||||||
<gl-alert :dismissible="false" variant="danger">{{ $options.i18n.alert }}</gl-alert>
|
|
||||||
</div>
|
|
||||||
<gl-skeleton-loader v-else-if="loading" />
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
ref="content-element"
|
|
||||||
v-safe-html:[$options.safeHtmlConfig]="body"
|
|
||||||
class="md"
|
|
||||||
></div>
|
|
||||||
</template>
|
|
||||||
</gl-drawer>
|
|
||||||
</template>
|
|
|
@ -1,32 +0,0 @@
|
||||||
import * as Sentry from '@sentry/browser';
|
|
||||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
|
||||||
import axios from '~/lib/utils/axios_utils';
|
|
||||||
|
|
||||||
export const splitDocument = (htmlString) => {
|
|
||||||
const htmlDocument = new DOMParser().parseFromString(htmlString, 'text/html');
|
|
||||||
const title = htmlDocument.querySelector('h1')?.innerText;
|
|
||||||
htmlDocument.querySelector('h1')?.remove();
|
|
||||||
return {
|
|
||||||
title,
|
|
||||||
body: htmlDocument.querySelector('body').innerHTML.toString(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getRenderedMarkdown = (documentPath) => {
|
|
||||||
return axios
|
|
||||||
.get(helpPagePath(documentPath))
|
|
||||||
.then(({ data }) => {
|
|
||||||
const { body, title } = splitDocument(data.html);
|
|
||||||
return {
|
|
||||||
body,
|
|
||||||
title,
|
|
||||||
hasFetchError: false,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
Sentry.captureException(e);
|
|
||||||
return {
|
|
||||||
hasFetchError: true,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -53,6 +53,8 @@ module WebHooks
|
||||||
|
|
||||||
ps = params.require(:hook).permit(*permitted).to_h
|
ps = params.require(:hook).permit(*permitted).to_h
|
||||||
|
|
||||||
|
ps.delete(:token) if action_name == 'update' && ps[:token] == WebHook::SECRET_MASK
|
||||||
|
|
||||||
ps[:url_variables] = ps[:url_variables].to_h { [_1[:key], _1[:value].presence] } if ps.key?(:url_variables)
|
ps[:url_variables] = ps[:url_variables].to_h { [_1[:key], _1[:value].presence] } if ps.key?(:url_variables)
|
||||||
|
|
||||||
if action_name == 'update' && ps.key?(:url_variables)
|
if action_name == 'update' && ps.key?(:url_variables)
|
||||||
|
|
|
@ -24,6 +24,6 @@ class AccessRequestsFinder
|
||||||
private
|
private
|
||||||
|
|
||||||
def can_see_access_requests?(current_user)
|
def can_see_access_requests?(current_user)
|
||||||
source && Ability.allowed?(current_user, :"admin_#{source.class.to_s.underscore}", source)
|
source && Ability.allowed?(current_user, :admin_member_access_request, source)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,7 +12,8 @@ module Groups::GroupMembersHelper
|
||||||
invite: group_members_list_data(group, invited.nil? ? [] : invited, { param_name: :invited_members_page, params: { page: nil } }),
|
invite: group_members_list_data(group, invited.nil? ? [] : invited, { param_name: :invited_members_page, params: { page: nil } }),
|
||||||
access_request: group_members_list_data(group, access_requests.nil? ? [] : access_requests),
|
access_request: group_members_list_data(group, access_requests.nil? ? [] : access_requests),
|
||||||
source_id: group.id,
|
source_id: group.id,
|
||||||
can_manage_members: can?(current_user, :admin_group_member, group)
|
can_manage_members: can?(current_user, :admin_group_member, group),
|
||||||
|
can_manage_access_requests: can?(current_user, :admin_member_access_request, group)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,8 @@ module Projects::ProjectMembersHelper
|
||||||
invite: project_members_list_data(project, invited.nil? ? [] : invited),
|
invite: project_members_list_data(project, invited.nil? ? [] : invited),
|
||||||
access_request: project_members_list_data(project, access_requests.nil? ? [] : access_requests),
|
access_request: project_members_list_data(project, access_requests.nil? ? [] : access_requests),
|
||||||
source_id: project.id,
|
source_id: project.id,
|
||||||
can_manage_members: Ability.allowed?(current_user, :admin_project_member, project)
|
can_manage_members: Ability.allowed?(current_user, :admin_project_member, project),
|
||||||
|
can_manage_access_requests: Ability.allowed?(current_user, :admin_member_access_request, project)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ class WebHook < ApplicationRecord
|
||||||
INITIAL_BACKOFF = 1.minute
|
INITIAL_BACKOFF = 1.minute
|
||||||
MAX_BACKOFF = 1.day
|
MAX_BACKOFF = 1.day
|
||||||
BACKOFF_GROWTH_FACTOR = 2.0
|
BACKOFF_GROWTH_FACTOR = 2.0
|
||||||
|
SECRET_MASK = '************'
|
||||||
|
|
||||||
attr_encrypted :token,
|
attr_encrypted :token,
|
||||||
mode: :per_attribute_iv,
|
mode: :per_attribute_iv,
|
||||||
|
@ -210,6 +211,10 @@ class WebHook < ApplicationRecord
|
||||||
# Overridden in child classes.
|
# Overridden in child classes.
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def masked_token
|
||||||
|
token.present? ? SECRET_MASK : nil
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def next_failure_count
|
def next_failure_count
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module MemberPolicyHelpers
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def record_is_access_request_of_self?
|
||||||
|
record_is_access_request? && record_belongs_to_self?
|
||||||
|
end
|
||||||
|
|
||||||
|
def record_is_access_request?
|
||||||
|
@subject.request? # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||||
|
end
|
||||||
|
|
||||||
|
def record_belongs_to_self?
|
||||||
|
@user && @subject.user == @user # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class GroupMemberPolicy < BasePolicy
|
class GroupMemberPolicy < BasePolicy
|
||||||
|
include MemberPolicyHelpers
|
||||||
|
|
||||||
delegate :group
|
delegate :group
|
||||||
|
|
||||||
with_scope :subject
|
with_scope :subject
|
||||||
|
@ -9,7 +11,11 @@ class GroupMemberPolicy < BasePolicy
|
||||||
|
|
||||||
desc "Membership is users' own"
|
desc "Membership is users' own"
|
||||||
with_score 0
|
with_score 0
|
||||||
condition(:is_target_user) { @user && @subject.user_id == @user.id }
|
condition(:target_is_self) { record_belongs_to_self? }
|
||||||
|
|
||||||
|
desc "Membership is users' own access request"
|
||||||
|
with_score 0
|
||||||
|
condition(:access_request_of_self) { record_is_access_request_of_self? }
|
||||||
|
|
||||||
rule { anonymous }.policy do
|
rule { anonymous }.policy do
|
||||||
prevent :update_group_member
|
prevent :update_group_member
|
||||||
|
@ -28,9 +34,13 @@ class GroupMemberPolicy < BasePolicy
|
||||||
|
|
||||||
rule { project_bot & can?(:admin_group_member) }.enable :destroy_project_bot_member
|
rule { project_bot & can?(:admin_group_member) }.enable :destroy_project_bot_member
|
||||||
|
|
||||||
rule { is_target_user }.policy do
|
rule { target_is_self }.policy do
|
||||||
enable :destroy_group_member
|
enable :destroy_group_member
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rule { access_request_of_self }.policy do
|
||||||
|
enable :withdraw_member_access_request
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
GroupMemberPolicy.prepend_mod_with('GroupMemberPolicy')
|
GroupMemberPolicy.prepend_mod_with('GroupMemberPolicy')
|
||||||
|
|
|
@ -283,6 +283,11 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
|
||||||
prevent :create_resource_access_tokens
|
prevent :create_resource_access_tokens
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rule { can?(:admin_group_member) }.policy do
|
||||||
|
# ability to read, approve or reject member access requests of other users
|
||||||
|
enable :admin_member_access_request
|
||||||
|
end
|
||||||
|
|
||||||
rule { support_bot & has_project_with_service_desk_enabled }.policy do
|
rule { support_bot & has_project_with_service_desk_enabled }.policy do
|
||||||
enable :read_label
|
enable :read_label
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ProjectMemberPolicy < BasePolicy
|
class ProjectMemberPolicy < BasePolicy
|
||||||
|
include MemberPolicyHelpers
|
||||||
delegate { @subject.project }
|
delegate { @subject.project }
|
||||||
|
|
||||||
condition(:target_is_holder_of_the_personal_namespace, scope: :subject) do
|
condition(:target_is_holder_of_the_personal_namespace, scope: :subject) do
|
||||||
@subject.project.personal_namespace_holder?(@subject.user)
|
@subject.project.personal_namespace_holder?(@subject.user)
|
||||||
end
|
end
|
||||||
|
|
||||||
condition(:target_is_self) { @user && @subject.user == @user }
|
desc "Membership is users' own access request"
|
||||||
|
with_score 0
|
||||||
|
condition(:access_request_of_self) { record_is_access_request_of_self? }
|
||||||
|
|
||||||
|
condition(:target_is_self) { record_belongs_to_self? }
|
||||||
condition(:project_bot) { @subject.user&.project_bot? }
|
condition(:project_bot) { @subject.user&.project_bot? }
|
||||||
|
|
||||||
rule { anonymous }.prevent_all
|
rule { anonymous }.prevent_all
|
||||||
|
@ -24,5 +29,11 @@ class ProjectMemberPolicy < BasePolicy
|
||||||
|
|
||||||
rule { project_bot & can?(:admin_project_member) }.enable :destroy_project_bot_member
|
rule { project_bot & can?(:admin_project_member) }.enable :destroy_project_bot_member
|
||||||
|
|
||||||
rule { target_is_self }.enable :destroy_project_member
|
rule { target_is_self }.policy do
|
||||||
|
enable :destroy_project_member
|
||||||
|
end
|
||||||
|
|
||||||
|
rule { access_request_of_self }.policy do
|
||||||
|
enable :withdraw_member_access_request
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -834,6 +834,8 @@ class ProjectPolicy < BasePolicy
|
||||||
|
|
||||||
rule { can?(:admin_project_member) }.policy do
|
rule { can?(:admin_project_member) }.policy do
|
||||||
enable :import_project_members_from_another_project
|
enable :import_project_members_from_another_project
|
||||||
|
# ability to read, approve or reject member access requests of other users
|
||||||
|
enable :admin_member_access_request
|
||||||
end
|
end
|
||||||
|
|
||||||
rule { registry_enabled & can?(:admin_container_image) }.policy do
|
rule { registry_enabled & can?(:admin_container_image) }.policy do
|
||||||
|
|
|
@ -16,7 +16,7 @@ module Members
|
||||||
private
|
private
|
||||||
|
|
||||||
def validate_access!(access_requester)
|
def validate_access!(access_requester)
|
||||||
raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester)
|
raise Gitlab::Access::AccessDeniedError unless can_approve_access_requester?(access_requester)
|
||||||
|
|
||||||
if approving_member_with_owner_access_level?(access_requester) &&
|
if approving_member_with_owner_access_level?(access_requester) &&
|
||||||
cannot_assign_owner_responsibilities_to_member_in_project?(access_requester)
|
cannot_assign_owner_responsibilities_to_member_in_project?(access_requester)
|
||||||
|
@ -24,8 +24,8 @@ module Members
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_update_access_requester?(access_requester)
|
def can_approve_access_requester?(access_requester)
|
||||||
can?(current_user, update_member_permission(access_requester), access_requester)
|
can?(current_user, :admin_member_access_request, access_requester.source)
|
||||||
end
|
end
|
||||||
|
|
||||||
def approving_member_with_owner_access_level?(access_requester)
|
def approving_member_with_owner_access_level?(access_requester)
|
||||||
|
|
|
@ -48,6 +48,10 @@ module Members
|
||||||
def authorized?(member, destroy_bot)
|
def authorized?(member, destroy_bot)
|
||||||
return can_destroy_bot_member?(member) if destroy_bot
|
return can_destroy_bot_member?(member) if destroy_bot
|
||||||
|
|
||||||
|
if member.request?
|
||||||
|
return can_destroy_member_access_request?(member) || can_withdraw_member_access_request?(member)
|
||||||
|
end
|
||||||
|
|
||||||
can_destroy_member?(member)
|
can_destroy_member?(member)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -106,6 +110,14 @@ module Members
|
||||||
can?(current_user, destroy_bot_member_permission(member), member)
|
can?(current_user, destroy_bot_member_permission(member), member)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def can_destroy_member_access_request?(member)
|
||||||
|
can?(current_user, :admin_member_access_request, member.source)
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_withdraw_member_access_request?(member)
|
||||||
|
can?(current_user, :withdraw_member_access_request, member)
|
||||||
|
end
|
||||||
|
|
||||||
def destroying_member_with_owner_access_level?(member)
|
def destroying_member_with_owner_access_level?(member)
|
||||||
member.owner?
|
member.owner?
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,9 +8,10 @@
|
||||||
data: { confirm: leave_confirmation_message(source), confirm_btn_variant: 'danger', qa_selector: 'leave_group_link' },
|
data: { confirm: leave_confirmation_message(source), confirm_btn_variant: 'danger', qa_selector: 'leave_group_link' },
|
||||||
class: 'js-leave-link'
|
class: 'js-leave-link'
|
||||||
- elsif requester = source.requesters.find_by(user_id: current_user.id) # rubocop: disable CodeReuse/ActiveRecord
|
- elsif requester = source.requesters.find_by(user_id: current_user.id) # rubocop: disable CodeReuse/ActiveRecord
|
||||||
= link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]),
|
- if can?(current_user, :withdraw_member_access_request, requester)
|
||||||
method: :delete,
|
= link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]),
|
||||||
data: { confirm: remove_member_message(requester) }
|
method: :delete,
|
||||||
|
data: { confirm: remove_member_message(requester) }
|
||||||
- elsif source.request_access_enabled && can?(current_user, :request_access, source)
|
- elsif source.request_access_enabled && can?(current_user, :request_access, source)
|
||||||
= link_to _('Request Access'), polymorphic_path([:request_access, source, :members]),
|
= link_to _('Request Access'), polymorphic_path([:request_access, source, :members]),
|
||||||
method: :post
|
method: :post
|
||||||
|
|
|
@ -10,11 +10,11 @@
|
||||||
= s_('Webhooks|URL must be percent-encoded if it contains one or more special characters.')
|
= s_('Webhooks|URL must be percent-encoded if it contains one or more special characters.')
|
||||||
.form-group
|
.form-group
|
||||||
= form.label :token, s_('Webhooks|Secret token'), class: 'label-bold'
|
= form.label :token, s_('Webhooks|Secret token'), class: 'label-bold'
|
||||||
= form.text_field :token, class: 'form-control gl-form-input', placeholder: ''
|
= form.password_field :token, value: hook.masked_token, autocomplete: 'new-password', class: 'form-control gl-form-input'
|
||||||
%p.form-text.text-muted
|
%p.form-text.text-muted
|
||||||
- code_start = '<code>'.html_safe
|
- code_start = '<code>'.html_safe
|
||||||
- code_end = '</code>'.html_safe
|
- code_end = '</code>'.html_safe
|
||||||
= s_('Webhooks|Used to validate received payloads. Sent with the request in the %{code_start}X-Gitlab-Token HTTP%{code_end} header.').html_safe % { code_start: code_start, code_end: code_end }
|
= s_('Webhooks|Used to validate received payloads. Sent with the request in the %{code_start}X-Gitlab-Token%{code_end} HTTP header.').html_safe % { code_start: code_start, code_end: code_end }
|
||||||
.form-group
|
.form-group
|
||||||
= form.label :url, s_('Webhooks|Trigger'), class: 'label-bold'
|
= form.label :url, s_('Webhooks|Trigger'), class: 'label-bold'
|
||||||
%ul.list-unstyled
|
%ul.list-unstyled
|
||||||
|
|
|
@ -13,7 +13,8 @@ require 'marginalia'
|
||||||
# matching against the raw SQL, and prepending the comment prevents color
|
# matching against the raw SQL, and prepending the comment prevents color
|
||||||
# coding from working in the development log.
|
# coding from working in the development log.
|
||||||
Marginalia::Comment.prepend_comment = true if Rails.env.production?
|
Marginalia::Comment.prepend_comment = true if Rails.env.production?
|
||||||
Marginalia::Comment.components = [:application, :correlation_id, :jid, :endpoint_id, :db_config_name]
|
Marginalia::Comment.components = [:application, :correlation_id, :jid, :endpoint_id, :db_config_name,
|
||||||
|
:console_hostname, :console_username]
|
||||||
|
|
||||||
# As mentioned in https://github.com/basecamp/marginalia/pull/93/files,
|
# As mentioned in https://github.com/basecamp/marginalia/pull/93/files,
|
||||||
# adding :line has some overhead because a regexp on the backtrace has
|
# adding :line has some overhead because a regexp on the backtrace has
|
||||||
|
|
|
@ -20,5 +20,9 @@ metadata:
|
||||||
description: Operations related to access requests
|
description: Operations related to access requests
|
||||||
- name: merge_requests
|
- name: merge_requests
|
||||||
description: Operations related to merge requests
|
description: Operations related to merge requests
|
||||||
|
- name: deploy_keys
|
||||||
|
description: Operations related to deploy keys
|
||||||
- name: deployments
|
- name: deployments
|
||||||
description: Operations related to deployments
|
description: Operations related to deployments
|
||||||
|
- name: release_links
|
||||||
|
description: Operations related to release assets (links)
|
||||||
|
|
|
@ -11,9 +11,141 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
It's a test that sometimes fails, but if you retry it enough times, it passes,
|
It's a test that sometimes fails, but if you retry it enough times, it passes,
|
||||||
eventually.
|
eventually.
|
||||||
|
|
||||||
|
## What are the potential cause for a test to be flaky?
|
||||||
|
|
||||||
|
### Unclean environment
|
||||||
|
|
||||||
|
**Label:** `flaky-test::unclean environment`
|
||||||
|
|
||||||
|
**Description:** The environment got dirtied by a previous test. The actual cause is probably not the flaky test here.
|
||||||
|
|
||||||
|
**Difficulty to reproduce:** Moderate. Usually, running the same spec files until the one that's failing reproduces the problem.
|
||||||
|
|
||||||
|
**Resolution:** Fix the previous tests and/or places where the environment is modified, so that
|
||||||
|
it's reset to a pristine test after each test.
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
- [Example 1](https://gitlab.com/gitlab-org/gitlab/-/issues/378414#note_1142026988): A migration
|
||||||
|
test might roll-back the database, perform its testing, and then roll-up the database in an
|
||||||
|
inconsistent state, so that following tests might not know about certain columns.
|
||||||
|
- [Example 2](https://gitlab.com/gitlab-org/gitlab/-/issues/368500): A test modifies data that is
|
||||||
|
used by a following test.
|
||||||
|
|
||||||
|
### Ordering assertion
|
||||||
|
|
||||||
|
**Label:** `flaky-test::ordering assertion`
|
||||||
|
|
||||||
|
**Description:** The test is expecting a specific order in the data under test yet the data is in
|
||||||
|
a non-deterministic order.
|
||||||
|
|
||||||
|
**Difficulty to reproduce:** Easy. Usually, running the test locally several times would reproduce
|
||||||
|
the problem.
|
||||||
|
|
||||||
|
**Resolution:** Depending on the problem, you might want to:
|
||||||
|
|
||||||
|
- loosen the assertion if the test shouldn't care about ordering but only on the elements
|
||||||
|
- fix the test by specifying a deterministic ordering
|
||||||
|
- fix the app code by specifying a deterministic ordering
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
- [Example 1](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10148/diffs): Without
|
||||||
|
specifying `ORDER BY`, database will not give deterministic ordering, or data race happening
|
||||||
|
in the tests.
|
||||||
|
|
||||||
|
### Dataset-specific
|
||||||
|
|
||||||
|
**Label:** `flaky-test::dataset-specific`
|
||||||
|
|
||||||
|
**Description:** The test assumes the dataset is in a particular (usually limited) state, which
|
||||||
|
might not be true depending on when the test run during the test suite.
|
||||||
|
|
||||||
|
**Difficulty to reproduce:** Moderate, as the amount of data needed to reproduce the issue might be
|
||||||
|
difficult to achieve locally.
|
||||||
|
|
||||||
|
**Resolution:** Fix the test to not assume that the dataset is in a particular state, don't hardcode IDs.
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
- [Example 1](https://gitlab.com/gitlab-org/gitlab/-/issues/378381): The database is recreated when
|
||||||
|
any table has more than 500 columns. It could pass in the merge request, but fail later in
|
||||||
|
`master` if the order of tests changes.
|
||||||
|
- [Example 2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91016/diffs): A test asserts
|
||||||
|
that trying to find a record with an unexisting ID retuns an error message. The test uses an
|
||||||
|
hardcoded ID that's supposed to not exist (e.g. `42`). If the test is run early in the test
|
||||||
|
suite, it might pass as not enough records were created before it, but as soon as it would run
|
||||||
|
later in the suite, there could be a record that actually has the ID `42`, hence the test would
|
||||||
|
start to fail.
|
||||||
|
|
||||||
|
### Random input
|
||||||
|
|
||||||
|
**Label:** `flaky-test::random input`
|
||||||
|
|
||||||
|
**Description:** The test use random values, that sometimes match the expectations, and sometimes not.
|
||||||
|
|
||||||
|
**Difficulty to reproduce:** Easy, as the test can be modified locally to use the "random value"
|
||||||
|
used at the time the test failed
|
||||||
|
|
||||||
|
**Resolution:** Once the problem is reproduced, it should be easy to debug and fix either the test
|
||||||
|
or the app.
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
- [Example 1](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/20121): The test isn't robust enough to handle a specific data, that only appears sporadically since the data input is random.
|
||||||
|
|
||||||
|
### Unreliable DOM Selector
|
||||||
|
|
||||||
|
**Label:** `flaky-test::unreliable dom selector`
|
||||||
|
|
||||||
|
**Description:** The DOM selector used in the test is unreliable.
|
||||||
|
|
||||||
|
**Difficulty to reproduce:** Moderate to difficult. Depending on whether the DOM selector is duplicated, or appears after a delay etc.
|
||||||
|
|
||||||
|
**Resolution:** It really depends on the problem here. It could be to wait for requests to finish, to scroll down the page etc.
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
- [Example 1](https://gitlab.com/gitlab-org/gitlab/-/issues/338341): A non-unique CSS selector
|
||||||
|
matching more than one element, or a non-waiting selector method that does not allow rendering
|
||||||
|
time before throwing an `element not found` error.
|
||||||
|
- [Example 2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101728/diffs): A CSS selector
|
||||||
|
only appears after a GraphQL requests has finished, and the UI has updated.
|
||||||
|
|
||||||
|
### Datetime-sensitive
|
||||||
|
|
||||||
|
**Label:** `flaky-test::datetime-sensitive`
|
||||||
|
|
||||||
|
**Description:** The test is assuming a specific date or time.
|
||||||
|
|
||||||
|
**Difficulty to reproduce:** Easy to moderate, depending on whether the test consistently fails after a certain date, or only fails at a given time or date.
|
||||||
|
|
||||||
|
**Resolution:** Freezing the time is usually a good solution.
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
- [Example 1](https://gitlab.com/gitlab-org/gitlab/-/issues/118612): A test that breaks after some time passed.
|
||||||
|
|
||||||
|
### Unstable infrastructure
|
||||||
|
|
||||||
|
**Label:** `flaky-test::unstable infrastructure`
|
||||||
|
|
||||||
|
**Description:** The test fails from time to time due to infrastructure issues.
|
||||||
|
|
||||||
|
**Difficulty to reproduce:** Hard. It's really hard to reproduce CI infrastructure issues. It might
|
||||||
|
be possible by using containers locally.
|
||||||
|
|
||||||
|
**Resolution:** Starting a conversation with the Infrastructure department in a dedicated issue is
|
||||||
|
usually a good idea.
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
- [Example 1](https://gitlab.com/gitlab-org/gitlab/-/issues/363214): The runner is under heavy load at this time.
|
||||||
|
- [Example 2](https://gitlab.com/gitlab-org/gitlab/-/issues/360559): The runner is having networking issues, making a job failing early
|
||||||
|
|
||||||
## Quarantined tests
|
## Quarantined tests
|
||||||
|
|
||||||
When a test frequently fails in `main`,
|
When a test frequently fails in `master`,
|
||||||
create [a ~"failure::flaky-test" issue](https://about.gitlab.com/handbook/engineering/workflow/#broken-master).
|
create [a ~"failure::flaky-test" issue](https://about.gitlab.com/handbook/engineering/workflow/#broken-master).
|
||||||
|
|
||||||
If the test cannot be fixed in a timely fashion, there is an impact on the
|
If the test cannot be fixed in a timely fashion, there is an impact on the
|
||||||
|
|
|
@ -171,11 +171,13 @@ module API
|
||||||
namespace do
|
namespace do
|
||||||
mount ::API::AccessRequests
|
mount ::API::AccessRequests
|
||||||
mount ::API::Appearance
|
mount ::API::Appearance
|
||||||
|
mount ::API::DeployKeys
|
||||||
mount ::API::Deployments
|
mount ::API::Deployments
|
||||||
mount ::API::Metadata
|
mount ::API::Metadata
|
||||||
mount ::API::MergeRequestDiffs
|
mount ::API::MergeRequestDiffs
|
||||||
mount ::API::UserCounts
|
mount ::API::UserCounts
|
||||||
mount ::API::ProjectRepositoryStorageMoves
|
mount ::API::ProjectRepositoryStorageMoves
|
||||||
|
mount ::API::Release::Links
|
||||||
mount ::API::SnippetRepositoryStorageMoves
|
mount ::API::SnippetRepositoryStorageMoves
|
||||||
mount ::API::Statistics
|
mount ::API::Statistics
|
||||||
|
|
||||||
|
@ -219,7 +221,6 @@ module API
|
||||||
mount ::API::DebianGroupPackages
|
mount ::API::DebianGroupPackages
|
||||||
mount ::API::DebianProjectPackages
|
mount ::API::DebianProjectPackages
|
||||||
mount ::API::DependencyProxy
|
mount ::API::DependencyProxy
|
||||||
mount ::API::DeployKeys
|
|
||||||
mount ::API::DeployTokens
|
mount ::API::DeployTokens
|
||||||
mount ::API::Discussions
|
mount ::API::Discussions
|
||||||
mount ::API::Environments
|
mount ::API::Environments
|
||||||
|
@ -294,7 +295,6 @@ module API
|
||||||
mount ::API::ProtectedBranches
|
mount ::API::ProtectedBranches
|
||||||
mount ::API::ProtectedTags
|
mount ::API::ProtectedTags
|
||||||
mount ::API::PypiPackages
|
mount ::API::PypiPackages
|
||||||
mount ::API::Release::Links
|
|
||||||
mount ::API::Releases
|
mount ::API::Releases
|
||||||
mount ::API::RemoteMirrors
|
mount ::API::RemoteMirrors
|
||||||
mount ::API::Repositories
|
mount ::API::Repositories
|
||||||
|
|
|
@ -21,7 +21,12 @@ module API
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'Return all deploy keys'
|
desc 'List all deploy keys' do
|
||||||
|
detail 'Get a list of all deploy keys across all projects of the GitLab instance. This endpoint requires administrator access and is not available on GitLab.com.'
|
||||||
|
success Entities::DeployKey
|
||||||
|
is_array true
|
||||||
|
tags %w[deploy_keys]
|
||||||
|
end
|
||||||
params do
|
params do
|
||||||
use :pagination
|
use :pagination
|
||||||
optional :public, type: Boolean, default: false, desc: "Only return deploy keys that are public"
|
optional :public, type: Boolean, default: false, desc: "Only return deploy keys that are public"
|
||||||
|
@ -35,13 +40,16 @@ module API
|
||||||
end
|
end
|
||||||
|
|
||||||
params do
|
params do
|
||||||
requires :id, type: String, desc: 'The ID of the project'
|
requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project owned by the authenticated user'
|
||||||
end
|
end
|
||||||
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||||
before { authorize_admin_project }
|
before { authorize_admin_project }
|
||||||
|
|
||||||
desc "Get a specific project's deploy keys" do
|
desc 'List deploy keys for project' do
|
||||||
|
detail "Get a list of a project's deploy keys."
|
||||||
success Entities::DeployKeysProject
|
success Entities::DeployKeysProject
|
||||||
|
is_array true
|
||||||
|
tags %w[deploy_keys]
|
||||||
end
|
end
|
||||||
params do
|
params do
|
||||||
use :pagination
|
use :pagination
|
||||||
|
@ -54,8 +62,10 @@ module API
|
||||||
end
|
end
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
desc 'Get single deploy key' do
|
desc 'Get a single deploy key' do
|
||||||
|
detail 'Get a single key.'
|
||||||
success Entities::DeployKeysProject
|
success Entities::DeployKeysProject
|
||||||
|
tags %w[deploy_keys]
|
||||||
end
|
end
|
||||||
params do
|
params do
|
||||||
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
|
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
|
||||||
|
@ -66,12 +76,14 @@ module API
|
||||||
present key, with: Entities::DeployKeysProject
|
present key, with: Entities::DeployKeysProject
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'Add new deploy key to a project' do
|
desc 'Add deploy key' do
|
||||||
|
detail "Creates a new deploy key for a project. If the deploy key already exists in another project, it's joined to the current project only if the original one is accessible by the same user."
|
||||||
success Entities::DeployKeysProject
|
success Entities::DeployKeysProject
|
||||||
|
tags %w[deploy_keys]
|
||||||
end
|
end
|
||||||
params do
|
params do
|
||||||
requires :key, type: String, desc: 'The new deploy key'
|
requires :key, type: String, desc: 'New deploy key'
|
||||||
requires :title, type: String, desc: 'The name of the deploy key'
|
requires :title, type: String, desc: "New deploy key's title"
|
||||||
optional :can_push, type: Boolean, desc: "Can deploy key push to the project's repository"
|
optional :can_push, type: Boolean, desc: "Can deploy key push to the project's repository"
|
||||||
end
|
end
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
@ -109,12 +121,14 @@ module API
|
||||||
end
|
end
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
desc 'Update an existing deploy key for a project' do
|
desc 'Update deploy key' do
|
||||||
|
detail 'Updates a deploy key for a project.'
|
||||||
success Entities::DeployKey
|
success Entities::DeployKey
|
||||||
|
tags %w[deploy_keys]
|
||||||
end
|
end
|
||||||
params do
|
params do
|
||||||
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
|
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
|
||||||
optional :title, type: String, desc: 'The name of the deploy key'
|
optional :title, type: String, desc: "New deploy key's title"
|
||||||
optional :can_push, type: Boolean, desc: "Can deploy key push to the project's repository"
|
optional :can_push, type: Boolean, desc: "Can deploy key push to the project's repository"
|
||||||
at_least_one_of :title, :can_push
|
at_least_one_of :title, :can_push
|
||||||
end
|
end
|
||||||
|
@ -143,9 +157,10 @@ module API
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'Enable a deploy key for a project' do
|
desc 'Enable a deploy key' do
|
||||||
detail 'This feature was added in GitLab 8.11'
|
detail 'Enables a deploy key for a project so this can be used. Returns the enabled key, with a status code 201 when successful. This feature was added in GitLab 8.11.'
|
||||||
success Entities::DeployKey
|
success Entities::DeployKey
|
||||||
|
tags %w[deploy_keys]
|
||||||
end
|
end
|
||||||
params do
|
params do
|
||||||
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
|
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
|
||||||
|
@ -161,7 +176,10 @@ module API
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'Delete deploy key for a project'
|
desc 'Delete deploy key' do
|
||||||
|
detail "Removes a deploy key from the project. If the deploy key is used only for this project, it's deleted from the system."
|
||||||
|
tags %w[deploy_keys]
|
||||||
|
end
|
||||||
params do
|
params do
|
||||||
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
|
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,17 +14,19 @@ module API
|
||||||
urgency :low
|
urgency :low
|
||||||
|
|
||||||
params do
|
params do
|
||||||
requires :id, type: String, desc: 'The ID of a project'
|
requires :id, type: [String, Integer], desc: 'The ID or URL-encoded path of the project'
|
||||||
end
|
end
|
||||||
resource 'projects/:id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
resource 'projects/:id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||||
params do
|
params do
|
||||||
requires :tag_name, type: String, desc: 'The name of the tag', as: :tag
|
requires :tag_name, type: String, desc: 'The tag associated with the release', as: :tag
|
||||||
end
|
end
|
||||||
resource 'releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do
|
resource 'releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do
|
||||||
resource :assets do
|
resource :assets do
|
||||||
desc 'Get a list of links of a release' do
|
desc 'List links of a release' do
|
||||||
detail 'This feature was introduced in GitLab 11.7.'
|
detail 'Get assets as links from a release. This feature was introduced in GitLab 11.7.'
|
||||||
success Entities::Releases::Link
|
success Entities::Releases::Link
|
||||||
|
is_array true
|
||||||
|
tags %w[release_links]
|
||||||
end
|
end
|
||||||
params do
|
params do
|
||||||
use :pagination
|
use :pagination
|
||||||
|
@ -36,16 +38,20 @@ module API
|
||||||
present paginate(release.links.sorted), with: Entities::Releases::Link
|
present paginate(release.links.sorted), with: Entities::Releases::Link
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'Create a link of a release' do
|
desc 'Create a release link' do
|
||||||
detail 'This feature was introduced in GitLab 11.7.'
|
detail 'Create an asset as a link from a release. This feature was introduced in GitLab 11.7.'
|
||||||
success Entities::Releases::Link
|
success Entities::Releases::Link
|
||||||
|
tags %w[release_links]
|
||||||
end
|
end
|
||||||
params do
|
params do
|
||||||
requires :name, type: String, desc: 'The name of the link'
|
requires :name, type: String, desc: 'The name of the link. Link names must be unique in the release'
|
||||||
requires :url, type: String, desc: 'The URL of the link'
|
requires :url, type: String, desc: 'The URL of the link. Link URLs must be unique in the release.'
|
||||||
optional :filepath, type: String, desc: 'The filepath of the link'
|
optional :filepath, type: String, desc: 'Optional path for a direct asset link'
|
||||||
optional :link_type, type: String, values: %w[other runbook image package], default: 'other',
|
optional :link_type,
|
||||||
desc: 'The link type, one of: "runbook", "image", "package" or "other"'
|
type: String,
|
||||||
|
values: %w[other runbook image package],
|
||||||
|
default: 'other',
|
||||||
|
desc: 'The type of the link: `other`, `runbook`, `image`, or `package`. Defaults to `other`'
|
||||||
end
|
end
|
||||||
route_setting :authentication, job_token_allowed: true
|
route_setting :authentication, job_token_allowed: true
|
||||||
post 'links' do
|
post 'links' do
|
||||||
|
@ -61,12 +67,13 @@ module API
|
||||||
end
|
end
|
||||||
|
|
||||||
params do
|
params do
|
||||||
requires :link_id, type: String, desc: 'The ID of the link'
|
requires :link_id, type: Integer, desc: 'The ID of the link'
|
||||||
end
|
end
|
||||||
resource 'links/:link_id' do
|
resource 'links/:link_id' do
|
||||||
desc 'Get a link detail of a release' do
|
desc 'Get a release link' do
|
||||||
detail 'This feature was introduced in GitLab 11.7.'
|
detail 'Get an asset as a link from a release. This feature was introduced in GitLab 11.7.'
|
||||||
success Entities::Releases::Link
|
success Entities::Releases::Link
|
||||||
|
tags %w[release_links]
|
||||||
end
|
end
|
||||||
route_setting :authentication, job_token_allowed: true
|
route_setting :authentication, job_token_allowed: true
|
||||||
get do
|
get do
|
||||||
|
@ -75,16 +82,21 @@ module API
|
||||||
present link, with: Entities::Releases::Link
|
present link, with: Entities::Releases::Link
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'Update a link of a release' do
|
desc 'Update a release link' do
|
||||||
detail 'This feature was introduced in GitLab 11.7.'
|
detail 'Update an asset as a link from a release. This feature was introduced in GitLab 11.7.'
|
||||||
success Entities::Releases::Link
|
success Entities::Releases::Link
|
||||||
|
tags %w[release_links]
|
||||||
end
|
end
|
||||||
params do
|
params do
|
||||||
optional :name, type: String, desc: 'The name of the link'
|
optional :name, type: String, desc: 'The name of the link'
|
||||||
optional :url, type: String, desc: 'The URL of the link'
|
optional :url, type: String, desc: 'The URL of the link'
|
||||||
optional :filepath, type: String, desc: 'The filepath of the link'
|
optional :filepath, type: String, desc: 'Optional path for a direct asset link'
|
||||||
optional :link_type, type: String, values: %w[other runbook image package], default: 'other',
|
optional :link_type,
|
||||||
desc: 'The link type, one of: "runbook", "image", "package" or "other"'
|
type: String,
|
||||||
|
values: %w[other runbook image package],
|
||||||
|
default: 'other',
|
||||||
|
desc: 'The type of the link: `other`, `runbook`, `image`, or `package`. Defaults to `other`'
|
||||||
|
|
||||||
at_least_one_of :name, :url
|
at_least_one_of :name, :url
|
||||||
end
|
end
|
||||||
route_setting :authentication, job_token_allowed: true
|
route_setting :authentication, job_token_allowed: true
|
||||||
|
@ -98,9 +110,10 @@ module API
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'Delete a link of a release' do
|
desc 'Delete a release link' do
|
||||||
detail 'This feature was introduced in GitLab 11.7.'
|
detail 'Deletes an asset as a link from a release. This feature was introduced in GitLab 11.7.'
|
||||||
success Entities::Releases::Link
|
success Entities::Releases::Link
|
||||||
|
tags %w[release_links]
|
||||||
end
|
end
|
||||||
route_setting :authentication, job_token_allowed: true
|
route_setting :authentication, job_token_allowed: true
|
||||||
delete do
|
delete do
|
||||||
|
|
|
@ -45,6 +45,18 @@ module Gitlab
|
||||||
def db_config_name
|
def db_config_name
|
||||||
::Gitlab::Database.db_config_name(marginalia_adapter)
|
::Gitlab::Database.db_config_name(marginalia_adapter)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def console_hostname
|
||||||
|
return unless ::Gitlab::Runtime.console?
|
||||||
|
|
||||||
|
@cached_console_hostname ||= Socket.gethostname # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||||
|
end
|
||||||
|
|
||||||
|
def console_username
|
||||||
|
return unless ::Gitlab::Runtime.console?
|
||||||
|
|
||||||
|
ENV['SUDO_USER'] || ENV['USER']
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -24768,9 +24768,6 @@ msgstr ""
|
||||||
msgid "March"
|
msgid "March"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "MardownDrawer|Could not fetch help contents."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Mark as done"
|
msgid "Mark as done"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -45517,7 +45514,7 @@ msgstr ""
|
||||||
msgid "Webhooks|URL preview"
|
msgid "Webhooks|URL preview"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Webhooks|Used to validate received payloads. Sent with the request in the %{code_start}X-Gitlab-Token HTTP%{code_end} header."
|
msgid "Webhooks|Used to validate received payloads. Sent with the request in the %{code_start}X-Gitlab-Token%{code_end} HTTP header."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Webhooks|Webhook disabled"
|
msgid "Webhooks|Webhook disabled"
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Admin::HooksController do
|
RSpec.describe Admin::HooksController do
|
||||||
let(:admin) { create(:admin) }
|
let_it_be(:admin) { create(:admin) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sign_in(admin)
|
sign_in(admin)
|
||||||
|
@ -33,7 +33,23 @@ RSpec.describe Admin::HooksController do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST #update' do
|
describe 'POST #update' do
|
||||||
let!(:hook) { create(:system_hook) }
|
let_it_be_with_reload(:hook) { create(:system_hook) }
|
||||||
|
|
||||||
|
context 'with an existing token' do
|
||||||
|
hook_params = {
|
||||||
|
token: WebHook::SECRET_MASK,
|
||||||
|
url: "http://example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
it 'does not change a token' do
|
||||||
|
expect do
|
||||||
|
post :update, params: { id: hook.id, hook: hook_params }
|
||||||
|
end.not_to change { hook.reload.token }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:found)
|
||||||
|
expect(flash[:alert]).to be_blank
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'sets all parameters' do
|
it 'sets all parameters' do
|
||||||
hook.update!(url_variables: { 'foo' => 'bar', 'baz' => 'woo' })
|
hook.update!(url_variables: { 'foo' => 'bar', 'baz' => 'woo' })
|
||||||
|
@ -61,8 +77,8 @@ RSpec.describe Admin::HooksController do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'DELETE #destroy' do
|
describe 'DELETE #destroy' do
|
||||||
let!(:hook) { create(:system_hook) }
|
let_it_be(:hook) { create(:system_hook) }
|
||||||
let!(:log) { create(:web_hook_log, web_hook: hook) }
|
let_it_be(:log) { create(:web_hook_log, web_hook: hook) }
|
||||||
let(:params) { { id: hook } }
|
let(:params) { { id: hook } }
|
||||||
|
|
||||||
it_behaves_like 'Web hook destroyer'
|
it_behaves_like 'Web hook destroyer'
|
||||||
|
|
|
@ -29,6 +29,22 @@ RSpec.describe Projects::HooksController do
|
||||||
{ namespace_id: project.namespace, project_id: project, id: hook.id }
|
{ namespace_id: project.namespace, project_id: project, id: hook.id }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with an existing token' do
|
||||||
|
hook_params = {
|
||||||
|
token: WebHook::SECRET_MASK,
|
||||||
|
url: "http://example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
it 'does not change a token' do
|
||||||
|
expect do
|
||||||
|
post :update, params: params.merge({ hook: hook_params })
|
||||||
|
end.not_to change { hook.reload.token }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:found)
|
||||||
|
expect(flash[:alert]).to be_blank
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'adds, updates and deletes URL variables' do
|
it 'adds, updates and deletes URL variables' do
|
||||||
hook.update!(url_variables: { 'a' => 'bar', 'b' => 'woo' })
|
hook.update!(url_variables: { 'a' => 'bar', 'b' => 'woo' })
|
||||||
|
|
||||||
|
|
|
@ -24,86 +24,52 @@ describe('AccessRequestActionButtons', () => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when user has `canRemove` permissions', () => {
|
it('renders remove member button', () => {
|
||||||
beforeEach(() => {
|
createComponent();
|
||||||
|
|
||||||
|
expect(findRemoveMemberButton().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets props correctly on remove member button', () => {
|
||||||
|
createComponent();
|
||||||
|
|
||||||
|
expect(findRemoveMemberButton().props()).toMatchObject({
|
||||||
|
memberId: member.id,
|
||||||
|
title: 'Deny access',
|
||||||
|
isAccessRequest: true,
|
||||||
|
isInvite: false,
|
||||||
|
icon: 'close',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when member is the current user', () => {
|
||||||
|
it('sets `message` prop correctly', () => {
|
||||||
|
createComponent();
|
||||||
|
|
||||||
|
expect(findRemoveMemberButton().props('message')).toBe(
|
||||||
|
`Are you sure you want to withdraw your access request for "${member.source.fullName}"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when member is not the current user', () => {
|
||||||
|
it('sets `message` prop correctly', () => {
|
||||||
createComponent({
|
createComponent({
|
||||||
|
isCurrentUser: false,
|
||||||
permissions: {
|
permissions: {
|
||||||
canRemove: true,
|
canRemove: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('renders remove member button', () => {
|
expect(findRemoveMemberButton().props('message')).toBe(
|
||||||
expect(findRemoveMemberButton().exists()).toBe(true);
|
`Are you sure you want to deny ${member.user.name}'s request to join "${member.source.fullName}"`,
|
||||||
});
|
);
|
||||||
|
|
||||||
it('sets props correctly', () => {
|
|
||||||
expect(findRemoveMemberButton().props()).toMatchObject({
|
|
||||||
memberId: member.id,
|
|
||||||
title: 'Deny access',
|
|
||||||
isAccessRequest: true,
|
|
||||||
isInvite: false,
|
|
||||||
icon: 'close',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when member is the current user', () => {
|
|
||||||
it('sets `message` prop correctly', () => {
|
|
||||||
expect(findRemoveMemberButton().props('message')).toBe(
|
|
||||||
`Are you sure you want to withdraw your access request for "${member.source.fullName}"`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when member is not the current user', () => {
|
|
||||||
it('sets `message` prop correctly', () => {
|
|
||||||
createComponent({
|
|
||||||
isCurrentUser: false,
|
|
||||||
permissions: {
|
|
||||||
canRemove: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findRemoveMemberButton().props('message')).toBe(
|
|
||||||
`Are you sure you want to deny ${member.user.name}'s request to join "${member.source.fullName}"`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when user does not have `canRemove` permissions', () => {
|
it('renders the approve button', () => {
|
||||||
it('does not render remove member button', () => {
|
createComponent();
|
||||||
createComponent({
|
|
||||||
permissions: {
|
|
||||||
canRemove: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findRemoveMemberButton().exists()).toBe(false);
|
expect(findApproveButton().exists()).toBe(true);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when user has `canUpdate` permissions', () => {
|
|
||||||
it('renders the approve button', () => {
|
|
||||||
createComponent({
|
|
||||||
permissions: {
|
|
||||||
canUpdate: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findApproveButton().exists()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when user does not have `canUpdate` permissions', () => {
|
|
||||||
it('does not render the approve button', () => {
|
|
||||||
createComponent({
|
|
||||||
permissions: {
|
|
||||||
canUpdate: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findApproveButton().exists()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -81,6 +81,7 @@ describe('MembersTabs', () => {
|
||||||
stubs: ['members-app'],
|
stubs: ['members-app'],
|
||||||
provide: {
|
provide: {
|
||||||
canManageMembers: true,
|
canManageMembers: true,
|
||||||
|
canManageAccessRequests: true,
|
||||||
canExportMembers: true,
|
canExportMembers: true,
|
||||||
exportCsvPath: '',
|
exportCsvPath: '',
|
||||||
...provide,
|
...provide,
|
||||||
|
@ -181,7 +182,9 @@ describe('MembersTabs', () => {
|
||||||
|
|
||||||
describe('when `canManageMembers` is `false`', () => {
|
describe('when `canManageMembers` is `false`', () => {
|
||||||
it('shows all tabs except `Invited` and `Access requests`', async () => {
|
it('shows all tabs except `Invited` and `Access requests`', async () => {
|
||||||
await createComponent({ provide: { canManageMembers: false } });
|
await createComponent({
|
||||||
|
provide: { canManageMembers: false, canManageAccessRequests: false },
|
||||||
|
});
|
||||||
|
|
||||||
expect(findTabByText('Members')).not.toBeUndefined();
|
expect(findTabByText('Members')).not.toBeUndefined();
|
||||||
expect(findTabByText('Groups')).not.toBeUndefined();
|
expect(findTabByText('Groups')).not.toBeUndefined();
|
||||||
|
|
|
@ -1,205 +0,0 @@
|
||||||
import { GlDrawer, GlAlert, GlSkeletonLoader } from '@gitlab/ui';
|
|
||||||
import { nextTick } from 'vue';
|
|
||||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
|
||||||
import MarkdownDrawer, { cache } from '~/vue_shared/components/markdown_drawer/markdown_drawer.vue';
|
|
||||||
import { getRenderedMarkdown } from '~/vue_shared/components/markdown_drawer/utils/fetch';
|
|
||||||
import { contentTop } from '~/lib/utils/common_utils';
|
|
||||||
|
|
||||||
jest.mock('~/vue_shared/components/markdown_drawer/utils/fetch', () => ({
|
|
||||||
getRenderedMarkdown: jest.fn().mockReturnValue({
|
|
||||||
title: 'test title test',
|
|
||||||
body: `<div id="content-body">
|
|
||||||
<div class="documentation md gl-mt-3">
|
|
||||||
test body
|
|
||||||
</div>
|
|
||||||
</div>`,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('~/lib/utils/common_utils', () => ({
|
|
||||||
contentTop: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('MarkdownDrawer', () => {
|
|
||||||
let wrapper;
|
|
||||||
const defaultProps = {
|
|
||||||
documentPath: 'user/search/global_search/advanced_search_syntax.json',
|
|
||||||
};
|
|
||||||
|
|
||||||
const createComponent = (props) => {
|
|
||||||
wrapper = shallowMountExtended(MarkdownDrawer, {
|
|
||||||
propsData: {
|
|
||||||
...defaultProps,
|
|
||||||
...props,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
wrapper.destroy();
|
|
||||||
wrapper = null;
|
|
||||||
Object.keys(cache).forEach((key) => delete cache[key]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const findDrawer = () => wrapper.findComponent(GlDrawer);
|
|
||||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
|
||||||
const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
|
|
||||||
const findDrawerTitle = () => wrapper.findComponent('[data-testid="title-element"]');
|
|
||||||
const findDrawerBody = () => wrapper.findComponent({ ref: 'content-element' });
|
|
||||||
|
|
||||||
describe('component', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
createComponent();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders correctly', () => {
|
|
||||||
expect(findDrawer().exists()).toBe(true);
|
|
||||||
expect(findDrawerTitle().text()).toBe('test title test');
|
|
||||||
expect(findDrawerBody().text()).toBe('test body');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.each`
|
|
||||||
hasNavbar | navbarHeight
|
|
||||||
${false} | ${0}
|
|
||||||
${true} | ${100}
|
|
||||||
`('computes offsetTop', ({ hasNavbar, navbarHeight }) => {
|
|
||||||
beforeEach(() => {
|
|
||||||
global.document.querySelector = jest.fn(() =>
|
|
||||||
hasNavbar
|
|
||||||
? {
|
|
||||||
dataset: {
|
|
||||||
page: 'test',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
);
|
|
||||||
contentTop.mockReturnValue(navbarHeight);
|
|
||||||
createComponent();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
contentTop.mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`computes offsetTop ${hasNavbar ? 'with' : 'without'} .navbar-gitlab`, () => {
|
|
||||||
expect(findDrawer().attributes('headerheight')).toBe(`${navbarHeight}px`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('watcher', () => {
|
|
||||||
let renderGLFMSpy;
|
|
||||||
let fetchMarkdownSpy;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
renderGLFMSpy = jest.spyOn(MarkdownDrawer.methods, 'renderGLFM');
|
|
||||||
fetchMarkdownSpy = jest.spyOn(MarkdownDrawer.methods, 'fetchMarkdown');
|
|
||||||
global.document.querySelector = jest.fn(() => ({
|
|
||||||
getBoundingClientRect: jest.fn(() => ({ bottom: 100 })),
|
|
||||||
dataset: {
|
|
||||||
page: 'test',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
createComponent();
|
|
||||||
await nextTick();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
renderGLFMSpy.mockClear();
|
|
||||||
fetchMarkdownSpy.mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('for documentPath triggers fetch', async () => {
|
|
||||||
expect(fetchMarkdownSpy).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
await wrapper.setProps({ documentPath: '/test/me' });
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(fetchMarkdownSpy).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('for open triggers renderGLFM', async () => {
|
|
||||||
wrapper.vm.fetchMarkdown();
|
|
||||||
wrapper.vm.openDrawer();
|
|
||||||
await nextTick();
|
|
||||||
expect(renderGLFMSpy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Markdown fetching', () => {
|
|
||||||
let renderGLFMSpy;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
renderGLFMSpy = jest.spyOn(MarkdownDrawer.methods, 'renderGLFM');
|
|
||||||
createComponent();
|
|
||||||
await nextTick();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
renderGLFMSpy.mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fetches the Markdown and caches it', async () => {
|
|
||||||
expect(getRenderedMarkdown).toHaveBeenCalledTimes(1);
|
|
||||||
expect(Object.keys(cache)).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('when the document changes, fetches it and caches it as well', async () => {
|
|
||||||
expect(getRenderedMarkdown).toHaveBeenCalledTimes(1);
|
|
||||||
expect(Object.keys(cache)).toHaveLength(1);
|
|
||||||
|
|
||||||
await wrapper.setProps({ documentPath: '/test/me2' });
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(getRenderedMarkdown).toHaveBeenCalledTimes(2);
|
|
||||||
expect(Object.keys(cache)).toHaveLength(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('when re-using an already fetched document, gets it from the cache', async () => {
|
|
||||||
await wrapper.setProps({ documentPath: '/test/me2' });
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(getRenderedMarkdown).toHaveBeenCalledTimes(2);
|
|
||||||
expect(Object.keys(cache)).toHaveLength(2);
|
|
||||||
|
|
||||||
await wrapper.setProps({ documentPath: defaultProps.documentPath });
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(getRenderedMarkdown).toHaveBeenCalledTimes(2);
|
|
||||||
expect(Object.keys(cache)).toHaveLength(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Markdown fetching returns error', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
getRenderedMarkdown.mockReturnValue({
|
|
||||||
hasFetchError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
createComponent();
|
|
||||||
await nextTick();
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
getRenderedMarkdown.mockClear();
|
|
||||||
});
|
|
||||||
it('shows alert', () => {
|
|
||||||
expect(findAlert().exists()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('While Markdown is fetching', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
getRenderedMarkdown.mockReturnValue(new Promise(() => {}));
|
|
||||||
|
|
||||||
createComponent();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
getRenderedMarkdown.mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows skeleton', async () => {
|
|
||||||
expect(findSkeleton().exists()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,42 +0,0 @@
|
||||||
export const MOCK_HTML = `<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<div id="content-body">
|
|
||||||
<h1>test title <strong>test</strong></h1>
|
|
||||||
<div class="documentation md gl-mt-3">
|
|
||||||
<a href="../advanced_search.md">Advanced Search</a>
|
|
||||||
<a href="../advanced_search2.md">Advanced Search2</a>
|
|
||||||
<h2>test header h2</h2>
|
|
||||||
<table class="testClass">
|
|
||||||
<tr>
|
|
||||||
<td>Emil</td>
|
|
||||||
<td>Tobias</td>
|
|
||||||
<td>Linus</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>16</td>
|
|
||||||
<td>14</td>
|
|
||||||
<td>10</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>`.replace(/\n/g, '');
|
|
||||||
|
|
||||||
export const MOCK_DRAWER_DATA = {
|
|
||||||
hasFetchError: false,
|
|
||||||
title: 'test title test',
|
|
||||||
body: ` <div id="content-body"> <div class="documentation md gl-mt-3"> <a href="../advanced_search.md">Advanced Search</a> <a href="../advanced_search2.md">Advanced Search2</a> <h2>test header h2</h2> <table class="testClass"> <tbody><tr> <td>Emil</td> <td>Tobias</td> <td>Linus</td> </tr> <tr> <td>16</td> <td>14</td> <td>10</td> </tr> </tbody></table> </div> </div>`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MOCK_DRAWER_DATA_ERROR = {
|
|
||||||
hasFetchError: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MOCK_TABLE_DATA_BEFORE = `<head></head><body><h1>test</h1></test><table><tbody><tr><td></td></tr></tbody></table></body>`;
|
|
||||||
|
|
||||||
export const MOCK_HTML_DATA_AFTER = {
|
|
||||||
body: '<table><tbody><tr><td></td></tr></tbody></table>',
|
|
||||||
title: 'test',
|
|
||||||
};
|
|
|
@ -1,43 +0,0 @@
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
|
||||||
import {
|
|
||||||
getRenderedMarkdown,
|
|
||||||
splitDocument,
|
|
||||||
} from '~/vue_shared/components/markdown_drawer/utils/fetch';
|
|
||||||
import axios from '~/lib/utils/axios_utils';
|
|
||||||
import {
|
|
||||||
MOCK_HTML,
|
|
||||||
MOCK_DRAWER_DATA,
|
|
||||||
MOCK_DRAWER_DATA_ERROR,
|
|
||||||
MOCK_TABLE_DATA_BEFORE,
|
|
||||||
MOCK_HTML_DATA_AFTER,
|
|
||||||
} from '../mock_data';
|
|
||||||
|
|
||||||
describe('utils/fetch', () => {
|
|
||||||
let mock;
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
mock.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.each`
|
|
||||||
axiosMock | type | toExpect
|
|
||||||
${{ code: 200, res: { html: MOCK_HTML } }} | ${'success'} | ${MOCK_DRAWER_DATA}
|
|
||||||
${{ code: 500, res: null }} | ${'error'} | ${MOCK_DRAWER_DATA_ERROR}
|
|
||||||
`('process markdown data', ({ axiosMock, type, toExpect }) => {
|
|
||||||
describe(`if api fetch responds with ${type}`, () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mock = new MockAdapter(axios);
|
|
||||||
mock.onGet().reply(axiosMock.code, axiosMock.res);
|
|
||||||
});
|
|
||||||
it(`should update drawer correctly`, async () => {
|
|
||||||
expect(await getRenderedMarkdown('/any/path')).toStrictEqual(toExpect);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('splitDocument', () => {
|
|
||||||
it(`should update tables correctly`, () => {
|
|
||||||
expect(splitDocument(MOCK_TABLE_DATA_BEFORE)).toStrictEqual(MOCK_HTML_DATA_AFTER);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -26,6 +26,7 @@ RSpec.describe Groups::GroupMembersHelper do
|
||||||
allow(helper).to receive(:group_group_member_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_members/:id')
|
allow(helper).to receive(:group_group_member_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_members/:id')
|
||||||
allow(helper).to receive(:group_group_link_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_links/:id')
|
allow(helper).to receive(:group_group_link_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_links/:id')
|
||||||
allow(helper).to receive(:can?).with(current_user, :admin_group_member, shared_group).and_return(true)
|
allow(helper).to receive(:can?).with(current_user, :admin_group_member, shared_group).and_return(true)
|
||||||
|
allow(helper).to receive(:can?).with(current_user, :admin_member_access_request, shared_group).and_return(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
subject do
|
subject do
|
||||||
|
@ -53,7 +54,8 @@ RSpec.describe Groups::GroupMembersHelper do
|
||||||
it 'returns expected json' do
|
it 'returns expected json' do
|
||||||
expected = {
|
expected = {
|
||||||
source_id: shared_group.id,
|
source_id: shared_group.id,
|
||||||
can_manage_members: true
|
can_manage_members: true,
|
||||||
|
can_manage_access_requests: true
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(subject).to include(expected)
|
expect(subject).to include(expected)
|
||||||
|
@ -99,6 +101,7 @@ RSpec.describe Groups::GroupMembersHelper do
|
||||||
allow(helper).to receive(:group_group_member_path).with(sub_shared_group, ':id').and_return('/groups/foo-bar/-/group_members/:id')
|
allow(helper).to receive(:group_group_member_path).with(sub_shared_group, ':id').and_return('/groups/foo-bar/-/group_members/:id')
|
||||||
allow(helper).to receive(:group_group_link_path).with(sub_shared_group, ':id').and_return('/groups/foo-bar/-/group_links/:id')
|
allow(helper).to receive(:group_group_link_path).with(sub_shared_group, ':id').and_return('/groups/foo-bar/-/group_links/:id')
|
||||||
allow(helper).to receive(:can?).with(current_user, :admin_group_member, sub_shared_group).and_return(true)
|
allow(helper).to receive(:can?).with(current_user, :admin_group_member, sub_shared_group).and_return(true)
|
||||||
|
allow(helper).to receive(:can?).with(current_user, :admin_member_access_request, sub_shared_group).and_return(true)
|
||||||
allow(helper).to receive(:can?).with(current_user, :export_group_memberships, sub_shared_group).and_return(true)
|
allow(helper).to receive(:can?).with(current_user, :export_group_memberships, sub_shared_group).and_return(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,8 @@ RSpec.describe Projects::ProjectMembersHelper do
|
||||||
it 'returns expected json' do
|
it 'returns expected json' do
|
||||||
expected = {
|
expected = {
|
||||||
source_id: project.id,
|
source_id: project.id,
|
||||||
can_manage_members: true
|
can_manage_members: true,
|
||||||
|
can_manage_access_requests: true
|
||||||
}.as_json
|
}.as_json
|
||||||
|
|
||||||
expect(subject).to include(expected)
|
expect(subject).to include(expected)
|
||||||
|
|
|
@ -299,7 +299,7 @@ RSpec.describe WebHook do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#executable?' do
|
describe '#executable?' do
|
||||||
let(:web_hook) { create(:project_hook, project: project) }
|
let_it_be_with_reload(:web_hook) { create(:project_hook, project: project) }
|
||||||
|
|
||||||
where(:recent_failures, :not_until, :executable) do
|
where(:recent_failures, :not_until, :executable) do
|
||||||
[
|
[
|
||||||
|
@ -757,4 +757,14 @@ RSpec.describe WebHook do
|
||||||
expect { described_class.new.update_last_failure }.not_to raise_error
|
expect { described_class.new.update_last_failure }.not_to raise_error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#masked_token' do
|
||||||
|
it { expect(hook.masked_token).to be_nil }
|
||||||
|
|
||||||
|
context 'with a token' do
|
||||||
|
let(:hook) { build(:project_hook, :token, project: project) }
|
||||||
|
|
||||||
|
it { expect(hook.masked_token).to eq described_class::SECRET_MASK }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -83,6 +83,31 @@ RSpec.describe GroupMemberPolicy do
|
||||||
specify { expect_allowed(:read_group) }
|
specify { expect_allowed(:read_group) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'for access requests' do
|
||||||
|
let_it_be(:group) { create(:group, :public) }
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
let(:current_user) { user }
|
||||||
|
|
||||||
|
context 'for own access request' do
|
||||||
|
let(:membership) { create(:group_member, :access_request, group: group, user: user) }
|
||||||
|
|
||||||
|
specify { expect_allowed(:withdraw_member_access_request) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context "for another user's access request" do
|
||||||
|
let(:membership) { create(:group_member, :access_request, group: group, user: create(:user)) }
|
||||||
|
|
||||||
|
specify { expect_disallowed(:withdraw_member_access_request) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for own, valid membership' do
|
||||||
|
let(:membership) { create(:group_member, :developer, group: group, user: user) }
|
||||||
|
|
||||||
|
specify { expect_disallowed(:withdraw_member_access_request) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with bot user' do
|
context 'with bot user' do
|
||||||
let(:current_user) { create(:user, :project_bot) }
|
let(:current_user) { create(:user, :project_bot) }
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,14 @@ require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe ProjectMemberPolicy do
|
RSpec.describe ProjectMemberPolicy do
|
||||||
let(:project) { create(:project) }
|
let(:project) { create(:project) }
|
||||||
let(:maintainer_user) { create(:user) }
|
let(:maintainer) { create(:user) }
|
||||||
let(:member) { create(:project_member, project: project, user: member_user) }
|
let(:member) { create(:project_member, project: project, user: member_user) }
|
||||||
|
let(:current_user) { maintainer }
|
||||||
|
|
||||||
subject { described_class.new(maintainer_user, member) }
|
subject { described_class.new(current_user, member) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
create(:project_member, :maintainer, project: project, user: maintainer_user)
|
create(:project_member, :maintainer, project: project, user: maintainer)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with regular member' do
|
context 'with regular member' do
|
||||||
|
@ -40,4 +41,29 @@ RSpec.describe ProjectMemberPolicy do
|
||||||
it { is_expected.not_to be_allowed(:update_project_member) }
|
it { is_expected.not_to be_allowed(:update_project_member) }
|
||||||
it { is_expected.not_to be_allowed(:destroy_project_member) }
|
it { is_expected.not_to be_allowed(:destroy_project_member) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'for access requests' do
|
||||||
|
let_it_be(:project) { create(:project, :public) }
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
let(:current_user) { user }
|
||||||
|
|
||||||
|
context 'for own access request' do
|
||||||
|
let(:member) { create(:project_member, :access_request, project: project, user: user) }
|
||||||
|
|
||||||
|
specify { expect_allowed(:withdraw_member_access_request) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context "for another user's access request" do
|
||||||
|
let(:member) { create(:project_member, :access_request, project: project, user: create(:user)) }
|
||||||
|
|
||||||
|
specify { expect_disallowed(:withdraw_member_access_request) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for own, valid membership' do
|
||||||
|
let(:member) { create(:project_member, :developer, project: project, user: user) }
|
||||||
|
|
||||||
|
specify { expect_disallowed(:withdraw_member_access_request) }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -77,7 +77,7 @@ RSpec.describe Members::DestroyService do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_examples 'a service destroying an access requester' do
|
shared_examples 'a service destroying an access request of another user' do
|
||||||
it_behaves_like 'a service destroying a member'
|
it_behaves_like 'a service destroying a member'
|
||||||
|
|
||||||
it 'calls Member#after_decline_request' do
|
it 'calls Member#after_decline_request' do
|
||||||
|
@ -85,12 +85,16 @@ RSpec.describe Members::DestroyService do
|
||||||
|
|
||||||
described_class.new(current_user).execute(member, **opts)
|
described_class.new(current_user).execute(member, **opts)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'a service destroying an access request of self' do
|
||||||
|
it_behaves_like 'a service destroying a member'
|
||||||
|
|
||||||
context 'when current user is the member' do
|
context 'when current user is the member' do
|
||||||
it 'does not call Member#after_decline_request' do
|
it 'does not call Member#after_decline_request' do
|
||||||
expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
|
expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
|
||||||
|
|
||||||
described_class.new(member_user).execute(member, **opts)
|
described_class.new(current_user).execute(member, **opts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -277,11 +281,24 @@ RSpec.describe Members::DestroyService do
|
||||||
group.add_owner(current_user)
|
group.add_owner(current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'a service destroying an access requester' do
|
it_behaves_like 'a service destroying an access request of another user' do
|
||||||
let(:member) { group_project.requesters.find_by(user_id: member_user.id) }
|
let(:member) { group_project.requesters.find_by(user_id: member_user.id) }
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'a service destroying an access requester' do
|
it_behaves_like 'a service destroying an access request of another user' do
|
||||||
|
let(:member) { group.requesters.find_by(user_id: member_user.id) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'on withdrawing their own access request' do
|
||||||
|
let(:opts) { { skip_subresources: true } }
|
||||||
|
let(:current_user) { member_user }
|
||||||
|
|
||||||
|
it_behaves_like 'a service destroying an access request of self' do
|
||||||
|
let(:member) { group_project.requesters.find_by(user_id: member_user.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'a service destroying an access request of self' do
|
||||||
let(:member) { group.requesters.find_by(user_id: member_user.id) }
|
let(:member) { group.requesters.find_by(user_id: member_user.id) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -76,6 +76,7 @@ RSpec.shared_context 'GroupPolicy context' do
|
||||||
register_group_runners
|
register_group_runners
|
||||||
read_billing
|
read_billing
|
||||||
edit_billing
|
edit_billing
|
||||||
|
admin_member_access_request
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ RSpec.shared_context 'ProjectPolicy context' do
|
||||||
admin_project admin_project_member admin_snippet admin_terraform_state
|
admin_project admin_project_member admin_snippet admin_terraform_state
|
||||||
admin_wiki create_deploy_token destroy_deploy_token
|
admin_wiki create_deploy_token destroy_deploy_token
|
||||||
push_to_delete_protected_branch read_deploy_token update_snippet
|
push_to_delete_protected_branch read_deploy_token update_snippet
|
||||||
destroy_upload
|
destroy_upload admin_member_access_request
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,6 @@ import Vue from 'vue';
|
||||||
import { createMockServer } from 'test_helpers/mock_server';
|
import { createMockServer } from 'test_helpers/mock_server';
|
||||||
import translateMixin from '~/vue_shared/translate';
|
import translateMixin from '~/vue_shared/translate';
|
||||||
|
|
||||||
// fixing toJSON error
|
|
||||||
// https://github.com/storybookjs/storybook/issues/14933
|
|
||||||
Vue.prototype.toJSON = () => {};
|
|
||||||
|
|
||||||
const stylesheetsRequireCtx = require.context(
|
const stylesheetsRequireCtx = require.context(
|
||||||
'../../app/assets/stylesheets',
|
'../../app/assets/stylesheets',
|
||||||
true,
|
true,
|
||||||
|
|
Loading…
Reference in New Issue