Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-10-31 00:16:38 +00:00
parent 892e58c41f
commit 453bb35fc8
42 changed files with 547 additions and 333 deletions

View File

@ -1,4 +1,26 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
/**
* @param {boolean|VueApollo} apolloProviderOption
* @returns {undefined | VueApollo}
*/
const getApolloProvider = (apolloProviderOption) => {
if (apolloProviderOption === true) {
Vue.use(VueApollo);
return new VueApollo({
defaultClient: createDefaultClient(),
});
}
if (apolloProviderOption instanceof VueApollo) {
return apolloProviderOption;
}
return undefined;
};
/**
* Initializes a component as a simple vue app, passing the necessary props. If the element
@ -8,6 +30,8 @@ import Vue from 'vue';
*
* @param {string} selector css selector for where to build
* @param {Vue.component} component The Vue compoment to be built as the root of the app
* @param {{withApolloProvider: boolean|VueApollo}} options. extra options to be passed to the vue app
* withApolloProvider: if true, instantiates a default apolloProvider. Also accepts and instance of VueApollo
*
* @example
* ```html
@ -15,13 +39,13 @@ import Vue from 'vue';
* ```
*
* ```javascript
* initSimpleApp('#mount-here', MyApp)
* initSimpleApp('#mount-here', MyApp, { withApolloProvider: true })
* ```
*
* This will mount MyApp as root on '#mount-here'. It will receive {'some': 'object'} as it's
* view model prop.
*/
export const initSimpleApp = (selector, component) => {
export const initSimpleApp = (selector, component, { withApolloProvider } = {}) => {
const element = document.querySelector(selector);
if (!element) {
@ -32,6 +56,7 @@ export const initSimpleApp = (selector, component) => {
return new Vue({
el: element,
apolloProvider: getApolloProvider(withApolloProvider),
render(h) {
return h(component, { props });
},

View File

@ -1,11 +1,18 @@
<script>
import { GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
import { GlAvatarLink, GlAvatarLabeled, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import PrivateIcon from '../icons/private_icon.vue';
import { AVATAR_SIZE } from '../../constants';
export default {
name: 'GroupAvatar',
avatarSize: AVATAR_SIZE,
components: { GlAvatarLink, GlAvatarLabeled },
components: { GlAvatarLink, GlAvatarLabeled, PrivateIcon },
directives: {
GlTooltip: GlTooltipDirective,
},
i18n: {
private: __('Private'),
},
props: {
member: {
type: Object,
@ -16,19 +23,36 @@ export default {
group() {
return this.member.sharedWithGroup;
},
isPrivate() {
return this.member.isSharedWithGroupPrivate;
},
avatarLabeledProps() {
const label = this.isPrivate ? this.$options.i18n.private : this.group.fullName;
return {
label,
src: this.group.avatarUrl,
alt: label,
size: AVATAR_SIZE,
entityName: this.isPrivate ? this.$options.i18n.private : this.group.name,
entityId: this.group.id,
};
},
},
};
</script>
<template>
<gl-avatar-link :href="group.webUrl">
<gl-avatar-labeled
:label="group.fullName"
:src="group.avatarUrl"
:alt="group.fullName"
:size="$options.avatarSize"
:entity-name="group.name"
:entity-id="group.id"
/>
<div v-if="isPrivate">
<gl-avatar-labeled v-bind="avatarLabeledProps">
<template #meta>
<div class="gl-p-1">
<private-icon />
</div>
</template>
</gl-avatar-labeled>
</div>
<gl-avatar-link v-else :href="group.webUrl">
<gl-avatar-labeled v-bind="avatarLabeledProps" />
</gl-avatar-link>
</template>

View File

@ -0,0 +1,19 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
name: 'GroupAvatar',
components: { GlIcon },
directives: {
GlTooltip: GlTooltipDirective,
},
i18n: {
tooltip: s__('Members|Private group information is only accessible to its members.'),
},
};
</script>
<template>
<gl-icon v-gl-tooltip="$options.i18n.tooltip" name="eye-slash" />
</template>

View File

@ -1,10 +1,12 @@
<script>
import { GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import PrivateIcon from '../icons/private_icon.vue';
export default {
name: 'MemberSource',
i18n: {
private: __('Private'),
inherited: __('Inherited'),
directMember: __('Direct member'),
directMemberWithCreatedBy: s__('Members|Direct member by %{createdBy}'),
@ -13,16 +15,24 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: { GlSprintf },
components: { GlSprintf, PrivateIcon },
props: {
memberSource: {
type: Object,
required: true,
required: false,
default() {
return {};
},
},
isDirectMember: {
type: Boolean,
required: true,
},
isSharedWithGroupPrivate: {
type: Boolean,
required: false,
default: false,
},
createdBy: {
type: Object,
required: false,
@ -43,7 +53,11 @@ export default {
</script>
<template>
<span v-if="showCreatedBy">
<div v-if="isSharedWithGroupPrivate" class="gl-display-flex gl-column-gap-2">
<span>{{ $options.i18n.private }}</span>
<private-icon />
</div>
<span v-else-if="showCreatedBy">
<gl-sprintf :message="messageWithCreatedBy">
<template #group>
<a v-gl-tooltip.hover="$options.i18n.inherited" :href="memberSource.webUrl">{{

View File

@ -270,6 +270,7 @@ export default {
:is-direct-member="isDirectMember"
:member-source="member.source"
:created-by="member.createdBy"
:is-shared-with-group-private="member.isSharedWithGroupPrivate"
/>
</members-table-cell>
</template>

View File

@ -1,4 +1,4 @@
import { initSimpleApp } from '~/helpers/init_simple_app_helper';
import { ShowMlModel } from '~/ml/model_registry/apps';
initSimpleApp('#js-mount-show-ml-model', ShowMlModel);
initSimpleApp('#js-mount-show-ml-model', ShowMlModel, { withApolloProvider: true });

View File

@ -10,16 +10,4 @@ module ColorsHelper
hex_color.length == 7 ? hex_color[1, 7].scan(/.{2}/).map(&:hex) : hex_color[1, 4].scan(/./).map { |v| (v * 2).hex }
end
def rgb_array_to_hex_color(rgb_array)
raise ArgumentError, "invalid RGB array `#{rgb_array}`" unless rgb_array_valid?(rgb_array)
"##{rgb_array.map{ "%02x" % _1 }.join}"
end
private
def rgb_array_valid?(rgb_array)
rgb_array.is_a?(Array) && rgb_array.length == 3 && rgb_array.all?{ _1 >= 0 && _1 <= 255 }
end
end

View File

@ -9,15 +9,6 @@ module EnvironmentHelper
end
# rubocop: enable CodeReuse/ActiveRecord
def environment_link_for_build(project, build)
environment = environment_for_build(project, build)
if environment
link_to environment.name, project_environment_path(project, environment)
else
content_tag :span, build.expanded_environment_name
end
end
def deployment_path(deployment)
[deployment.project, deployment.deployable]
end
@ -30,45 +21,6 @@ module EnvironmentHelper
link_to link_label, deployment_path(deployment)
end
def last_deployment_link_for_environment_build(project, build)
environment = environment_for_build(project, build)
return unless environment
deployment_link(environment.last_deployment)
end
def render_deployment_status(deployment)
status = deployment.status
status_text =
case status
when 'created'
s_('Deployment|created')
when 'running'
s_('Deployment|running')
when 'success'
s_('Deployment|success')
when 'failed'
s_('Deployment|failed')
when 'canceled'
s_('Deployment|canceled')
when 'skipped'
s_('Deployment|skipped')
when 'blocked'
s_('Deployment|blocked')
end
ci_icon_utilities = "gl-display-inline-flex gl-align-items-center gl-line-height-0 gl-px-3 gl-py-2 gl-rounded-base"
klass = "ci-status ci-#{status.dasherize} #{ci_icon_utilities}"
text = "#{ci_icon_for_status(status)} <span class=\"gl-ml-2\">#{status_text}</span>".html_safe
if deployment.deployable.instance_of?(::Ci::Build)
link_to(text, deployment_path(deployment), class: klass)
else
content_tag(:span, text, class: klass)
end
end
def environments_detail_data(user, project, environment)
{
name: environment.name,

View File

@ -33,15 +33,6 @@ module EnvironmentsHelper
metrics_data
end
def environment_logs_data(project, environment)
{
"environment_name": environment.name,
"environments_path": api_v4_projects_environments_path(id: project.id),
"environment_id": environment.id,
"clusters_path": project_clusters_path(project, format: :json)
}
end
def can_destroy_environment?(environment)
can?(current_user, :destroy_environment, environment)
end

View File

@ -18,13 +18,6 @@ module GraphHelper
ids.zip(parent_spaces)
end
def success_ratio(counts)
return 100 if counts[:failed] == 0
ratio = (counts[:success].to_f / (counts[:success] + counts[:failed])) * 100
ratio.to_i
end
def should_render_dora_charts
false
end

View File

@ -30,22 +30,11 @@ module MembersHelper
"#{text} #{action} the #{member.source.human_name} #{source_text(member)}?"
end
def remove_member_title(member)
action = member.request? ? 'Deny access request' : 'Remove user'
"#{action} from #{source_text(member)}"
end
def leave_confirmation_message(member_source)
"Are you sure you want to leave the " \
"\"#{member_source.human_name}\" #{member_source.model_name.to_s.humanize(capitalize: false)}?"
end
def filter_group_project_member_path(options = {})
options = params.slice(:search, :sort).merge(options).permit!
"#{request.path}?#{options.to_param}"
end
def member_path(member)
if member.is_a?(GroupMember)
group_group_member_path(member.source, member)

View File

@ -57,10 +57,6 @@ module NavHelper
end
end
def nav_control_class
"nav-control" if current_user
end
def user_dropdown_class
class_names = []
class_names << 'header-user-dropdown-toggle'
@ -82,10 +78,6 @@ module NavHelper
%w[system_info background_migrations background_jobs health_check]
end
def admin_analytics_nav_links
%w[dev_ops_report usage_trends]
end
def show_super_sidebar?(user = current_user)
# The new sidebar is not enabled for anonymous use
# Once we enable the new sidebar by default, this

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
class GroupGroupLinkPolicy < ::BasePolicy # rubocop:disable Gitlab/NamespacedClass
condition(:can_read_shared_with_group) { can?(:read_group, @subject.shared_with_group) }
condition(:group_member) { @subject.shared_group.member?(@user) }
rule { can_read_shared_with_group | group_member }.enable :read_shared_with_group
end

View File

@ -2,9 +2,13 @@
class ProjectGroupLinkPolicy < BasePolicy # rubocop:disable Gitlab/NamespacedClass
condition(:group_owner_or_project_admin) { group_owner? || project_admin? }
condition(:can_read_group) { can?(:read_group, @subject.group) }
condition(:project_member) { @subject.project.member?(@user) }
rule { group_owner_or_project_admin }.enable :admin_project_group_link
rule { can_read_group | project_member }.enable :read_shared_with_group
private
def group_owner?

View File

@ -4,7 +4,7 @@ module GroupLink
class GroupGroupLinkEntity < GroupLink::GroupLinkEntity
include RequestAwareEntity
expose :source do |group_link|
expose :source, if: ->(group_link) { can_read_shared_group?(group_link) } do |group_link|
GroupEntity.represent(group_link.shared_from, only: [:id, :full_name, :web_url])
end

View File

@ -19,16 +19,28 @@ module GroupLink
group_link.class.access_options
end
expose :is_shared_with_group_private do |group_link|
!can_read_shared_group?(group_link)
end
expose :shared_with_group do
expose :avatar_url do |group_link|
expose :avatar_url, if: ->(group_link) { can_read_shared_group?(group_link) } do |group_link|
group_link.shared_with_group.avatar_url(only_path: false, size: Member::AVATAR_SIZE)
end
expose :web_url do |group_link|
expose :web_url, if: ->(group_link) { can_read_shared_group?(group_link) } do |group_link|
group_link.shared_with_group.web_url
end
expose :shared_with_group, merge: true, using: GroupBasicEntity
# We have to expose shared_with_group.id because we use this to get distinct
# with ancestors
expose :shared_with_group, merge: true do |group_link|
if can_read_shared_group?(group_link)
GroupBasicEntity.represent(group_link.shared_with_group)
else
GroupBasicEntity.represent(group_link.shared_with_group, only: [:id])
end
end
end
expose :can_update do |group_link, options|
@ -45,6 +57,10 @@ module GroupLink
private
def can_read_shared_group?(group_link)
can?(current_user, :read_shared_with_group, group_link)
end
def current_user
options[:current_user]
end

View File

@ -4,7 +4,7 @@ module GroupLink
class ProjectGroupLinkEntity < GroupLink::GroupLinkEntity
include RequestAwareEntity
expose :source do |group_link|
expose :source, if: ->(group_link) { can_read_shared_group?(group_link) } do |group_link|
ProjectEntity.represent(group_link.shared_from, only: [:id, :full_name])
end

View File

@ -10,9 +10,10 @@
.tree-controls
.d-block.d-sm-flex.flex-wrap.align-items-start.gl-children-ml-sm-3.gl-first-child-ml-sm-0<
= render_if_exists 'projects/tree/lock_link'
= render 'projects/buttons/compare', project: @project, ref: @ref, root_ref: @repository&.root_ref
#js-tree-history-link{ data: { history_link: project_commits_path(@project, @ref) } }
= render 'projects/buttons/compare', project: @project, ref: @ref, root_ref: @repository&.root_ref
= render 'projects/find_file_link'
= render 'shared/web_ide_button', blob: nil
= render 'projects/buttons/download', project: @project, ref: @ref

View File

@ -25,3 +25,4 @@ The following timeouts are available.
| Default | 55 seconds | Timeout for most Gitaly calls (not enforced for `git` `fetch` and `push` operations, or Sidekiq jobs). For example, checking if a repository exists on disk. Makes sure that Gitaly calls made within a web request cannot exceed the entire request timeout. It should be shorter than the [worker timeout](../operations/puma.md#change-the-worker-timeout) that can be configured for [Puma](../../install/requirements.md#puma-settings). If a Gitaly call timeout exceeds the worker timeout, the remaining time from the worker timeout is used to avoid having to terminate the worker. |
| Fast | 10 seconds | Timeout for fast Gitaly operations used within requests, sometimes multiple times. For example, checking if a repository exists on disk. If fast operations exceed this threshold, there may be a problem with a storage shard. Failing fast can help maintain the stability of the GitLab instance. |
| Medium | 30 seconds | Timeout for Gitaly operations that should be fast (possibly within requests) but preferably not used multiple times within a request. For example, loading blobs. Timeout that should be set between Default and Fast. |
You can also [configure negotiation timeouts](../gitaly/configure_gitaly.md#configure-negotiation-timeouts).

View File

@ -204,6 +204,7 @@ You can define how long a job can run before it times out.
1. Expand **General pipelines**.
1. In the **Timeout** field, enter the number of minutes, or a human-readable value like `2 hours`.
Must be 10 minutes or more, and less than one month. Default is 60 minutes.
Pending jobs are dropped after 24 hours of inactivity.
Jobs that exceed the timeout are marked as failed.

View File

@ -130,6 +130,11 @@ After sharing the `Frontend` group with the `Engineering` group:
- The **Groups** tab lists the `Engineering` group.
- The **Groups** tab lists a group regardless of whether it is a public or private group.
- From [GitLab 16.6](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134623),
the invited group's name and membership source will be hidden unless:
- the invited group is public, or
- the current user is a member of the invited group, or
- the current user is a member of the current group.
- All direct members of the `Engineering` group have access to the `Frontend` group. The least access is granted between the access in the `Engineering` group and the access in the `Frontend` group.
- If `Member1` has the Maintainer role in `Engineering` and `Engineering` is added to `Frontend` with the Developer role, `Member1` has the Developer role in `Frontend`.
- If `Member2` has the Guest role in `Engineering` and `Engineering` is added to `Frontend` with the Developer role, `Member2` has the Guest role in `Frontend`.

View File

@ -24,7 +24,7 @@ Supported clients:
### Authenticate to the Package Registry
You need an token to publish a package. There are different tokens available depending on what you're trying to achieve. For more information, review the [guidance on tokens](../package_registry/index.md#authenticate-with-the-registry).
You need a token to publish a package. There are different tokens available depending on what you're trying to achieve. For more information, review the [guidance on tokens](../package_registry/index.md#authenticate-with-the-registry).
Create a token and save it to use later in the process.

View File

@ -76,6 +76,11 @@ In addition:
- On the group's page, the project is listed on the **Shared projects** tab.
- On the project's **Members** page, the group is listed on the **Groups** tab.
- From [GitLab 16.6](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134623),
the invited group's name and membership source will be hidden unless:
- the group is public, or
- the current user is a member of the group, or
- the current user is a member of the project.
- Each user is assigned a maximum role.
- Members who have the **Project Invite** badge next to their profile on the usage quota page count towards the billable members of the shared project's top-level group.

View File

@ -489,3 +489,31 @@ p = Project.find_by_full_path('<namespace/project>')
m = p.merge_requests.find_by(iid: <iid>)
Issuable::DestroyService.new(container: m.project, current_user: u).execute(m)
```
### Merge request pre-receive hook failed
If a merge request times out, you might see messages that indicate a Puma worker
timeout problem:
- In the GitLab UI:
```plaintext
Something went wrong during merge pre-receive hook.
500 Internal Server Error. Try again.
```
- In the `gitlab-rails/api_json.log` log file:
```plaintext
Rack::Timeout::RequestTimeoutException
Request ran for longer than 60000ms
```
This error can happen if your merge request:
- Contains many diffs.
- Is many commits behind the target branch.
Users in self-managed installations can request an administrator review server logs
to determine the cause of the error. GitLab SaaS users should
[contact Support](https://about.gitlab.com/support/#contact-support) for help.

View File

@ -16846,27 +16846,6 @@ msgstr ""
msgid "Deployment|Waiting"
msgstr ""
msgid "Deployment|blocked"
msgstr ""
msgid "Deployment|canceled"
msgstr ""
msgid "Deployment|created"
msgstr ""
msgid "Deployment|failed"
msgstr ""
msgid "Deployment|running"
msgstr ""
msgid "Deployment|skipped"
msgstr ""
msgid "Deployment|success"
msgstr ""
msgid "Deprecated API rate limits"
msgstr ""
@ -29553,6 +29532,9 @@ msgstr ""
msgid "Members|Membership"
msgstr ""
msgid "Members|Private group information is only accessible to its members."
msgstr ""
msgid "Members|Remove \"%{groupName}\""
msgstr ""

View File

@ -44,6 +44,9 @@
"valid_roles": {
"type": "object"
},
"is_shared_with_group_private": {
"type": "boolean"
},
"shared_with_group": {
"type": "object",
"required": [
@ -89,4 +92,4 @@
"type": "boolean"
}
}
}
}

View File

@ -1,7 +1,9 @@
import { createWrapper } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { initSimpleApp } from '~/helpers/init_simple_app_helper';
import createDefaultClient from '~/lib/graphql';
const MockComponent = Vue.component('MockComponent', {
props: {
@ -25,10 +27,10 @@ const findMock = () => wrapper.findComponent(MockComponent);
const didCreateApp = () => wrapper !== undefined;
const initMock = (html, props = {}) => {
const initMock = (html, options = {}) => {
setHTMLFixture(html);
const app = initSimpleApp('#mount-here', MockComponent, { props });
const app = initSimpleApp('#mount-here', MockComponent, options);
wrapper = app ? createWrapper(app) : undefined;
};
@ -58,4 +60,35 @@ describe('helpers/init_simple_app_helper/initSimpleApp', () => {
count: 123,
});
});
describe('options', () => {
describe('withApolloProvider', () => {
describe('if not true or not VueApollo', () => {
it('apolloProvider not created', () => {
initMock('<div id="mount-here"></div>', { withApolloProvider: false });
expect(wrapper.vm.$apollo).toBeUndefined();
});
});
describe('if true, creates default provider', () => {
it('creates a default apolloProvider', () => {
initMock('<div id="mount-here"></div>', { withApolloProvider: true });
expect(wrapper.vm.$apollo).not.toBeUndefined();
});
});
describe('if VueApollo, sets as default provider', () => {
it('uses the provided apolloClient', () => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({ defaultClient: createDefaultClient() });
initMock('<div id="mount-here"></div>', { withApolloProvider: apolloProvider });
expect(wrapper.vm.$apolloProvider).toBe(apolloProvider);
});
});
});
});
});

View File

@ -1,8 +1,9 @@
import { GlAvatarLink } from '@gitlab/ui';
import { GlAvatarLabeled, GlAvatarLink } from '@gitlab/ui';
import { getByText as getByTextHelper } from '@testing-library/dom';
import { mount, createWrapper } from '@vue/test-utils';
import GroupAvatar from '~/members/components/avatars/group_avatar.vue';
import { group as member } from '../../mock_data';
import PrivateIcon from '~/members/components/icons/private_icon.vue';
import { group as member, privateGroup as privateMember } from '../../mock_data';
describe('MemberList', () => {
let wrapper;
@ -21,11 +22,9 @@ describe('MemberList', () => {
const getByText = (text, options) =>
createWrapper(getByTextHelper(wrapper.element, text, options));
beforeEach(() => {
createComponent();
});
it('renders link to group', () => {
createComponent();
const link = wrapper.findComponent(GlAvatarLink);
expect(link.exists()).toBe(true);
@ -33,10 +32,26 @@ describe('MemberList', () => {
});
it("renders group's full name", () => {
createComponent();
expect(getByText(group.fullName).exists()).toBe(true);
});
it("renders group's avatar", () => {
createComponent();
expect(wrapper.find('img').attributes('src')).toBe(group.avatarUrl);
});
describe('when group is private', () => {
beforeEach(() => {
createComponent({ member: privateMember });
});
it('renders private avatar with icon', () => {
expect(wrapper.findComponent(GlAvatarLink).exists()).toBe(false);
expect(wrapper.findComponent(GlAvatarLabeled).props('label')).toBe('Private');
expect(wrapper.findComponent(PrivateIcon).exists()).toBe(true);
});
});
});

View File

@ -0,0 +1,30 @@
import { GlIcon } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import PrivateIcon from '~/members/components/icons/private_icon.vue';
describe('PrivateIcon', () => {
let wrapper;
const createComponent = () => {
wrapper = mountExtended(PrivateIcon, {
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
beforeEach(() => {
createComponent();
});
it('renders private icon with tooltip', () => {
const icon = wrapper.findComponent(GlIcon);
const tooltipDirective = getBinding(icon.element, 'gl-tooltip');
expect(icon.props('name')).toBe('eye-slash');
expect(tooltipDirective.value).toBe(
'Private group information is only accessible to its members.',
);
});
});

View File

@ -1,6 +1,7 @@
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import MemberSource from '~/members/components/table/member_source.vue';
import PrivateIcon from '~/members/components/icons/private_icon.vue';
describe('MemberSource', () => {
let wrapper;
@ -30,6 +31,20 @@ describe('MemberSource', () => {
const getTooltipDirective = (elementWrapper) => getBinding(elementWrapper.element, 'gl-tooltip');
describe('when source is private', () => {
beforeEach(() => {
createComponent({
isSharedWithGroupPrivate: true,
isDirectMember: false,
});
});
it('displays private with icon', () => {
expect(wrapper.findByText('Private').exists()).toBe(true);
expect(wrapper.findComponent(PrivateIcon).exists()).toBe(true);
});
});
describe('direct member', () => {
describe('when created by is available', () => {
it('displays "Direct member by <user name>"', () => {

View File

@ -27,6 +27,7 @@ import {
directMember,
invite,
accessRequest,
privateGroup,
pagination,
} from '../../mock_data';
@ -245,6 +246,24 @@ describe('MembersTable', () => {
});
});
});
describe('Source field', () => {
beforeEach(() => {
createComponent({
members: [privateGroup],
tableFields: ['source'],
});
});
it('passes correct props to `MemberSource` component', () => {
expect(wrapper.findComponent(MemberSource).props()).toMatchObject({
memberSource: {},
isDirectMember: true,
isSharedWithGroupPrivate: true,
createdBy: null,
});
});
});
});
describe('when `members` is an empty array', () => {

View File

@ -70,6 +70,19 @@ export const group = {
validRoles: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 },
};
export const privateGroup = {
accessLevel: { integerValue: 10, stringValue: 'Guest' },
isSharedWithGroupPrivate: true,
sharedWithGroup: {
id: 24,
},
id: 3,
isDirectMember: true,
createdAt: '2020-08-06T15:31:07.662Z',
expiresAt: null,
validRoles: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 },
};
export const modalData = {
isAccessRequest: true,
isInvite: true,

View File

@ -39,51 +39,4 @@ RSpec.describe ColorsHelper do
end
end
end
describe '#rgb_array_to_hex_color' do
context 'valid RGB array' do
where(:rgb_array, :hex_color) do
[0, 0, 0] | '#000000'
[0, 0, 255] | '#0000ff'
[0, 255, 0] | '#00ff00'
[255, 0, 0] | '#ff0000'
[12, 34, 56] | '#0c2238'
[222, 111, 88] | '#de6f58'
[255, 255, 255] | '#ffffff'
end
with_them do
it 'returns correct hex color' do
expect(helper.rgb_array_to_hex_color(rgb_array)).to eq(hex_color)
end
end
end
context 'invalid RGB array' do
where(:rgb_array) do
[
'',
'#000000',
0,
nil,
[],
[0],
[0, 0],
[0, 0, 0, 0],
[-1, 0, 0],
[0, -1, 0],
[0, 0, -1],
[256, 0, 0],
[0, 256, 0],
[0, 0, 256]
]
end
with_them do
it 'raise ArgumentError' do
expect { helper.rgb_array_to_hex_color(rgb_array) }.to raise_error(ArgumentError)
end
end
end
end
end

View File

@ -3,45 +3,6 @@
require 'spec_helper'
RSpec.describe EnvironmentHelper, feature_category: :environment_management do
describe '#render_deployment_status' do
context 'when using a manual deployment' do
it 'renders a span tag' do
deploy = build(:deployment, deployable: nil, status: :success)
html = helper.render_deployment_status(deploy)
expect(html).to have_css('span.ci-status.ci-success')
end
end
context 'when using a deployment from a build' do
it 'renders a link tag' do
deploy = build(:deployment, status: :success)
html = helper.render_deployment_status(deploy)
expect(html).to have_css('a.ci-status.ci-success')
end
end
context 'when deploying from a bridge' do
it 'renders a span tag' do
deploy = build(:deployment, deployable: create(:ci_bridge), status: :success)
html = helper.render_deployment_status(deploy)
expect(html).to have_css('span.ci-status.ci-success')
end
end
context 'for a blocked deployment' do
subject { helper.render_deployment_status(deployment) }
let(:deployment) { build(:deployment, :blocked) }
it 'indicates the status' do
expect(subject).to have_text('blocked')
end
end
end
describe '#environments_detail_data_json' do
subject { helper.environments_detail_data_json(user, project, environment) }

View File

@ -95,17 +95,4 @@ RSpec.describe EnvironmentsHelper, feature_category: :environment_management do
expect(subject).to eq(true)
end
end
describe '#environment_logs_data' do
it 'returns logs data' do
expected_data = {
"environment_name": environment.name,
"environments_path": api_v4_projects_environments_path(id: project.id),
"environment_id": environment.id,
"clusters_path": project_clusters_path(project, format: :json)
}
expect(helper.environment_logs_data(project, environment)).to eq(expected_data)
end
end
end

View File

@ -19,14 +19,9 @@ RSpec.describe Groups::GroupMembersHelper do
let(:members_collection) { members }
before do
allow(helper).to receive(:can?).with(current_user, :export_group_memberships, group).and_return(false)
allow(helper).to receive(:can?).with(current_user, :owner_access, group).and_return(true)
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:can?).with(current_user, :export_group_memberships, shared_group).and_return(true)
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(: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
subject do
@ -54,8 +49,8 @@ RSpec.describe Groups::GroupMembersHelper do
it 'returns expected json' do
expected = {
source_id: shared_group.id,
can_manage_members: true,
can_manage_access_requests: true,
can_manage_members: be_in([true, false]),
can_manage_access_requests: be_in([true, false]),
group_name: shared_group.name,
group_path: shared_group.full_path
}
@ -102,9 +97,6 @@ RSpec.describe Groups::GroupMembersHelper do
before 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_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_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)
end
subject do

View File

@ -45,21 +45,6 @@ RSpec.describe MembersHelper do
end
end
describe '#remove_member_title' do
let(:requester) { create(:user) }
let(:project) { create(:project, :public) }
let(:project_member) { build(:project_member, project: project) }
let(:project_member_request) { project.request_access(requester) }
let(:group) { create(:group) }
let(:group_member) { build(:group_member, group: group) }
let(:group_member_request) { group.request_access(requester) }
it { expect(remove_member_title(project_member)).to eq 'Remove user from project' }
it { expect(remove_member_title(project_member_request)).to eq 'Deny access request from project' }
it { expect(remove_member_title(group_member)).to eq 'Remove user from group and any subresources' }
it { expect(remove_member_title(group_member_request)).to eq 'Deny access request from group' }
end
describe '#leave_confirmation_message' do
let(:project) { build_stubbed(:project) }
let(:group) { build_stubbed(:group) }

View File

@ -2,7 +2,7 @@
require "spec_helper"
RSpec.describe Users::CalloutsHelper do
RSpec.describe Users::CalloutsHelper, feature_category: :navigation do
let_it_be(:user, refind: true) { create(:user) }
before do

View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GroupGroupLinkPolicy, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :private) }
let_it_be(:group2) { create(:group, :private) }
let(:group_group_link) do
create(:group_group_link, shared_group: group, shared_with_group: group2)
end
subject(:policy) { described_class.new(user, group_group_link) }
describe 'read_shared_with_group' do
context 'when the user is a shared_group member' do
before_all do
group.add_guest(user)
end
it 'can read_shared_with_group' do
expect(policy).to be_allowed(:read_shared_with_group)
end
end
context 'when the user is not a shared_group member' do
context 'when user is not a shared_with_group member' do
context 'when the shared_with_group is private' do
it 'cannot read_shared_with_group' do
expect(policy).to be_disallowed(:read_shared_with_group)
end
context 'when the shared group is public' do
let_it_be(:group) { create(:group, :public) }
it 'cannot read_shared_with_group' do
expect(policy).to be_disallowed(:read_shared_with_group)
end
end
end
context 'when the shared_with_group is public' do
let_it_be(:group2) { create(:group, :public) }
it 'can read_shared_with_group' do
expect(policy).to be_allowed(:read_shared_with_group)
end
end
end
context 'when user is a shared_with_group member' do
before_all do
group2.add_developer(user)
end
it 'can read_shared_with_group' do
expect(policy).to be_allowed(:read_shared_with_group)
end
end
end
end
end

View File

@ -4,9 +4,8 @@ require 'spec_helper'
RSpec.describe ProjectGroupLinkPolicy, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :private) }
let_it_be(:group2) { create(:group, :private) }
let_it_be(:project) { create(:project, :private, group: group) }
let_it_be(:project) { create(:project, :private) }
let(:project_group_link) do
create(:project_group_link, project: project, group: group2, group_access: Gitlab::Access::DEVELOPER)
@ -14,42 +13,92 @@ RSpec.describe ProjectGroupLinkPolicy, feature_category: :system_access do
subject(:policy) { described_class.new(user, project_group_link) }
context 'when the user is a group owner' do
before do
project_group_link.group.add_owner(user)
end
describe 'admin_project_group_link' do
context 'when the user is a group owner' do
before_all do
group2.add_owner(user)
end
context 'when user is not project maintainer' do
it 'can admin group_project_link' do
expect(policy).to be_allowed(:admin_project_group_link)
context 'when user is not project maintainer' do
it 'can admin group_project_link' do
expect(policy).to be_allowed(:admin_project_group_link)
end
end
context 'when user is a project maintainer' do
before do
project_group_link.project.add_maintainer(user)
end
it 'can admin group_project_link' do
expect(policy).to be_allowed(:admin_project_group_link)
end
end
end
context 'when user is a project maintainer' do
before do
project_group_link.project.add_maintainer(user)
context 'when user is not a group owner' do
context 'when user is a project maintainer' do
it 'can admin group_project_link' do
project_group_link.project.add_maintainer(user)
expect(policy).to be_allowed(:admin_project_group_link)
end
end
it 'can admin group_project_link' do
expect(policy).to be_allowed(:admin_project_group_link)
context 'when user is not a project maintainer' do
it 'cannot admin group_project_link' do
project_group_link.project.add_developer(user)
expect(policy).to be_disallowed(:admin_project_group_link)
end
end
end
end
context 'when user is not a group owner' do
context 'when user is a project maintainer' do
it 'can admin group_project_link' do
project_group_link.project.add_maintainer(user)
describe 'read_shared_with_group' do
context 'when the user is a project member' do
before_all do
project.add_guest(user)
end
expect(policy).to be_allowed(:admin_project_group_link)
it 'can read_shared_with_group' do
expect(policy).to be_allowed(:read_shared_with_group)
end
end
context 'when user is not a project maintainer' do
it 'cannot admin group_project_link' do
project_group_link.project.add_developer(user)
context 'when the user is not a project member' do
context 'when user is not a group member' do
context 'when the group is private' do
it 'cannot read_shared_with_group' do
expect(policy).to be_disallowed(:read_shared_with_group)
end
expect(policy).to be_disallowed(:admin_project_group_link)
context 'when the project is public' do
let_it_be(:project) { create(:project, :public) }
it 'cannot read_shared_with_group' do
expect(policy).to be_disallowed(:read_shared_with_group)
end
end
end
context 'when the group is public' do
let_it_be(:group2) { create(:group, :public) }
it 'can read_shared_with_group' do
expect(policy).to be_allowed(:read_shared_with_group)
end
end
end
context 'when user is a group member' do
before_all do
group2.add_guest(user)
end
it 'can read_shared_with_group' do
expect(policy).to be_allowed(:read_shared_with_group)
end
end
end
end

View File

@ -9,8 +9,8 @@ RSpec.describe GroupLink::GroupGroupLinkEntity do
let(:entity) { described_class.new(group_group_link, { current_user: current_user, source: shared_group }) }
before do
allow(entity).to receive(:current_user).and_return(current_user)
subject(:as_json) do
entity.as_json
end
it 'matches json schema' do
@ -19,7 +19,7 @@ RSpec.describe GroupLink::GroupGroupLinkEntity do
context 'source' do
it 'exposes `source`' do
expect(entity.as_json[:source]).to include(
expect(as_json[:source]).to include(
id: shared_group.id,
full_name: shared_group.full_name,
web_url: shared_group.web_url
@ -38,9 +38,9 @@ RSpec.describe GroupLink::GroupGroupLinkEntity do
end
end
context 'when current user has `:admin_group_member` permissions' do
before do
allow(entity).to receive(:can?).with(current_user, :admin_group_member, shared_group).and_return(true)
context 'when current user has owner permissions for the shared group' do
before_all do
shared_group.add_owner(current_user)
end
context 'when direct_member? is true' do
@ -49,10 +49,8 @@ RSpec.describe GroupLink::GroupGroupLinkEntity do
end
it 'exposes `can_update` and `can_remove` as `true`' do
json = entity.as_json
expect(json[:can_update]).to be true
expect(json[:can_remove]).to be true
expect(as_json[:can_update]).to be true
expect(as_json[:can_remove]).to be true
end
end
@ -62,10 +60,51 @@ RSpec.describe GroupLink::GroupGroupLinkEntity do
end
it 'exposes `can_update` and `can_remove` as `true`' do
json = entity.as_json
expect(as_json[:can_update]).to be false
expect(as_json[:can_remove]).to be false
end
end
end
expect(json[:can_update]).to be false
expect(json[:can_remove]).to be false
context 'when current user is not a group member' do
context 'when shared with group is public' do
it 'does expose shared_with_group details' do
expect(as_json[:shared_with_group].keys).to include(:id, :avatar_url, :web_url, :name)
end
it 'does expose source details' do
expect(as_json[:source].keys).to include(:id, :full_name)
end
it 'sets is_shared_with_group_private to false' do
expect(as_json[:is_shared_with_group_private]).to be false
end
end
context 'when shared with group is private' do
let_it_be(:shared_with_group) { create(:group, :private) }
let_it_be(:group_group_link) do
create(
:group_group_link,
{
shared_group: shared_group,
shared_with_group: shared_with_group,
expires_at: '2020-05-12'
}
)
end
it 'does not expose shared_with_group details' do
expect(as_json[:shared_with_group].keys).to contain_exactly(:id)
end
it 'does not expose source details' do
expect(as_json[:source]).to be_nil
end
it 'sets is_shared_with_group_private to true' do
expect(as_json[:is_shared_with_group_private]).to be true
end
end
end

View File

@ -8,51 +8,69 @@ RSpec.describe GroupLink::ProjectGroupLinkEntity do
let(:entity) { described_class.new(project_group_link, { current_user: current_user, source: project_group_link.project }) }
before do
allow(entity).to receive(:current_user).and_return(current_user)
subject(:as_json) do
entity.as_json
end
it 'matches json schema' do
expect(entity.to_json).to match_schema('group_link/project_group_link')
end
context 'when current user has `admin_project_member` permissions' do
before do
allow(entity).to receive(:can?).with(current_user, :admin_project_group_link, project_group_link).and_return(false)
allow(entity).to receive(:can?).with(current_user, :admin_project_member, project_group_link.project).and_return(true)
context 'when current user is a project maintainer' do
before_all do
project_group_link.project.add_maintainer(current_user)
end
it 'exposes `can_update` and `can_remove` as `true`' do
json = entity.as_json
expect(json[:can_update]).to be true
expect(json[:can_remove]).to be false
expect(as_json[:can_update]).to be true
expect(as_json[:can_remove]).to be true
end
end
context 'when current user is a group owner' do
before do
allow(entity).to receive(:can?).with(current_user, :admin_project_group_link, project_group_link).and_return(true)
allow(entity).to receive(:can?).with(current_user, :admin_project_member, project_group_link.project).and_return(false)
before_all do
project_group_link.group.add_owner(current_user)
end
it 'exposes `can_remove` as true' do
json = entity.as_json
expect(json[:can_remove]).to be true
expect(as_json[:can_remove]).to be true
end
end
context 'when current user is not a group owner' do
before do
allow(entity).to receive(:can?).with(current_user, :admin_project_group_link, project_group_link).and_return(false)
allow(entity).to receive(:can?).with(current_user, :admin_project_member, project_group_link.project).and_return(false)
it 'exposes `can_remove` as false' do
expect(as_json[:can_remove]).to be false
end
it 'exposes `can_remove` as false' do
json = entity.as_json
context 'when group is public' do
it 'does expose shared_with_group details' do
expect(as_json[:shared_with_group].keys).to include(:id, :avatar_url, :web_url, :name)
end
expect(json[:can_remove]).to be false
it 'does expose source details' do
expect(as_json[:source].keys).to include(:id, :full_name)
end
it 'sets is_shared_with_group_private to false' do
expect(as_json[:is_shared_with_group_private]).to be false
end
end
context 'when group is private' do
let_it_be(:private_group) { create(:group, :private) }
let_it_be(:project_group_link) { create(:project_group_link, group: private_group) }
it 'does not expose shared_with_group details' do
expect(as_json[:shared_with_group].keys).to contain_exactly(:id)
end
it 'does not expose source details' do
expect(as_json[:source]).to be_nil
end
it 'sets is_shared_with_group_private to true' do
expect(as_json[:is_shared_with_group_private]).to be true
end
end
end
end