Commit a375aa56 authored by Jackie Fraser's avatar Jackie Fraser Committed by Sean McGivern

Add API get /invitations for project and group

parent 702ca824
......@@ -419,6 +419,10 @@ class Member < ApplicationRecord
invite? && user_id.nil?
end
def created_by_name
created_by&.name
end
private
def send_invite
......
---
title: Add API get /invitations for project and group
merge_request: 46046
author:
type: added
......@@ -6,7 +6,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Invitations API
Use the Invitations API to send email to users you want to join a group or project.
Use the Invitations API to send email to users you want to join a group or project, and to list pending
invitations.
## Valid access levels
......@@ -64,3 +65,43 @@ When there was any error sending the email:
}
}
```
## List all invitations pending for a group or project
Gets a list of invited group or project members viewable by the authenticated user.
Returns invitations to direct members only, and not through inherited ancestors' groups.
This function takes pagination parameters `page` and `per_page` to restrict the list of users.
```plaintext
GET /groups/:id/invitations
GET /projects/:id/invitations
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `page` | integer | no | Page to retrieve |
| `per_page`| integer | no | Number of member invitations to return per page |
| `query` | string | no | A query string to search for invited members by invite email. Query text must match email address exactly. When empty, returns all invitations. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/invitations?query=member@example.org"
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/invitations?query=member@example.org"
```
Example response:
```json
[
{
"id": 1,
"invite_email": "member@example.org",
"invited_at": "2020-10-22T14:13:35Z",
"access_level": 30,
"expires_at": "2020-11-22T14:13:35Z",
"user_name": "Raymond Smith",
"created_by_name": "Administrator"
},
]
```
......@@ -8,7 +8,8 @@ module API
expose :expires_at
expose :invite_email
expose :invite_token
expose :user_id
expose :user_name, if: -> (member, _) { member.user.present? }
expose :created_by_name
end
end
end
......@@ -27,6 +27,13 @@ module API
members
end
def retrieve_member_invitations(source, query = nil)
members = source_members(source).where.not(invite_token: nil)
members = members.includes(:user)
members = members.where(invite_email: query) if query.present?
members
end
def source_members(source)
source.members
end
......@@ -52,6 +59,10 @@ module API
def present_members(members)
present members, with: Entities::Member, current_user: current_user, show_seat_info: params[:show_seat_info]
end
def present_member_invitations(invitations)
present invitations, with: Entities::Invitation, current_user: current_user
end
end
end
end
......
......@@ -2,6 +2,8 @@
module API
class Invitations < ::API::Base
include PaginationParams
feature_category :users
before { authenticate! }
......@@ -29,6 +31,23 @@ module API
::Members::InviteService.new(current_user, params).execute(source)
end
desc 'Get a list of group or project invitations viewable by the authenticated user' do
detail 'This feature was introduced in GitLab 13.6'
success Entities::Invitation
end
params do
optional :query, type: String, desc: 'A query string to search for members'
use :pagination
end
get ":id/invitations" do
source = find_source(source_type, params[:id])
query = params[:query]
invitations = paginate(retrieve_member_invitations(source, query))
present_member_invitations invitations
end
end
end
end
......
......@@ -7,7 +7,8 @@ RSpec.describe API::Invitations do
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
let(:stranger) { create(:user) }
let(:email) { 'email@example.org' }
let(:email) { 'email1@example.com' }
let(:email2) { 'email2@example.com' }
let(:project) do
create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
......@@ -75,7 +76,7 @@ RSpec.describe API::Invitations do
it 'invites a list of new email addresses' do
expect do
email_list = 'email1@example.com,email2@example.com'
email_list = [email, email2].join(',')
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: email_list, access_level: Member::DEVELOPER }
......@@ -204,4 +205,97 @@ RSpec.describe API::Invitations do
let(:source) { group }
end
end
shared_examples 'GET /:source_type/:id/invitations' do |source_type|
context "with :source_type == #{source_type.pluralize}" do
it_behaves_like 'a 404 response when source is private' do
let(:route) { get invitations_url(source, stranger) }
end
%i[maintainer developer access_requester stranger].each do |type|
context "when authenticated as a #{type}" do
it 'returns 200' do
user = public_send(type)
get invitations_url(source, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(0)
end
end
end
it 'avoids N+1 queries' do
# Establish baseline
get invitations_url(source, maintainer)
control = ActiveRecord::QueryRecorder.new do
get invitations_url(source, maintainer)
end
invite_member_by_email(source, source_type, email, maintainer)
expect do
get invitations_url(source, maintainer)
end.not_to exceed_query_limit(control)
end
it 'does not find confirmed members' do
get invitations_url(source, developer)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(0)
expect(json_response.map { |u| u['id'] }).not_to match_array [maintainer.id, developer.id]
end
it 'finds all members with no query string specified' do
invite_member_by_email(source, source_type, email, developer)
invite_member_by_email(source, source_type, email2, developer)
get invitations_url(source, developer), params: { query: '' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.count).to eq(2)
expect(json_response.map { |u| u['invite_email'] }).to match_array [email, email2]
end
it 'finds the invitation by invite_email with query string' do
invite_member_by_email(source, source_type, email, developer)
invite_member_by_email(source, source_type, email2, developer)
get invitations_url(source, developer), params: { query: email }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.count).to eq(1)
expect(json_response.first['invite_email']).to eq(email)
expect(json_response.first['created_by_name']).to eq(developer.name)
expect(json_response.first['user_name']).to eq(nil)
end
def invite_member_by_email(source, source_type, email, created_by)
create(:"#{source_type}_member", invite_token: '123', invite_email: email, source: source, user: nil, created_by: created_by)
end
end
end
describe 'GET /projects/:id/invitations' do
it_behaves_like 'GET /:source_type/:id/invitations', 'project' do
let(:source) { project }
end
end
describe 'GET /groups/:id/invitations' do
it_behaves_like 'GET /:source_type/:id/invitations', 'group' do
let(:source) { group }
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment