Repository: xanzy/go-gitlab
Pull request #1102 (2021-03-10 10:55:44)

Add project token support
Add support for project tokens, as desribed in issue https://github.com/xanzy/go-gitlab/issues/1098


Patch:
diff --git a/gitlab.go b/gitlab.go
index f579451..265aaf2 100644
--- a/gitlab.go
+++ b/gitlab.go
@@ -152,6 +152,7 @@ type Client struct {
 	PipelineTriggers      *PipelineTriggersService
 	Pipelines             *PipelinesService
 	ProjectBadges         *ProjectBadgesService
+	ProjectAccessTokens   *ProjectAccessTokensService
 	ProjectCluster        *ProjectClustersService
 	ProjectImportExport   *ProjectImportExportService
 	ProjectMembers        *ProjectMembersService
@@ -322,6 +323,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
 	c.PipelineTriggers = &PipelineTriggersService{client: c}
 	c.Pipelines = &PipelinesService{client: c}
 	c.ProjectBadges = &ProjectBadgesService{client: c}
+	c.ProjectAccessTokens = &ProjectAccessTokensService{client: c}
 	c.ProjectCluster = &ProjectClustersService{client: c}
 	c.ProjectImportExport = &ProjectImportExportService{client: c}
 	c.ProjectMembers = &ProjectMembersService{client: c}
diff --git a/project_access_tokens.go b/project_access_tokens.go
new file mode 100644
index 0000000..9bca815
--- /dev/null
+++ b/project_access_tokens.go
@@ -0,0 +1,138 @@
+//
+// Copyright 2021, Patrick Webster
+//
+// 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"
+	"net/http"
+	"time"
+)
+
+// ProjectAccessTokensService handles communication with the
+// project access tokens related methods of the GitLab API.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/resource_access_tokens.html
+type ProjectAccessTokensService struct {
+	client *Client
+}
+
+// ProjectAccessToken represents a GitLab Project Access Token.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/resource_access_tokens.html
+type ProjectAccessToken struct {
+	ID        int        `json:"id"`
+	UserID    int        `json:"user_id"`
+	Name      string     `json:"name"`
+	Scopes    []string   `json:"scopes"`
+	CreatedAt *time.Time `json:"created_at"`
+	ExpiresAt *ISOTime   `json:"expires_at"`
+	Active    bool       `json:"active"`
+	Revoked   bool       `json:"revoked"`
+	Token     string     `json:"token"`
+}
+
+func (v ProjectAccessToken) String() string {
+	return Stringify(v)
+}
+
+// ListProjectAccessTokensOptions represents the available options for
+// listing variables in a project.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/resource_access_tokens.html#list-project-access-tokens
+type ListProjectAccessTokensOptions ListOptions
+
+// ListProjectAccessTokens gets a list of all Project Access Tokens in a
+// project.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/resource_access_tokens.html#list-project-access-tokens
+func (s *ProjectAccessTokensService) ListProjectAccessTokens(pid interface{}, opt *ListProjectAccessTokensOptions, options ...RequestOptionFunc) ([]*ProjectAccessToken, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/access_tokens", pathEscape(project))
+
+	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var pats []*ProjectAccessToken
+	resp, err := s.client.Do(req, &pats)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return pats, resp, err
+}
+
+// CreateProjectAccessTokenOptions represents the available CreateVariable()
+// options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/resource_access_tokens.html#create-a-project-access-token
+type CreateProjectAccessTokenOptions struct {
+	Name      *string  `url:"name,omitempty" json:"name,omitempty"`
+	Scopes    []string `url:"scopes,omitempty" json:"scopes,omitempty"`
+	ExpiresAt *ISOTime `url:"expires_at,omitempty" json:"expires_at,omitempty"`
+}
+
+// CreateProjectAccessToken creates a new Project Access Token.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/resource_access_tokens.html#create-a-project-access-token
+func (s *ProjectAccessTokensService) CreateProjectAccessToken(pid interface{}, opt *CreateProjectAccessTokenOptions, options ...RequestOptionFunc) (*ProjectAccessToken, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/access_tokens", pathEscape(project))
+
+	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	pat := new(ProjectAccessToken)
+	resp, err := s.client.Do(req, pat)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return pat, resp, err
+}
+
+// RemoveProjectAccessToken deletes a Project Access Token.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/resource_access_tokens.html#revoke-a-project-access-token
+func (s *ProjectAccessTokensService) DeleteProjectAccessToken(pid interface{}, id int, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("projects/%s/access_tokens/%d", pathEscape(project), id)
+
+	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
diff --git a/projects.go b/projects.go
index 1aaba56..b843c19 100644
--- a/projects.go
+++ b/projects.go
@@ -106,7 +106,6 @@ type Project struct {
 	PagesAccessLevel                          AccessControlValue         `json:"pages_access_level"`
 	OperationsAccessLevel                     AccessControlValue         `json:"operations_access_level"`
 	AutocloseReferencedIssues                 bool                       `json:"autoclose_referenced_issues"`
-	SuggestionCommitMessage                   string                     `json:"suggestion_commit_message"`
 	CIForwardDeploymentEnabled                bool                       `json:"ci_forward_deployment_enabled"`
 	SharedWithGroups                          []struct {
 		GroupID          int    `json:"group_id"`
@@ -536,7 +535,6 @@ type CreateProjectOptions struct {
 	PackagesEnabled                           *bool                                `url:"packages_enabled,omitempty" json:"packages_enabled,omitempty"`
 	ServiceDeskEnabled                        *bool                                `url:"service_desk_enabled,omitempty" json:"service_desk_enabled,omitempty"`
 	AutocloseReferencedIssues                 *bool                                `url:"autoclose_referenced_issues,omitempty" json:"autoclose_referenced_issues,omitempty"`
-	SuggestionCommitMessage                   *string                              `url:"suggestion_commit_message,omitempty" json:"suggestion_commit_message,omitempty"`
 
 	// Deprecated members
 	IssuesEnabled        *bool `url:"issues_enabled,omitempty" json:"issues_enabled,omitempty"`
@@ -674,7 +672,6 @@ type EditProjectOptions struct {
 	PackagesEnabled                           *bool                                `url:"packages_enabled,omitempty" json:"packages_enabled,omitempty"`
 	ServiceDeskEnabled                        *bool                                `url:"service_desk_enabled,omitempty" json:"service_desk_enabled,omitempty"`
 	AutocloseReferencedIssues                 *bool                                `url:"autoclose_referenced_issues,omitempty" json:"autoclose_referenced_issues,omitempty"`
-	SuggestionCommitMessage                   *string                              `url:"suggestion_commit_message,omitempty" json:"suggestion_commit_message,omitempty"`
 
 	// Deprecated members
 	IssuesEnabled        *bool `url:"issues_enabled,omitempty" json:"issues_enabled,omitempty"`


Test patch:
diff --git a/project_access_tokens_test.go b/project_access_tokens_test.go
new file mode 100644
index 0000000..942bbb7
--- /dev/null
+++ b/project_access_tokens_test.go
@@ -0,0 +1,122 @@
+//
+// Copyright 2021, Patrick Webster
+//
+// 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 (
+	"net/http"
+	"reflect"
+	"testing"
+	"time"
+)
+
+func TestListProjectAccessTokens(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/1/access_tokens", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		mustWriteHTTPResponse(t, w, "testdata/list_project_access_tokens.json")
+	})
+
+	projectAccessTokens, _, err := client.ProjectAccessTokens.ListProjectAccessTokens(1, &ListProjectAccessTokensOptions{Page: 1, PerPage: 20})
+	if err != nil {
+		t.Errorf("ProjectAccessTokens.ListProjectAccessTokens returned error: %v", err)
+	}
+
+	time1, err := time.Parse(time.RFC3339, "2021-03-09T21:11:47.271Z")
+	if err != nil {
+		t.Errorf("ProjectAccessTokens.ListProjectAccessTokens returned error: %v", err)
+	}
+	time2, err := time.Parse(time.RFC3339, "2021-03-09T21:11:47.340Z")
+	if err != nil {
+		t.Errorf("ProjectAccessTokens.ListProjectAccessTokens returned error: %v", err)
+	}
+
+	want := []*ProjectAccessToken{
+		{
+			ID:        1876,
+			UserID:    2453,
+			Name:      "token 10",
+			Scopes:    []string{"api", "read_api", "read_repository", "write_repository"},
+			CreatedAt: &time1,
+			Active:    true,
+			Revoked:   false,
+		},
+		{
+			ID:        1877,
+			UserID:    2456,
+			Name:      "token 8",
+			Scopes:    []string{"api", "read_api", "read_repository", "write_repository"},
+			CreatedAt: &time2,
+			Active:    true,
+			Revoked:   false,
+		},
+	}
+
+	if !reflect.DeepEqual(want, projectAccessTokens) {
+		t.Errorf("ProjectAccessTokens.ListProjectAccessTokens returned %+v, want %+v", projectAccessTokens, want)
+	}
+}
+
+func TestCreateProjectAccessToken(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/1/access_tokens", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPost)
+		mustWriteHTTPResponse(t, w, "testdata/create_project_access_token.json")
+	})
+
+	projectAccessToken, _, err := client.ProjectAccessTokens.CreateProjectAccessToken(1, nil)
+	if err != nil {
+		t.Errorf("ProjectAccessTokens.CreateProjectAccessToken returned error: %v", err)
+	}
+
+	time1, err := time.Parse(time.RFC3339, "2021-03-09T21:11:47.271Z")
+	if err != nil {
+		t.Errorf("ProjectAccessTokens.CreateProjectAccessToken returned error: %v", err)
+	}
+	want := &ProjectAccessToken{
+		ID:        1876,
+		UserID:    2453,
+		Name:      "token 10",
+		Scopes:    []string{"api", "read_api", "read_repository", "write_repository"},
+		ExpiresAt: nil,
+		CreatedAt: &time1,
+		Active:    true,
+		Revoked:   false,
+		Token:     "2UsevZE1x1ZdFZW4MNzH",
+	}
+
+	if !reflect.DeepEqual(want, projectAccessToken) {
+		t.Errorf("ProjectAccessTokens.CreateProjectAccessToken returned %+v, want %+v", projectAccessToken, want)
+	}
+}
+
+func TestDeleteProjectAccessToken(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/1/access_tokens/1234", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodDelete)
+	})
+
+	_, err := client.ProjectAccessTokens.DeleteProjectAccessToken("1", 1234)
+	if err != nil {
+		t.Errorf("Pipelines.DeleteProjectAccessToken returned error: %v", err)
+	}
+}
diff --git a/testdata/create_project_access_token.json b/testdata/create_project_access_token.json
new file mode 100644
index 0000000..232ca76
--- /dev/null
+++ b/testdata/create_project_access_token.json
@@ -0,0 +1,16 @@
+{
+    "id": 1876,
+    "name": "token 10",
+    "revoked": false,
+    "created_at": "2021-03-09T21:11:47.271Z",
+    "scopes": [
+        "api",
+        "read_api",
+        "read_repository",
+        "write_repository"
+    ],
+    "user_id": 2453,
+    "active": true,
+    "expires_at": null,
+    "token": "2UsevZE1x1ZdFZW4MNzH"
+}
diff --git a/testdata/list_project_access_tokens.json b/testdata/list_project_access_tokens.json
new file mode 100644
index 0000000..a801641
--- /dev/null
+++ b/testdata/list_project_access_tokens.json
@@ -0,0 +1,32 @@
+[
+    {
+        "id": 1876,
+        "name": "token 10",
+        "revoked": false,
+        "created_at": "2021-03-09T21:11:47.271Z",
+        "scopes": [
+            "api",
+            "read_api",
+            "read_repository",
+            "write_repository"
+        ],
+        "user_id": 2453,
+        "active": true,
+        "expires_at": null
+    },
+    {
+        "id": 1877,
+        "name": "token 8",
+        "revoked": false,
+        "created_at": "2021-03-09T21:11:47.340Z",
+        "scopes": [
+            "api",
+            "read_api",
+            "read_repository",
+            "write_repository"
+        ],
+        "user_id": 2456,
+        "active": true,
+        "expires_at": null
+    }
+]