Repository: xanzy/go-gitlab
Pull request #1779 (2023-08-21 13:40:41)

Add support for Group Protected Environments
Add support for Group Protected Environments, similar to Project Protected Environments.

https://docs.gitlab.com/ee/api/group_protected_environments.html


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 417fee3..cc7b34b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -4,8 +4,7 @@ We want to make contributing to this project as easy as possible.
 
 ## Reporting Issues
 
-If you have an issue, please report it on the [issue tracker](
-https://github.com/xanzy/go-gitlab/issues).
+If you have an issue, please report it on the [issue tracker](https://github.com/xanzy/go-gitlab/issues).
 
 When you are up for writing a PR to solve the issue you encountered, it's not
 needed to first open a separate issue. In that case only opening a PR with a
@@ -13,8 +12,8 @@ description of the issue you are trying to solve is just fine.
 
 ## Contributing Code
 
-Pull requests are always welcome. When in doubt if your contribution fits within
-the rest of the project, feel free to first open an issue to discuss your idea.
+Pull requests are always welcome. When in doubt if your contribution fits in with
+the rest of the project, free to first open an issue to discuss your idea.
 
 This is not needed when fixing a bug or adding an enhancement, as long as the
 enhancement you are trying to add can be found in the public GitLab API docs as
@@ -22,8 +21,10 @@ this project only supports what is in the public API docs.
 
 ## Coding style
 
-We try to follow the Go best practices, where it makes sense, and use [`gofumpt`](
-https://github.com/mvdan/gofumpt) to format code in this project.
+We try to follow the Go best practices, where it makes sense, and use
+[`gofumpt`](https://github.com/mvdan/gofumpt) to format code in this project.
+As a general rule of thumb we prefer to keep line width for comments below 80
+chars and for code (where possible and sensible) below 100 chars.
 
 Before making a PR, please look at the rest this package and try to make sure
 your contribution is consistent with the rest of the coding style.
@@ -35,18 +36,18 @@ easier to find things.
 ### Setting up your local development environment to Contribute to `go-gitlab`
 
 1. [Fork](https://github.com/xanzy/go-gitlab/fork), then clone the repository.
-    ```sh
-    git clone https://github.com/<your-username>/go-gitlab.git
-    # or via ssh
-    git clone git@github.com:<your-username>/go-gitlab.git
-    ```
+   ```sh
+   git clone https://github.com/<your-username>/go-gitlab.git
+   # or via ssh
+   git clone git@github.com:<your-username>/go-gitlab.git
+   ```
 1. Install dependencies:
-    ```sh
-    make setup
-    ```
+   ```sh
+   make setup
+   ```
 1. Make your changes on your feature branch
 1. Run the tests and `gofumpt`
-    ```sh
-    make test && make fmt
-    ```
+   ```sh
+   make test && make fmt
+   ```
 1. Open up your pull request
diff --git a/commits.go b/commits.go
index 0c597d8..7c724c2 100644
--- a/commits.go
+++ b/commits.go
@@ -571,10 +571,10 @@ type GPGSignature struct {
 	KeySubkeyID        int    `json:"gpg_key_subkey_id"`
 }
 
-// GetGPGSignature gets a GPG signature of a commit.
+// GetGPGSiganature gets a GPG signature of a commit.
 //
 // GitLab API docs: https://docs.gitlab.com/ee/api/commits.html#get-gpg-signature-of-a-commit
-func (s *CommitsService) GetGPGSignature(pid interface{}, sha string, options ...RequestOptionFunc) (*GPGSignature, *Response, error) {
+func (s *CommitsService) GetGPGSiganature(pid interface{}, sha string, options ...RequestOptionFunc) (*GPGSignature, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
 		return nil, nil, err
diff --git a/gitlab.go b/gitlab.go
index 322f1a4..d6a4bda 100644
--- a/gitlab.go
+++ b/gitlab.go
@@ -144,6 +144,7 @@ type Client struct {
 	GroupLabels                  *GroupLabelsService
 	GroupMembers                 *GroupMembersService
 	GroupMilestones              *GroupMilestonesService
+	GroupProtectedEnvironments   *GroupProtectedEnvironmentsService
 	GroupRepositoryStorageMove   *GroupRepositoryStorageMoveService
 	GroupVariables               *GroupVariablesService
 	GroupWikis                   *GroupWikisService
@@ -365,6 +366,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
 	c.GroupLabels = &GroupLabelsService{client: c}
 	c.GroupMembers = &GroupMembersService{client: c}
 	c.GroupMilestones = &GroupMilestonesService{client: c}
+	c.GroupProtectedEnvironments = &GroupProtectedEnvironmentsService{client: c}
 	c.GroupRepositoryStorageMove = &GroupRepositoryStorageMoveService{client: c}
 	c.GroupVariables = &GroupVariablesService{client: c}
 	c.GroupWikis = &GroupWikisService{client: c}
diff --git a/group_protected_environments.go b/group_protected_environments.go
new file mode 100644
index 0000000..53bee70
--- /dev/null
+++ b/group_protected_environments.go
@@ -0,0 +1,278 @@
+//
+// Copyright 2023, 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"
+)
+
+// GroupProtectedEnvironmentsService handles communication with the group-level
+// protected environment methods of the GitLab API.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_protected_environments.html
+type GroupProtectedEnvironmentsService struct {
+	client *Client
+}
+
+// GroupProtectedEnvironment represents a group-level protected environment.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_protected_environments.html
+type GroupProtectedEnvironment struct {
+	Name                  string                               `json:"name"`
+	DeployAccessLevels    []*GroupEnvironmentAccessDescription `json:"deploy_access_levels"`
+	RequiredApprovalCount int                                  `json:"required_approval_count"`
+	ApprovalRules         []*GroupEnvironmentApprovalRule      `json:"approval_rules"`
+}
+
+// GroupEnvironmentAccessDescription represents the access decription for a
+// group-level protected environment.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_protected_environments.html
+type GroupEnvironmentAccessDescription struct {
+	ID                     int              `json:"id"`
+	AccessLevel            AccessLevelValue `json:"access_level"`
+	AccessLevelDescription string           `json:"access_level_description"`
+	UserID                 int              `json:"user_id"`
+	GroupID                int              `json:"group_id"`
+}
+
+// GroupEnvironmentApprovalRule represents the approval rules for a group-level
+// protected environment.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_protected_environments.html#protect-a-single-environment
+type GroupEnvironmentApprovalRule struct {
+	ID                     int              `json:"id"`
+	UserID                 int              `json:"user_id"`
+	GroupID                int              `json:"group_id"`
+	AccessLevel            AccessLevelValue `json:"access_level"`
+	AccessLevelDescription string           `json:"access_level_description"`
+	RequiredApprovalCount  int              `json:"required_approvals"`
+	GroupInheritanceType   int              `json:"group_inheritance_type"`
+}
+
+// ListGroupProtectedEnvironmentsOptions represents the available
+// ListGroupProtectedEnvironments() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_protected_environments.html#list-group-level-protected-environments
+type ListGroupProtectedEnvironmentsOptions ListOptions
+
+// ListGroupProtectedEnvironments returns a list of protected environments from
+// a group.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_protected_environments.html#list-group-level-protected-environments
+func (s *GroupProtectedEnvironmentsService) ListGroupProtectedEnvironments(gid interface{}, opt *ListGroupProtectedEnvironmentsOptions, options ...RequestOptionFunc) ([]*GroupProtectedEnvironment, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("groups/%s/protected_environments", PathEscape(group))
+
+	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var pes []*GroupProtectedEnvironment
+	resp, err := s.client.Do(req, &pes)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return pes, resp, nil
+}
+
+// GetGroupProtectedEnvironment returns a single group-level protected
+// environment.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_protected_environments.html#get-a-single-protected-environment
+func (s *GroupProtectedEnvironmentsService) GetGroupProtectedEnvironment(gid interface{}, environment string, options ...RequestOptionFunc) (*GroupProtectedEnvironment, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("groups/%s/protected_environments/%s", PathEscape(group), environment)
+
+	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	pe := new(GroupProtectedEnvironment)
+	resp, err := s.client.Do(req, pe)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return pe, resp, nil
+}
+
+// ProtectGroupEnvironmentOptions represents the available
+// ProtectGroupEnvironment() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_protected_environments.html#protect-a-single-environment
+type ProtectGroupEnvironmentOptions struct {
+	Name                  *string                                 `url:"name,omitempty" json:"name,omitempty"`
+	DeployAccessLevels    *[]*GroupEnvironmentAccessOptions       `url:"deploy_access_levels,omitempty" json:"deploy_access_levels,omitempty"`
+	RequiredApprovalCount *int                                    `url:"required_approval_count,omitempty" json:"required_approval_count,omitempty"`
+	ApprovalRules         *[]*GroupEnvironmentApprovalRuleOptions `url:"approval_rules,omitempty" json:"approval_rules,omitempty"`
+}
+
+// GroupEnvironmentAccessOptions represents the options for an access decription
+// for a group-level protected environment.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_protected_environments.html#protect-a-single-environment
+type GroupEnvironmentAccessOptions struct {
+	AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"`
+	UserID      *int              `url:"user_id,omitempty" json:"user_id,omitempty"`
+	GroupID     *int              `url:"group_id,omitempty" json:"group_id,omitempty"`
+}
+
+// GroupEnvironmentApprovalRuleOptions represents the approval rules for a
+// group-level protected environment.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_protected_environments.html#protect-a-single-environment
+type GroupEnvironmentApprovalRuleOptions struct {
+	UserID                 *int              `url:"user_id,omitempty" json:"user_id,omitempty"`
+	GroupID                *int              `url:"group_id,omitempty" json:"group_id,omitempty"`
+	AccessLevel            *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"`
+	AccessLevelDescription *string           `url:"access_level_description,omitempty" json:"access_level_description,omitempty"`
+	RequiredApprovalCount  *int              `url:"required_approvals,omitempty" json:"required_approvals,omitempty"`
+	GroupInheritanceType   *int              `url:"group_inheritance_type,omitempty" json:"group_inheritance_type,omitempty"`
+}
+
+// ProtectGroupEnvironment protects a single group-level environment.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_protected_environments.html#protect-a-single-environment
+func (s *GroupProtectedEnvironmentsService) ProtectGroupEnvironment(gid interface{}, opt *ProtectGroupEnvironmentOptions, options ...RequestOptionFunc) (*GroupProtectedEnvironment, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("groups/%s/protected_environments", PathEscape(group))
+
+	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	pe := new(GroupProtectedEnvironment)
+	resp, err := s.client.Do(req, pe)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return pe, resp, nil
+}
+
+// UpdateGroupProtectedEnvironmentOptions represents the available
+// UpdateGroupProtectedEnvironment() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_protected_environments.html#update-a-protected-environment
+type UpdateGroupProtectedEnvironmentOptions struct {
+	Name                  *string                                       `url:"name,omitempty" json:"name,omitempty"`
+	DeployAccessLevels    *[]*UpdateGroupEnvironmentAccessOptions       `url:"deploy_access_levels,omitempty" json:"deploy_access_levels,omitempty"`
+	RequiredApprovalCount *int                                          `url:"required_approval_count,omitempty" json:"required_approval_count,omitempty"`
+	ApprovalRules         *[]*UpdateGroupEnvironmentApprovalRuleOptions `url:"approval_rules,omitempty" json:"approval_rules,omitempty"`
+}
+
+// UpdateGroupEnvironmentAccessOptions represents the options for updates to the
+// access decription for a group-level protected environment.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_protected_environments.html#update-a-protected-environment
+type UpdateGroupEnvironmentAccessOptions struct {
+	AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"`
+	ID          *int              `url:"id,omitempty" json:"id,omitempty"`
+	UserID      *int              `url:"user_id,omitempty" json:"user_id,omitempty"`
+	GroupID     *int              `url:"group_id,omitempty" json:"group_id,omitempty"`
+	Destroy     *bool             `url:"_destroy,omitempty" json:"_destroy,omitempty"`
+}
+
+// UpdateGroupEnvironmentApprovalRuleOptions represents the updates to the
+// approval rules for a group-level protected environment.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_protected_environments.html#update-a-protected-environment
+type UpdateGroupEnvironmentApprovalRuleOptions struct {
+	ID                     *int              `url:"id,omitempty" json:"id,omitempty"`
+	UserID                 *int              `url:"user_id,omitempty" json:"user_id,omitempty"`
+	GroupID                *int              `url:"group_id,omitempty" json:"group_id,omitempty"`
+	AccessLevel            *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"`
+	AccessLevelDescription *string           `url:"access_level_description,omitempty" json:"access_level_description,omitempty"`
+	RequiredApprovalCount  *int              `url:"required_approvals,omitempty" json:"required_approvals,omitempty"`
+	GroupInheritanceType   *int              `url:"group_inheritance_type,omitempty" json:"group_inheritance_type,omitempty"`
+	Destroy                *bool             `url:"_destroy,omitempty" json:"_destroy,omitempty"`
+}
+
+// UpdateGroupProtectedEnvironment updates a single group-level protected
+// environment.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_protected_environments.html#update-a-protected-environment
+func (s *GroupProtectedEnvironmentsService) UpdateGroupProtectedEnvironment(gid interface{}, environment string, opt *UpdateGroupProtectedEnvironmentOptions, options ...RequestOptionFunc) (*GroupProtectedEnvironment, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("groups/%s/protected_environments/%s", PathEscape(group), environment)
+
+	req, err := s.client.NewRequest(http.MethodPut, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	pe := new(GroupProtectedEnvironment)
+	resp, err := s.client.Do(req, pe)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return pe, resp, nil
+}
+
+// UnprotectGroupEnvironment unprotects the given protected group-level
+// environment.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_protected_environments.html#unprotect-a-single-environment
+func (s *GroupProtectedEnvironmentsService) UnprotectGroupEnvironment(gid interface{}, environment string, options ...RequestOptionFunc) (*Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("groups/%s/protected_environments/%s", PathEscape(group), environment)
+
+	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/snippets.go b/snippets.go
index c25548b..12687c3 100644
--- a/snippets.go
+++ b/snippets.go
@@ -57,7 +57,6 @@ type Snippet struct {
 		Path   string `json:"path"`
 		RawURL string `json:"raw_url"`
 	} `json:"files"`
-	RepositoryStorage string `json:"repository_storage"`
 }
 
 func (s Snippet) String() string {
@@ -282,33 +281,3 @@ func (s *SnippetsService) ExploreSnippets(opt *ExploreSnippetsOptions, options .
 
 	return ps, resp, nil
 }
-
-// ListAllSnippetsOptions represents the available ListAllSnippets() options.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/ee/api/snippets.html#list-all-snippets
-type ListAllSnippetsOptions struct {
-	ListOptions
-	CreatedAfter      *ISOTime `url:"created_after,omitempty" json:"created_after,omitempty"`
-	CreatedBefore     *ISOTime `url:"created_before,omitempty" json:"created_before,omitempty"`
-	RepositoryStorage *string  `url:"repository_storage,omitempty" json:"repository_storage,omitempty"`
-}
-
-// ListAllSnippets gets all snippets the current user has access to.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/ee/api/snippets.html#list-all-snippets
-func (s *SnippetsService) ListAllSnippets(opt *ListAllSnippetsOptions, options ...RequestOptionFunc) ([]*Snippet, *Response, error) {
-	req, err := s.client.NewRequest(http.MethodGet, "snippets/all", opt, options)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	var ps []*Snippet
-	resp, err := s.client.Do(req, &ps)
-	if err != nil {
-		return nil, resp, err
-	}
-
-	return ps, resp, nil
-}
diff --git a/users.go b/users.go
index 04a35ad..d3eff76 100644
--- a/users.go
+++ b/users.go
@@ -1159,15 +1159,14 @@ func (s *UsersService) RejectUser(user int, options ...RequestOptionFunc) error
 // GitLab API docs:
 // https://docs.gitlab.com/ee/api/users.html#get-all-impersonation-tokens-of-a-user
 type ImpersonationToken struct {
-	ID         int        `json:"id"`
-	Name       string     `json:"name"`
-	Active     bool       `json:"active"`
-	Token      string     `json:"token"`
-	Scopes     []string   `json:"scopes"`
-	Revoked    bool       `json:"revoked"`
-	CreatedAt  *time.Time `json:"created_at"`
-	ExpiresAt  *ISOTime   `json:"expires_at"`
-	LastUsedAt *time.Time `json:"last_used_at"`
+	ID        int        `json:"id"`
+	Name      string     `json:"name"`
+	Active    bool       `json:"active"`
+	Token     string     `json:"token"`
+	Scopes    []string   `json:"scopes"`
+	Revoked   bool       `json:"revoked"`
+	CreatedAt *time.Time `json:"created_at"`
+	ExpiresAt *ISOTime   `json:"expires_at"`
 }
 
 // GetAllImpersonationTokensOptions represents the available


Test patch:
diff --git a/commits_test.go b/commits_test.go
index fd00e0c..38c43e0 100644
--- a/commits_test.go
+++ b/commits_test.go
@@ -232,7 +232,7 @@ func TestGetGPGSignature(t *testing.T) {
 		mustWriteHTTPResponse(t, w, "testdata/get_signature.json")
 	})
 
-	sig, resp, err := client.Commits.GetGPGSignature("1", "b0b3a907f41409829b307a28b82fdbd552ee5a27", nil)
+	sig, resp, err := client.Commits.GetGPGSiganature("1", "b0b3a907f41409829b307a28b82fdbd552ee5a27", nil)
 	if err != nil {
 		t.Fatalf("Commits.GetGPGSignature returned error: %v, response: %v", err, resp)
 	}
diff --git a/group_protected_environments_test.go b/group_protected_environments_test.go
new file mode 100644
index 0000000..b8ede66
--- /dev/null
+++ b/group_protected_environments_test.go
@@ -0,0 +1,574 @@
+//
+// Copyright 2023, 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"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGroupListProtectedEnvironments(t *testing.T) {
+	mux, client := setup(t)
+
+	mux.HandleFunc("/api/v4/groups/1/protected_environments", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprint(w, `[{
+      "name":"staging",
+      "deploy_access_levels": [
+        {
+          "access_level": 40,
+          "access_level_description": "Maintainers"
+        }
+      ],
+      "required_approval_count": 1,
+      "approval_rules": [
+        {
+           "id": 38,
+           "user_id": 42,
+           "group_id": null,
+           "access_level": null,
+           "access_level_description": "qa-group",
+           "required_approvals": 1,
+           "group_inheritance_type": 0
+        },
+        {
+           "id": 39,
+           "user_id": null,
+           "group_id": 135,
+           "access_level": 30,
+           "access_level_description": "security-group",
+           "required_approvals": 2,
+           "group_inheritance_type": 1
+        }
+      ]
+    },{
+      "name":"production",
+      "deploy_access_levels": [
+        {
+          "access_level": 30,
+          "access_level_description": "Developers + Maintainers"
+        }
+      ]
+    }]`)
+	})
+
+	expected := []*GroupProtectedEnvironment{
+		{
+			Name: "staging",
+			DeployAccessLevels: []*GroupEnvironmentAccessDescription{
+				{
+					AccessLevel:            40,
+					AccessLevelDescription: "Maintainers",
+				},
+			},
+			RequiredApprovalCount: 1,
+			ApprovalRules: []*GroupEnvironmentApprovalRule{
+				{
+					ID:                     38,
+					UserID:                 42,
+					AccessLevelDescription: "qa-group",
+					RequiredApprovalCount:  1,
+				},
+				{
+					ID:                     39,
+					GroupID:                135,
+					AccessLevel:            30,
+					AccessLevelDescription: "security-group",
+					RequiredApprovalCount:  2,
+					GroupInheritanceType:   1,
+				},
+			},
+		},
+		{
+			Name: "production",
+			DeployAccessLevels: []*GroupEnvironmentAccessDescription{
+				{
+					AccessLevel:            30,
+					AccessLevelDescription: "Developers + Maintainers",
+				},
+			},
+		},
+	}
+
+	opt := &ListGroupProtectedEnvironmentsOptions{}
+	environments, _, err := client.GroupProtectedEnvironments.ListGroupProtectedEnvironments(1, opt)
+	assert.NoError(t, err, "failed to get response")
+	assert.Equal(t, expected, environments)
+}
+
+func TestGroupGetProtectedEnvironment(t *testing.T) {
+	mux, client := setup(t)
+
+	// Test with RequiredApprovalCount
+	environmentName := "development"
+
+	mux.HandleFunc(fmt.Sprintf("/api/v4/groups/1/protected_environments/%s", environmentName), func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprintf(w, `{
+      "name":"%s",
+      "deploy_access_levels": [
+        {
+          "access_level": 30,
+          "access_level_description": "Developers + Maintainers"
+        }
+      ],
+      "required_approval_count": 1,
+      "approval_rules": [
+        {
+           "id": 1,
+           "user_id": null,
+           "group_id": 10,
+           "access_level": 5,
+           "access_level_description": "devops",
+           "required_approvals": 0,
+           "group_inheritance_type": 0
+        }
+      ]
+    }`, environmentName)
+	})
+
+	expected := &GroupProtectedEnvironment{
+		Name: environmentName,
+		DeployAccessLevels: []*GroupEnvironmentAccessDescription{
+			{
+				AccessLevel:            30,
+				AccessLevelDescription: "Developers + Maintainers",
+			},
+		},
+		RequiredApprovalCount: 1,
+		ApprovalRules: []*GroupEnvironmentApprovalRule{
+			{
+				ID:                     1,
+				GroupID:                10,
+				AccessLevel:            5,
+				AccessLevelDescription: "devops",
+			},
+		},
+	}
+
+	environment, _, err := client.GroupProtectedEnvironments.GetGroupProtectedEnvironment(1, environmentName)
+	assert.NoError(t, err, "failed to get response")
+	assert.Equal(t, expected, environment)
+
+	// Test without RequiredApprovalCount nor ApprovalRules
+	environmentName = "testing"
+
+	mux.HandleFunc(fmt.Sprintf("/api/v4/groups/2/protected_environments/%s", environmentName), func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprintf(w, `{
+      "name":"%s",
+      "deploy_access_levels": [
+        {
+          "access_level": 30,
+          "access_level_description": "Developers + Maintainers"
+        }
+      ]
+    }`, environmentName)
+	})
+
+	expected = &GroupProtectedEnvironment{
+		Name: environmentName,
+		DeployAccessLevels: []*GroupEnvironmentAccessDescription{
+			{
+				AccessLevel:            30,
+				AccessLevelDescription: "Developers + Maintainers",
+			},
+		},
+	}
+
+	environment, _, err = client.GroupProtectedEnvironments.GetGroupProtectedEnvironment(2, environmentName)
+	assert.NoError(t, err, "failed to get response")
+	assert.Equal(t, expected, environment)
+}
+
+func TestGroupProtectEnvironments(t *testing.T) {
+	mux, client := setup(t)
+
+	// Test with RequiredApprovalCount and ApprovalRules
+	environmentName := "other"
+
+	mux.HandleFunc("/api/v4/groups/1/protected_environments", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPost)
+		fmt.Fprintf(w, `{
+      "name":"%s",
+      "deploy_access_levels": [
+        {
+          "access_level": 30,
+          "access_level_description": "Developers + Maintainers"
+        }
+      ],
+      "required_approval_count": 2,
+      "approval_rules": [
+        {
+           "id": 1,
+           "user_id": null,
+           "group_id": 10,
+           "access_level": 5,
+           "access_level_description": "devops",
+           "required_approvals": 0,
+           "group_inheritance_type": 0
+        }
+      ]
+    }`, environmentName)
+	})
+
+	expected := &GroupProtectedEnvironment{
+		Name: environmentName,
+		DeployAccessLevels: []*GroupEnvironmentAccessDescription{
+			{
+				AccessLevel:            30,
+				AccessLevelDescription: "Developers + Maintainers",
+			},
+		},
+		RequiredApprovalCount: 2,
+		ApprovalRules: []*GroupEnvironmentApprovalRule{
+			{
+				ID:                     1,
+				GroupID:                10,
+				AccessLevel:            5,
+				AccessLevelDescription: "devops",
+			},
+		},
+	}
+
+	opt := &ProtectGroupEnvironmentOptions{
+		Name: String(environmentName),
+		DeployAccessLevels: &[]*GroupEnvironmentAccessOptions{
+			{AccessLevel: AccessLevel(30)},
+		},
+		RequiredApprovalCount: Int(2),
+		ApprovalRules: &[]*GroupEnvironmentApprovalRuleOptions{
+			{
+				GroupID:                Int(10),
+				AccessLevel:            AccessLevel(0),
+				AccessLevelDescription: String("devops"),
+			},
+		},
+	}
+
+	environment, _, err := client.GroupProtectedEnvironments.ProtectGroupEnvironment(1, opt)
+	assert.NoError(t, err, "failed to get response")
+	assert.Equal(t, expected, environment)
+
+	// Test without RequiredApprovalCount nor ApprovalRules
+	environmentName = "staging"
+
+	mux.HandleFunc("/api/v4/groups/2/protected_environments", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPost)
+		fmt.Fprintf(w, `{
+      "name":"%s",
+      "deploy_access_levels": [
+        {
+          "access_level": 30,
+          "access_level_description": "Developers + Maintainers"
+        }
+      ]
+    }`, environmentName)
+	})
+
+	expected = &GroupProtectedEnvironment{
+		Name: environmentName,
+		DeployAccessLevels: []*GroupEnvironmentAccessDescription{
+			{
+				AccessLevel:            30,
+				AccessLevelDescription: "Developers + Maintainers",
+			},
+		},
+	}
+
+	opt = &ProtectGroupEnvironmentOptions{
+		Name: String(environmentName),
+		DeployAccessLevels: &[]*GroupEnvironmentAccessOptions{
+			{AccessLevel: AccessLevel(30)},
+		},
+	}
+	environment, _, err = client.GroupProtectedEnvironments.ProtectGroupEnvironment(2, opt)
+	assert.NoError(t, err, "failed to get response")
+	assert.Equal(t, expected, environment)
+}
+
+func TestGroupUpdateProtectedEnvironments(t *testing.T) {
+	mux, client := setup(t)
+
+	// Test with DeployAccessLevels, RequiredApprovalCount, and ApprovalRules as if adding new to existing protected environment
+	environmentName := "other"
+
+	mux.HandleFunc(fmt.Sprintf("/api/v4/groups/1/protected_environments/%s", environmentName), func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPut)
+		fmt.Fprintf(w, `{
+      "name":"%s",
+      "deploy_access_levels": [
+        {
+          "id": 42,
+          "access_level": 30,
+          "access_level_description": "Developers + Maintainers"
+        }
+      ],
+      "required_approval_count": 2,
+      "approval_rules": [
+        {
+           "id": 1,
+           "user_id": null,
+           "group_id": 10,
+           "access_level": 5,
+           "access_level_description": "devops",
+           "required_approvals": 0,
+           "group_inheritance_type": 0
+        }
+      ]
+    }`, environmentName)
+	})
+
+	expected := &GroupProtectedEnvironment{
+		Name: environmentName,
+		DeployAccessLevels: []*GroupEnvironmentAccessDescription{
+			{
+				ID:                     42,
+				AccessLevel:            30,
+				AccessLevelDescription: "Developers + Maintainers",
+			},
+		},
+		RequiredApprovalCount: 2,
+		ApprovalRules: []*GroupEnvironmentApprovalRule{
+			{
+				ID:                     1,
+				GroupID:                10,
+				AccessLevel:            5,
+				AccessLevelDescription: "devops",
+			},
+		},
+	}
+
+	opt := &UpdateGroupProtectedEnvironmentOptions{
+		Name: String(environmentName),
+		DeployAccessLevels: &[]*UpdateGroupEnvironmentAccessOptions{
+			{AccessLevel: AccessLevel(30)},
+		},
+		RequiredApprovalCount: Int(2),
+		ApprovalRules: &[]*UpdateGroupEnvironmentApprovalRuleOptions{
+			{
+				GroupID:                Int(10),
+				AccessLevel:            AccessLevel(0),
+				AccessLevelDescription: String("devops"),
+			},
+		},
+	}
+
+	environment, _, err := client.GroupProtectedEnvironments.UpdateGroupProtectedEnvironment(1, environmentName, opt)
+	assert.NoError(t, err, "failed to get response")
+	assert.Equal(t, expected, environment)
+
+	// Test with DeployAccessLevels only, as if adding new to existing protected environment
+	mux.HandleFunc(fmt.Sprintf("/api/v4/groups/2/protected_environments/%s", environmentName), func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPut)
+		fmt.Fprintf(w, `{
+      "name":"%s",
+      "deploy_access_levels": [
+        {
+          "id": 42,
+          "access_level": 30,
+          "access_level_description": "Developers + Maintainers"
+        }
+      ]
+    }`, environmentName)
+	})
+
+	expected = &GroupProtectedEnvironment{
+		Name: environmentName,
+		DeployAccessLevels: []*GroupEnvironmentAccessDescription{
+			{
+				ID:                     42,
+				AccessLevel:            30,
+				AccessLevelDescription: "Developers + Maintainers",
+			},
+		},
+	}
+
+	opt = &UpdateGroupProtectedEnvironmentOptions{
+		Name: String(environmentName),
+		DeployAccessLevels: &[]*UpdateGroupEnvironmentAccessOptions{
+			{AccessLevel: AccessLevel(30)},
+		},
+	}
+	environment, _, err = client.GroupProtectedEnvironments.UpdateGroupProtectedEnvironment(2, environmentName, opt)
+	assert.NoError(t, err, "failed to get response")
+	assert.Equal(t, expected, environment)
+
+	// Test update to DeployAccessLevel
+	mux.HandleFunc(fmt.Sprintf("/api/v4/groups/3/protected_environments/%s", environmentName), func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPut)
+		fmt.Fprintf(w, `{
+      "name":"%s",
+      "deploy_access_levels": [
+        {
+          "id": 42,
+          "access_level": 30,
+          "access_level_description": "Developers + Maintainers"
+        }
+      ],
+	  "required_approval_count": 2
+    }`, environmentName)
+	})
+
+	expected = &GroupProtectedEnvironment{
+		Name: environmentName,
+		DeployAccessLevels: []*GroupEnvironmentAccessDescription{
+			{
+				ID:                     42,
+				AccessLevel:            30,
+				AccessLevelDescription: "Developers + Maintainers",
+			},
+		},
+		RequiredApprovalCount: 2,
+	}
+
+	opt = &UpdateGroupProtectedEnvironmentOptions{
+		Name: String(environmentName),
+		DeployAccessLevels: &[]*UpdateGroupEnvironmentAccessOptions{
+			{
+				ID:          Int(42),
+				AccessLevel: AccessLevel(30),
+			},
+		},
+	}
+	environment, _, err = client.GroupProtectedEnvironments.UpdateGroupProtectedEnvironment(3, environmentName, opt)
+	assert.NoError(t, err, "failed to get response")
+	assert.Equal(t, expected, environment)
+
+	// Test update to ApprovalRules
+	mux.HandleFunc(fmt.Sprintf("/api/v4/groups/4/protected_environments/%s", environmentName), func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPut)
+		fmt.Fprintf(w, `{
+      "name":"%s",
+      "deploy_access_levels": [
+        {
+          "id": 42,
+          "access_level": 30,
+          "access_level_description": "Developers + Maintainers"
+        }
+      ],
+      "required_approval_count": 2,
+      "approval_rules": [
+        {
+           "id": 1,
+           "user_id": null,
+           "group_id": 10,
+           "access_level": 5,
+           "access_level_description": "devops",
+           "required_approvals": 0,
+           "group_inheritance_type": 0
+        }
+      ]
+    }`, environmentName)
+	})
+
+	expected = &GroupProtectedEnvironment{
+		Name: environmentName,
+		DeployAccessLevels: []*GroupEnvironmentAccessDescription{
+			{
+				ID:                     42,
+				AccessLevel:            30,
+				AccessLevelDescription: "Developers + Maintainers",
+			},
+		},
+		RequiredApprovalCount: 2,
+		ApprovalRules: []*GroupEnvironmentApprovalRule{
+			{
+				ID:                     1,
+				GroupID:                10,
+				AccessLevel:            5,
+				AccessLevelDescription: "devops",
+			},
+		},
+	}
+
+	opt = &UpdateGroupProtectedEnvironmentOptions{
+		Name: String(environmentName),
+		ApprovalRules: &[]*UpdateGroupEnvironmentApprovalRuleOptions{
+			{
+				ID:                     Int(1),
+				GroupID:                Int(10),
+				AccessLevel:            AccessLevel(0),
+				AccessLevelDescription: String("devops"),
+			},
+		},
+	}
+
+	environment, _, err = client.GroupProtectedEnvironments.UpdateGroupProtectedEnvironment(4, environmentName, opt)
+	assert.NoError(t, err, "failed to get response")
+	assert.Equal(t, expected, environment)
+
+	// Test destroy ApprovalRule
+	mux.HandleFunc(fmt.Sprintf("/api/v4/groups/5/protected_environments/%s", environmentName), func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPut)
+		fmt.Fprintf(w, `{
+      "name":"%s",
+      "deploy_access_levels": [
+        {
+          "id": 42,
+          "access_level": 30,
+          "access_level_description": "Developers + Maintainers"
+        }
+      ],
+      "required_approval_count": 0,
+      "approval_rules": []
+    }`, environmentName)
+	})
+
+	expected = &GroupProtectedEnvironment{
+		Name: environmentName,
+		DeployAccessLevels: []*GroupEnvironmentAccessDescription{
+			{
+				ID:                     42,
+				AccessLevel:            30,
+				AccessLevelDescription: "Developers + Maintainers",
+			},
+		},
+		RequiredApprovalCount: 0,
+		ApprovalRules:         []*GroupEnvironmentApprovalRule{},
+	}
+
+	opt = &UpdateGroupProtectedEnvironmentOptions{
+		Name: String(environmentName),
+		ApprovalRules: &[]*UpdateGroupEnvironmentApprovalRuleOptions{
+			{
+				ID:      Int(1),
+				Destroy: Bool(true),
+			},
+		},
+		RequiredApprovalCount: Int(0),
+	}
+
+	environment, _, err = client.GroupProtectedEnvironments.UpdateGroupProtectedEnvironment(5, environmentName, opt)
+	assert.NoError(t, err, "failed to get response")
+	assert.Equal(t, expected, environment)
+}
+
+func TestGroupUnprotectEnvironments(t *testing.T) {
+	mux, client := setup(t)
+
+	mux.HandleFunc("/api/v4/groups/1/protected_environments/staging", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodDelete)
+	})
+
+	resp, err := client.GroupProtectedEnvironments.UnprotectGroupEnvironment(1, "staging")
+	assert.NoError(t, err, "failed to get response")
+	assert.Equal(t, http.StatusOK, resp.StatusCode)
+}
diff --git a/snippets_test.go b/snippets_test.go
index 7da054b..42f904c 100644
--- a/snippets_test.go
+++ b/snippets_test.go
@@ -129,20 +129,3 @@ func TestSnippetsService_ExploreSnippets(t *testing.T) {
 	want := []*Snippet{{ID: 42, Title: "test"}}
 	require.Equal(t, want, ss)
 }
-
-func TestSnippetsService_ListAllSnippets(t *testing.T) {
-	mux, client := setup(t)
-
-	mux.HandleFunc("/api/v4/snippets/all", func(w http.ResponseWriter, r *http.Request) {
-		testMethod(t, r, http.MethodGet)
-		fmt.Fprint(w, `[{"id":113,"title":"Internal Snippet"}, {"id":114,"title":"Private Snippet"}]`)
-	})
-
-	opt := &ListAllSnippetsOptions{ListOptions: ListOptions{Page: 1, PerPage: 10}}
-
-	ss, _, err := client.Snippets.ListAllSnippets(opt)
-	require.NoError(t, err)
-
-	want := []*Snippet{{ID: 113, Title: "Internal Snippet"}, {ID: 114, Title: "Private Snippet"}}
-	require.Equal(t, want, ss)
-}