Repository: xanzy/go-gitlab
Pull request #1198 (2021-08-29 11:56:15)

Added more tests
I also added the "Enabled" and "Managed" Field to the GroupCluster

If you need anything changed I can still change things


Patch:
diff --git a/README.md b/README.md
index beb003c..3c8e401 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,6 @@ to add new and/or missing endpoints. Currently the following services are suppor
 - [x] Notes (comments)
 - [x] Notification Settings
 - [x] Open Source License Templates
-- [x] Pages
 - [x] Pages Domains
 - [x] Personal Access Tokens
 - [x] Pipeline Schedules
diff --git a/container_registry.go b/container_registry.go
index dd2b5b3..8adb9d1 100644
--- a/container_registry.go
+++ b/container_registry.go
@@ -125,13 +125,13 @@ func (s *ContainerRegistryService) DeleteRegistryRepository(pid interface{}, rep
 // ListRegistryRepositoryTags() options.
 //
 // GitLab API docs:
-// https://docs.gitlab.com/ee/api/container_registry.html#list-repository-tags
+// https://docs.gitlab.com/ee/api/container_registry.html#list-registry-repository-tags
 type ListRegistryRepositoryTagsOptions ListOptions
 
 // ListRegistryRepositoryTags gets a list of tags for given registry repository.
 //
 // GitLab API docs:
-// https://docs.gitlab.com/ee/api/container_registry.html#list-repository-tags
+// https://docs.gitlab.com/ee/api/container_registry.html#list-registry-repository-tags
 func (s *ContainerRegistryService) ListRegistryRepositoryTags(pid interface{}, repository int, opt *ListRegistryRepositoryTagsOptions, options ...RequestOptionFunc) ([]*RegistryRepositoryTag, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -159,7 +159,7 @@ func (s *ContainerRegistryService) ListRegistryRepositoryTags(pid interface{}, r
 // GetRegistryRepositoryTagDetail get details of a registry repository tag
 //
 // GitLab API docs:
-// https://docs.gitlab.com/ee/api/container_registry.html#get-details-of-a-repository-tag
+// https://docs.gitlab.com/ee/api/container_registry.html#get-details-of-a-registry-repository-tag
 func (s *ContainerRegistryService) GetRegistryRepositoryTagDetail(pid interface{}, repository int, tagName string, options ...RequestOptionFunc) (*RegistryRepositoryTag, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -188,7 +188,7 @@ func (s *ContainerRegistryService) GetRegistryRepositoryTagDetail(pid interface{
 // DeleteRegistryRepositoryTag deletes a registry repository tag.
 //
 // GitLab API docs:
-// https://docs.gitlab.com/ee/api/container_registry.html#delete-a-repository-tag
+// https://docs.gitlab.com/ee/api/container_registry.html#delete-a-registry-repository-tag
 func (s *ContainerRegistryService) DeleteRegistryRepositoryTag(pid interface{}, repository int, tagName string, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/deploy_keys.go b/deploy_keys.go
index e44e65d..914f46e 100644
--- a/deploy_keys.go
+++ b/deploy_keys.go
@@ -178,7 +178,7 @@ func (s *DeployKeysService) DeleteDeployKey(pid interface{}, deployKey int, opti
 // EnableDeployKey enables a deploy key.
 //
 // GitLab API docs:
-// https://docs.gitlab.com/ce/api/deploy_keys.html#enable-deploy-key
+// https://docs.gitlab.com/ce/api/deploy_keys.html#enable-a-deploy-key
 func (s *DeployKeysService) EnableDeployKey(pid interface{}, deployKey int, options ...RequestOptionFunc) (*DeployKey, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/event_webhook_types.go b/event_webhook_types.go
index 040100f..b467860 100644
--- a/event_webhook_types.go
+++ b/event_webhook_types.go
@@ -499,7 +499,6 @@ type MergeEvent struct {
 		TimeEstimate             int          `json:"time_estimate"`
 		Source                   *Repository  `json:"source"`
 		Target                   *Repository  `json:"target"`
-		HeadPipelineID           *int         `json:"head_pipeline_id"`
 		LastCommit               struct {
 			ID        string     `json:"id"`
 			Message   string     `json:"message"`
diff --git a/gitignore_templates.go b/gitignore_templates.go
index bf066c9..6597d44 100644
--- a/gitignore_templates.go
+++ b/gitignore_templates.go
@@ -66,7 +66,7 @@ func (s *GitIgnoreTemplatesService) ListTemplates(opt *ListTemplatesOptions, opt
 // GetTemplate get a git ignore template
 //
 // GitLab API docs:
-// https://docs.gitlab.com/ce/api/templates/gitignores.html#single-gitignore-template
+// https://docs.gitlab.com/ce/api/templates/gitignores.html#get-a-single-gitignore-template
 func (s *GitIgnoreTemplatesService) GetTemplate(key string, options ...RequestOptionFunc) (*GitIgnoreTemplate, *Response, error) {
 	u := fmt.Sprintf("templates/gitignores/%s", url.PathEscape(key))
 
diff --git a/gitlab.go b/gitlab.go
index 3dc0426..aa6529d 100644
--- a/gitlab.go
+++ b/gitlab.go
@@ -151,7 +151,6 @@ type Client struct {
 	Notes                 *NotesService
 	NotificationSettings  *NotificationSettingsService
 	Packages              *PackagesService
-	Pages                 *PagesService
 	PagesDomains          *PagesDomainsService
 	PipelineSchedules     *PipelineSchedulesService
 	PipelineTriggers      *PipelineTriggersService
@@ -338,7 +337,6 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
 	c.Notes = &NotesService{client: c}
 	c.NotificationSettings = &NotificationSettingsService{client: c}
 	c.Packages = &PackagesService{client: c}
-	c.Pages = &PagesService{client: c}
 	c.PagesDomains = &PagesDomainsService{client: c}
 	c.PipelineSchedules = &PipelineSchedulesService{client: c}
 	c.PipelineTriggers = &PipelineTriggersService{client: c}
diff --git a/group_clusters.go b/group_clusters.go
index c0e905a..c3f15c2 100644
--- a/group_clusters.go
+++ b/group_clusters.go
@@ -39,6 +39,8 @@ type GroupCluster struct {
 	Name               string              `json:"name"`
 	Domain             string              `json:"domain"`
 	CreatedAt          *time.Time          `json:"created_at"`
+	Managed            bool                `json:"managed"`
+	Enabled            bool                `json:"enabled"`
 	ProviderType       string              `json:"provider_type"`
 	PlatformType       string              `json:"platform_type"`
 	EnvironmentScope   string              `json:"environment_scope"`
diff --git a/namespaces.go b/namespaces.go
index a47c770..420da15 100644
--- a/namespaces.go
+++ b/namespaces.go
@@ -19,7 +19,6 @@ package gitlab
 import (
 	"fmt"
 	"net/http"
-	"time"
 )
 
 // NamespacesService handles communication with the namespace related methods
@@ -34,21 +33,13 @@ type NamespacesService struct {
 //
 // GitLab API docs: https://docs.gitlab.com/ce/api/namespaces.html
 type Namespace struct {
-	ID                          int        `json:"id"`
-	Name                        string     `json:"name"`
-	Path                        string     `json:"path"`
-	Kind                        string     `json:"kind"`
-	FullPath                    string     `json:"full_path"`
-	ParentID                    int        `json:"parent_id"`
-	AvatarURL                   *string    `json:"avatar_url"`
-	WebURL                      string     `json:"web_url"`
-	MembersCountWithDescendants int        `json:"members_count_with_descendants"`
-	BillableMembersCount        int        `json:"billable_members_count"`
-	Plan                        string     `json:"plan"`
-	TrialEndsOn                 *time.Time `json:"trial_ends_on"`
-	Trial                       bool       `json:"trial"`
-	MaxSeatsUsed                *int       `json:"max_seats_used"`
-	SeatsInUse                  *int       `json:"seats_in_use"`
+	ID                          int    `json:"id"`
+	Name                        string `json:"name"`
+	Path                        string `json:"path"`
+	Kind                        string `json:"kind"`
+	FullPath                    string `json:"full_path"`
+	ParentID                    int    `json:"parent_id"`
+	MembersCountWithDescendants int    `json:"members_count_with_descendants"`
 }
 
 func (n Namespace) String() string {
@@ -60,8 +51,7 @@ func (n Namespace) String() string {
 // GitLab API docs: https://docs.gitlab.com/ce/api/namespaces.html#list-namespaces
 type ListNamespacesOptions struct {
 	ListOptions
-	Search    *string `url:"search,omitempty" json:"search,omitempty"`
-	OwnedOnly *bool   `url:"owned_only,omitempty" json:"owned_only,omitempty"`
+	Search *string `url:"search,omitempty" json:"search,omitempty"`
 }
 
 // ListNamespaces gets a list of projects accessible by the authenticated user.
@@ -131,45 +121,3 @@ func (s *NamespacesService) GetNamespace(id interface{}, options ...RequestOptio
 
 	return n, resp, err
 }
-
-// NamespaceExistance represents a namespace exists result.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/ee/api/namespaces.html#get-existence-of-a-namespace
-type NamespaceExistance struct {
-	Exists   bool     `json:"exists"`
-	Suggests []string `json:"suggests"`
-}
-
-// NamespaceExistsOptions represents the available NamespaceExists() options.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/ee/api/namespaces.html#get-existence-of-a-namespace
-type NamespaceExistsOptions struct {
-	ParentID *int `url:"parent_id,omitempty" json:"parent_id,omitempty"`
-}
-
-// NamespaceExists checks the existence of a namespace.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/ee/api/namespaces.html#get-existence-of-a-namespace
-func (s *NamespacesService) NamespaceExists(id interface{}, opt *NamespaceExistsOptions, options ...RequestOptionFunc) (*NamespaceExistance, *Response, error) {
-	namespace, err := parseID(id)
-	if err != nil {
-		return nil, nil, err
-	}
-	u := fmt.Sprintf("namespaces/%s/exists", namespace)
-
-	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	n := new(NamespaceExistance)
-	resp, err := s.client.Do(req, n)
-	if err != nil {
-		return nil, resp, err
-	}
-
-	return n, resp, err
-}
diff --git a/pages.go b/pages.go
deleted file mode 100644
index 167551b..0000000
--- a/pages.go
+++ /dev/null
@@ -1,45 +0,0 @@
-//
-// 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"
-	"net/http"
-)
-
-type PagesService struct {
-	client *Client
-}
-
-// UnpublishPages unpublished pages. The user must have admin privileges.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/ee/api/pages.html#unpublish-pages
-func (s *PagesService) UnpublishPages(gid interface{}, options ...RequestOptionFunc) (*Response, error) {
-	page, err := parseID(gid)
-	if err != nil {
-		return nil, err
-	}
-	u := fmt.Sprintf("projects/%s/pages", pathEscape(page))
-
-	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
-	if err != nil {
-		return nil, err
-	}
-
-	return s.client.Do(req, nil)
-}


Test patch:
diff --git a/ci_yml_templates_test.go b/ci_yml_templates_test.go
new file mode 100644
index 0000000..b132033
--- /dev/null
+++ b/ci_yml_templates_test.go
@@ -0,0 +1,102 @@
+//
+// 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"
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+func TestListAllTemplates(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/templates/gitlab_ci_ymls", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprintf(w, `[
+			{
+			   "content":"5-Minute-Production-App",
+			   "name":"5-Minute-Production-App"
+			},
+			{
+			   "content":"Android",
+			   "name":"Android"
+			},
+			{
+			   "content":"Android-Fastlane",
+			   "name":"Android-Fastlane"
+			},
+			{
+			   "content":"Auto-DevOps",
+			   "name":"Auto-DevOps"
+			}
+		 ]`)
+	})
+
+	templates, _, err := client.CIYMLTemplate.ListAllTemplates(&ListCIYMLTemplatesOptions{})
+	if err != nil {
+		t.Errorf("CIYMLTemplates.ListAllTemplates returned error: %v", err)
+	}
+
+	want := []*CIYMLTemplate{
+		{
+			Name:    "5-Minute-Production-App",
+			Content: "5-Minute-Production-App",
+		},
+		{
+			Name:    "Android",
+			Content: "Android"},
+		{
+			Name:    "Android-Fastlane",
+			Content: "Android-Fastlane"},
+		{
+			Name:    "Auto-DevOps",
+			Content: "Auto-DevOps",
+		},
+	}
+	if !reflect.DeepEqual(want, templates) {
+		t.Errorf("CIYMLTemplates.ListAllTemplates returned %+v, want %+v", templates, want)
+	}
+}
+
+func TestGetTemplate(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/templates/gitlab_ci_ymls/Ruby", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprintf(w, `{
+			"name": "Ruby",
+			"content": "# This file is a template, and might need editing before it works on your project."
+		  }`)
+	})
+
+	template, _, err := client.CIYMLTemplate.GetTemplate("Ruby")
+	if err != nil {
+		t.Errorf("CIYMLTemplates.GetTemplate returned error: %v", err)
+	}
+
+	want := &CIYMLTemplate{
+		Name:    "Ruby",
+		Content: "# This file is a template, and might need editing before it works on your project.",
+	}
+	if !reflect.DeepEqual(want, template) {
+		t.Errorf("CIYMLTemplates.GetTemplate returned %+v, want %+v", template, want)
+	}
+}
diff --git a/container_registry_test.go b/container_registry_test.go
new file mode 100644
index 0000000..f2ffabd
--- /dev/null
+++ b/container_registry_test.go
@@ -0,0 +1,276 @@
+//
+// 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"
+	"net/http"
+	"reflect"
+	"testing"
+	"time"
+)
+
+func TestListRegistryRepositories(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/5/registry/repositories", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprintf(w, `[
+			{
+			  "id": 1,
+			  "name": "",
+			  "path": "group/project",
+			  "project_id": 9,
+			  "location": "gitlab.example.com:5000/group/project",
+			  "created_at": "2019-01-10T13:38:57.391Z",
+			  "cleanup_policy_started_at": "2020-01-10T15:40:57.391Z"
+			},
+			{
+			  "id": 2,
+			  "name": "releases",
+			  "path": "group/project/releases",
+			  "project_id": 9,
+			  "location": "gitlab.example.com:5000/group/project/releases",
+			  "created_at": "2019-01-10T13:39:08.229Z",
+			  "cleanup_policy_started_at": "2020-08-17T03:12:35.489Z"
+			}
+		  ]`)
+	})
+
+	repositories, _, err := client.ContainerRegistry.ListRegistryRepositories(5, &ListRegistryRepositoriesOptions{})
+	if err != nil {
+		t.Errorf("ContainerRegistry.ListRegistryRepositories returned error: %v", err)
+	}
+
+	timeLayout := "2006-01-02T15:04:05.000Z"
+	createdAt1, err := time.Parse(timeLayout, "2019-01-10T13:38:57.391Z")
+	if err != nil {
+		t.Errorf("ContainerRepository.ListRegistryRepositories error while parsing time: %v", err)
+	}
+
+	createdAt2, err := time.Parse(timeLayout, "2019-01-10T13:39:08.229Z")
+	if err != nil {
+		t.Errorf("ContainerRepository.ListRegistryRepositories error while parsing time: %v", err)
+	}
+
+	cleanupPolicyStartedAt1, err := time.Parse(timeLayout, "2020-01-10T15:40:57.391Z")
+	if err != nil {
+		t.Errorf("ContainerRepository.ListRegistryRepositories error while parsing time: %v", err)
+	}
+
+	cleanupPolicyStartedAt2, err := time.Parse(timeLayout, "2020-08-17T03:12:35.489Z")
+	if err != nil {
+		t.Errorf("ContainerRepository.ListRegistryRepositories error while parsing time: %v", err)
+	}
+
+	want := []*RegistryRepository{
+		{
+			ID:                     1,
+			Name:                   "",
+			Path:                   "group/project",
+			Location:               "gitlab.example.com:5000/group/project",
+			CreatedAt:              &createdAt1,
+			CleanupPolicyStartedAt: &cleanupPolicyStartedAt1,
+		},
+		{
+			ID:                     2,
+			Name:                   "releases",
+			Path:                   "group/project/releases",
+			Location:               "gitlab.example.com:5000/group/project/releases",
+			CreatedAt:              &createdAt2,
+			CleanupPolicyStartedAt: &cleanupPolicyStartedAt2,
+		},
+	}
+	if !reflect.DeepEqual(want, repositories) {
+		t.Errorf("ContainerRepository.ListRegistryRepositories returned %+v, want %+v", repositories, want)
+	}
+}
+
+func TestDeleteRegistryRepository(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/5/registry/repositories/2", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodDelete)
+	})
+
+	_, err := client.ContainerRegistry.DeleteRegistryRepository(5, 2)
+	if err != nil {
+		t.Errorf("ContainerRegistry.DeleteRegistryRepository returned error: %v", err)
+	}
+}
+
+func TestListRegistryRepositoryTags(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/5/registry/repositories/2/tags", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprintf(w, `[
+			{
+			  "name": "A",
+			  "path": "group/project:A",
+			  "location": "gitlab.example.com:5000/group/project:A"
+			},
+			{
+			  "name": "latest",
+			  "path": "group/project:latest",
+			  "location": "gitlab.example.com:5000/group/project:latest"
+			}
+		  ]`)
+	})
+
+	opt := &ListRegistryRepositoryTagsOptions{}
+	registryRepositoryTags, _, err := client.ContainerRegistry.ListRegistryRepositoryTags(5, 2, opt)
+	if err != nil {
+		t.Errorf("ContainerRegistry.ListRegistryRepositoryTags returned error: %v", err)
+	}
+
+	want := []*RegistryRepositoryTag{
+		{
+			Name:     "A",
+			Path:     "group/project:A",
+			Location: "gitlab.example.com:5000/group/project:A",
+		},
+		{
+			Name:     "latest",
+			Path:     "group/project:latest",
+			Location: "gitlab.example.com:5000/group/project:latest",
+		},
+	}
+	if !reflect.DeepEqual(want, registryRepositoryTags) {
+		t.Errorf("ContainerRepository.ListRegistryRepositoryTags returned %+v, want %+v", registryRepositoryTags, want)
+	}
+}
+
+func TestGetRegistryRepositoryTagDetail(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/5/registry/repositories/2/tags/v10.0.0", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprintf(w, `{
+			"name": "v10.0.0",
+			"path": "group/project:latest",
+			"location": "gitlab.example.com:5000/group/project:latest",
+			"revision": "e9ed9d87c881d8c2fd3a31b41904d01ba0b836e7fd15240d774d811a1c248181",
+			"short_revision": "e9ed9d87c",
+			"digest": "sha256:c3490dcf10ffb6530c1303522a1405dfaf7daecd8f38d3e6a1ba19ea1f8a1751",
+			"created_at": "2019-01-06T16:49:51.272+00:00",
+			"total_size": 350224384
+		  }`)
+	})
+
+	repositoryTag, _, err := client.ContainerRegistry.GetRegistryRepositoryTagDetail(5, 2, "v10.0.0")
+	if err != nil {
+		t.Errorf("ContainerRegistry.GetRegistryRepositoryTagDetail returned error: %v", err)
+	}
+
+	createdAt, err := time.Parse(timeLayout, "2019-01-06T16:49:51.272+00:00")
+	if err != nil {
+		t.Errorf("ContainerRepository.ListRegistryRepositories error while parsing time: %v", err)
+	}
+
+	want := &RegistryRepositoryTag{
+		Name:          "v10.0.0",
+		Path:          "group/project:latest",
+		Location:      "gitlab.example.com:5000/group/project:latest",
+		Revision:      "e9ed9d87c881d8c2fd3a31b41904d01ba0b836e7fd15240d774d811a1c248181",
+		ShortRevision: "e9ed9d87c",
+		Digest:        "sha256:c3490dcf10ffb6530c1303522a1405dfaf7daecd8f38d3e6a1ba19ea1f8a1751",
+		CreatedAt:     &createdAt,
+		TotalSize:     350224384,
+	}
+	if !reflect.DeepEqual(want, repositoryTag) {
+		t.Errorf("ContainerRepository.ListRegistryRepositories returned %+v, want %+v", repositoryTag, want)
+	}
+}
+
+func TestDeleteRegistryRepositoryTag(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/5/registry/repositories/2/tags/v10.0.0", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodDelete)
+	})
+
+	_, err := client.ContainerRegistry.DeleteRegistryRepositoryTag(5, 2, "v10.0.0")
+	if err != nil {
+		t.Errorf("ContainerRepository.DeleteRegistryRepositoryTag returned error: %v", err)
+	}
+}
+
+func TestDeleteRegistryRepositoryTags(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/5/registry/repositories/2/tags", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodDelete)
+	})
+
+	var tests = []struct {
+		event           string
+		nameRegexDelete string
+		keepN           int
+		nameRegexKeep   string
+		olderThan       string
+	}{
+		{
+			"keep_atleast_5_remove_2_days_old",
+			"[0-9a-z]{40}",
+			0,
+			"",
+			"2d",
+		},
+		{
+			"remove_all_keep_5",
+			".*",
+			5,
+			"",
+			"",
+		},
+		{
+			"remove_all_tags_keep_tags_beginning_with_stable",
+			".*",
+			0,
+			"stable.*",
+			"",
+		},
+		{
+			"remove_all_tags_older_than_1_month",
+			".*",
+			0,
+			"",
+			"1month",
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.event, func(t *testing.T) {
+			opt := &DeleteRegistryRepositoryTagsOptions{
+				NameRegexpDelete: &tc.nameRegexDelete,
+				NameRegexpKeep:   &tc.nameRegexKeep,
+				KeepN:            &tc.keepN,
+				OlderThan:        &tc.olderThan,
+			}
+			_, err := client.ContainerRegistry.DeleteRegistryRepositoryTags(5, 2, opt)
+			if err != nil {
+				t.Errorf("ContainerRegistry.DeleteRegistryRepositoryTags returned error: %v", err)
+			}
+		})
+	}
+}
diff --git a/deploy_keys_test.go b/deploy_keys_test.go
new file mode 100644
index 0000000..7a157c3
--- /dev/null
+++ b/deploy_keys_test.go
@@ -0,0 +1,310 @@
+//
+// 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"
+	"net/http"
+	"reflect"
+	"testing"
+	"time"
+)
+
+func TestListAllDeployKeys(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/deploy_keys", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprintf(w, `[
+			{
+			  "id": 1,
+			  "title": "Public key",
+			  "key": "ssh-rsa AAAA...",
+			  "created_at": "2013-10-02T10:12:29Z"
+			},
+			{
+			  "id": 3,
+			  "title": "Another Public key",
+			  "key": "ssh-rsa AAAA...",
+			  "created_at": "2013-10-02T11:12:29Z"
+			}
+		  ]`)
+	})
+
+	deployKeys, _, err := client.DeployKeys.ListAllDeployKeys()
+	if err != nil {
+		t.Errorf("DeployKeys.ListAllDeployKeys returned error: %v", err)
+	}
+
+	createdAt, err := time.Parse(timeLayout, "2013-10-02T10:12:29Z")
+	if err != nil {
+		t.Errorf("DeployKeys.ListAllDeployKeys returned an error while parsing time: %v", err)
+	}
+
+	createdAt2, err := time.Parse(timeLayout, "2013-10-02T11:12:29Z")
+	if err != nil {
+		t.Errorf("DeployKeys.ListAllDeployKeys returned an error while parsing time: %v", err)
+	}
+
+	want := []*DeployKey{
+		{
+			ID:        1,
+			Title:     "Public key",
+			Key:       "ssh-rsa AAAA...",
+			CreatedAt: &createdAt,
+		},
+		{
+			ID:        3,
+			Title:     "Another Public key",
+			Key:       "ssh-rsa AAAA...",
+			CreatedAt: &createdAt2,
+		},
+	}
+	if !reflect.DeepEqual(want, deployKeys) {
+		t.Errorf("DeployKeys.ListAllDeployKeys returned %+v, want %+v", deployKeys, want)
+	}
+}
+
+func TestListProjectDeployKeys(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/5/deploy_keys", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprintf(w, `[
+			{
+			  "id": 1,
+			  "title": "Public key",
+			  "key": "ssh-rsa AAAA...",
+			  "created_at": "2013-10-02T10:12:29Z",
+			  "can_push": false
+			},
+			{
+			  "id": 3,
+			  "title": "Another Public key",
+			  "key": "ssh-rsa AAAA...",
+			  "created_at": "2013-10-02T11:12:29Z",
+			  "can_push": false
+			}
+		  ]`)
+	})
+
+	deployKeys, _, err := client.DeployKeys.ListProjectDeployKeys(5, &ListProjectDeployKeysOptions{})
+	if err != nil {
+		t.Errorf("DeployKeys.ListProjectDeployKeys returned error: %v", err)
+	}
+
+	createdAt, err := time.Parse(timeLayout, "2013-10-02T10:12:29Z")
+	if err != nil {
+		t.Errorf("DeployKeys.ListAllDeployKeys returned an error while parsing time: %v", err)
+	}
+
+	createdAt2, err := time.Parse(timeLayout, "2013-10-02T11:12:29Z")
+	if err != nil {
+		t.Errorf("DeployKeys.ListAllDeployKeys returned an error while parsing time: %v", err)
+	}
+
+	want := []*DeployKey{
+		{
+			ID:        1,
+			Title:     "Public key",
+			Key:       "ssh-rsa AAAA...",
+			CreatedAt: &createdAt,
+			CanPush:   Bool(false),
+		},
+		{
+			ID:        3,
+			Title:     "Another Public key",
+			Key:       "ssh-rsa AAAA...",
+			CreatedAt: &createdAt2,
+			CanPush:   Bool(false),
+		},
+	}
+	if !reflect.DeepEqual(want, deployKeys) {
+		t.Errorf("DeployKeys.ListProjectDeployKeys returned %+v, want %+v", deployKeys, want)
+	}
+}
+
+func TestGetDeployKey(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/5/deploy_keys/11", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprintf(w, `{
+			"id": 1,
+			"title": "Public key",
+			"key": "ssh-rsa AAAA...",
+			"created_at": "2013-10-02T10:12:29Z",
+			"can_push": false
+		  }`)
+	})
+
+	deployKey, _, err := client.DeployKeys.GetDeployKey(5, 11)
+	if err != nil {
+		t.Errorf("DeployKeys.GetDeployKey returned error: %v", err)
+	}
+
+	createdAt, err := time.Parse(timeLayout, "2013-10-02T10:12:29Z")
+	if err != nil {
+		t.Errorf("DeployKeys.ListAllDeployKeys returned an error while parsing time: %v", err)
+	}
+
+	want := &DeployKey{
+		ID:        1,
+		Title:     "Public key",
+		Key:       "ssh-rsa AAAA...",
+		CreatedAt: &createdAt,
+		CanPush:   Bool(false),
+	}
+	if !reflect.DeepEqual(want, deployKey) {
+		t.Errorf("DeployKeys.GetDeployKey returned %+v, want %+v", deployKey, want)
+	}
+}
+
+func TestAddDeployKey(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/5/deploy_keys", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPost)
+		fmt.Fprintf(w, `{
+			"key" : "ssh-rsa AAAA...",
+			"id" : 12,
+			"title" : "My deploy key",
+			"can_push": true,
+			"created_at" : "2015-08-29T12:44:31.550Z"
+		 }`)
+	})
+
+	opt := &AddDeployKeyOptions{
+		Key:     String("ssh-rsa AAAA..."),
+		Title:   String("My deploy key"),
+		CanPush: Bool(true),
+	}
+	deployKey, _, err := client.DeployKeys.AddDeployKey(5, opt)
+	if err != nil {
+		t.Errorf("DeployKey.AddDeployKey returned error: %v", err)
+	}
+
+	createdAt, err := time.Parse(timeLayout, "2015-08-29T12:44:31.550Z")
+	if err != nil {
+		t.Errorf("DeployKeys.ListAllDeployKeys returned an error while parsing time: %v", err)
+	}
+
+	want := &DeployKey{
+		Title:     *String("My deploy key"),
+		ID:        12,
+		Key:       *String("ssh-rsa AAAA..."),
+		CanPush:   Bool(true),
+		CreatedAt: &createdAt,
+	}
+	if !reflect.DeepEqual(want, deployKey) {
+		t.Errorf("DeployKeys.AddDeployKey returned %+v, want %+v", deployKey, want)
+	}
+}
+
+func TestDeleteDeployKey(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/5/deploy_keys/13", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodDelete)
+	})
+
+	_, err := client.DeployKeys.DeleteDeployKey(5, 13)
+	if err != nil {
+		t.Errorf("Deploykeys.DeleteDeployKey returned error: %v", err)
+	}
+}
+
+func TestEnableDeployKey(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/5/deploy_keys/13/enable", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPost)
+		fmt.Fprintf(w, `{
+			"key" : "ssh-rsa AAAA...",
+			"id" : 12,
+			"title" : "My deploy key",
+			"created_at" : "2015-08-29T12:44:31.550Z"
+		 }`)
+	})
+
+	deployKey, _, err := client.DeployKeys.EnableDeployKey(5, 13)
+	if err != nil {
+		t.Errorf("DeployKeys.EnableDeployKey returned error: %v", err)
+	}
+
+	createdAt, err := time.Parse(timeLayout, "2015-08-29T12:44:31.550Z")
+	if err != nil {
+		t.Errorf("DeployKeys.ListAllDeployKeys returned an error while parsing time: %v", err)
+	}
+
+	want := &DeployKey{
+		ID:        12,
+		Title:     "My deploy key",
+		Key:       "ssh-rsa AAAA...",
+		CreatedAt: &createdAt,
+	}
+	if !reflect.DeepEqual(want, deployKey) {
+		t.Errorf("DeployKeys.EnableDeployKey returned %+v, want %+v", deployKey, want)
+	}
+}
+
+func TestUpdateDeployKey(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/5/deploy_keys/11", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPut)
+		fmt.Fprintf(w, `{
+			"id": 11,
+			"title": "New deploy key",
+			"key": "ssh-rsa AAAA...",
+			"created_at": "2015-08-29T12:44:31.550Z",
+			"can_push": true
+		 }`)
+	})
+
+	opt := &UpdateDeployKeyOptions{
+		Title:   String("New deploy key"),
+		CanPush: Bool(true),
+	}
+	deployKey, _, err := client.DeployKeys.UpdateDeployKey(5, 11, opt)
+	if err != nil {
+		t.Errorf("DeployKeys.UpdateDeployKey returned error: %v", err)
+	}
+
+	createdAt, err := time.Parse(timeLayout, "2015-08-29T12:44:31.550Z")
+	if err != nil {
+		t.Errorf("DeployKeys.ListAllDeployKeys returned an error while parsing time: %v", err)
+	}
+
+	want := &DeployKey{
+		ID:        11,
+		Title:     *String("New deploy key"),
+		Key:       "ssh-rsa AAAA...",
+		CanPush:   Bool(true),
+		CreatedAt: &createdAt,
+	}
+	if !reflect.DeepEqual(want, deployKey) {
+		t.Errorf("DeployKeys.UpdateDeployKey returned %+v, want %+v", deployKey, want)
+	}
+}
diff --git a/gitignore_templates_test.go b/gitignore_templates_test.go
new file mode 100644
index 0000000..8353456
--- /dev/null
+++ b/gitignore_templates_test.go
@@ -0,0 +1,232 @@
+//
+// 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"
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+func TestListTemplates(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/templates/gitignores", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprintf(w, `[
+			{
+			  "content": "Actionscript",
+			  "name": "Actionscript"
+			},
+			{
+			  "content": "Ada",
+			  "name": "Ada"
+			},
+			{
+			  "content": "Agda",
+			  "name": "Agda"
+			},
+			{
+			  "content": "Android",
+			  "name": "Android"
+			},
+			{
+			  "content": "AppEngine",
+			  "name": "AppEngine"
+			},
+			{
+			  "content": "AppceleratorTitanium",
+			  "name": "AppceleratorTitanium"
+			},
+			{
+			  "content": "ArchLinuxPackages",
+			  "name": "ArchLinuxPackages"
+			},
+			{
+			  "content": "Autotools",
+			  "name": "Autotools"
+			},
+			{
+			  "content": "C",
+			  "name": "C"
+			},
+			{
+			  "content": "C++",
+			  "name": "C++"
+			},
+			{
+			  "content": "CFWheels",
+			  "name": "CFWheels"
+			},
+			{
+			  "content": "CMake",
+			  "name": "CMake"
+			},
+			{
+			  "content": "CUDA",
+			  "name": "CUDA"
+			},
+			{
+			  "content": "CakePHP",
+			  "name": "CakePHP"
+			},
+			{
+			  "content": "ChefCookbook",
+			  "name": "ChefCookbook"
+			},
+			{
+			  "content": "Clojure",
+			  "name": "Clojure"
+			},
+			{
+			  "content": "CodeIgniter",
+			  "name": "CodeIgniter"
+			},
+			{
+			  "content": "CommonLisp",
+			  "name": "CommonLisp"
+			},
+			{
+			  "content": "Composer",
+			  "name": "Composer"
+			},
+			{
+			  "content": "Concrete5",
+			  "name": "Concrete5"
+			}
+		  ]`)
+	})
+
+	templates, _, err := client.GitIgnoreTemplates.ListTemplates(&ListTemplatesOptions{})
+	if err != nil {
+		t.Errorf("GitIgnoreTemplates.ListTemplates returned error: %v", err)
+	}
+
+	want := []*GitIgnoreTemplate{
+		{
+			Name:    "Actionscript",
+			Content: "Actionscript",
+		},
+		{
+			Name:    "Ada",
+			Content: "Ada",
+		},
+		{
+			Name:    "Agda",
+			Content: "Agda",
+		},
+		{
+			Name:    "Android",
+			Content: "Android",
+		},
+		{
+			Name:    "AppEngine",
+			Content: "AppEngine",
+		},
+		{
+			Name:    "AppceleratorTitanium",
+			Content: "AppceleratorTitanium",
+		},
+		{
+			Name:    "ArchLinuxPackages",
+			Content: "ArchLinuxPackages",
+		},
+		{
+			Name:    "Autotools",
+			Content: "Autotools",
+		},
+		{
+			Name:    "C",
+			Content: "C",
+		},
+		{
+			Name:    "C++",
+			Content: "C++",
+		},
+		{
+			Name:    "CFWheels",
+			Content: "CFWheels",
+		},
+		{
+			Name:    "CMake",
+			Content: "CMake",
+		},
+		{
+			Name:    "CUDA",
+			Content: "CUDA",
+		},
+		{
+			Name:    "CakePHP",
+			Content: "CakePHP",
+		},
+		{
+			Name:    "ChefCookbook",
+			Content: "ChefCookbook",
+		},
+		{
+			Name:    "Clojure",
+			Content: "Clojure",
+		},
+		{
+			Name:    "CodeIgniter",
+			Content: "CodeIgniter",
+		},
+		{
+			Name:    "CommonLisp",
+			Content: "CommonLisp",
+		},
+		{
+			Name:    "Composer",
+			Content: "Composer",
+		},
+		{
+			Name:    "Concrete5",
+			Content: "Concrete5",
+		},
+	}
+	if !reflect.DeepEqual(want, templates) {
+		t.Errorf("GitIgnoreTemplates.ListTemplates returned %+v, want %+v", templates, want)
+	}
+}
+
+func TestGetTemplates(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/templates/gitignores/Ruby", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprintf(w, `{
+			"name": "Ruby",
+			"content": "*.gem\n*.rbc\n/.config\n/coverage/\n/InstalledFiles\n/pkg/\n/spec/reports/"
+		  }`)
+	})
+
+	template, _, err := client.GitIgnoreTemplates.GetTemplate("Ruby")
+	if err != nil {
+		t.Errorf("GitIgnoreTempaltes.GetTemplate returned an error: %v", err)
+	}
+
+	want := &GitIgnoreTemplate{
+		Name:    "Ruby",
+		Content: "*.gem\n*.rbc\n/.config\n/coverage/\n/InstalledFiles\n/pkg/\n/spec/reports/",
+	}
+	if !reflect.DeepEqual(want, template) {
+		t.Errorf("GitIgnoreTemplates.GetTemplate returned %+v, want %+v", template, want)
+	}
+}
diff --git a/gitlab_test.go b/gitlab_test.go
index 17f0d21..c462d8b 100644
--- a/gitlab_test.go
+++ b/gitlab_test.go
@@ -32,6 +32,8 @@ import (
 	retryablehttp "github.com/hashicorp/go-retryablehttp"
 )
 
+var timeLayout = "2006-01-02T15:04:05Z07:00"
+
 // setup sets up a test HTTP server along with a gitlab.Client that is
 // configured to talk to that test server.  Tests should register handlers on
 // mux which provide mock responses for the API method being tested.
diff --git a/group_clusters_test.go b/group_clusters_test.go
new file mode 100644
index 0000000..075e06a
--- /dev/null
+++ b/group_clusters_test.go
@@ -0,0 +1,466 @@
+//
+// Copyright 2021, Paul Shoemaker
+//
+// 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"
+	"reflect"
+	"testing"
+	"time"
+)
+
+func TestGroupListClusters(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/groups/26/clusters", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprintf(w, `[
+			{
+			  "id":18,
+			  "name":"cluster-1",
+			  "domain":"example.com",
+			  "created_at":"2019-01-02T20:18:12.563Z",
+			  "managed": true,
+			  "enabled": true,
+			  "provider_type":"user",
+			  "platform_type":"kubernetes",
+			  "environment_scope":"*",
+			  "cluster_type":"group_type",
+			  "user":
+			  {
+				"id":1,
+				"name":"Administrator",
+				"username":"root",
+				"state":"active",
+				"avatar_url":"https://www.gravatar.com/avatar/4249f4df72b..",
+				"web_url":"https://gitlab.example.com/root"
+			  },
+			  "platform_kubernetes":
+			  {
+				"api_url":"https://104.197.68.152",
+				"authorization_type":"rbac",
+			    "ca_cert":"-----BEGIN CERTIFICATE-----\r\nAAAAA\r\n-----END CERTIFICATE-----"
+			  },
+			  "management_project":
+			  {
+				"id":2,
+				"description": "sdhfgnbsdjfhg",
+				"name":"project2",
+				"name_with_namespace":"John Doe8 / project2",
+				"path":"project2",
+				"path_with_namespace":"namespace2/project2",
+				"created_at":"2019-10-11T02:55:54.138Z"
+			  }
+			},
+			{
+			  "id":19,
+			  "name":"cluster-2"
+			}
+		  ]`)
+	})
+
+	clusters, _, err := client.GroupCluster.ListClusters(26)
+	if err != nil {
+		t.Errorf("GroupCluster.ListClusters returned error: %v", err)
+	}
+
+	createdAt, err := time.Parse(timeLayout, "2019-01-02T20:18:12.563Z")
+	if err != nil {
+		t.Errorf("DeployKeys.ListAllDeployKeys returned an error while parsing time: %v", err)
+	}
+
+	createdAt2, err := time.Parse(timeLayout, "2019-10-11T02:55:54.138Z")
+	if err != nil {
+		t.Errorf("DeployKeys.ListAllDeployKeys returned an error while parsing time: %v", err)
+	}
+
+	want := []*GroupCluster{
+		{
+			ID:               18,
+			Name:             "cluster-1",
+			Domain:           "example.com",
+			CreatedAt:        &createdAt,
+			Managed:          true,
+			Enabled:          true,
+			ProviderType:     "user",
+			PlatformType:     "kubernetes",
+			EnvironmentScope: "*",
+			ClusterType:      "group_type",
+			User: &User{
+				ID:        1,
+				Name:      "Administrator",
+				Username:  "root",
+				State:     "active",
+				AvatarURL: "https://www.gravatar.com/avatar/4249f4df72b..",
+				WebURL:    "https://gitlab.example.com/root",
+			},
+			PlatformKubernetes: &PlatformKubernetes{
+				APIURL:            "https://104.197.68.152",
+				AuthorizationType: "rbac",
+				CaCert:            "-----BEGIN CERTIFICATE-----\r\nAAAAA\r\n-----END CERTIFICATE-----",
+			},
+			ManagementProject: &ManagementProject{
+				ID:                2,
+				Description:       "sdhfgnbsdjfhg",
+				Name:              "project2",
+				NameWithNamespace: "John Doe8 / project2",
+				Path:              "project2",
+				PathWithNamespace: "namespace2/project2",
+				CreatedAt:         &createdAt2,
+			},
+		},
+		{
+			ID:   19,
+			Name: "cluster-2",
+		},
+	}
+	if !reflect.DeepEqual(want, clusters) {
+		t.Errorf("GroupCluster.ListClusters returned %+v, want %+v", clusters, want)
+	}
+}
+
+func TestGetGroupCluster(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/groups/26/clusters/18", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprintf(w, `{
+			"id":18,
+			"name":"cluster-1",
+			"domain":"example.com",
+			"created_at":"2019-01-02T20:18:12.563Z",
+			"managed": true,
+			"enabled": true,
+			"provider_type":"user",
+			"platform_type":"kubernetes",
+			"environment_scope":"*",
+			"cluster_type":"group_type",
+			"user":
+			{
+			  "id":1,
+			  "name":"Administrator",
+			  "username":"root",
+			  "state":"active",
+			  "avatar_url":"https://www.gravatar.com/avatar/4249f4df72b..",
+			  "web_url":"https://gitlab.example.com/root"
+			},
+			"platform_kubernetes":
+			{
+			  "api_url":"https://104.197.68.152",
+			  "authorization_type":"rbac",
+			  "ca_cert":"-----BEGIN CERTIFICATE-----\r\nAAAAA\r\n-----END CERTIFICATE-----"
+			},
+			"management_project":
+			{
+			  "id":2,
+			  "description": "skjdfgsdfg",
+			  "name":"project2",
+			  "name_with_namespace":"John Doe8 / project2",
+			  "path":"project2",
+			  "path_with_namespace":"namespace2/project2",
+			  "created_at":"2019-10-11T02:55:54.138Z"
+			},
+			"group":
+			{
+			  "id":26,
+			  "name":"group-with-clusters-api",
+			  "web_url":"https://gitlab.example.com/group-with-clusters-api"
+			}
+		  }`)
+	})
+
+	cluster, _, err := client.GroupCluster.GetCluster(26, 18)
+	if err != nil {
+		t.Errorf("GroupCluster.GetCluster returned error: %v", err)
+	}
+
+	createdAt, err := time.Parse(timeLayout, "2019-01-02T20:18:12.563Z")
+	if err != nil {
+		t.Errorf("DeployKeys.ListAllDeployKeys returned an error while parsing time: %v", err)
+	}
+
+	createdAt2, err := time.Parse(timeLayout, "2019-10-11T02:55:54.138Z")
+	if err != nil {
+		t.Errorf("DeployKeys.ListAllDeployKeys returned an error while parsing time: %v", err)
+	}
+
+	want := &GroupCluster{
+		ID:               18,
+		Name:             "cluster-1",
+		Domain:           "example.com",
+		CreatedAt:        &createdAt,
+		Managed:          true,
+		Enabled:          true,
+		ProviderType:     "user",
+		PlatformType:     "kubernetes",
+		EnvironmentScope: "*",
+		ClusterType:      "group_type",
+		User: &User{
+			ID:        1,
+			Name:      "Administrator",
+			Username:  "root",
+			State:     "active",
+			AvatarURL: "https://www.gravatar.com/avatar/4249f4df72b..",
+			WebURL:    "https://gitlab.example.com/root",
+		},
+		PlatformKubernetes: &PlatformKubernetes{
+			APIURL:            "https://104.197.68.152",
+			AuthorizationType: "rbac",
+			CaCert:            "-----BEGIN CERTIFICATE-----\r\nAAAAA\r\n-----END CERTIFICATE-----",
+		},
+		ManagementProject: &ManagementProject{
+			ID:                2,
+			Description:       "skjdfgsdfg",
+			Name:              "project2",
+			NameWithNamespace: "John Doe8 / project2",
+			Path:              "project2",
+			PathWithNamespace: "namespace2/project2",
+			CreatedAt:         &createdAt2,
+		},
+		Group: &Group{
+			ID:     26,
+			Name:   "group-with-clusters-api",
+			WebURL: "https://gitlab.example.com/group-with-clusters-api",
+		},
+	}
+	if !reflect.DeepEqual(want, cluster) {
+		t.Errorf("GroupCluster.GetCluster returned %+v, want %+v", cluster, want)
+	}
+}
+
+func TestAddGroupCluster(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/groups/26/clusters/user", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPost)
+		fmt.Fprintf(w, `{
+			"id":24,
+			"name":"cluster-5",
+			"created_at":"2019-01-03T21:53:40.610Z",
+			"managed": true,
+			"enabled": true,
+			"provider_type":"user",
+			"platform_type":"kubernetes",
+			"environment_scope":"*",
+			"cluster_type":"group_type",
+			"user":
+			{
+			  "id":1,
+			  "name":"Administrator",
+			  "username":"root",
+			  "state":"active",
+			  "avatar_url":"https://www.gravatar.com/avatar/4249f4df72b..",
+			  "web_url":"https://gitlab.example.com/root"
+			},
+			"platform_kubernetes":
+			{
+			  "api_url":"https://35.111.51.20",
+			  "authorization_type":"rbac",
+			  "ca_cert":"-----BEGIN CERTIFICATE-----\r\nAAAAA\r\n-----END CERTIFICATE-----"
+			},
+			"management_project":null,
+			"group":
+			{
+			  "id":26,
+			  "name":"group-with-clusters-api",
+			  "web_url":"https://gitlab.example.com/root/group-with-clusters-api"
+			}
+		  }`)
+	})
+
+	cluster, _, err := client.GroupCluster.AddCluster(26, &AddGroupClusterOptions{})
+	if err != nil {
+		t.Errorf("GroupCluster.AddCluster returned error: %v", err)
+	}
+
+	createdAt, err := time.Parse(timeLayout, "2019-01-03T21:53:40.610Z")
+	if err != nil {
+		t.Errorf("DeployKeys.ListAllDeployKeys returned an error while parsing time: %v", err)
+	}
+
+	want := &GroupCluster{
+		ID:               24,
+		Name:             "cluster-5",
+		CreatedAt:        &createdAt,
+		Managed:          true,
+		Enabled:          true,
+		ProviderType:     "user",
+		PlatformType:     "kubernetes",
+		EnvironmentScope: "*",
+		ClusterType:      "group_type",
+		User: &User{
+			ID:        1,
+			Name:      "Administrator",
+			Username:  "root",
+			State:     "active",
+			AvatarURL: "https://www.gravatar.com/avatar/4249f4df72b..",
+			WebURL:    "https://gitlab.example.com/root",
+		},
+		PlatformKubernetes: &PlatformKubernetes{
+			APIURL:            "https://35.111.51.20",
+			AuthorizationType: "rbac",
+			CaCert:            "-----BEGIN CERTIFICATE-----\r\nAAAAA\r\n-----END CERTIFICATE-----",
+		},
+		ManagementProject: nil,
+		Group: &Group{
+			ID:     26,
+			Name:   "group-with-clusters-api",
+			WebURL: "https://gitlab.example.com/root/group-with-clusters-api",
+		},
+	}
+	if !reflect.DeepEqual(want, cluster) {
+		t.Errorf("GroupCluster.AddCluster returned %+v, want %+v", cluster, want)
+	}
+}
+
+func TestEditGroupCluster(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/groups/26/clusters/24", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPut)
+		fmt.Fprintf(w, `{
+			"id":24,
+			"name":"new-cluster-name",
+			"domain":"new-domain.com",
+			"created_at":"2019-01-03T21:53:40.610Z",
+			"managed": true,
+			"enabled": true,
+			"provider_type":"user",
+			"platform_type":"kubernetes",
+			"environment_scope":"*",
+			"cluster_type":"group_type",
+			"user":
+			{
+			  "id":1,
+			  "name":"Administrator",
+			  "username":"root",
+			  "state":"active",
+			  "avatar_url":"https://www.gravatar.com/avatar/4249f4df72b..",
+			  "web_url":"https://gitlab.example.com/root"
+			},
+			"platform_kubernetes":
+			{
+			  "api_url":"https://new-api-url.com",
+			  "authorization_type":"rbac"
+			},
+			"management_project":
+			{
+			  "id":2,
+			  "description":"sjdkfngjkdsfngdfgndfg",
+			  "name":"project2",
+			  "name_with_namespace":"John Doe8 / project2",
+			  "path":"project2",
+			  "path_with_namespace":"namespace2/project2",
+			  "created_at":"2019-10-11T02:55:54.138Z"
+			},
+			"group":
+			{
+			  "id":26,
+			  "name":"group-with-clusters-api",
+			  "web_url":"https://gitlab.example.com/group-with-clusters-api"
+			}
+		  }`)
+	})
+
+	name := "new-cluster-name"
+	domain := "new-domain.com"
+	enviromentScope := "*"
+	apiURL := "https://new-api-url.com"
+	opt := &EditGroupClusterOptions{
+		Name:             &name,
+		Domain:           &domain,
+		EnvironmentScope: &enviromentScope,
+		PlatformKubernetes: &EditGroupPlatformKubernetesOptions{
+			APIURL: &apiURL,
+		},
+	}
+	cluster, _, err := client.GroupCluster.EditCluster(26, 24, opt)
+	if err != nil {
+		t.Errorf("GroupCluster.EditCluster returned error: %v", err)
+	}
+
+	createdAt, err := time.Parse(timeLayout, "2019-01-03T21:53:40.610Z")
+	if err != nil {
+		t.Errorf("DeployKeys.ListAllDeployKeys returned an error while parsing time: %v", err)
+	}
+
+	createdAt2, err := time.Parse(timeLayout, "2019-10-11T02:55:54.138Z")
+	if err != nil {
+		t.Errorf("DeployKeys.ListAllDeployKeys returned an error while parsing time: %v", err)
+	}
+
+	want := &GroupCluster{
+		ID:               24,
+		Name:             "new-cluster-name",
+		Domain:           "new-domain.com",
+		CreatedAt:        &createdAt,
+		Managed:          true,
+		Enabled:          true,
+		ProviderType:     "user",
+		PlatformType:     "kubernetes",
+		EnvironmentScope: "*",
+		ClusterType:      "group_type",
+		User: &User{
+			ID:        1,
+			Name:      "Administrator",
+			Username:  "root",
+			State:     "active",
+			AvatarURL: "https://www.gravatar.com/avatar/4249f4df72b..",
+			WebURL:    "https://gitlab.example.com/root",
+		},
+		PlatformKubernetes: &PlatformKubernetes{
+			APIURL:            "https://new-api-url.com",
+			AuthorizationType: "rbac",
+		},
+		ManagementProject: &ManagementProject{
+			ID:                2,
+			Description:       "sjdkfngjkdsfngdfgndfg",
+			Name:              "project2",
+			NameWithNamespace: "John Doe8 / project2",
+			Path:              "project2",
+			PathWithNamespace: "namespace2/project2",
+			CreatedAt:         &createdAt2,
+		},
+		Group: &Group{
+			ID:     26,
+			Name:   "group-with-clusters-api",
+			WebURL: "https://gitlab.example.com/group-with-clusters-api",
+		},
+	}
+	if !reflect.DeepEqual(want, cluster) {
+		t.Errorf("GroupCluster.EditCluster returned %+v, want %+v", cluster, want)
+	}
+}
+
+func TestDeleteGroupCluster(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/groups/26/clusters/23", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodDelete)
+	})
+
+	_, err := client.GroupCluster.DeleteCluster(26, 23)
+	if err != nil {
+		t.Errorf("GroupCluster.DeleteCluster returned error: %v", err)
+	}
+}
diff --git a/namespaces_test.go b/namespaces_test.go
deleted file mode 100644
index 80be019..0000000
--- a/namespaces_test.go
+++ /dev/null
@@ -1,279 +0,0 @@
-//
-// 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"
-	"net/http"
-	"reflect"
-	"testing"
-)
-
-func TestListNamespaces(t *testing.T) {
-	mux, server, client := setup(t)
-	defer teardown(server)
-
-	mux.HandleFunc("/api/v4/namespaces", func(w http.ResponseWriter, r *http.Request) {
-		testMethod(t, r, http.MethodGet)
-		fmt.Fprintf(w, `[
-			{
-			  "id": 1,
-			  "name": "user1",
-			  "path": "user1",
-			  "kind": "user",
-			  "full_path": "user1",
-			  "avatar_url": "https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-			  "web_url": "https://gitlab.example.com/user1",
-			  "billable_members_count": 1,
-			  "plan": "default",
-			  "trial_ends_on": null,
-			  "trial": false
-			},
-			{
-			  "id": 2,
-			  "name": "group1",
-			  "path": "group1",
-			  "kind": "group",
-			  "full_path": "group1",
-			  "web_url": "https://gitlab.example.com/groups/group1",
-			  "members_count_with_descendants": 2,
-			  "billable_members_count": 2,
-			  "plan": "default",
-			  "trial_ends_on": null,
-			  "trial": false
-			},
-			{
-			  "id": 3,
-			  "name": "bar",
-			  "path": "bar",
-			  "kind": "group",
-			  "full_path": "foo/bar",
-			  "parent_id": 9,
-			  "web_url": "https://gitlab.example.com/groups/foo/bar",
-			  "members_count_with_descendants": 5,
-			  "billable_members_count": 5,
-			  "plan": "default",
-			  "trial_ends_on": null,
-			  "trial": false
-			}
-		  ]`)
-	})
-
-	testCases := []struct {
-		event     string
-		search    *string
-		ownedOnly *bool
-	}{
-		{"with_nothing", nil, nil},
-		{"with_search", String("foobar"), nil},
-		{"with_owned_only", nil, Bool(false)},
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.event, func(t *testing.T) {
-			namespaces, _, err := client.Namespaces.ListNamespaces(&ListNamespacesOptions{Search: tc.search, OwnedOnly: tc.ownedOnly})
-			if err != nil {
-				t.Errorf("Namespaces.ListNamespaces returned error: %v", err)
-			}
-
-			want := []*Namespace{
-				{
-					ID:                   1,
-					Name:                 "user1",
-					Path:                 "user1",
-					Kind:                 "user",
-					FullPath:             "user1",
-					AvatarURL:            String("https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon"),
-					WebURL:               "https://gitlab.example.com/user1",
-					Plan:                 "default",
-					BillableMembersCount: 1,
-					TrialEndsOn:          nil,
-					Trial:                false,
-				},
-				{
-					ID:                          2,
-					Name:                        "group1",
-					Path:                        "group1",
-					Kind:                        "group",
-					FullPath:                    "group1",
-					AvatarURL:                   nil,
-					WebURL:                      "https://gitlab.example.com/groups/group1",
-					MembersCountWithDescendants: 2,
-					BillableMembersCount:        2,
-					Plan:                        "default",
-					TrialEndsOn:                 nil,
-					Trial:                       false,
-				},
-				{
-					ID:                          3,
-					Name:                        "bar",
-					Path:                        "bar",
-					Kind:                        "group",
-					FullPath:                    "foo/bar",
-					ParentID:                    9,
-					AvatarURL:                   nil,
-					WebURL:                      "https://gitlab.example.com/groups/foo/bar",
-					MembersCountWithDescendants: 5,
-					BillableMembersCount:        5,
-					Plan:                        "default",
-					TrialEndsOn:                 nil,
-					Trial:                       false,
-				},
-			}
-
-			if !reflect.DeepEqual(namespaces, want) {
-				t.Errorf("Namespaces.ListNamespaces returned \ngot:\n%v\nwant:\n%v", Stringify(namespaces), Stringify(want))
-			}
-		})
-	}
-}
-
-func TestGetNamespace(t *testing.T) {
-	mux, server, client := setup(t)
-	defer teardown(server)
-
-	mux.HandleFunc("/api/v4/namespaces/2", func(w http.ResponseWriter, r *http.Request) {
-		testMethod(t, r, http.MethodGet)
-		fmt.Fprintf(w, `{
-			"id": 2,
-			"name": "group1",
-			"path": "group1",
-			"kind": "group",
-			"full_path": "group1",
-			"avatar_url": null,
-			"web_url": "https://gitlab.example.com/groups/group1",
-			"members_count_with_descendants": 2,
-			"billable_members_count": 2,
-			"max_seats_used": 0,
-			"seats_in_use": 0,
-			"plan": "default",
-			"trial_ends_on": null,
-			"trial": false
-		  }`)
-	})
-
-	namespace, _, err := client.Namespaces.GetNamespace(2)
-	if err != nil {
-		t.Errorf("Namespaces.GetNamespace returned error: %v", err)
-	}
-
-	want := &Namespace{
-		ID:                          2,
-		Name:                        "group1",
-		Path:                        "group1",
-		Kind:                        "group",
-		FullPath:                    "group1",
-		AvatarURL:                   nil,
-		WebURL:                      "https://gitlab.example.com/groups/group1",
-		MembersCountWithDescendants: 2,
-		BillableMembersCount:        2,
-		MaxSeatsUsed:                Int(0),
-		SeatsInUse:                  Int(0),
-		Plan:                        "default",
-		TrialEndsOn:                 nil,
-		Trial:                       false,
-	}
-
-	if !reflect.DeepEqual(namespace, want) {
-		t.Errorf("Namespaces.ListNamespaces returned \ngot:\n%v\nwant:\n%v", Stringify(namespace), Stringify(want))
-	}
-}
-
-func TestNamespaceExists(t *testing.T) {
-	mux, server, client := setup(t)
-	defer teardown(server)
-
-	mux.HandleFunc("/api/v4/namespaces/my-group/exists", func(w http.ResponseWriter, r *http.Request) {
-		testMethod(t, r, http.MethodGet)
-		fmt.Fprintf(w, `{
-			"exists": true,
-			"suggests": [
-				"my-group1"
-			]
-		}`)
-	})
-
-	opt := &NamespaceExistsOptions{
-		ParentID: Int(1),
-	}
-	exists, _, err := client.Namespaces.NamespaceExists("my-group", opt)
-	if err != nil {
-		t.Errorf("Namespaces.NamespaceExists returned error: %v", err)
-	}
-
-	want := &NamespaceExistance{
-		Exists:   true,
-		Suggests: []string{"my-group1"},
-	}
-	if !reflect.DeepEqual(exists, want) {
-		t.Errorf("Namespaces.NamespaceExists returned \ngot:\n%v\nwant:\n%v", Stringify(exists), Stringify(want))
-	}
-}
-
-func TestSearchNamespace(t *testing.T) {
-	mux, server, client := setup(t)
-	defer teardown(server)
-
-	mux.HandleFunc("/api/v4/namespaces", func(w http.ResponseWriter, r *http.Request) {
-		testMethod(t, r, http.MethodGet)
-		fmt.Fprintf(w, `[
-			{
-			  "id": 4,
-			  "name": "twitter",
-			  "path": "twitter",
-			  "kind": "group",
-			  "full_path": "twitter",
-			  "avatar_url": null,
-			  "web_url": "https://gitlab.example.com/groups/twitter",
-			  "members_count_with_descendants": 2,
-			  "billable_members_count": 2,
-			  "max_seats_used": 0,
-			  "seats_in_use": 0,
-			  "plan": "default",
-			  "trial_ends_on": null,
-			  "trial": false
-			}
-		  ]`)
-	})
-
-	namespaces, _, err := client.Namespaces.SearchNamespace("?search=twitter")
-	if err != nil {
-		t.Errorf("Namespaces.SearchNamespaces returned error: %v", err)
-	}
-
-	want := []*Namespace{
-		{
-			ID:                          4,
-			Name:                        "twitter",
-			Path:                        "twitter",
-			Kind:                        "group",
-			FullPath:                    "twitter",
-			AvatarURL:                   nil,
-			WebURL:                      "https://gitlab.example.com/groups/twitter",
-			MembersCountWithDescendants: 2,
-			BillableMembersCount:        2,
-			MaxSeatsUsed:                Int(0),
-			SeatsInUse:                  Int(0),
-			Plan:                        "default",
-			TrialEndsOn:                 nil,
-			Trial:                       false,
-		},
-	}
-	if !reflect.DeepEqual(namespaces, want) {
-		t.Errorf("Namespaces.SearchNamespaces returned \ngot:\n%v\nwant:\n%v", Stringify(namespaces), Stringify(want))
-	}
-}
diff --git a/pages_test.go b/notifications_test.go
similarity index 52%
rename from pages_test.go
rename to notifications_test.go
index 1d92ffb..392ad48 100644
--- a/pages_test.go
+++ b/notifications_test.go
@@ -17,20 +17,34 @@
 package gitlab
 
 import (
+	"fmt"
 	"net/http"
+	"reflect"
 	"testing"
 )
 
-func TestUnpublishPages(t *testing.T) {
+func TestGetGlobalSettings(t *testing.T) {
 	mux, server, client := setup(t)
 	defer teardown(server)
 
-	mux.HandleFunc("/api/v4/projects/2/pages", func(w http.ResponseWriter, r *http.Request) {
-		testMethod(t, r, http.MethodDelete)
+	mux.HandleFunc("/api/v4/notification_settings", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprintf(w, `{
+			"level": "participating",
+			"notification_email": "admin@example.com"
+		  }`)
 	})
 
-	_, err := client.Pages.UnpublishPages(2)
+	settings, _, err := client.NotificationSettings.GetGlobalSettings()
 	if err != nil {
-		t.Errorf("Pages.UnpublishPages returned error: %v", err)
+		t.Errorf("NotifcationSettings.GetGlobalSettings returned error: %v", err)
+	}
+
+	want := &NotificationSettings{
+		Level:             1,
+		NotificationEmail: "admin@example.com",
+	}
+	if !reflect.DeepEqual(settings, want) {
+		t.Errorf("NotificationSettings.GetGlobalSettings returned %+v, want %+v", settings, want)
 	}
 }