Repository: xanzy/go-gitlab
Pull request #1524 (2022-08-10 16:21:02)

Add member webhook
Hi,

this PR adds group member events as documented [here](https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#group-member-events).



Best regards
Markus


Patch:
diff --git a/event_parsing.go b/event_parsing.go
index e17a0f1..bd668b0 100644
--- a/event_parsing.go
+++ b/event_parsing.go
@@ -33,6 +33,7 @@ const (
 	EventTypeDeployment    EventType = "Deployment Hook"
 	EventTypeIssue         EventType = "Issue Hook"
 	EventTypeJob           EventType = "Job Hook"
+	EventTypeMember        EventType = "Member Hook"
 	EventTypeMergeRequest  EventType = "Merge Request Hook"
 	EventTypeNote          EventType = "Note Hook"
 	EventTypePipeline      EventType = "Pipeline Hook"
@@ -80,20 +81,19 @@ func HookEventType(r *http.Request) EventType {
 //
 // Example usage:
 //
-// func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-//     payload, err := ioutil.ReadAll(r.Body)
-//     if err != nil { ... }
-//     event, err := gitlab.ParseHook(gitlab.HookEventType(r), payload)
-//     if err != nil { ... }
-//     switch event := event.(type) {
-//     case *gitlab.PushEvent:
-//         processPushEvent(event)
-//     case *gitlab.MergeEvent:
-//         processMergeEvent(event)
-//     ...
-//     }
-// }
-//
+//	func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+//	    payload, err := ioutil.ReadAll(r.Body)
+//	    if err != nil { ... }
+//	    event, err := gitlab.ParseHook(gitlab.HookEventType(r), payload)
+//	    if err != nil { ... }
+//	    switch event := event.(type) {
+//	    case *gitlab.PushEvent:
+//	        processPushEvent(event)
+//	    case *gitlab.MergeEvent:
+//	        processMergeEvent(event)
+//	    ...
+//	    }
+//	}
 func ParseHook(eventType EventType, payload []byte) (event interface{}, err error) {
 	switch eventType {
 	case EventTypeSystemHook:
@@ -109,20 +109,19 @@ func ParseHook(eventType EventType, payload []byte) (event interface{}, err erro
 //
 // Example usage:
 //
-// func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-//     payload, err := ioutil.ReadAll(r.Body)
-//     if err != nil { ... }
-//     event, err := gitlab.ParseSystemhook(payload)
-//     if err != nil { ... }
-//     switch event := event.(type) {
-//     case *gitlab.PushSystemEvent:
-//         processPushSystemEvent(event)
-//     case *gitlab.MergeSystemEvent:
-//         processMergeSystemEvent(event)
-//     ...
-//     }
-// }
-//
+//	func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+//	    payload, err := ioutil.ReadAll(r.Body)
+//	    if err != nil { ... }
+//	    event, err := gitlab.ParseSystemhook(payload)
+//	    if err != nil { ... }
+//	    switch event := event.(type) {
+//	    case *gitlab.PushSystemEvent:
+//	        processPushSystemEvent(event)
+//	    case *gitlab.MergeSystemEvent:
+//	        processMergeSystemEvent(event)
+//	    ...
+//	    }
+//	}
 func ParseSystemhook(payload []byte) (event interface{}, err error) {
 	e := &systemHookEvent{}
 	err = json.Unmarshal(payload, e)
@@ -195,20 +194,19 @@ func WebhookEventType(r *http.Request) EventType {
 //
 // Example usage:
 //
-// func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-//     payload, err := ioutil.ReadAll(r.Body)
-//     if err != nil { ... }
-//     event, err := gitlab.ParseWebhook(gitlab.HookEventType(r), payload)
-//     if err != nil { ... }
-//     switch event := event.(type) {
-//     case *gitlab.PushEvent:
-//         processPushEvent(event)
-//     case *gitlab.MergeEvent:
-//         processMergeEvent(event)
-//     ...
-//     }
-// }
-//
+//	func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+//	    payload, err := ioutil.ReadAll(r.Body)
+//	    if err != nil { ... }
+//	    event, err := gitlab.ParseWebhook(gitlab.HookEventType(r), payload)
+//	    if err != nil { ... }
+//	    switch event := event.(type) {
+//	    case *gitlab.PushEvent:
+//	        processPushEvent(event)
+//	    case *gitlab.MergeEvent:
+//	        processMergeEvent(event)
+//	    ...
+//	    }
+//	}
 func ParseWebhook(eventType EventType, payload []byte) (event interface{}, err error) {
 	switch eventType {
 	case EventTypeBuild:
@@ -219,6 +217,8 @@ func ParseWebhook(eventType EventType, payload []byte) (event interface{}, err e
 		event = &IssueEvent{}
 	case EventTypeJob:
 		event = &JobEvent{}
+	case EventTypeMember:
+		event = &MemberEvent{}
 	case EventTypeMergeRequest:
 		event = &MergeEvent{}
 	case EventTypeNote, EventConfidentialNote:
diff --git a/event_webhook_types.go b/event_webhook_types.go
index f3900dc..70c10f3 100644
--- a/event_webhook_types.go
+++ b/event_webhook_types.go
@@ -347,6 +347,26 @@ type JobEvent struct {
 	} `json:"runner"`
 }
 
+// MemberEvent represents a member event.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#group-member-events
+type MemberEvent struct {
+	CreatedAt    *time.Time `json:"created_at"`
+	UpdatedAt    *time.Time `json:"updated_at"`
+	GroupName    string     `json:"group_name"`
+	GroupPath    string     `json:"group_path"`
+	GroupID      int        `json:"group_id"`
+	UserUsername string     `json:"user_username"`
+	UserName     string     `json:"user_name"`
+	UserEmail    string     `json:"user_email"`
+	UserID       int        `json:"user_id"`
+	GroupAccess  string     `json:"group_access"`
+	GroupPlan    string     `json:"group_plan"`
+	ExpiresAt    *time.Time `json:"expires_at"`
+	EventName    string     `json:"event_name"`
+}
+
 // MergeCommentEvent represents a comment on a merge event.
 //
 // GitLab API docs:
@@ -537,10 +557,6 @@ type MergeEvent struct {
 			Previous []*EventUser `json:"previous"`
 			Current  []*EventUser `json:"current"`
 		} `json:"assignees"`
-		Reviewers struct {
-			Previous []*EventUser `json:"previous"`
-			Current  []*EventUser `json:"current"`
-		} `json:"reviewers"`
 		Description struct {
 			Previous string `json:"previous"`
 			Current  string `json:"current"`
diff --git a/group_hooks.go b/group_hooks.go
index 6e75d6a..acb51d9 100644
--- a/group_hooks.go
+++ b/group_hooks.go
@@ -47,22 +47,17 @@ type GroupHook struct {
 	CreatedAt                *time.Time `json:"created_at"`
 }
 
-// ListGroupHooksOptions represents the available ListGroupHooks() options.
-//
-// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#list-group-hooks
-type ListGroupHooksOptions ListOptions
-
 // ListGroupHooks gets a list of group hooks.
 //
 // GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#list-group-hooks
-func (s *GroupsService) ListGroupHooks(gid interface{}, opt *ListGroupHooksOptions, options ...RequestOptionFunc) ([]*GroupHook, *Response, error) {
+func (s *GroupsService) ListGroupHooks(gid interface{}, options ...RequestOptionFunc) ([]*GroupHook, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
 		return nil, nil, err
 	}
 	u := fmt.Sprintf("groups/%s/hooks", PathEscape(group))
 
-	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
+	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/groups.go b/groups.go
index 96a1e3a..cfd9ef5 100644
--- a/groups.go
+++ b/groups.go
@@ -75,7 +75,6 @@ type Group struct {
 	LDAPCN                         string           `json:"ldap_cn"`
 	LDAPAccess                     AccessLevelValue `json:"ldap_access"`
 	LDAPGroupLinks                 []*LDAPGroupLink `json:"ldap_group_links"`
-	SAMLGroupLinks                 []*SAMLGroupLink `json:"saml_group_links"`
 	SharedRunnersMinutesLimit      int              `json:"shared_runners_minutes_limit"`
 	ExtraSharedRunnersMinutesLimit int              `json:"extra_shared_runners_minutes_limit"`
 	PreventForkingOutsideGroup     bool             `json:"prevent_forking_outside_group"`
@@ -101,14 +100,6 @@ type LDAPGroupLink struct {
 	Provider    string           `json:"provider"`
 }
 
-// SAMLGroupLink represents a GitLab SAML group link.
-//
-// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#saml-group-links
-type SAMLGroupLink struct {
-	Name        string           `json:"name"`
-	AccessLevel AccessLevelValue `json:"access_level"`
-}
-
 // ListGroupsOptions represents the available ListGroups() options.
 //
 // GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#list-project-groups
@@ -753,113 +744,6 @@ func (s *GroupsService) DeleteGroupLDAPLinkForProvider(gid interface{}, provider
 	return s.client.Do(req, nil)
 }
 
-// ListGroupSAMLLinks lists the group's SAML links. Available only for users who
-// can edit groups.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/ee/api/groups.html#list-saml-group-links
-func (s *GroupsService) ListGroupSAMLLinks(gid interface{}, options ...RequestOptionFunc) ([]*SAMLGroupLink, *Response, error) {
-	group, err := parseID(gid)
-	if err != nil {
-		return nil, nil, err
-	}
-	u := fmt.Sprintf("groups/%s/saml_group_links", PathEscape(group))
-
-	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	var gl []*SAMLGroupLink
-	resp, err := s.client.Do(req, &gl)
-	if err != nil {
-		return nil, resp, err
-	}
-
-	return gl, resp, nil
-}
-
-// GetGroupSAMLLink get a specific group SAML link. Available only for users who
-// can edit groups.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/ee/api/groups.html#get-saml-group-link
-func (s *GroupsService) GetGroupSAMLLink(gid interface{}, samlGroupName string, options ...RequestOptionFunc) (*SAMLGroupLink, *Response, error) {
-	group, err := parseID(gid)
-	if err != nil {
-		return nil, nil, err
-	}
-	u := fmt.Sprintf("groups/%s/saml_group_links/%s", PathEscape(group), PathEscape(samlGroupName))
-
-	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	gl := new(SAMLGroupLink)
-	resp, err := s.client.Do(req, &gl)
-	if err != nil {
-		return nil, resp, err
-	}
-
-	return gl, resp, nil
-}
-
-// AddGroupSAMLLinkOptions represents the available AddGroupSAMLLink() options.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/ee/api/groups.html#add-saml-group-link
-type AddGroupSAMLLinkOptions struct {
-	SAMLGroupName *string           `url:"saml_group_name,omitempty" json:"saml_group_name,omitempty"`
-	AccessLevel   *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"`
-}
-
-// AddGroupSAMLLink creates a new group SAML link. Available only for users who
-// can edit groups.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/ee/api/groups.html#add-saml-group-link
-func (s *GroupsService) AddGroupSAMLLink(gid interface{}, opt *AddGroupSAMLLinkOptions, options ...RequestOptionFunc) (*SAMLGroupLink, *Response, error) {
-	group, err := parseID(gid)
-	if err != nil {
-		return nil, nil, err
-	}
-	u := fmt.Sprintf("groups/%s/saml_group_links", PathEscape(group))
-
-	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	gl := new(SAMLGroupLink)
-	resp, err := s.client.Do(req, &gl)
-	if err != nil {
-		return nil, resp, err
-	}
-
-	return gl, resp, err
-}
-
-// DeleteGroupSAMLLink deletes a group SAML link. Available only for users who
-// can edit groups.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/ee/api/groups.html#delete-saml-group-link
-func (s *GroupsService) DeleteGroupSAMLLink(gid interface{}, samlGroupName string, options ...RequestOptionFunc) (*Response, error) {
-	group, err := parseID(gid)
-	if err != nil {
-		return nil, err
-	}
-	u := fmt.Sprintf("groups/%s/saml_group_links/%s", PathEscape(group), PathEscape(samlGroupName))
-
-	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
-	if err != nil {
-		return nil, err
-	}
-
-	return s.client.Do(req, nil)
-}
-
 // ShareGroupWithGroupOptions represents the available ShareGroupWithGroup() options.
 //
 // GitLab API docs:
diff --git a/issue_links.go b/issue_links.go
index 4fe81ff..f40b65c 100644
--- a/issue_links.go
+++ b/issue_links.go
@@ -45,12 +45,12 @@ type IssueLink struct {
 //
 // GitLab API docs:
 // https://docs.gitlab.com/ee/api/issue_links.html#list-issue-relations
-func (s *IssueLinksService) ListIssueRelations(pid interface{}, issue int, options ...RequestOptionFunc) ([]*Issue, *Response, error) {
+func (s *IssueLinksService) ListIssueRelations(pid interface{}, issueIID int, options ...RequestOptionFunc) ([]*Issue, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
 		return nil, nil, err
 	}
-	u := fmt.Sprintf("projects/%s/issues/%d/links", PathEscape(project), issue)
+	u := fmt.Sprintf("projects/%s/issues/%d/links", PathEscape(project), issueIID)
 
 	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
 	if err != nil {
@@ -66,31 +66,6 @@ func (s *IssueLinksService) ListIssueRelations(pid interface{}, issue int, optio
 	return is, resp, err
 }
 
-// GetIssueLink gets a specific issue link.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/ee/api/issue_links.html#get-an-issue-link
-func (s *IssueLinksService) GetIssueLink(pid interface{}, issue, issueLink int, options ...RequestOptionFunc) (*IssueLink, *Response, error) {
-	project, err := parseID(pid)
-	if err != nil {
-		return nil, nil, err
-	}
-	u := fmt.Sprintf("projects/%s/issues/%d/links/%d", PathEscape(project), issue, issueLink)
-
-	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	il := new(IssueLink)
-	resp, err := s.client.Do(req, il)
-	if err != nil {
-		return nil, resp, err
-	}
-
-	return il, resp, err
-}
-
 // CreateIssueLinkOptions represents the available CreateIssueLink() options.
 //
 // GitLab API docs: https://docs.gitlab.com/ee/api/issue_links.html
@@ -105,12 +80,12 @@ type CreateIssueLinkOptions struct {
 //
 // GitLab API docs:
 // https://docs.gitlab.com/ee/api/issue_links.html#create-an-issue-link
-func (s *IssueLinksService) CreateIssueLink(pid interface{}, issue int, opt *CreateIssueLinkOptions, options ...RequestOptionFunc) (*IssueLink, *Response, error) {
+func (s *IssueLinksService) CreateIssueLink(pid interface{}, issueIID int, opt *CreateIssueLinkOptions, options ...RequestOptionFunc) (*IssueLink, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
 		return nil, nil, err
 	}
-	u := fmt.Sprintf("projects/%s/issues/%d/links", PathEscape(project), issue)
+	u := fmt.Sprintf("projects/%s/issues/%d/links", PathEscape(project), issueIID)
 
 	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
 	if err != nil {
@@ -130,26 +105,26 @@ func (s *IssueLinksService) CreateIssueLink(pid interface{}, issue int, opt *Cre
 //
 // GitLab API docs:
 // https://docs.gitlab.com/ee/api/issue_links.html#delete-an-issue-link
-func (s *IssueLinksService) DeleteIssueLink(pid interface{}, issue, issueLink int, options ...RequestOptionFunc) (*IssueLink, *Response, error) {
+func (s *IssueLinksService) DeleteIssueLink(pid interface{}, issueIID, issueLinkID int, options ...RequestOptionFunc) (*IssueLink, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
 		return nil, nil, err
 	}
 	u := fmt.Sprintf("projects/%s/issues/%d/links/%d",
 		PathEscape(project),
-		issue,
-		issueLink)
+		issueIID,
+		issueLinkID)
 
 	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
 	if err != nil {
 		return nil, nil, err
 	}
 
-	il := new(IssueLink)
-	resp, err := s.client.Do(req, &il)
+	i := new(IssueLink)
+	resp, err := s.client.Do(req, &i)
 	if err != nil {
 		return nil, resp, err
 	}
 
-	return il, resp, err
+	return i, resp, err
 }
diff --git a/license.go b/license.go
index ddec3be..618fde6 100644
--- a/license.go
+++ b/license.go
@@ -70,8 +70,8 @@ func (l License) String() string {
 //
 // GitLab API docs:
 // https://docs.gitlab.com/ee/api/license.html#retrieve-information-about-the-current-license
-func (s *LicenseService) GetLicense(options ...RequestOptionFunc) (*License, *Response, error) {
-	req, err := s.client.NewRequest(http.MethodGet, "license", nil, options)
+func (s *LicenseService) GetLicense() (*License, *Response, error) {
+	req, err := s.client.NewRequest(http.MethodGet, "license", nil, nil)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/projects.go b/projects.go
index 5111926..ffd42dc 100644
--- a/projects.go
+++ b/projects.go
@@ -82,7 +82,7 @@ type Project struct {
 	ForksCount                                int                        `json:"forks_count"`
 	StarCount                                 int                        `json:"star_count"`
 	RunnersToken                              string                     `json:"runners_token"`
-	PublicJobs                                bool                       `json:"public_jobs"`
+	PublicBuilds                              bool                       `json:"public_builds"`
 	AllowMergeOnSkippedPipeline               bool                       `json:"allow_merge_on_skipped_pipeline"`
 	OnlyAllowMergeIfPipelineSucceeds          bool                       `json:"only_allow_merge_if_pipeline_succeeds"`
 	OnlyAllowMergeIfAllDiscussionsAreResolved bool                       `json:"only_allow_merge_if_all_discussions_are_resolved"`
@@ -144,9 +144,6 @@ type Project struct {
 	ExternalAuthorizationClassificationLabel string             `json:"external_authorization_classification_label"`
 	RequirementsAccessLevel                  AccessControlValue `json:"requirements_access_level"`
 	SecurityAndComplianceAccessLevel         AccessControlValue `json:"security_and_compliance_access_level"`
-
-	// Deprecated members
-	PublicBuilds bool `json:"public_builds"`
 }
 
 // BasicProject included in other service responses (such as todos).
diff --git a/releases.go b/releases.go
index 7a7a555..f7876d2 100644
--- a/releases.go
+++ b/releases.go
@@ -67,12 +67,7 @@ type Release struct {
 //
 // GitLab API docs:
 // https://docs.gitlab.com/ce/api/releases/index.html#list-releases
-type ListReleasesOptions struct {
-	ListOptions
-	OrderBy                *string `url:"order_by,omitempty" json:"order_by,omitempty"`
-	Sort                   *string `url:"sort,omitempty" json:"sort,omitempty"`
-	IncludeHTMLDescription *bool   `url:"include_html_description,omitempty" json:"include_html_description,omitempty"`	
-}
+type ListReleasesOptions ListOptions
 
 // ListReleases gets a pagenated of releases accessible by the authenticated user.
 //
diff --git a/users.go b/users.go
index d03e7e1..b170e28 100644
--- a/users.go
+++ b/users.go
@@ -117,7 +117,6 @@ type ListUsersOptions struct {
 	Active          *bool `url:"active,omitempty" json:"active,omitempty"`
 	Blocked         *bool `url:"blocked,omitempty" json:"blocked,omitempty"`
 	ExcludeInternal *bool `url:"exclude_internal,omitempty" json:"exclude_internal,omitempty"`
-	ExcludeExternal *bool `url:"exclude_external,omitempty" json:"exclude_external,omitempty"`
 
 	// The options below are only available for admins.
 	Search               *string    `url:"search,omitempty" json:"search,omitempty"`


Test patch:
diff --git a/event_parsing_webhook_test.go b/event_parsing_webhook_test.go
index 373200b..f63db48 100644
--- a/event_parsing_webhook_test.go
+++ b/event_parsing_webhook_test.go
@@ -218,6 +218,24 @@ func TestParseMergeRequestCommentHook(t *testing.T) {
 	}
 }
 
+func TestParseMemberHook(t *testing.T) {
+	raw := loadFixture("testdata/webhooks/member.json")
+
+	parsedEvent, err := ParseWebhook("Member Hook", raw)
+	if err != nil {
+		t.Errorf("Error parsing member hook: %s", err)
+	}
+
+	event, ok := parsedEvent.(*MemberEvent)
+	if !ok {
+		t.Errorf("Expected MemberEvent, but parsing produced %T", parsedEvent)
+	}
+
+	if event.EventName != "user_add_to_group" {
+		t.Errorf("EventName is %v, want %v", event.EventName, "user_add_to_group")
+	}
+}
+
 func TestParseMergeRequestHook(t *testing.T) {
 	raw := loadFixture("testdata/webhooks/merge_request.json")
 
diff --git a/event_webhook_types_test.go b/event_webhook_types_test.go
index a61a0f9..d18d807 100644
--- a/event_webhook_types_test.go
+++ b/event_webhook_types_test.go
@@ -23,11 +23,13 @@ import (
 )
 
 const (
-	expectedID       = 1
-	expectedName     = "User1"
-	expectedUsername = "user1"
-	excpectedAvatar  = "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
-	expectedEmail    = "test.user1@example.com"
+	ExpectedGroup     = "webhook-test"
+	excpectedAvatar   = "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+	expectedEmail     = "test.user1@example.com"
+	expectedEventName = "user_add_to_group"
+	expectedID        = 1
+	expectedName      = "User1"
+	expectedUsername  = "user1"
 )
 
 func TestBuildEventUnmarshal(t *testing.T) {
@@ -234,17 +236,75 @@ func TestMergeEventUnmarshal(t *testing.T) {
 	if event.Assignees[0].Email != expectedEmail {
 		t.Errorf("Assignees[0].Email is %v, want %v", event.Assignees[0].Email, expectedEmail)
 	}
+}
+
+func TestMemberEventUnmarshal(t *testing.T) {
+	jsonObject := loadFixture("testdata/webhooks/member.json")
+
+	var event *MemberEvent
+	err := json.Unmarshal(jsonObject, &event)
+
+	if err != nil {
+		t.Errorf("Member Event can not unmarshaled: %v\n ", err.Error())
+	}
+
+	if event == nil {
+		t.Errorf("Member Event is null")
+	}
+
+	if event.GroupName != ExpectedGroup {
+		t.Errorf("Name is %v, want %v", event.GroupName, ExpectedGroup)
+	}
+
+	if event.GroupPath != ExpectedGroup {
+		t.Errorf("GroupPath is %v, want %v", event.GroupPath, ExpectedGroup)
+	}
+
+	if event.GroupID != 100 {
+		t.Errorf(
+			"GroupID is %v, want %v", event.GroupID, 100)
+	}
+
+	if event.UserUsername != expectedUsername {
+		t.Errorf(
+			"UserUsername is %v, want %v", event.UserUsername, expectedUsername)
+	}
+
+	if event.UserName != expectedName {
+		t.Errorf(
+			"UserName is %v, want %v", event.UserName, expectedName)
+	}
+
+	if event.UserEmail != "testuser@webhooktest.com" {
+		t.Errorf(
+			"UserEmail is %v, want %v", event.UserEmail, "testuser@webhooktest.com")
+	}
+
+	if event.UserID != 64 {
+		t.Errorf(
+			"UserID is %v, want %v", event.UserID, 64)
+	}
+
+	if event.GroupAccess != "Guest" {
+		t.Errorf(
+			"GroupAccess is %v, want %v", event.GroupAccess, "Guest")
+	}
+
+	if event.EventName != expectedEventName {
+		t.Errorf(
+			"EventName is %v, want %v", event.EventName, expectedEventName)
+	}
 
-	if len(event.Changes.Reviewers.Previous) != 0 {
-		t.Errorf("Changes.Reviewer.Previeous length is %d, want %d", len(event.Changes.Reviewers.Previous), 0)
+	if event.CreatedAt.Format(time.RFC3339) != "2020-12-11T04:57:22Z" {
+		t.Errorf("CreatedAt is %v, want %v", event.CreatedAt.Format(time.RFC3339), "2020-12-11T04:57:22Z")
 	}
 
-	if len(event.Changes.Reviewers.Current) != 1 {
-		t.Errorf("Changes.Reviewer.Current length is %d, want %d", len(event.Changes.Reviewers.Current), 1)
+	if event.UpdatedAt.Format(time.RFC3339) != "2020-12-11T04:57:22Z" {
+		t.Errorf("UpdatedAt is %v, want %v", event.UpdatedAt.Format(time.RFC3339), "2020-12-11T04:57:22Z")
 	}
 
-	if event.Changes.Reviewers.Current[0].Username != expectedUsername {
-		t.Errorf("Changes.Reviewer.Current[0].Username is %v, want %v", event.Changes.Reviewers.Current[0].Username, expectedUsername)
+	if event.ExpiresAt.Format(time.RFC3339) != "2020-12-14T00:00:00Z" {
+		t.Errorf("ExpiresAt is %v, want %v", event.ExpiresAt.Format(time.RFC3339), "2020-12-14T00:00:00Z")
 	}
 }
 
diff --git a/group_hooks_test.go b/group_hooks_test.go
index 6834231..07775c8 100644
--- a/group_hooks_test.go
+++ b/group_hooks_test.go
@@ -55,7 +55,7 @@ func TestListGroupHooks(t *testing.T) {
 ]`)
 	})
 
-	groupHooks, _, err := client.Groups.ListGroupHooks(1, nil)
+	groupHooks, _, err := client.Groups.ListGroupHooks(1)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/groups_test.go b/groups_test.go
index 0ab7b6b..8a37a43 100644
--- a/groups_test.go
+++ b/groups_test.go
@@ -364,106 +364,6 @@ func TestAddGroupLDAPLinkFilter(t *testing.T) {
 	}
 }
 
-func TestListGroupSAMLLinks(t *testing.T) {
-	mux, server, client := setup(t)
-	defer teardown(server)
-
-	mux.HandleFunc("/api/v4/groups/1/saml_group_links",
-		func(w http.ResponseWriter, r *http.Request) {
-			testMethod(t, r, http.MethodGet)
-			fmt.Fprint(w, `[
-	{
-		"access_level":30,
-		"name":"gitlab_group_example_developer"
-	},
-	{
-		"access_level":40,
-		"name":"gitlab_group_example_maintainer"
-	}
-]`)
-		})
-
-	links, _, err := client.Groups.ListGroupSAMLLinks(1)
-	if err != nil {
-		t.Errorf("Groups.ListGroupSAMLLinks returned error: %v", err)
-	}
-
-	want := []*SAMLGroupLink{
-		{
-			AccessLevel: DeveloperPermissions,
-			Name:        "gitlab_group_example_developer",
-		},
-		{
-			AccessLevel: MaintainerPermissions,
-			Name:        "gitlab_group_example_maintainer",
-		},
-	}
-	if !reflect.DeepEqual(want, links) {
-		t.Errorf("Groups.ListGroupSAMLLinks returned %+v, want %+v", links, want)
-	}
-}
-
-func TestGetGroupSAMLLink(t *testing.T) {
-	mux, server, client := setup(t)
-	defer teardown(server)
-
-	mux.HandleFunc("/api/v4/groups/1/saml_group_links/gitlab_group_example_developer",
-		func(w http.ResponseWriter, r *http.Request) {
-			testMethod(t, r, http.MethodGet)
-			fmt.Fprint(w, `
-{
-	"access_level":30,
-	"name":"gitlab_group_example_developer"
-}`)
-		})
-
-	links, _, err := client.Groups.GetGroupSAMLLink(1, "gitlab_group_example_developer")
-	if err != nil {
-		t.Errorf("Groups.GetGroupSAMLLinks returned error: %v", err)
-	}
-
-	want := &SAMLGroupLink{
-		AccessLevel: DeveloperPermissions,
-		Name:        "gitlab_group_example_developer",
-	}
-	if !reflect.DeepEqual(want, links) {
-		t.Errorf("Groups.GetGroupSAMLLink returned %+v, want %+v", links, want)
-	}
-}
-
-func TestAddGroupSAMLLink(t *testing.T) {
-	mux, server, client := setup(t)
-	defer teardown(server)
-
-	mux.HandleFunc("/api/v4/groups/1/saml_group_links",
-		func(w http.ResponseWriter, r *http.Request) {
-			testMethod(t, r, http.MethodPost)
-			fmt.Fprint(w, `
-{
-	"access_level":30,
-	"name":"gitlab_group_example_developer"
-}`)
-		})
-
-	opt := &AddGroupSAMLLinkOptions{
-		SAMLGroupName: String("gitlab_group_example_developer"),
-		AccessLevel:   AccessLevel(DeveloperPermissions),
-	}
-
-	link, _, err := client.Groups.AddGroupSAMLLink(1, opt)
-	if err != nil {
-		t.Errorf("Groups.AddGroupSAMLLink returned error: %v", err)
-	}
-
-	want := &SAMLGroupLink{
-		AccessLevel: DeveloperPermissions,
-		Name:        "gitlab_group_example_developer",
-	}
-	if !reflect.DeepEqual(want, link) {
-		t.Errorf("Groups.AddGroupSAMLLink returned %+v, want %+v", link, want)
-	}
-}
-
 func TestRestoreGroup(t *testing.T) {
 	mux, server, client := setup(t)
 	defer teardown(server)
diff --git a/testdata/webhooks/member.json b/testdata/webhooks/member.json
new file mode 100644
index 0000000..4924d41
--- /dev/null
+++ b/testdata/webhooks/member.json
@@ -0,0 +1,15 @@
+{
+  "created_at": "2020-12-11T04:57:22Z",
+  "updated_at": "2020-12-11T04:57:22Z",
+  "group_name": "webhook-test",
+  "group_path": "webhook-test",
+  "group_id": 100,
+  "user_username": "user1",
+  "user_name": "User1",
+  "user_email": "testuser@webhooktest.com",
+  "user_id": 64,
+  "group_access": "Guest",
+  "group_plan": null,
+  "expires_at": "2020-12-14T00:00:00Z",
+  "event_name": "user_add_to_group"
+}
diff --git a/testdata/webhooks/merge_request.json b/testdata/webhooks/merge_request.json
index 721dd89..c52d560 100644
--- a/testdata/webhooks/merge_request.json
+++ b/testdata/webhooks/merge_request.json
@@ -159,18 +159,6 @@
         "type": "ProjectLabel",
         "group_id": 41
       }]
-    },
-    "reviewers": {
-      "previous": [],
-      "current": [
-        {
-          "id": 1,
-          "name": "User1",
-          "username": "user1",
-          "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40&d=identicon",
-          "email": "[REDACTED]"
-        }
-      ]
     }
   }
 }