Repository: xanzy/go-gitlab
Pull request #1035 (2021-01-15 21:11:56)

Support for groups/projects invites
Support for groups/projects invites #1029


Patch:
diff --git a/README.md b/README.md
index da5cf7b..1db5083 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,7 @@ to add new and/or missing endpoints. Currently the following services are suppor
 - [x] Group-Level Variables
 - [x] Groups
 - [x] Instance Clusters
+- [x] Invites
 - [x] Issue Boards
 - [x] Issues
 - [x] Jobs
diff --git a/gitlab.go b/gitlab.go
index 5bab400..625d64d 100644
--- a/gitlab.go
+++ b/gitlab.go
@@ -129,6 +129,7 @@ type Client struct {
 	Groups                *GroupsService
 	InstanceCluster       *InstanceClustersService
 	InstanceVariables     *InstanceVariablesService
+	Invites               *InvitesService
 	IssueLinks            *IssueLinksService
 	Issues                *IssuesService
 	IssuesStatistics      *IssuesStatisticsService
@@ -293,6 +294,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
 	c.Groups = &GroupsService{client: c}
 	c.InstanceCluster = &InstanceClustersService{client: c}
 	c.InstanceVariables = &InstanceVariablesService{client: c}
+	c.Invites = &InvitesService{client: c}
 	c.IssueLinks = &IssueLinksService{client: c}
 	c.Issues = &IssuesService{client: c, timeStats: timeStats}
 	c.IssuesStatistics = &IssuesStatisticsService{client: c}
diff --git a/invites.go b/invites.go
new file mode 100644
index 0000000..1c2662b
--- /dev/null
+++ b/invites.go
@@ -0,0 +1,174 @@
+//
+// Copyright 2021, Sander van Harmelen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package gitlab
+
+import (
+	"fmt"
+	"time"
+)
+
+// InvitesService handles communication with the invitation related
+// methods of the GitLab API.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/invitations.html
+type InvitesService struct {
+	client *Client
+}
+
+// PendingInvite represents a pending invite.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/invitations.html
+type PendingInvite struct {
+	ID            int              `json:"id"`
+	InviteEmail   string           `json:"invite_email"`
+	CreatedAt     *time.Time       `json:"created_at"`
+	AccessLevel   AccessLevelValue `json:"access_level"`
+	ExpiresAt     *time.Time       `json:"expires_at"`
+	UserName      string           `json:"user_name"`
+	CreatedByName string           `json:"created_by_name"`
+}
+
+// ListPendingInvitationsOptions represents the available
+// ListPendingInvitations() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/invitations.html#list-all-invitations-pending-for-a-group-or-project
+type ListPendingInvitationsOptions struct {
+	ListOptions
+	Query *string `url:"query,omitempty" json:"query,omitempty"`
+}
+
+// ListPendingGroupInvitations gets a list of invited group members.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/invitations.html#list-all-invitations-pending-for-a-group-or-project
+func (s *InvitesService) ListPendingGroupInvitations(gid interface{}, opt *ListPendingInvitationsOptions, options ...RequestOptionFunc) ([]*PendingInvite, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("groups/%s/invitations", pathEscape(group))
+
+	req, err := s.client.NewRequest("GET", u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var pis []*PendingInvite
+	resp, err := s.client.Do(req, &pis)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return pis, resp, err
+}
+
+// ListPendingProjectInvitations gets a list of invited project members.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/invitations.html#list-all-invitations-pending-for-a-group-or-project
+func (s *InvitesService) ListPendingProjectInvitations(pid interface{}, opt *ListPendingInvitationsOptions, options ...RequestOptionFunc) ([]*PendingInvite, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/invitations", pathEscape(project))
+
+	req, err := s.client.NewRequest("GET", u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var pis []*PendingInvite
+	resp, err := s.client.Do(req, &pis)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return pis, resp, err
+}
+
+// InvitesOptions represents the available GroupInvites() and ProjectInvites()
+// options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/invitations.html#invite-by-email-to-group-or-project
+type InvitesOptions struct {
+	ID          interface{}       `url:"id,omitempty" json:"id,omitempty"`
+	Email       *string           `url:"email,omitempty" json:"email,omitempty"`
+	AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"`
+	ExpiresAt   *ISOTime          `url:"expires_at,omitempty" json:"expires_at,omitempty"`
+}
+
+// InvitesResult represents an invitations result.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/invitations.html#invite-by-email-to-group-or-project
+type InvitationsResult struct {
+	Status  string            `json:"status"`
+	Message map[string]string `json:"message,omitempty"`
+}
+
+// GroupInvites invites new users by email to join a group.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/invitations.html#invite-by-email-to-group-or-project
+func (s *InvitesService) GroupInvites(gid interface{}, opt *InvitesOptions, options ...RequestOptionFunc) (*InvitationsResult, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("groups/%s/invitations", pathEscape(group))
+
+	req, err := s.client.NewRequest("POST", u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	ir := new(InvitationsResult)
+	resp, err := s.client.Do(req, ir)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return ir, resp, err
+}
+
+// ProjectInvites invites new users by email to join a project.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/invitations.html#invite-by-email-to-group-or-project
+func (s *InvitesService) ProjectInvites(pid interface{}, opt *InvitesOptions, options ...RequestOptionFunc) (*InvitationsResult, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/invitations", pathEscape(project))
+
+	req, err := s.client.NewRequest("POST", u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	ir := new(InvitationsResult)
+	resp, err := s.client.Do(req, ir)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return ir, resp, err
+}


Test patch:
diff --git a/invites_test.go b/invites_test.go
new file mode 100644
index 0000000..6c0e206
--- /dev/null
+++ b/invites_test.go
@@ -0,0 +1,152 @@
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+func TestListGroupPendingInvites(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/groups/test/invitations", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `[{"id":1},{"id":2}]`)
+	})
+
+	opt := &ListPendingInvitationsOptions{
+		ListOptions: ListOptions{2, 3},
+	}
+
+	projects, _, err := client.Invites.ListPendingGroupInvitations("test", opt)
+	if err != nil {
+		t.Errorf("Invites.ListPendingGroupInvitations returned error: %v", err)
+	}
+
+	want := []*PendingInvite{{ID: 1}, {ID: 2}}
+	if !reflect.DeepEqual(want, projects) {
+		t.Errorf("Invites.ListPendingGroupInvitations returned %+v, want %+v", projects, want)
+	}
+}
+
+func TestGroupInvites(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/groups/test/invitations", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "POST")
+		fmt.Fprint(w, `{"status": "success"}`)
+	})
+
+	opt := &InvitesOptions{
+		Email: String("example@member.org"),
+	}
+
+	projects, _, err := client.Invites.GroupInvites("test", opt)
+	if err != nil {
+		t.Errorf("Invites.GroupInvites returned error: %v", err)
+	}
+
+	want := &InvitationsResult{Status: "success"}
+	if !reflect.DeepEqual(want, projects) {
+		t.Errorf("Invites.GroupInvites returned %+v, want %+v", projects, want)
+	}
+}
+
+func TestGroupInvitesError(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/groups/test/invitations", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "POST")
+		fmt.Fprint(w, `{"status": "error","message": {"example@member.org": "Already invited"}}`)
+	})
+
+	opt := &InvitesOptions{
+		Email: String("example@member.org"),
+	}
+
+	projects, _, err := client.Invites.GroupInvites("test", opt)
+	if err != nil {
+		t.Errorf("Invites.GroupInvites returned error: %v", err)
+	}
+
+	want := &InvitationsResult{Status: "error", Message: map[string]string{"example@member.org": "Already invited"}}
+	if !reflect.DeepEqual(want, projects) {
+		t.Errorf("Invites.GroupInvites returned %+v, want %+v", projects, want)
+	}
+}
+
+func TestListProjectPendingInvites(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/test/invitations", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `[{"id":1},{"id":2}]`)
+	})
+
+	opt := &ListPendingInvitationsOptions{
+		ListOptions: ListOptions{2, 3},
+	}
+
+	projects, _, err := client.Invites.ListPendingProjectInvitations("test", opt)
+	if err != nil {
+		t.Errorf("Invites.ListPendingProjectInvitations returned error: %v", err)
+	}
+
+	want := []*PendingInvite{{ID: 1}, {ID: 2}}
+	if !reflect.DeepEqual(want, projects) {
+		t.Errorf("Invites.ListPendingProjectInvitations returned %+v, want %+v", projects, want)
+	}
+}
+
+func TestProjectInvites(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/test/invitations", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "POST")
+		fmt.Fprint(w, `{"status": "success"}`)
+	})
+
+	opt := &InvitesOptions{
+		Email: String("example@member.org"),
+	}
+
+	projects, _, err := client.Invites.ProjectInvites("test", opt)
+	if err != nil {
+		t.Errorf("Invites.ProjectInvites returned error: %v", err)
+	}
+
+	want := &InvitationsResult{Status: "success"}
+	if !reflect.DeepEqual(want, projects) {
+		t.Errorf("Invites.ProjectInvites returned %+v, want %+v", projects, want)
+	}
+}
+
+func TestProjectInvitesError(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/test/invitations", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "POST")
+		fmt.Fprint(w, `{"status": "error","message": {"example@member.org": "Already invited"}}`)
+	})
+
+	opt := &InvitesOptions{
+		Email: String("example@member.org"),
+	}
+
+	projects, _, err := client.Invites.ProjectInvites("test", opt)
+	if err != nil {
+		t.Errorf("Invites.ProjectInvites returned error: %v", err)
+	}
+
+	want := &InvitationsResult{Status: "error", Message: map[string]string{"example@member.org": "Already invited"}}
+	if !reflect.DeepEqual(want, projects) {
+		t.Errorf("Invites.ProjectInvites returned %+v, want %+v", projects, want)
+	}
+}