Repository: xanzy/go-gitlab
Pull request #1916 (2024-04-15 21:17:56)

Add new project and group access token events to webhook event types
👋 

This PR complements #1913 by also supporting the new events when parsing webhooks.

**Docs/Examples:** https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#project-and-group-access-token-events

Let me know if you have any questions :)

Thanks!


Patch:
diff --git a/event_parsing.go b/event_parsing.go
index 3943aba..0c3f2ff 100644
--- a/event_parsing.go
+++ b/event_parsing.go
@@ -27,24 +27,25 @@ type EventType string
 
 // List of available event types.
 const (
-	EventConfidentialIssue EventType = "Confidential Issue Hook"
-	EventConfidentialNote  EventType = "Confidential Note Hook"
-	EventTypeBuild         EventType = "Build Hook"
-	EventTypeDeployment    EventType = "Deployment Hook"
-	EventTypeFeatureFlag   EventType = "Feature Flag 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"
-	EventTypePush          EventType = "Push Hook"
-	EventTypeRelease       EventType = "Release Hook"
-	EventTypeServiceHook   EventType = "Service Hook"
-	EventTypeSubGroup      EventType = "Subgroup Hook"
-	EventTypeSystemHook    EventType = "System Hook"
-	EventTypeTagPush       EventType = "Tag Push Hook"
-	EventTypeWikiPage      EventType = "Wiki Page Hook"
+	EventConfidentialIssue       EventType = "Confidential Issue Hook"
+	EventConfidentialNote        EventType = "Confidential Note Hook"
+	EventTypeBuild               EventType = "Build Hook"
+	EventTypeDeployment          EventType = "Deployment Hook"
+	EventTypeFeatureFlag         EventType = "Feature Flag 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"
+	EventTypePush                EventType = "Push Hook"
+	EventTypeRelease             EventType = "Release Hook"
+	EventTypeResourceAccessToken EventType = "Resource Access Token Hook"
+	EventTypeServiceHook         EventType = "Service Hook"
+	EventTypeSubGroup            EventType = "Subgroup Hook"
+	EventTypeSystemHook          EventType = "System Hook"
+	EventTypeTagPush             EventType = "Tag Push Hook"
+	EventTypeWikiPage            EventType = "Wiki Page Hook"
 )
 
 const (
@@ -252,6 +253,24 @@ func ParseWebhook(eventType EventType, payload []byte) (event interface{}, err e
 		event = &PushEvent{}
 	case EventTypeRelease:
 		event = &ReleaseEvent{}
+	case EventTypeResourceAccessToken:
+		data := map[string]interface{}{}
+		err := json.Unmarshal(payload, &data)
+		if err != nil {
+			return nil, err
+		}
+
+		_, groupEvent := data["group"]
+		_, projectEvent := data["project"]
+
+		switch {
+		case groupEvent:
+			event = &GroupResourceAccessTokenEvent{}
+		case projectEvent:
+			event = &ProjectResourceAccessTokenEvent{}
+		default:
+			return nil, fmt.Errorf("unexpected resource access token payload")
+		}
 	case EventTypeServiceHook:
 		service := &serviceEvent{}
 		err := json.Unmarshal(payload, service)
diff --git a/event_webhook_types.go b/event_webhook_types.go
index 01b0983..b75a8eb 100644
--- a/event_webhook_types.go
+++ b/event_webhook_types.go
@@ -131,7 +131,7 @@ type CommitCommentEvent struct {
 	} `json:"commit"`
 }
 
-// DeploymentEvent represents a deployment event
+// DeploymentEvent represents a deployment event.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#deployment-events
@@ -171,7 +171,7 @@ type DeploymentEvent struct {
 	CommitTitle string     `json:"commit_title"`
 }
 
-// FeatureFlagEvent represents a feature flag event
+// FeatureFlagEvent represents a feature flag event.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#feature-flag-events
@@ -205,6 +205,30 @@ type FeatureFlagEvent struct {
 	} `json:"object_attributes"`
 }
 
