diff --git a/projects.go b/projects.go index 6c34de2de..3d98c900a 100644 --- a/projects.go +++ b/projects.go @@ -1197,31 +1197,43 @@ type ProjectMember struct { AvatarURL string `json:"avatar_url"` } +// ProjectHookCustomHeader represents a project hook custom header +// Note: "Key" is returned from the Get operation, but "Value" is not +// The List operation doesn't return any headers at all. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/projects.html#list-project-hooks +type ProjectHookCustomHeader struct { + Key string `json:"key"` + Value string `json:"value"` +} + // ProjectHook represents a project hook. // // GitLab API docs: // https://docs.gitlab.com/ee/api/projects.html#list-project-hooks type ProjectHook struct { - ID int `json:"id"` - URL string `json:"url"` - ConfidentialNoteEvents bool `json:"confidential_note_events"` - ProjectID int `json:"project_id"` - PushEvents bool `json:"push_events"` - PushEventsBranchFilter string `json:"push_events_branch_filter"` - IssuesEvents bool `json:"issues_events"` - ConfidentialIssuesEvents bool `json:"confidential_issues_events"` - MergeRequestsEvents bool `json:"merge_requests_events"` - TagPushEvents bool `json:"tag_push_events"` - NoteEvents bool `json:"note_events"` - JobEvents bool `json:"job_events"` - PipelineEvents bool `json:"pipeline_events"` - WikiPageEvents bool `json:"wiki_page_events"` - DeploymentEvents bool `json:"deployment_events"` - ReleasesEvents bool `json:"releases_events"` - EnableSSLVerification bool `json:"enable_ssl_verification"` - CreatedAt *time.Time `json:"created_at"` - ResourceAccessTokenEvents bool `json:"resource_access_token_events"` - CustomWebhookTemplate string `json:"custom_webhook_template"` + ID int `json:"id"` + URL string `json:"url"` + ConfidentialNoteEvents bool `json:"confidential_note_events"` + ProjectID int `json:"project_id"` + PushEvents bool `json:"push_events"` + PushEventsBranchFilter string `json:"push_events_branch_filter"` + IssuesEvents bool `json:"issues_events"` + ConfidentialIssuesEvents bool `json:"confidential_issues_events"` + MergeRequestsEvents bool `json:"merge_requests_events"` + TagPushEvents bool `json:"tag_push_events"` + NoteEvents bool `json:"note_events"` + JobEvents bool `json:"job_events"` + PipelineEvents bool `json:"pipeline_events"` + WikiPageEvents bool `json:"wiki_page_events"` + DeploymentEvents bool `json:"deployment_events"` + ReleasesEvents bool `json:"releases_events"` + EnableSSLVerification bool `json:"enable_ssl_verification"` + CreatedAt *time.Time `json:"created_at"` + ResourceAccessTokenEvents bool `json:"resource_access_token_events"` + CustomWebhookTemplate string `json:"custom_webhook_template"` + CustomHeaders []ProjectHookCustomHeader `json:"custom_headers"` } // ListProjectHooksOptions represents the available ListProjectHooks() options. @@ -1284,24 +1296,25 @@ func (s *ProjectsService) GetProjectHook(pid interface{}, hook int, options ...R // GitLab API docs: // https://docs.gitlab.com/ee/api/projects.html#add-project-hook type AddProjectHookOptions struct { - ConfidentialIssuesEvents *bool `url:"confidential_issues_events,omitempty" json:"confidential_issues_events,omitempty"` - ConfidentialNoteEvents *bool `url:"confidential_note_events,omitempty" json:"confidential_note_events,omitempty"` - DeploymentEvents *bool `url:"deployment_events,omitempty" json:"deployment_events,omitempty"` - EnableSSLVerification *bool `url:"enable_ssl_verification,omitempty" json:"enable_ssl_verification,omitempty"` - IssuesEvents *bool `url:"issues_events,omitempty" json:"issues_events,omitempty"` - JobEvents *bool `url:"job_events,omitempty" json:"job_events,omitempty"` - MergeRequestsEvents *bool `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"` - NoteEvents *bool `url:"note_events,omitempty" json:"note_events,omitempty"` - PipelineEvents *bool `url:"pipeline_events,omitempty" json:"pipeline_events,omitempty"` - PushEvents *bool `url:"push_events,omitempty" json:"push_events,omitempty"` - PushEventsBranchFilter *string `url:"push_events_branch_filter,omitempty" json:"push_events_branch_filter,omitempty"` - ReleasesEvents *bool `url:"releases_events,omitempty" json:"releases_events,omitempty"` - TagPushEvents *bool `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"` - Token *string `url:"token,omitempty" json:"token,omitempty"` - URL *string `url:"url,omitempty" json:"url,omitempty"` - WikiPageEvents *bool `url:"wiki_page_events,omitempty" json:"wiki_page_events,omitempty"` - ResourceAccessTokenEvents *bool `url:"resource_access_token_events,omitempty" json:"resource_access_token_events,omitempty"` - CustomWebhookTemplate *string `url:"custom_webhook_template,omitempty" json:"custom_webhook_template,omitempty"` + ConfidentialIssuesEvents *bool `url:"confidential_issues_events,omitempty" json:"confidential_issues_events,omitempty"` + ConfidentialNoteEvents *bool `url:"confidential_note_events,omitempty" json:"confidential_note_events,omitempty"` + DeploymentEvents *bool `url:"deployment_events,omitempty" json:"deployment_events,omitempty"` + EnableSSLVerification *bool `url:"enable_ssl_verification,omitempty" json:"enable_ssl_verification,omitempty"` + IssuesEvents *bool `url:"issues_events,omitempty" json:"issues_events,omitempty"` + JobEvents *bool `url:"job_events,omitempty" json:"job_events,omitempty"` + MergeRequestsEvents *bool `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"` + NoteEvents *bool `url:"note_events,omitempty" json:"note_events,omitempty"` + PipelineEvents *bool `url:"pipeline_events,omitempty" json:"pipeline_events,omitempty"` + PushEvents *bool `url:"push_events,omitempty" json:"push_events,omitempty"` + PushEventsBranchFilter *string `url:"push_events_branch_filter,omitempty" json:"push_events_branch_filter,omitempty"` + ReleasesEvents *bool `url:"releases_events,omitempty" json:"releases_events,omitempty"` + TagPushEvents *bool `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"` + Token *string `url:"token,omitempty" json:"token,omitempty"` + URL *string `url:"url,omitempty" json:"url,omitempty"` + WikiPageEvents *bool `url:"wiki_page_events,omitempty" json:"wiki_page_events,omitempty"` + ResourceAccessTokenEvents *bool `url:"resource_access_token_events,omitempty" json:"resource_access_token_events,omitempty"` + CustomWebhookTemplate *string `url:"custom_webhook_template,omitempty" json:"custom_webhook_template,omitempty"` + CustomHeaders []*ProjectHookCustomHeader `url:"custom_headers,omitempty" json:"custom_headers,omitempty"` } // AddProjectHook adds a hook to a specified project. @@ -1334,24 +1347,25 @@ func (s *ProjectsService) AddProjectHook(pid interface{}, opt *AddProjectHookOpt // GitLab API docs: // https://docs.gitlab.com/ee/api/projects.html#edit-project-hook type EditProjectHookOptions struct { - ConfidentialIssuesEvents *bool `url:"confidential_issues_events,omitempty" json:"confidential_issues_events,omitempty"` - ConfidentialNoteEvents *bool `url:"confidential_note_events,omitempty" json:"confidential_note_events,omitempty"` - DeploymentEvents *bool `url:"deployment_events,omitempty" json:"deployment_events,omitempty"` - EnableSSLVerification *bool `url:"enable_ssl_verification,omitempty" json:"enable_ssl_verification,omitempty"` - IssuesEvents *bool `url:"issues_events,omitempty" json:"issues_events,omitempty"` - JobEvents *bool `url:"job_events,omitempty" json:"job_events,omitempty"` - MergeRequestsEvents *bool `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"` - NoteEvents *bool `url:"note_events,omitempty" json:"note_events,omitempty"` - PipelineEvents *bool `url:"pipeline_events,omitempty" json:"pipeline_events,omitempty"` - PushEvents *bool `url:"push_events,omitempty" json:"push_events,omitempty"` - PushEventsBranchFilter *string `url:"push_events_branch_filter,omitempty" json:"push_events_branch_filter,omitempty"` - ReleasesEvents *bool `url:"releases_events,omitempty" json:"releases_events,omitempty"` - TagPushEvents *bool `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"` - Token *string `url:"token,omitempty" json:"token,omitempty"` - URL *string `url:"url,omitempty" json:"url,omitempty"` - WikiPageEvents *bool `url:"wiki_page_events,omitempty" json:"wiki_page_events,omitempty"` - ResourceAccessTokenEvents *bool `url:"resource_access_token_events,omitempty" json:"resource_access_token_events,omitempty"` - CustomWebhookTemplate *string `url:"custom_webhook_template,omitempty" json:"custom_webhook_template,omitempty"` + ConfidentialIssuesEvents *bool `url:"confidential_issues_events,omitempty" json:"confidential_issues_events,omitempty"` + ConfidentialNoteEvents *bool `url:"confidential_note_events,omitempty" json:"confidential_note_events,omitempty"` + DeploymentEvents *bool `url:"deployment_events,omitempty" json:"deployment_events,omitempty"` + EnableSSLVerification *bool `url:"enable_ssl_verification,omitempty" json:"enable_ssl_verification,omitempty"` + IssuesEvents *bool `url:"issues_events,omitempty" json:"issues_events,omitempty"` + JobEvents *bool `url:"job_events,omitempty" json:"job_events,omitempty"` + MergeRequestsEvents *bool `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"` + NoteEvents *bool `url:"note_events,omitempty" json:"note_events,omitempty"` + PipelineEvents *bool `url:"pipeline_events,omitempty" json:"pipeline_events,omitempty"` + PushEvents *bool `url:"push_events,omitempty" json:"push_events,omitempty"` + PushEventsBranchFilter *string `url:"push_events_branch_filter,omitempty" json:"push_events_branch_filter,omitempty"` + ReleasesEvents *bool `url:"releases_events,omitempty" json:"releases_events,omitempty"` + TagPushEvents *bool `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"` + Token *string `url:"token,omitempty" json:"token,omitempty"` + URL *string `url:"url,omitempty" json:"url,omitempty"` + WikiPageEvents *bool `url:"wiki_page_events,omitempty" json:"wiki_page_events,omitempty"` + ResourceAccessTokenEvents *bool `url:"resource_access_token_events,omitempty" json:"resource_access_token_events,omitempty"` + CustomWebhookTemplate *string `url:"custom_webhook_template,omitempty" json:"custom_webhook_template,omitempty"` + CustomHeaders []*ProjectHookCustomHeader `url:"custom_headers,omitempty" json:"custom_headers,omitempty"` } // EditProjectHook edits a hook for a specified project. @@ -1399,6 +1413,53 @@ func (s *ProjectsService) DeleteProjectHook(pid interface{}, hook int, options . return s.client.Do(req, nil) } +// SetProjectHookCustomHeaderOptions represents a project hook custom header. +// If the header isn't present, it will be created. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/projects.html#set-a-custom-header +type SetProjectHookCustomHeaderOptions struct { + Value *string `json:"value,omitempty"` +} + +// SetProjectCustomHeader creates or updates a project custom webhook header. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/projects.html#set-a-custom-header +func (s *ProjectsService) SetProjectCustomHeader(pid interface{}, hook int, key string, opt *SetProjectHookCustomHeaderOptions, options ...RequestOptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/hooks/%d/custom_headers/%s", PathEscape(project), hook, key) + + req, err := s.client.NewRequest(http.MethodPut, u, opt, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// DeleteProjectCustomHeader deletes a project custom webhook header. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/projects.html#delete-a-custom-header +func (s *ProjectsService) DeleteProjectCustomHeader(pid interface{}, hook int, key string, options ...RequestOptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/hooks/%d/custom_headers/%s", PathEscape(project), hook, key) + + req, err := s.client.NewRequest(http.MethodDelete, u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + // ProjectForkRelation represents a project fork relationship. // // GitLab API docs: diff --git a/projects_test.go b/projects_test.go index 7a1f5cd45..c361fe9ad 100644 --- a/projects_test.go +++ b/projects_test.go @@ -1470,9 +1470,10 @@ func TestProjectModelsOptionalMergeAttribute(t *testing.T) { } // Test that the "CustomWebhookTemplate" serializes properly -func TestProjectAddWebhook_CustomTemplate(t *testing.T) { +func TestProjectAddWebhook_CustomTemplateStuff(t *testing.T) { mux, client := setup(t) customWebhookSet := false + authValueSet := false mux.HandleFunc("/api/v4/projects/1/hooks", func(w http.ResponseWriter, r *http.Request) { @@ -1484,27 +1485,49 @@ func TestProjectAddWebhook_CustomTemplate(t *testing.T) { t.Fatalf("Unable to read body properly. Error: %v", err) } customWebhookSet = strings.Contains(string(body), "custom_webhook_template") + authValueSet = strings.Contains(string(body), `"value":"stuff"`) fmt.Fprint(w, `{ - "custom_webhook_template": "testValue" + "custom_webhook_template": "testValue", + "custom_headers": [ + { + "key": "Authorization" + }, + { + "key": "Favorite-Pet" + } + ] }`) }, ) hook, resp, err := client.Projects.AddProjectHook(1, &AddProjectHookOptions{ CustomWebhookTemplate: Ptr(`{"example":"{{object_kind}}"}`), + CustomHeaders: []*ProjectHookCustomHeader{ + { + Key: "Authorization", + Value: "stuff", + }, + { + Key: "Favorite-Pet", + Value: "Cats", + }, + }, }) assert.NoError(t, err) assert.Equal(t, http.StatusCreated, resp.StatusCode) assert.Equal(t, true, customWebhookSet) + assert.Equal(t, true, authValueSet) assert.Equal(t, "testValue", hook.CustomWebhookTemplate) + assert.Equal(t, 2, len(hook.CustomHeaders)) } // Test that the "CustomWebhookTemplate" serializes properly when editing -func TestProjectEditWebhook_CustomTemplate(t *testing.T) { +func TestProjectEditWebhook_CustomTemplateStuff(t *testing.T) { mux, client := setup(t) customWebhookSet := false + authValueSet := false mux.HandleFunc("/api/v4/projects/1/hooks/1", func(w http.ResponseWriter, r *http.Request) { @@ -1516,18 +1539,41 @@ func TestProjectEditWebhook_CustomTemplate(t *testing.T) { t.Fatalf("Unable to read body properly. Error: %v", err) } customWebhookSet = strings.Contains(string(body), "custom_webhook_template") + authValueSet = strings.Contains(string(body), `"value":"stuff"`) - fmt.Fprint(w, "{}") + fmt.Fprint(w, `{ + "custom_webhook_template": "testValue", + "custom_headers": [ + { + "key": "Authorization" + }, + { + "key": "Favorite-Pet" + } + ]}`) }, ) - _, resp, err := client.Projects.EditProjectHook(1, 1, &EditProjectHookOptions{ + hook, resp, err := client.Projects.EditProjectHook(1, 1, &EditProjectHookOptions{ CustomWebhookTemplate: Ptr(`{"example":"{{object_kind}}"}`), + CustomHeaders: []*ProjectHookCustomHeader{ + { + Key: "Authorization", + Value: "stuff", + }, + { + Key: "Favorite-Pet", + Value: "Cats", + }, + }, }) assert.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, true, customWebhookSet) + assert.Equal(t, true, authValueSet) + assert.Equal(t, "testValue", hook.CustomWebhookTemplate) + assert.Equal(t, 2, len(hook.CustomHeaders)) } func TestGetProjectPushRules(t *testing.T) { @@ -1703,3 +1749,94 @@ func TestEditProjectPushRules(t *testing.T) { t.Errorf("Projects.EditProjectPushRule returned %+v, want %+v", rule, want) } } + +func TestGetProjectWebhookHeader(t *testing.T) { + mux, client := setup(t) + + // Removed most of the arguments to keep test slim + mux.HandleFunc("/api/v4/projects/1/hooks/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, `{ + "id": 1, + "custom_webhook_template": "{\"event\":\"{{object_kind}}\"}", + "custom_headers": [ + { + "key": "Authorization" + }, + { + "key": "OtherKey" + } + ] + }`) + }) + + hook, _, err := client.Projects.GetProjectHook(1, 1) + if err != nil { + t.Errorf("Projects.GetProjectHook returned error: %v", err) + } + + want := &ProjectHook{ + ID: 1, + CustomWebhookTemplate: "{\"event\":\"{{object_kind}}\"}", + CustomHeaders: []ProjectHookCustomHeader{ + { + Key: "Authorization", + }, + { + Key: "OtherKey", + }, + }, + } + + if !reflect.DeepEqual(want, hook) { + t.Errorf("Projects.GetProjectHook returned %+v, want %+v", hook, want) + } +} + +func TestSetProjectWebhookHeader(t *testing.T) { + mux, client := setup(t) + var bodyJson map[string]interface{} + + // Removed most of the arguments to keep test slim + mux.HandleFunc("/api/v4/projects/1/hooks/1/custom_headers/Authorization", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodPut) + w.WriteHeader(http.StatusNoContent) + + // validate that the `value` body is sent properly + body, err := io.ReadAll(r.Body) + if err != nil { + t.Fatalf("Unable to read body properly. Error: %v", err) + } + + // Unmarshal the body into JSON so we can check it + _ = json.Unmarshal(body, &bodyJson) + + fmt.Fprint(w, ``) + }) + + req, err := client.Projects.SetProjectCustomHeader(1, 1, "Authorization", &SetProjectHookCustomHeaderOptions{Value: Ptr("testValue")}) + if err != nil { + t.Errorf("Projects.SetProjectCustomHeader returned error: %v", err) + } + + assert.Equal(t, bodyJson["value"], "testValue") + assert.Equal(t, http.StatusNoContent, req.StatusCode) +} + +func TestDeleteProjectWebhookHeader(t *testing.T) { + mux, client := setup(t) + + // Removed most of the arguments to keep test slim + mux.HandleFunc("/api/v4/projects/1/hooks/1/custom_headers/Authorization", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodDelete) + w.WriteHeader(http.StatusNoContent) + fmt.Fprint(w, ``) + }) + + req, err := client.Projects.DeleteProjectCustomHeader(1, 1, "Authorization") + if err != nil { + t.Errorf("Projects.DeleteProjectCustomHeader returned error: %v", err) + } + + assert.Equal(t, http.StatusNoContent, req.StatusCode) +}