diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-10-31 03:16:38 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-10-31 03:16:38 +0300 |
commit | 453bb35fc81741c1dd7178cde1f64df2c9ec7252 (patch) | |
tree | 44edb488772b5fc89967865e4e0e70c8feab832f | |
parent | 892e58c41fd07cc59b9e06b2a856ee61790d5bc4 (diff) |
Add latest changes from gitlab-org/gitlab@master
42 files changed, 546 insertions, 332 deletions
diff --git a/app/assets/javascripts/helpers/init_simple_app_helper.js b/app/assets/javascripts/helpers/init_simple_app_helper.js index 695fc455f13..f7bef8c563e 100644 --- a/app/assets/javascripts/helpers/init_simple_app_helper.js +++ b/app/assets/javascripts/helpers/init_simple_app_helper.js @@ -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 }); }, diff --git a/app/assets/javascripts/members/components/avatars/group_avatar.vue b/app/assets/javascripts/members/components/avatars/group_avatar.vue index 3b176bf2b43..83b5855492b 100644 --- a/app/assets/javascripts/members/components/avatars/group_avatar.vue +++ b/app/assets/javascripts/members/components/avatars/group_avatar.vue @@ -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> diff --git a/app/assets/javascripts/members/components/icons/private_icon.vue b/app/assets/javascripts/members/components/icons/private_icon.vue new file mode 100644 index 00000000000..6168ea955f3 --- /dev/null +++ b/app/assets/javascripts/members/components/icons/private_icon.vue @@ -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> diff --git a/app/assets/javascripts/members/components/table/member_source.vue b/app/assets/javascripts/members/components/table/member_source.vue index ed1971d020b..f1a1c4cecaa 100644 --- a/app/assets/javascripts/members/components/table/member_source.vue +++ b/app/assets/javascripts/members/components/table/member_source.vue @@ -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">{{ diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue index 68f624e9a3d..2b3294c1c79 100644 --- a/app/assets/javascripts/members/components/table/members_table.vue +++ b/app/assets/javascripts/members/components/table/members_table.vue @@ -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> diff --git a/app/assets/javascripts/pages/projects/ml/models/show/index.js b/app/assets/javascripts/pages/projects/ml/models/show/index.js index 87ee5c851f6..c8e25e0f0e8 100644 --- a/app/assets/javascripts/pages/projects/ml/models/show/index.js +++ b/app/assets/javascripts/pages/projects/ml/models/show/index.js @@ -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 }); diff --git a/app/helpers/colors_helper.rb b/app/helpers/colors_helper.rb index 3cd7263c39e..34b18b80be4 100644 --- a/app/helpers/colors_helper.rb +++ b/app/helpers/colors_helper.rb @@ -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 diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb index 6e9379a5926..fa47a12a72c 100644 --- a/app/helpers/environment_helper.rb +++ b/app/helpers/environment_helper.rb @@ -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, diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index 80a56493653..6d48dac5c6d 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -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 diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb index ab72442857b..e74005cf77b 100644 --- a/app/helpers/graph_helper.rb +++ b/app/helpers/graph_helper.rb @@ -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 diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index e4c1d7932aa..600e5f06c61 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -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) diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 4d0d44b3318..21c81eefce2 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -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 diff --git a/app/policies/group_group_link_policy.rb b/app/policies/group_group_link_policy.rb new file mode 100644 index 00000000000..0108f0b7fca --- /dev/null +++ b/app/policies/group_group_link_policy.rb @@ -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 diff --git a/app/policies/project_group_link_policy.rb b/app/policies/project_group_link_policy.rb index 00bb246d70b..7ad2985ecc5 100644 --- a/app/policies/project_group_link_policy.rb +++ b/app/policies/project_group_link_policy.rb @@ -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? diff --git a/app/serializers/group_link/group_group_link_entity.rb b/app/serializers/group_link/group_group_link_entity.rb index d5d7eea74ea..f855d89f593 100644 --- a/app/serializers/group_link/group_group_link_entity.rb +++ b/app/serializers/group_link/group_group_link_entity.rb @@ -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 diff --git a/app/serializers/group_link/group_link_entity.rb b/app/serializers/group_link/group_link_entity.rb index 4cc7e9f3c8c..66645e736a9 100644 --- a/app/serializers/group_link/group_link_entity.rb +++ b/app/serializers/group_link/group_link_entity.rb @@ -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 diff --git a/app/serializers/group_link/project_group_link_entity.rb b/app/serializers/group_link/project_group_link_entity.rb index d246bff1c58..fbad69bf2c5 100644 --- a/app/serializers/group_link/project_group_link_entity.rb +++ b/app/serializers/group_link/project_group_link_entity.rb @@ -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 diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 37f27aa7caf..bed37d9cb63 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -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 diff --git a/doc/administration/settings/gitaly_timeouts.md b/doc/administration/settings/gitaly_timeouts.md index 3304db3d148..edb44249aab 100644 --- a/doc/administration/settings/gitaly_timeouts.md +++ b/doc/administration/settings/gitaly_timeouts.md @@ -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). diff --git a/doc/ci/pipelines/settings.md b/doc/ci/pipelines/settings.md index 265fd674190..3baaba091d3 100644 --- a/doc/ci/pipelines/settings.md +++ b/doc/ci/pipelines/settings.md @@ -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. diff --git a/doc/user/group/manage.md b/doc/user/group/manage.md index d671b0434b6..d67e52a6fc3 100644 --- a/doc/user/group/manage.md +++ b/doc/user/group/manage.md @@ -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`. diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md index 6765aa2cbb1..2c395538455 100644 --- a/doc/user/packages/maven_repository/index.md +++ b/doc/user/packages/maven_repository/index.md @@ -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. diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md index deefe9040fa..4474cb55929 100644 --- a/doc/user/project/members/share_project_with_groups.md +++ b/doc/user/project/members/share_project_with_groups.md @@ -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. diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 22cd8f9b89e..58cfd90c13f 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -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. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 48df831f014..8de70afd7fa 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/fixtures/api/schemas/group_link/group_link.json b/spec/fixtures/api/schemas/group_link/group_link.json index 885ed6d18e0..4db38952ecc 100644 --- a/spec/fixtures/api/schemas/group_link/group_link.json +++ b/spec/fixtures/api/schemas/group_link/group_link.json @@ -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" } } -}
\ No newline at end of file +} diff --git a/spec/frontend/helpers/init_simple_app_helper_spec.js b/spec/frontend/helpers/init_simple_app_helper_spec.js index 7938e3851d0..de39a6f9d70 100644 --- a/spec/frontend/helpers/init_simple_app_helper_spec.js +++ b/spec/frontend/helpers/init_simple_app_helper_spec.js @@ -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); + }); + }); + }); + }); }); diff --git a/spec/frontend/members/components/avatars/group_avatar_spec.js b/spec/frontend/members/components/avatars/group_avatar_spec.js index 8e4263f88fe..1463aa5ae59 100644 --- a/spec/frontend/members/components/avatars/group_avatar_spec.js +++ b/spec/frontend/members/components/avatars/group_avatar_spec.js @@ -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(() => { + it('renders link to group', () => { createComponent(); - }); - it('renders link to group', () => { 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); + }); + }); }); diff --git a/spec/frontend/members/components/icons/private_icon_spec.js b/spec/frontend/members/components/icons/private_icon_spec.js new file mode 100644 index 00000000000..ea2b65e3307 --- /dev/null +++ b/spec/frontend/members/components/icons/private_icon_spec.js @@ -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.', + ); + }); +}); diff --git a/spec/frontend/members/components/table/member_source_spec.js b/spec/frontend/members/components/table/member_source_spec.js index bbfbb19fd92..16b8d239944 100644 --- a/spec/frontend/members/components/table/member_source_spec.js +++ b/spec/frontend/members/components/table/member_source_spec.js @@ -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>"', () => { diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js index 4539478bf9a..791155fcd1b 100644 --- a/spec/frontend/members/components/table/members_table_spec.js +++ b/spec/frontend/members/components/table/members_table_spec.js @@ -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', () => { diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js index 4598c74757e..4fb0b76fa49 100644 --- a/spec/frontend/members/mock_data.js +++ b/spec/frontend/members/mock_data.js @@ -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, diff --git a/spec/helpers/colors_helper_spec.rb b/spec/helpers/colors_helper_spec.rb index ca5cafb7ebe..328796ef1ae 100644 --- a/spec/helpers/colors_helper_spec.rb +++ b/spec/helpers/colors_helper_spec.rb @@ -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 diff --git a/spec/helpers/environment_helper_spec.rb b/spec/helpers/environment_helper_spec.rb index 1383bf34881..0c04417fca6 100644 --- a/spec/helpers/environment_helper_spec.rb +++ b/spec/helpers/environment_helper_spec.rb @@ -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) } diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb index 6624404bc49..997f9e6eac7 100644 --- a/spec/helpers/environments_helper_spec.rb +++ b/spec/helpers/environments_helper_spec.rb @@ -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 diff --git a/spec/helpers/groups/group_members_helper_spec.rb b/spec/helpers/groups/group_members_helper_spec.rb index a9c6822e2c1..3a7c852b683 100644 --- a/spec/helpers/groups/group_members_helper_spec.rb +++ b/spec/helpers/groups/group_members_helper_spec.rb @@ -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 diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb index 68a12d8dbf7..d2d758cdc7c 100644 --- a/spec/helpers/members_helper_spec.rb +++ b/spec/helpers/members_helper_spec.rb @@ -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) } diff --git a/spec/helpers/users/callouts_helper_spec.rb b/spec/helpers/users/callouts_helper_spec.rb index 10f021330db..2a0ba1279b0 100644 --- a/spec/helpers/users/callouts_helper_spec.rb +++ b/spec/helpers/users/callouts_helper_spec.rb @@ -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 diff --git a/spec/policies/group_group_link_policy_spec.rb b/spec/policies/group_group_link_policy_spec.rb new file mode 100644 index 00000000000..34bc1bc3bec --- /dev/null +++ b/spec/policies/group_group_link_policy_spec.rb @@ -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 diff --git a/spec/policies/project_group_link_policy_spec.rb b/spec/policies/project_group_link_policy_spec.rb index 9461f33decb..1047d3acb1e 100644 --- a/spec/policies/project_group_link_policy_spec.rb +++ b/spec/policies/project_group_link_policy_spec.rb @@ -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 + + 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 - expect(policy).to be_disallowed(:admin_project_group_link) + it 'can read_shared_with_group' do + expect(policy).to be_allowed(:read_shared_with_group) + end end end end diff --git a/spec/serializers/group_link/group_group_link_entity_spec.rb b/spec/serializers/group_link/group_group_link_entity_spec.rb index 502cdc5c048..8f31c53e841 100644 --- a/spec/serializers/group_link/group_group_link_entity_spec.rb +++ b/spec/serializers/group_link/group_group_link_entity_spec.rb @@ -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 + + 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 - expect(json[:can_update]).to be false - expect(json[:can_remove]).to be false + it 'sets is_shared_with_group_private to true' do + expect(as_json[:is_shared_with_group_private]).to be true end end end diff --git a/spec/serializers/group_link/project_group_link_entity_spec.rb b/spec/serializers/group_link/project_group_link_entity_spec.rb index 1a8fcb2cfd3..00bfc43f17e 100644 --- a/spec/serializers/group_link/project_group_link_entity_spec.rb +++ b/spec/serializers/group_link/project_group_link_entity_spec.rb @@ -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 + + 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 - expect(json[:can_remove]).to be false + 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 |