+// GroupResourceAccessTokenEvent represents a resource access token event for a
+// group.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#project-and-group-access-token-events
+type GroupResourceAccessTokenEvent struct {
+	EventName  string `json:"event_name"`
+	ObjectKind string `json:"object_kind"`
+	GroupID    int    `json:"group_id"`
+	Group      struct {
+		GroupID   int    `json:"group_id"`
+		GroupName string `json:"group_name"`
+		GroupPath string `json:"group_path"`
+		FullPath  string `json:"full_path"`
+	} `json:"group"`
+	ObjectAttributes struct {
+		ID        int        `json:"id"`
+		UserID    int        `json:"user_id"`
+		Name      string     `json:"name"`
+		CreatedAt *time.Time `json:"created_at"`
+		ExpiresAt *ISOTime   `json:"expires_at"`
+	} `json:"object_attributes"`
+}
+
 // IssueCommentEvent represents a comment on an issue event.
 //
 // GitLab API docs:
@@ -737,7 +761,8 @@ type MergeEvent struct {
 	Reviewers []*EventUser `json:"reviewers"`
 }
 
-// EventUser represents a user record in an event and is used as an even initiator or a merge assignee.
+// EventUser represents a user record in an event and is used as an even
+// initiator or a merge assignee.
 type EventUser struct {
 	ID        int    `json:"id"`
 	Name      string `json:"name"`
@@ -753,7 +778,8 @@ type MergeParams struct {
 
 // UnmarshalJSON decodes the merge parameters
 //
-// This allows support of ForceRemoveSourceBranch for both type bool (>11.9) and string (<11.9)
+// This allows support of ForceRemoveSourceBranch for both type
+// bool (>11.9) and string (<11.9)
 func (p *MergeParams) UnmarshalJSON(b []byte) error {
 	type Alias MergeParams
 	raw := struct {
@@ -899,6 +925,42 @@ type PipelineEvent struct {
 	} `json:"builds"`
 }
 
+// ProjectResourceAccessTokenEvent represents a resource access token event for
+// a project.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#project-and-group-access-token-events
+type ProjectResourceAccessTokenEvent struct {
+	EventName  string `json:"event_name"`
+	ObjectKind string `json:"object_kind"`
+	ProjectID  int    `json:"project_id"`
+	Project    struct {
+		ID                int    `json:"id"`
+		Name              string `json:"name"`
+		Description       string `json:"description"`
+		WebURL            string `json:"web_url"`
+		AvatarURL         string `json:"avatar_url"`
+		GitSSHURL         string `json:"git_ssh_url"`
+		GitHTTPURL        string `json:"git_http_url"`
+		Namespace         string `json:"namespace"`
+		VisibilityLevel   int    `json:"visibility_level"`
+		PathWithNamespace string `json:"path_with_namespace"`
+		DefaultBranch     string `json:"default_branch"`
+		CIConfigPath      string `json:"ci_config_path"`
+		Homepage          string `json:"homepage"`
+		URL               string `json:"url"`
+		SSHURL            string `json:"ssh_url"`
+		HTTPURL           string `json:"http_url"`
+	} `json:"project"`
+	ObjectAttributes struct {
+		ID        int        `json:"id"`
+		UserID    int        `json:"user_id"`
+		Name      string     `json:"name"`
+		CreatedAt *time.Time `json:"created_at"`
+		ExpiresAt *ISOTime   `json:"expires_at"`
+	} `json:"object_attributes"`
+}
+
 // PushEvent represents a push event.
 //
 // GitLab API docs:


Test patch:
diff --git a/event_parsing_webhook_test.go b/event_parsing_webhook_test.go
index c9d6b9d..163eeff 100644
--- a/event_parsing_webhook_test.go
+++ b/event_parsing_webhook_test.go
@@ -101,7 +101,7 @@ func TestParseCommitCommentHook(t *testing.T) {
 	}
 }
 
-func TestParseFeatureFLagHook(t *testing.T) {
+func TestParseFeatureFlagHook(t *testing.T) {
 	raw := loadFixture("testdata/webhooks/feature_flag.json")
 
 	parsedEvent, err := ParseWebhook("Feature Flag Hook", raw)
@@ -147,6 +147,24 @@ func TestParseFeatureFLagHook(t *testing.T) {
 	}
 }
 
+func TestParseGroupResourceAccessTokenHook(t *testing.T) {
+	raw := loadFixture("testdata/webhooks/resource_access_token_group.json")
+
+	parsedEvent, err := ParseWebhook("Resource Access Token Hook", raw)
+	if err != nil {
+		t.Errorf("Error parsing group resource access token hook: %s", err)
+	}
+
+	event, ok := parsedEvent.(*GroupResourceAccessTokenEvent)
+	if !ok {
+		t.Errorf("Expected GroupResourceAccessTokenEvent, but parsing produced %T", parsedEvent)
+	}
+
+	if event.GroupID != 35 {
+		t.Errorf("GroupID is %v, want %v", event.GroupID, 35)
+	}
+}
+
 func TestParseHookWebHook(t *testing.T) {
 	parsedEvent1, err := ParseHook("Merge Request Hook", loadFixture("testdata/webhooks/merge_request.json"))
 	if err != nil {
@@ -351,6 +369,24 @@ func TestParsePipelineHook(t *testing.T) {
 	}
 }
 
+func TestParseProjectResourceAccessTokenHook(t *testing.T) {
+	raw := loadFixture("testdata/webhooks/resource_access_token_project.json")
+
+	parsedEvent, err := ParseWebhook("Resource Access Token Hook", raw)
+	if err != nil {
+		t.Errorf("Error parsing project resource access token hook: %s", err)
+	}
+
+	event, ok := parsedEvent.(*ProjectResourceAccessTokenEvent)
+	if !ok {
+		t.Errorf("Expected ProjectResourceAccessTokenEvent, but parsing produced %T", parsedEvent)
+	}
+
+	if event.ProjectID != 7 {
+		t.Errorf("ProjectID is %v, want %v", event.ProjectID, 7)
+	}
+}
+
 func TestParsePushHook(t *testing.T) {
 	raw := loadFixture("testdata/webhooks/push.json")
 
diff --git a/event_webhook_types_test.go b/event_webhook_types_test.go
index 29a1969..f31b2fe 100644
--- a/event_webhook_types_test.go
+++ b/event_webhook_types_test.go
@@ -286,6 +286,48 @@ func TestFeatureFlagEventUnmarshal(t *testing.T) {
 	}
 }
 
+func TestGroupResourceAccessTokenEventUnmarshal(t *testing.T) {
+	jsonObject := loadFixture("testdata/webhooks/resource_access_token_group.json")
+	var event *GroupResourceAccessTokenEvent
+	err := json.Unmarshal(jsonObject, &event)
+	if err != nil {
+		t.Errorf("could not unmarshal event: %v\n ", err.Error())
+	}
+
+	if event == nil {
+		t.Errorf("event is null")
+	}
+
+	createdAt, err := time.Parse(time.RFC3339, "2024-02-05T03:13:44.855Z")
+	if err != nil {
+		t.Fatalf("could not parse time: %v", err)
+	}
+
+	expiresAt, err := ParseISOTime("2024-01-26")
+	if err != nil {
+		t.Fatalf("could not parse ISO time: %v", err)
+	}
+
+	expected := &GroupResourceAccessTokenEvent{
+		GroupID:    35,
+		ObjectKind: "access_token",
+		EventName:  "expiring_access_token",
+	}
+
+	expected.Group.GroupID = 35
+	expected.Group.GroupName = "Twitter"
+	expected.Group.GroupPath = "twitter"
+	expected.Group.FullPath = "twitter"
+
+	expected.ObjectAttributes.ID = 25
+	expected.ObjectAttributes.UserID = 90
+	expected.ObjectAttributes.Name = "acd"
+	expected.ObjectAttributes.CreatedAt = &createdAt
+	expected.ObjectAttributes.ExpiresAt = &expiresAt
+
+	assert.Equal(t, expected, event)
+}
+
 func TestIssueCommentEventUnmarshal(t *testing.T) {
 	jsonObject := loadFixture("testdata/webhooks/note_issue.json")
 
@@ -1014,6 +1056,60 @@ func TestPipelineEventUnmarshal(t *testing.T) {
 	}
 }
 
+func TestProjectResourceAccessTokenEventUnmarshal(t *testing.T) {
+	jsonObject := loadFixture("testdata/webhooks/resource_access_token_project.json")
+	var event *ProjectResourceAccessTokenEvent
+	err := json.Unmarshal(jsonObject, &event)
+	if err != nil {
+		t.Errorf("could not unmarshal event: %v\n ", err.Error())
+	}
+
+	if event == nil {
+		t.Errorf("event is null")
+	}
+
+	createdAt, err := time.Parse(time.RFC3339, "2024-02-05T03:13:44.855Z")
+	if err != nil {
+		t.Fatalf("could not parse time: %v", err)
+	}
+
+	expiresAt, err := ParseISOTime("2024-01-26")
+	if err != nil {
+		t.Fatalf("could not parse ISO time: %v", err)
+	}
+
+	expected := &ProjectResourceAccessTokenEvent{
+		ProjectID:  7,
+		ObjectKind: "access_token",
+		EventName:  "expiring_access_token",
+	}
+
+	expected.ObjectAttributes.ID = 25
+	expected.ObjectAttributes.UserID = 90
+	expected.ObjectAttributes.Name = "acd"
+	expected.ObjectAttributes.CreatedAt = &createdAt
+	expected.ObjectAttributes.ExpiresAt = &expiresAt
+
+	expected.Project.ID = 7
+	expected.Project.Name = "Flight"
+	expected.Project.Description = "Eum dolore maxime atque reprehenderit voluptatem."
+	expected.Project.WebURL = "https://example.com/flightjs/Flight"
+	expected.Project.AvatarURL = ""
+	expected.Project.GitSSHURL = "ssh://git@example.com/flightjs/Flight.git"
+	expected.Project.GitHTTPURL = "https://example.com/flightjs/Flight.git"
+	expected.Project.Namespace = "Flightjs"
+	expected.Project.VisibilityLevel = 0
+	expected.Project.PathWithNamespace = "flightjs/Flight"
+	expected.Project.DefaultBranch = "master"
+	expected.Project.CIConfigPath = ""
+	expected.Project.Homepage = "https://example.com/flightjs/Flight"
+	expected.Project.URL = "ssh://git@example.com/flightjs/Flight.git"
+	expected.Project.SSHURL = "ssh://git@example.com/flightjs/Flight.git"
+	expected.Project.HTTPURL = "https://example.com/flightjs/Flight.git"
+
+	assert.Equal(t, expected, event)
+}
+
 func TestPushEventUnmarshal(t *testing.T) {
 	jsonObject := loadFixture("testdata/webhooks/push.json")
 	var event *PushEvent
diff --git a/testdata/webhooks/resource_access_token_group.json b/testdata/webhooks/resource_access_token_group.json
new file mode 100644
index 0000000..613c795
--- /dev/null
+++ b/testdata/webhooks/resource_access_token_group.json
@@ -0,0 +1,18 @@
+{
+  "object_kind": "access_token",
+  "group_id": 35,
+  "group": {
+    "group_name": "Twitter",
+    "group_path": "twitter",
+    "full_path": "twitter",
+    "group_id": 35
+  },
+  "object_attributes": {
+    "user_id": 90,
+    "created_at": "2024-02-05T03:13:44.855Z",
+    "id": 25,
+    "name": "acd",
+    "expires_at": "2024-01-26"
+  },
+  "event_name": "expiring_access_token"
+}
diff --git a/testdata/webhooks/resource_access_token_project.json b/testdata/webhooks/resource_access_token_project.json
new file mode 100644
index 0000000..efc4dd9
--- /dev/null
+++ b/testdata/webhooks/resource_access_token_project.json
@@ -0,0 +1,30 @@
+{
+  "object_kind": "access_token",
+  "project_id": 7,
+  "project": {
+    "id": 7,
+    "name": "Flight",
+    "description": "Eum dolore maxime atque reprehenderit voluptatem.",
+    "web_url": "https://example.com/flightjs/Flight",
+    "avatar_url": null,
+    "git_ssh_url": "ssh://git@example.com/flightjs/Flight.git",
+    "git_http_url": "https://example.com/flightjs/Flight.git",
+    "namespace": "Flightjs",
+    "visibility_level": 0,
+    "path_with_namespace": "flightjs/Flight",
+    "default_branch": "master",
+    "ci_config_path": null,
+    "homepage": "https://example.com/flightjs/Flight",
+    "url": "ssh://git@example.com/flightjs/Flight.git",
+    "ssh_url": "ssh://git@example.com/flightjs/Flight.git",
+    "http_url": "https://example.com/flightjs/Flight.git"
+  },
+  "object_attributes": {
+    "user_id": 90,
+    "created_at": "2024-02-05T03:13:44.855Z",
+    "id": 25,
+    "name": "acd",
+    "expires_at": "2024-01-26"
+  },
+  "event_name": "expiring_access_token"
+}