diff --git a/ee/app/models/ee/member.rb b/ee/app/models/ee/member.rb index 2d2847b2e15134a045a91cf052ea2fb763564404..0537e3d0f30fddc636a05a7b3ed931725408c8a0 100644 --- a/ee/app/models/ee/member.rb +++ b/ee/app/models/ee/member.rb @@ -33,6 +33,13 @@ module Member scope :with_csv_entity_associations, -> do includes(:user, source: [:route, :parent]) end + + scope :awaiting_or_invited_for_group, -> (group) do + awaiting + .or(::Member.invite) + .in_hierarchy(group) + .includes(:user) + end end override :notification_service diff --git a/ee/lib/api/entities/pending_member.rb b/ee/lib/api/entities/pending_member.rb new file mode 100644 index 0000000000000000000000000000000000000000..f8d1529c52964bff95da2bdba5922834609e784d --- /dev/null +++ b/ee/lib/api/entities/pending_member.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module API + module Entities + class PendingMember < Grape::Entity + expose :id + expose :user_name, as: :name, if: -> (_) { user.present? } + expose :user_username, as: :username, if: -> (_) { user.present? } + expose :email + expose :web_url, if: -> (_) { user.present? } + expose :invite?, as: :invited + + expose :avatar_url do |_| + user&.avatar_url || GravatarService.new.execute(email) + end + + expose :approved do |member| + member.active? + end + + def email + object.invite_email || object.user.email + end + + def web_url + Gitlab::Routing.url_helpers.user_url(user) + end + + def user + object.user + end + end + end +end diff --git a/ee/lib/ee/api/members.rb b/ee/lib/ee/api/members.rb index cbf8a292e4bd5d5ae41292f712a7f0fccf153c9b..c2e894196fa302eef1d930a8eb8497903113941d 100644 --- a/ee/lib/ee/api/members.rb +++ b/ee/lib/ee/api/members.rb @@ -94,6 +94,21 @@ module Members end end + desc 'Lists all pending members for a group including invited users' + params do + use :pagination + end + get ":id/pending_members" do + group = find_group!(params[:id]) + + bad_request! unless group.root? + bad_request! unless can?(current_user, :admin_group_member, group) + + members = ::Member.awaiting_or_invited_for_group(group) + + present paginate(members), with: ::API::Entities::PendingMember + end + desc 'Gets a list of billable users of root group.' do success Entities::Member end diff --git a/ee/spec/lib/api/entities/pending_member_spec.rb b/ee/spec/lib/api/entities/pending_member_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c230f6e7cf0b620539a5a1f500bea47ec74f9a0d --- /dev/null +++ b/ee/spec/lib/api/entities/pending_member_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Entities::PendingMember do + subject(:pending_member) { described_class.new(member).as_json } + + context 'with a user present' do + let(:member) { create(:group_member, :awaiting) } + + it 'exposes correct attributes' do + expect(pending_member.keys).to match_array [ + :id, + :name, + :username, + :email, + :avatar_url, + :web_url, + :approved, + :invited + ] + end + end + + context 'with no user present' do + let(:member) { create(:group_member, :invited) } + + it 'exposes correct attributes' do + expect(pending_member.keys).to match_array [ + :id, + :email, + :avatar_url, + :approved, + :invited + ] + end + end +end diff --git a/ee/spec/models/member_spec.rb b/ee/spec/models/member_spec.rb index 81c2ce7233a08f0b6aadb1d81d820485bf41f2f8..207888f51e26dcd3eddc082b5a761749648f2c13 100644 --- a/ee/spec/models/member_spec.rb +++ b/ee/spec/models/member_spec.rb @@ -245,4 +245,23 @@ end end end + + describe '.awaiting_or_invited_for_group' do + let_it_be(:active_group_member) { create(:group_member, group: group) } + let_it_be(:awaiting_group_member) { create(:group_member, :awaiting, group: group) } + let_it_be(:awaiting_subgroup_member) { create(:group_member, :awaiting, group: sub_group) } + let_it_be(:awaiting_project_member) { create(:project_member, :awaiting, project: project) } + let_it_be(:awaiting_invited_member) { create(:group_member, :awaiting, :invited, group: group) } + let_it_be(:active_invited_member) { create(:group_member, :invited, group: group) } + + it 'returns the correct members' do + expect(described_class.awaiting_or_invited_for_group(group)).to match_array [ + awaiting_group_member, + awaiting_subgroup_member, + awaiting_project_member, + awaiting_invited_member, + active_invited_member + ] + end + end end diff --git a/ee/spec/requests/api/members_spec.rb b/ee/spec/requests/api/members_spec.rb index b98e4233953a7483044ada9cf7163fbafe92f0b0..2721d86c55e5fa96b28355f605efc9ae91f6ac49 100644 --- a/ee/spec/requests/api/members_spec.rb +++ b/ee/spec/requests/api/members_spec.rb @@ -1103,6 +1103,72 @@ end end end + + describe 'GET /groups/:id/pending_members' do + let(:url) { "/groups/#{group.id}/pending_members" } + + context 'when the current user is not authorized' do + it 'returns a bad request response' do + get api(url, not_an_owner) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'when the current user is authorized' do + let_it_be(:pending_group_member) { create(:group_member, :awaiting, group: group) } + let_it_be(:pending_subgroup_member) { create(:group_member, :awaiting, group: subgroup) } + let_it_be(:pending_project_member) { create(:project_member, :awaiting, project: project) } + let_it_be(:pending_invited_member) { create(:group_member, :awaiting, :invited, group: group) } + + it 'returns only pending members' do + create(:group_member, group: group) + + get api(url, owner) + + expect(json_response.map { |m| m['id'] }).to match_array [ + pending_group_member.id, + pending_subgroup_member.id, + pending_project_member.id, + pending_invited_member.id + ] + end + + it 'includes activated invited members' do + pending_invited_member.activate! + + get api(url, owner) + + expect(json_response.map { |m| m['id'] }).to match_array [ + pending_group_member.id, + pending_subgroup_member.id, + pending_project_member.id, + pending_invited_member.id + ] + end + + it 'paginates the response' do + get api(url, owner) + + expect_paginated_array_response(*[ + pending_group_member.id, + pending_subgroup_member.id, + pending_project_member.id, + pending_invited_member.id + ]) + end + + context 'when the group ID is a subgroup' do + let(:url) { "/groups/#{subgroup.id}/pending_members" } + + it 'returns a bad request response' do + get api(url, owner) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end + end end context 'filtering project and group members' do