diff --git a/github/github-iterators.go b/github/github-iterators.go index 701004531d7..138b1255089 100644 --- a/github/github-iterators.go +++ b/github/github-iterators.go @@ -6719,6 +6719,37 @@ func (s *SecretScanningService) ListAlertsForRepoIter(ctx context.Context, owner } } +// ListCustomPatternsForOrgIter returns an iterator that paginates through all results of ListCustomPatternsForOrg. +func (s *SecretScanningService) ListCustomPatternsForOrgIter(ctx context.Context, org string, opts *SecretScanningCustomPatternListOptions) iter.Seq2[*SecretScanningCustomPattern, error] { + return func(yield func(*SecretScanningCustomPattern, error) bool) { + // Create a copy of opts to avoid mutating the caller's struct + if opts == nil { + opts = &SecretScanningCustomPatternListOptions{} + } else { + opts = Ptr(*opts) + } + + for { + results, resp, err := s.ListCustomPatternsForOrg(ctx, org, opts) + if err != nil { + yield(nil, err) + return + } + + for _, item := range results { + if !yield(item, nil) { + return + } + } + + if resp.NextPage == 0 { + break + } + opts.ListOptions.Page = resp.NextPage + } + } +} + // ListCustomPatternsForRepoIter returns an iterator that paginates through all results of ListCustomPatternsForRepo. func (s *SecretScanningService) ListCustomPatternsForRepoIter(ctx context.Context, owner string, repo string, opts *SecretScanningCustomPatternListOptions) iter.Seq2[*SecretScanningCustomPattern, error] { return func(yield func(*SecretScanningCustomPattern, error) bool) { diff --git a/github/github-iterators_test.go b/github/github-iterators_test.go index ea51452061f..5cc42d1883d 100644 --- a/github/github-iterators_test.go +++ b/github/github-iterators_test.go @@ -14919,6 +14919,78 @@ func TestSecretScanningService_ListAlertsForRepoIter(t *testing.T) { } } +func TestSecretScanningService_ListCustomPatternsForOrgIter(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + var callNum int + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + callNum++ + switch callNum { + case 1: + w.Header().Set("Link", `; rel="next"`) + fmt.Fprint(w, `[{},{},{}]`) + case 2: + fmt.Fprint(w, `[{},{},{},{}]`) + case 3: + fmt.Fprint(w, `[{},{}]`) + case 4: + w.WriteHeader(http.StatusNotFound) + case 5: + fmt.Fprint(w, `[{},{}]`) + } + }) + + iter := client.SecretScanning.ListCustomPatternsForOrgIter(t.Context(), "", nil) + var gotItems int + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 7; gotItems != want { + t.Errorf("client.SecretScanning.ListCustomPatternsForOrgIter call 1 got %v items; want %v", gotItems, want) + } + + opts := &SecretScanningCustomPatternListOptions{} + iter = client.SecretScanning.ListCustomPatternsForOrgIter(t.Context(), "", opts) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 2; gotItems != want { + t.Errorf("client.SecretScanning.ListCustomPatternsForOrgIter call 2 got %v items; want %v", gotItems, want) + } + + iter = client.SecretScanning.ListCustomPatternsForOrgIter(t.Context(), "", nil) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err == nil { + t.Error("expected error; got nil") + } + } + if gotItems != 1 { + t.Errorf("client.SecretScanning.ListCustomPatternsForOrgIter call 3 got %v items; want 1 (an error)", gotItems) + } + + iter = client.SecretScanning.ListCustomPatternsForOrgIter(t.Context(), "", nil) + gotItems = 0 + iter(func(item *SecretScanningCustomPattern, err error) bool { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + return false + }) + if gotItems != 1 { + t.Errorf("client.SecretScanning.ListCustomPatternsForOrgIter call 4 got %v items; want 1 (an error)", gotItems) + } +} + func TestSecretScanningService_ListCustomPatternsForRepoIter(t *testing.T) { t.Parallel() client, mux, _ := setup(t) diff --git a/github/secret_scanning_custom_patterns.go b/github/secret_scanning_custom_patterns.go index 5b7036e87a6..d4be2d41e62 100644 --- a/github/secret_scanning_custom_patterns.go +++ b/github/secret_scanning_custom_patterns.go @@ -261,3 +261,89 @@ func (s *SecretScanningService) DeleteCustomPatternsForRepo(ctx context.Context, return s.client.Do(req, nil) } + +// ListCustomPatternsForOrg lists the secret scanning custom patterns defined at the organization level. +// +// GitHub API docs: https://docs.github.com/rest/secret-scanning/custom-patterns?apiVersion=2022-11-28#list-organization-custom-patterns +// +//meta:operation GET /orgs/{org}/secret-scanning/custom-patterns +func (s *SecretScanningService) ListCustomPatternsForOrg(ctx context.Context, org string, opts *SecretScanningCustomPatternListOptions) ([]*SecretScanningCustomPattern, *Response, error) { + u := fmt.Sprintf("orgs/%v/secret-scanning/custom-patterns", org) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(ctx, "GET", u, nil) + if err != nil { + return nil, nil, err + } + + var patterns []*SecretScanningCustomPattern + resp, err := s.client.Do(req, &patterns) + if err != nil { + return nil, resp, err + } + + return patterns, resp, nil +} + +// CreateCustomPatternsForOrg creates one or more secret scanning custom patterns at organization level. +// +// GitHub API docs: https://docs.github.com/rest/secret-scanning/custom-patterns?apiVersion=2022-11-28#bulk-create-organization-custom-patterns +// +//meta:operation POST /orgs/{org}/secret-scanning/custom-patterns +func (s *SecretScanningService) CreateCustomPatternsForOrg(ctx context.Context, org string, body SecretScanningCreateCustomPatternsRequest) (*SecretScanningCreateCustomPatternsResponse, *Response, error) { + u := fmt.Sprintf("orgs/%v/secret-scanning/custom-patterns", org) + + req, err := s.client.NewRequest(ctx, "POST", u, body) + if err != nil { + return nil, nil, err + } + + var result *SecretScanningCreateCustomPatternsResponse + resp, err := s.client.Do(req, &result) + if err != nil { + return nil, resp, err + } + + return result, resp, nil +} + +// UpdateCustomPatternForOrg updates a single secret scanning custom pattern at organization level. +// +// GitHub API docs: https://docs.github.com/rest/secret-scanning/custom-patterns?apiVersion=2022-11-28#update-an-organization-custom-pattern +// +//meta:operation PATCH /orgs/{org}/secret-scanning/custom-patterns/{pattern_id} +func (s *SecretScanningService) UpdateCustomPatternForOrg(ctx context.Context, org string, patternID int64, body SecretScanningUpdateCustomPatternRequest) (*SecretScanningCustomPattern, *Response, error) { + u := fmt.Sprintf("orgs/%v/secret-scanning/custom-patterns/%v", org, patternID) + + req, err := s.client.NewRequest(ctx, "PATCH", u, body) + if err != nil { + return nil, nil, err + } + + var pattern *SecretScanningCustomPattern + resp, err := s.client.Do(req, &pattern) + if err != nil { + return nil, resp, err + } + + return pattern, resp, nil +} + +// DeleteCustomPatternsForOrg deletes one or more secret scanning custom patterns at the organization level. +// +// GitHub API docs: https://docs.github.com/rest/secret-scanning/custom-patterns?apiVersion=2022-11-28#bulk-delete-organization-custom-patterns +// +//meta:operation DELETE /orgs/{org}/secret-scanning/custom-patterns +func (s *SecretScanningService) DeleteCustomPatternsForOrg(ctx context.Context, org string, body SecretScanningDeleteCustomPatternsRequest) (*Response, error) { + u := fmt.Sprintf("orgs/%v/secret-scanning/custom-patterns", org) + + req, err := s.client.NewRequest(ctx, "DELETE", u, body) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/github/secret_scanning_custom_patterns_test.go b/github/secret_scanning_custom_patterns_test.go index 58a69b30c4a..c88cca76c1a 100644 --- a/github/secret_scanning_custom_patterns_test.go +++ b/github/secret_scanning_custom_patterns_test.go @@ -244,3 +244,235 @@ func TestSecretScanningService_DeleteCustomPatternsForRepo(t *testing.T) { return client.SecretScanning.DeleteCustomPatternsForRepo(ctx, "o", "r", input) }) } + +func TestSecretScanningService_ListCustomPatternsForOrg(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/secret-scanning/custom-patterns", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{ + "state": "published", + "push_protection": "enabled", + "sort": "created", + "direction": "desc", + "page": "2", + }) + fmt.Fprint(w, `[ + { + "id": 1, + "name": "Custom pattern", + "pattern": "[A-Z]{2}-[0-9]{4}", + "slug": "custom-pattern", + "state": "published", + "push_protection_enabled": true, + "start_delimiter": "\\b", + "end_delimiter": "\\b", + "must_match": ["ID-.*"], + "must_not_match": ["TEST-.*"], + "custom_pattern_version": "v1", + "created_at": `+referenceTimeStr+`, + "updated_at": `+referenceTimeStr+` + } + ]`) + }) + + ctx := t.Context() + opts := &SecretScanningCustomPatternListOptions{ + State: "published", + PushProtection: "enabled", + Sort: "created", + Direction: "desc", + ListOptions: ListOptions{Page: 2}, + } + patterns, _, err := client.SecretScanning.ListCustomPatternsForOrg(ctx, "o", opts) + if err != nil { + t.Errorf("SecretScanning.ListCustomPatternsForOrg returned error: %v", err) + } + + want := []*SecretScanningCustomPattern{ + { + ID: 1, + Name: "Custom pattern", + Pattern: "[A-Z]{2}-[0-9]{4}", + Slug: "custom-pattern", + State: "published", + PushProtectionEnabled: true, + StartDelimiter: Ptr(`\b`), + EndDelimiter: Ptr(`\b`), + MustMatch: []string{"ID-.*"}, + MustNotMatch: []string{"TEST-.*"}, + CustomPatternVersion: Ptr("v1"), + CreatedAt: &referenceTimestamp, + UpdatedAt: &referenceTimestamp, + }, + } + if !cmp.Equal(patterns, want) { + t.Errorf("SecretScanning.ListCustomPatternsForOrg returned %+v, want %+v", patterns, want) + } + + const methodName = "ListCustomPatternsForOrg" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.SecretScanning.ListCustomPatternsForOrg(ctx, "\n", &SecretScanningCustomPatternListOptions{}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + _, resp, err := client.SecretScanning.ListCustomPatternsForOrg(ctx, "o", &SecretScanningCustomPatternListOptions{}) + return resp, err + }) +} + +func TestSecretScanningService_CreateCustomPatternsForOrg(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := SecretScanningCreateCustomPatternsRequest{ + Patterns: []*SecretScanningCustomPatternRequest{ + { + Name: "Custom pattern", + Pattern: "[A-Z]{2}-[0-9]{4}", + }, + }, + } + + mux.HandleFunc("/orgs/o/secret-scanning/custom-patterns", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testJSONBody(t, r, input) + fmt.Fprint(w, `{ + "created_patterns": [ + { + "id": 1, + "name": "Custom pattern", + "pattern": "[A-Z]{2}-[0-9]{4}", + "slug": "custom-pattern", + "state": "published", + "push_protection_enabled": false + } + ] + }`) + }) + + ctx := t.Context() + result, _, err := client.SecretScanning.CreateCustomPatternsForOrg(ctx, "o", input) + if err != nil { + t.Errorf("SecretScanning.CreateCustomPatternsForOrg returned error: %v", err) + } + + want := &SecretScanningCreateCustomPatternsResponse{ + CreatedPatterns: []*SecretScanningCustomPattern{ + { + ID: 1, + Name: "Custom pattern", + Pattern: "[A-Z]{2}-[0-9]{4}", + Slug: "custom-pattern", + State: "published", + PushProtectionEnabled: false, + }, + }, + } + if !cmp.Equal(result, want) { + t.Errorf("SecretScanning.CreateCustomPatternsForOrg returned %+v, want %+v", result, want) + } + + const methodName = "CreateCustomPatternsForOrg" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.SecretScanning.CreateCustomPatternsForOrg(ctx, "\n", input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + _, resp, err := client.SecretScanning.CreateCustomPatternsForOrg(ctx, "o", input) + return resp, err + }) +} + +func TestSecretScanningService_UpdateCustomPatternForOrg(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := SecretScanningUpdateCustomPatternRequest{ + Pattern: Ptr("[A-Z]{3}-[0-9]{4}"), + CustomPatternVersion: Ptr("v1"), + } + + mux.HandleFunc("/orgs/o/secret-scanning/custom-patterns/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + testJSONBody(t, r, input) + fmt.Fprint(w, `{ + "id": 1, + "name": "Custom pattern", + "pattern": "[A-Z]{3}-[0-9]{4}", + "slug": "custom-pattern", + "state": "published", + "push_protection_enabled": false, + "custom_pattern_version": "v2" + }`) + }) + + ctx := t.Context() + pattern, _, err := client.SecretScanning.UpdateCustomPatternForOrg(ctx, "o", 1, input) + if err != nil { + t.Errorf("SecretScanning.UpdateCustomPatternForOrg returned error: %v", err) + } + + want := &SecretScanningCustomPattern{ + ID: 1, + Name: "Custom pattern", + Pattern: "[A-Z]{3}-[0-9]{4}", + Slug: "custom-pattern", + State: "published", + PushProtectionEnabled: false, + CustomPatternVersion: Ptr("v2"), + } + if !cmp.Equal(pattern, want) { + t.Errorf("SecretScanning.UpdateCustomPatternForOrg returned %+v, want %+v", pattern, want) + } + + const methodName = "UpdateCustomPatternForOrg" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.SecretScanning.UpdateCustomPatternForOrg(ctx, "\n", 1, input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + _, resp, err := client.SecretScanning.UpdateCustomPatternForOrg(ctx, "o", 1, input) + return resp, err + }) +} + +func TestSecretScanningService_DeleteCustomPatternsForOrg(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := SecretScanningDeleteCustomPatternsRequest{ + Patterns: []*SecretScanningCustomPatternToDelete{ + {PatternID: 1}, + }, + } + + mux.HandleFunc("/orgs/o/secret-scanning/custom-patterns", func(_ http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + testJSONBody(t, r, input) + }) + + ctx := t.Context() + _, err := client.SecretScanning.DeleteCustomPatternsForOrg(ctx, "o", input) + if err != nil { + t.Errorf("SecretScanning.DeleteCustomPatternsForOrg returned error: %v", err) + } + + const methodName = "DeleteCustomPatternsForOrg" + + testBadOptions(t, methodName, func() (err error) { + _, err = client.SecretScanning.DeleteCustomPatternsForOrg(ctx, "\n", input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.SecretScanning.DeleteCustomPatternsForOrg(ctx, "o", input) + }) +}