From aaddcb019dee7ecbf4543ed9be6a2793b278364f Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 16 Apr 2025 13:39:34 +0200 Subject: [PATCH] Issue edit early arg parsing --- pkg/cmd/issue/edit/edit.go | 33 ++++-- pkg/cmd/issue/edit/edit_test.go | 89 +++++++++----- pkg/cmd/issue/shared/lookup.go | 176 ++++++++++++++++++---------- pkg/cmd/issue/shared/lookup_test.go | 159 +++++++++++++++---------- 4 files changed, 298 insertions(+), 159 deletions(-) diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go index 18067319f..accea8add 100644 --- a/pkg/cmd/issue/edit/edit.go +++ b/pkg/cmd/issue/edit/edit.go @@ -28,7 +28,7 @@ type EditOptions struct { EditFieldsSurvey func(prShared.EditPrompter, *prShared.Editable, string) error FetchOptions func(*api.Client, ghrepo.Interface, *prShared.Editable) error - SelectorArgs []string + IssueNumbers []int Interactive bool prShared.Editable @@ -69,10 +69,22 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman `), Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - // support `-R, --repo` override - opts.BaseRepo = f.BaseRepo + issueNumbers, baseRepo, err := shared.ParseIssuesFromArgs(args) + if err != nil { + return err + } - opts.SelectorArgs = args + // If the args provided the base repo then use that directly. + if baseRepo, present := baseRepo.Value(); present { + opts.BaseRepo = func() (ghrepo.Interface, error) { + return baseRepo, nil + } + } else { + // support `-R, --repo` override + opts.BaseRepo = f.BaseRepo + } + + opts.IssueNumbers = issueNumbers flags := cmd.Flags() @@ -134,7 +146,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman return cmdutil.FlagErrorf("field to edit flag required when not running interactively") } - if opts.Interactive && len(opts.SelectorArgs) > 1 { + if opts.Interactive && len(opts.IssueNumbers) > 1 { return cmdutil.FlagErrorf("multiple issues cannot be edited interactively") } @@ -167,6 +179,11 @@ func editRun(opts *EditOptions) error { return err } + baseRepo, err := opts.BaseRepo() + if err != nil { + return err + } + // Prompt the user which fields they'd like to edit. editable := opts.Editable if opts.Interactive { @@ -192,7 +209,7 @@ func editRun(opts *EditOptions) error { } // Get all specified issues and make sure they are within the same repo. - issues, repo, err := shared.IssuesFromArgsWithFields(httpClient, opts.BaseRepo, opts.SelectorArgs, lookupFields) + issues, err := shared.FindIssuesOrPRs(httpClient, baseRepo, opts.IssueNumbers, lookupFields) if err != nil { return err } @@ -200,7 +217,7 @@ func editRun(opts *EditOptions) error { // Fetch editable shared fields once for all issues. apiClient := api.NewClientFromHTTP(httpClient) opts.IO.StartProgressIndicatorWithLabel("Fetching repository information") - err = opts.FetchOptions(apiClient, repo, &editable) + err = opts.FetchOptions(apiClient, baseRepo, &editable) opts.IO.StopProgressIndicator() if err != nil { return err @@ -250,7 +267,7 @@ func editRun(opts *EditOptions) error { go func(issue *api.Issue) { defer g.Done() - err := prShared.UpdateIssue(httpClient, repo, issue.ID, issue.IsPullRequest(), editable) + err := prShared.UpdateIssue(httpClient, baseRepo, issue.ID, issue.IsPullRequest(), editable) if err != nil { failedIssueChan <- fmt.Sprintf("failed to update %s: %s", issue.URL, err) return diff --git a/pkg/cmd/issue/edit/edit_test.go b/pkg/cmd/issue/edit/edit_test.go index 40fe6491c..a9d43c3ec 100644 --- a/pkg/cmd/issue/edit/edit_test.go +++ b/pkg/cmd/issue/edit/edit_test.go @@ -26,11 +26,12 @@ func TestNewCmdEdit(t *testing.T) { require.NoError(t, err) tests := []struct { - name string - input string - stdin string - output EditOptions - wantsErr bool + name string + input string + stdin string + output EditOptions + expectedBaseRepo ghrepo.Interface + wantsErr bool }{ { name: "no argument", @@ -42,7 +43,7 @@ func TestNewCmdEdit(t *testing.T) { name: "issue number argument", input: "23", output: EditOptions{ - SelectorArgs: []string{"23"}, + IssueNumbers: []int{23}, Interactive: true, }, wantsErr: false, @@ -51,7 +52,7 @@ func TestNewCmdEdit(t *testing.T) { name: "title flag", input: "23 --title test", output: EditOptions{ - SelectorArgs: []string{"23"}, + IssueNumbers: []int{23}, Editable: prShared.Editable{ Title: prShared.EditableString{ Value: "test", @@ -65,7 +66,7 @@ func TestNewCmdEdit(t *testing.T) { name: "body flag", input: "23 --body test", output: EditOptions{ - SelectorArgs: []string{"23"}, + IssueNumbers: []int{23}, Editable: prShared.Editable{ Body: prShared.EditableString{ Value: "test", @@ -80,7 +81,7 @@ func TestNewCmdEdit(t *testing.T) { input: "23 --body-file -", stdin: "this is on standard input", output: EditOptions{ - SelectorArgs: []string{"23"}, + IssueNumbers: []int{23}, Editable: prShared.Editable{ Body: prShared.EditableString{ Value: "this is on standard input", @@ -94,7 +95,7 @@ func TestNewCmdEdit(t *testing.T) { name: "body from file", input: fmt.Sprintf("23 --body-file '%s'", tmpFile), output: EditOptions{ - SelectorArgs: []string{"23"}, + IssueNumbers: []int{23}, Editable: prShared.Editable{ Body: prShared.EditableString{ Value: "a body from file", @@ -113,7 +114,7 @@ func TestNewCmdEdit(t *testing.T) { name: "add-assignee flag", input: "23 --add-assignee monalisa,hubot", output: EditOptions{ - SelectorArgs: []string{"23"}, + IssueNumbers: []int{23}, Editable: prShared.Editable{ Assignees: prShared.EditableSlice{ Add: []string{"monalisa", "hubot"}, @@ -127,7 +128,7 @@ func TestNewCmdEdit(t *testing.T) { name: "remove-assignee flag", input: "23 --remove-assignee monalisa,hubot", output: EditOptions{ - SelectorArgs: []string{"23"}, + IssueNumbers: []int{23}, Editable: prShared.Editable{ Assignees: prShared.EditableSlice{ Remove: []string{"monalisa", "hubot"}, @@ -141,7 +142,7 @@ func TestNewCmdEdit(t *testing.T) { name: "add-label flag", input: "23 --add-label feature,TODO,bug", output: EditOptions{ - SelectorArgs: []string{"23"}, + IssueNumbers: []int{23}, Editable: prShared.Editable{ Labels: prShared.EditableSlice{ Add: []string{"feature", "TODO", "bug"}, @@ -155,7 +156,7 @@ func TestNewCmdEdit(t *testing.T) { name: "remove-label flag", input: "23 --remove-label feature,TODO,bug", output: EditOptions{ - SelectorArgs: []string{"23"}, + IssueNumbers: []int{23}, Editable: prShared.Editable{ Labels: prShared.EditableSlice{ Remove: []string{"feature", "TODO", "bug"}, @@ -169,7 +170,7 @@ func TestNewCmdEdit(t *testing.T) { name: "add-project flag", input: "23 --add-project Cleanup,Roadmap", output: EditOptions{ - SelectorArgs: []string{"23"}, + IssueNumbers: []int{23}, Editable: prShared.Editable{ Projects: prShared.EditableProjects{ EditableSlice: prShared.EditableSlice{ @@ -185,7 +186,7 @@ func TestNewCmdEdit(t *testing.T) { name: "remove-project flag", input: "23 --remove-project Cleanup,Roadmap", output: EditOptions{ - SelectorArgs: []string{"23"}, + IssueNumbers: []int{23}, Editable: prShared.Editable{ Projects: prShared.EditableProjects{ EditableSlice: prShared.EditableSlice{ @@ -201,7 +202,7 @@ func TestNewCmdEdit(t *testing.T) { name: "milestone flag", input: "23 --milestone GA", output: EditOptions{ - SelectorArgs: []string{"23"}, + IssueNumbers: []int{23}, Editable: prShared.Editable{ Milestone: prShared.EditableString{ Value: "GA", @@ -215,7 +216,7 @@ func TestNewCmdEdit(t *testing.T) { name: "remove-milestone flag", input: "23 --remove-milestone", output: EditOptions{ - SelectorArgs: []string{"23"}, + IssueNumbers: []int{23}, Editable: prShared.Editable{ Milestone: prShared.EditableString{ Value: "", @@ -234,7 +235,7 @@ func TestNewCmdEdit(t *testing.T) { name: "add label to multiple issues", input: "23 34 --add-label bug", output: EditOptions{ - SelectorArgs: []string{"23", "34"}, + IssueNumbers: []int{23, 34}, Editable: prShared.Editable{ Labels: prShared.EditableSlice{ Add: []string{"bug"}, @@ -244,6 +245,31 @@ func TestNewCmdEdit(t *testing.T) { }, wantsErr: false, }, + { + name: "argument is hash prefixed number", + // Escaping is required here to avoid what I think is shellex treating it as a comment. + input: "\\#23", + output: EditOptions{ + IssueNumbers: []int{23}, + Interactive: true, + }, + wantsErr: false, + }, + { + name: "argument is a URL", + input: "https://github.com/cli/cli/issues/23", + output: EditOptions{ + IssueNumbers: []int{23}, + Interactive: true, + }, + expectedBaseRepo: ghrepo.New("cli", "cli"), + wantsErr: false, + }, + { + name: "URL arguments parse as different repos", + input: "https://github.com/cli/cli/issues/23 https://github.com/cli/go-gh/issues/23", + wantsErr: true, + }, { name: "interactive multiple issues", input: "23 34", @@ -282,14 +308,23 @@ func TestNewCmdEdit(t *testing.T) { _, err = cmd.ExecuteC() if tt.wantsErr { - assert.Error(t, err) + require.Error(t, err) return } - assert.NoError(t, err) - assert.Equal(t, tt.output.SelectorArgs, gotOpts.SelectorArgs) + require.NoError(t, err) + assert.Equal(t, tt.output.IssueNumbers, gotOpts.IssueNumbers) assert.Equal(t, tt.output.Interactive, gotOpts.Interactive) assert.Equal(t, tt.output.Editable, gotOpts.Editable) + if tt.expectedBaseRepo != nil { + baseRepo, err := gotOpts.BaseRepo() + require.NoError(t, err) + require.True( + t, + ghrepo.IsSame(tt.expectedBaseRepo, baseRepo), + "expected base repo %+v, got %+v", tt.expectedBaseRepo, baseRepo, + ) + } }) } } @@ -306,7 +341,7 @@ func Test_editRun(t *testing.T) { { name: "non-interactive", input: &EditOptions{ - SelectorArgs: []string{"123"}, + IssueNumbers: []int{123}, Interactive: false, Editable: prShared.Editable{ Title: prShared.EditableString{ @@ -359,7 +394,7 @@ func Test_editRun(t *testing.T) { { name: "non-interactive multiple issues", input: &EditOptions{ - SelectorArgs: []string{"456", "123"}, + IssueNumbers: []int{456, 123}, Interactive: false, Editable: prShared.Editable{ Assignees: prShared.EditableSlice{ @@ -409,7 +444,7 @@ func Test_editRun(t *testing.T) { { name: "non-interactive multiple issues with fetch failures", input: &EditOptions{ - SelectorArgs: []string{"123", "9999"}, + IssueNumbers: []int{123, 9999}, Interactive: false, Editable: prShared.Editable{ Assignees: prShared.EditableSlice{ @@ -454,7 +489,7 @@ func Test_editRun(t *testing.T) { { name: "non-interactive multiple issues with update failures", input: &EditOptions{ - SelectorArgs: []string{"123", "456"}, + IssueNumbers: []int{123, 456}, Interactive: false, Editable: prShared.Editable{ Assignees: prShared.EditableSlice{ @@ -524,7 +559,7 @@ func Test_editRun(t *testing.T) { { name: "interactive", input: &EditOptions{ - SelectorArgs: []string{"123"}, + IssueNumbers: []int{123}, Interactive: true, FieldsToEditSurvey: func(p prShared.EditPrompter, eo *prShared.Editable) error { eo.Title.Edited = true diff --git a/pkg/cmd/issue/shared/lookup.go b/pkg/cmd/issue/shared/lookup.go index be79f9a73..bff6018d9 100644 --- a/pkg/cmd/issue/shared/lookup.go +++ b/pkg/cmd/issue/shared/lookup.go @@ -13,10 +13,89 @@ import ( "github.com/cli/cli/v2/api" fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/ghrepo" + o "github.com/cli/cli/v2/pkg/option" "github.com/cli/cli/v2/pkg/set" "golang.org/x/sync/errgroup" ) +func ParseIssuesFromArgs(args []string) ([]int, o.Option[ghrepo.Interface], error) { + var repo o.Option[ghrepo.Interface] + issueNumbers := make([]int, len(args)) + + for i, arg := range args { + // For each argument, parse the issue number and an optional repo + issueNumber, issueRepo, err := parseIssueFromArg(arg) + if err != nil { + return nil, o.None[ghrepo.Interface](), err + } + + // if this is our first issue repo found, then we need to set it + if repo.IsNone() { + repo = issueRepo + } + + // if there is an issue repo returned, then we need to check if it is the same as the previous one + if issueRepo.IsSome() && repo.IsSome() { + // Unwraps are safe because we've checked for presence above + if !ghrepo.IsSame(repo.Unwrap(), issueRepo.Unwrap()) { + return nil, o.None[ghrepo.Interface](), fmt.Errorf( + "multiple issues must be in same repo: found %q, expected %q", + ghrepo.FullName(issueRepo.Unwrap()), + ghrepo.FullName(repo.Unwrap()), + ) + } + } + + // add the issue number to the list + issueNumbers[i] = issueNumber + } + + return issueNumbers, repo, nil +} + +func parseIssueFromArg(arg string) (int, o.Option[ghrepo.Interface], error) { + issueLocator := tryParseIssueFromURL(arg) + if issueLocator, present := issueLocator.Value(); present { + return issueLocator.issueNumber, o.Some(issueLocator.repo), nil + } + + issueNumber, err := strconv.Atoi(strings.TrimPrefix(arg, "#")) + if err != nil { + return 0, o.None[ghrepo.Interface](), fmt.Errorf("invalid issue format: %q", arg) + } + + return issueNumber, o.None[ghrepo.Interface](), nil +} + +type issueLocator struct { + issueNumber int + repo ghrepo.Interface +} + +// tryParseIssueFromURL tries to parse an issue number and repo from a URL. +func tryParseIssueFromURL(maybeURL string) o.Option[issueLocator] { + u, err := url.Parse(maybeURL) + if err != nil { + return o.None[issueLocator]() + } + + if u.Scheme != "https" && u.Scheme != "http" { + return o.None[issueLocator]() + } + + m := issueURLRE.FindStringSubmatch(u.Path) + if m == nil { + return o.None[issueLocator]() + } + + repo := ghrepo.NewWithHost(m[1], m[2], u.Hostname()) + issueNumber, _ := strconv.Atoi(m[3]) + return o.Some(issueLocator{ + issueNumber: issueNumber, + repo: repo, + }) +} + // IssueFromArgWithFields loads an issue or pull request with the specified fields. If some of the fields // could not be fetched by GraphQL, this returns a non-nil issue and a *PartialLoadError. func IssueFromArgWithFields(httpClient *http.Client, baseRepoFn func() (ghrepo.Interface, error), arg string, fields []string) (*api.Issue, ghrepo.Interface, error) { @@ -36,70 +115,6 @@ func IssueFromArgWithFields(httpClient *http.Client, baseRepoFn func() (ghrepo.I return issue, baseRepo, err } -// IssuesFromArgsWithFields loads 1 or more issues or pull requests with the specified fields. If some of the fields -// could not be fetched by GraphQL, this returns non-nil issues and a *PartialLoadError. -func IssuesFromArgsWithFields(httpClient *http.Client, baseRepoFn func() (ghrepo.Interface, error), args []string, fields []string) ([]*api.Issue, ghrepo.Interface, error) { - var issuesRepo ghrepo.Interface - issueNumbers := make([]int, 0, len(args)) - - for _, arg := range args { - issueNumber, baseRepo, err := IssueNumberAndRepoFromArg(arg) - if err != nil { - return nil, nil, err - } - - issueNumbers = append(issueNumbers, issueNumber) - if baseRepo == nil { - var err error - if baseRepo, err = baseRepoFn(); err != nil { - return nil, nil, err - } - } - - if issuesRepo == nil { - issuesRepo = baseRepo - continue - } - - if !ghrepo.IsSame(issuesRepo, baseRepo) { - return nil, nil, fmt.Errorf( - "multiple issues must be in same repo: found %q, expected %q", - ghrepo.FullName(baseRepo), - ghrepo.FullName(issuesRepo), - ) - } - } - - issuesChan := make(chan *api.Issue, len(args)) - g := errgroup.Group{} - for _, num := range issueNumbers { - issueNumber := num - g.Go(func() error { - issue, err := findIssueOrPR(httpClient, issuesRepo, issueNumber, fields) - if err != nil { - return err - } - - issuesChan <- issue - return nil - }) - } - - err := g.Wait() - close(issuesChan) - - if err != nil { - return nil, nil, err - } - - issues := make([]*api.Issue, 0, len(args)) - for issue := range issuesChan { - issues = append(issues, issue) - } - - return issues, issuesRepo, nil -} - var issueURLRE = regexp.MustCompile(`^/([^/]+)/([^/]+)/(?:issues|pull)/(\d+)`) func issueMetadataFromURL(s string) (int, ghrepo.Interface) { @@ -142,6 +157,39 @@ type PartialLoadError struct { error } +// FindIssuesOrPRs loads 1 or more issues or pull requests with the specified fields. If some of the fields +// could not be fetched by GraphQL, this returns non-nil issues and a *PartialLoadError. +func FindIssuesOrPRs(httpClient *http.Client, repo ghrepo.Interface, issueNumbers []int, fields []string) ([]*api.Issue, error) { + issuesChan := make(chan *api.Issue, len(issueNumbers)) + g := errgroup.Group{} + for _, num := range issueNumbers { + issueNumber := num + g.Go(func() error { + issue, err := findIssueOrPR(httpClient, repo, issueNumber, fields) + if err != nil { + return err + } + + issuesChan <- issue + return nil + }) + } + + err := g.Wait() + close(issuesChan) + + if err != nil { + return nil, err + } + + issues := make([]*api.Issue, 0, len(issueNumbers)) + for issue := range issuesChan { + issues = append(issues, issue) + } + + return issues, nil +} + func findIssueOrPR(httpClient *http.Client, repo ghrepo.Interface, number int, fields []string) (*api.Issue, error) { fieldSet := set.NewStringSet() fieldSet.AddValues(fields) diff --git a/pkg/cmd/issue/shared/lookup_test.go b/pkg/cmd/issue/shared/lookup_test.go index 44f496de4..d9e8bc31a 100644 --- a/pkg/cmd/issue/shared/lookup_test.go +++ b/pkg/cmd/issue/shared/lookup_test.go @@ -7,7 +7,9 @@ import ( "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/httpmock" + o "github.com/cli/cli/v2/pkg/option" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIssueFromArgWithFields(t *testing.T) { @@ -214,53 +216,85 @@ func TestIssueFromArgWithFields(t *testing.T) { } } -func TestIssuesFromArgsWithFields(t *testing.T) { - type args struct { - baseRepoFn func() (ghrepo.Interface, error) - selectors []string - } +func TestParseIssuesFromArgs(t *testing.T) { tests := []struct { - name string - args args - httpStub func(*httpmock.Registry) - wantIssues []int - wantRepo string - wantErr bool - wantErrMsg string + behavior string + args []string + expectedIssueNumbers []int + expectedRepo o.Option[ghrepo.Interface] + expectedErr bool }{ { - name: "multiple repos", - args: args{ - selectors: []string{"1", "https://github.com/OWNER/OTHERREPO/issues/2"}, - baseRepoFn: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - }, - httpStub: func(r *httpmock.Registry) { - r.Register( - httpmock.GraphQL(`query IssueByNumber\b`), - httpmock.StringResponse(`{"data":{"repository":{ - "hasIssuesEnabled": true, - "issue":{"number":1} - }}}`)) - r.Register( - httpmock.GraphQL(`query IssueByNumber\b`), - httpmock.StringResponse(`{"data":{"repository":{ - "hasIssuesEnabled": true, - "issue":{"number":2} - }}}`)) - }, - wantErr: true, - wantErrMsg: "multiple issues must be in same repo", + behavior: "when given issue numbers, returns them with no repo", + args: []string{"1", "2"}, + expectedIssueNumbers: []int{1, 2}, + expectedRepo: o.None[ghrepo.Interface](), }, { - name: "multiple issues", - args: args{ - selectors: []string{"1", "2"}, - baseRepoFn: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, + behavior: "when given # prefixed issue numbers, returns them with no repo", + args: []string{"#1", "#2"}, + expectedIssueNumbers: []int{1, 2}, + expectedRepo: o.None[ghrepo.Interface](), + }, + { + behavior: "when given URLs, returns them with the repo", + args: []string{ + "https://github.com/OWNER/REPO/issues/1", + "https://github.com/OWNER/REPO/issues/2", }, + expectedIssueNumbers: []int{1, 2}, + expectedRepo: o.Some(ghrepo.New("OWNER", "REPO")), + }, + { + behavior: "when given URLs in different repos, errors", + args: []string{ + "https://github.com/OWNER/REPO/issues/1", + "https://github.com/OWNER/OTHERREPO/issues/2", + }, + expectedErr: true, + }, + { + behavior: "when given an unparseable argument, errors", + args: []string{"://"}, + expectedErr: true, + }, + { + behavior: "when given a URL that isn't an issue or PR url, errors", + args: []string{"https://github.com"}, + expectedErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.behavior, func(t *testing.T) { + issueNumbers, repo, err := ParseIssuesFromArgs(tc.args) + + if tc.expectedErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tc.expectedIssueNumbers, issueNumbers) + assert.Equal(t, tc.expectedRepo, repo) + }) + } + +} + +func TestFindIssuesOrPRs(t *testing.T) { + tests := []struct { + name string + issueNumbers []int + baseRepo ghrepo.Interface + httpStub func(*httpmock.Registry) + wantIssueNumbers []int + wantErr bool + }{ + { + name: "multiple issues", + issueNumbers: []int{1, 2}, + baseRepo: ghrepo.New("OWNER", "REPO"), httpStub: func(r *httpmock.Registry) { r.Register( httpmock.GraphQL(`query IssueByNumber\b`), @@ -275,43 +309,48 @@ func TestIssuesFromArgsWithFields(t *testing.T) { "issue":{"number":2} }}}`)) }, - wantIssues: []int{1, 2}, - wantRepo: "https://github.com/OWNER/REPO", + wantIssueNumbers: []int{1, 2}, + }, + { + name: "any find error results in total error", + issueNumbers: []int{1, 2}, + baseRepo: ghrepo.New("OWNER", "REPO"), + httpStub: func(r *httpmock.Registry) { + r.Register( + httpmock.GraphQL(`query IssueByNumber\b`), + httpmock.StringResponse(`{"data":{"repository":{ + "hasIssuesEnabled": true, + "issue":{"number":1} + }}}`)) + r.Register( + httpmock.GraphQL(`query IssueByNumber\b`), + httpmock.StatusStringResponse(500, "internal server error")) + }, + wantErr: true, }, } for _, tt := range tests { - if !tt.wantErr && len(tt.args.selectors) != len(tt.wantIssues) { - t.Fatal("number of selectors and issues not equal") - } t.Run(tt.name, func(t *testing.T) { reg := &httpmock.Registry{} if tt.httpStub != nil { tt.httpStub(reg) } httpClient := &http.Client{Transport: reg} - issues, repo, err := IssuesFromArgsWithFields(httpClient, tt.args.baseRepoFn, tt.args.selectors, []string{"number"}) + issues, err := FindIssuesOrPRs(httpClient, tt.baseRepo, tt.issueNumbers, []string{"number"}) if (err != nil) != tt.wantErr { - t.Errorf("IssuesFromArgsWithFields() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("FindIssuesOrPRs() error = %v, wantErr %v", err, tt.wantErr) if issues == nil { return } } if tt.wantErr { - assert.Error(t, err) - assert.ErrorContains(t, err, tt.wantErrMsg) + require.Error(t, err) return } - assert.NoError(t, err) + + require.NoError(t, err) for i := range issues { - assert.Contains(t, tt.wantIssues, issues[i].Number) - } - if repo != nil { - repoURL := ghrepo.GenerateRepoURL(repo, "") - if repoURL != tt.wantRepo { - t.Errorf("want repo %s, got %s", tt.wantRepo, repoURL) - } - } else if tt.wantRepo != "" { - t.Errorf("want repo %sw, got nil", tt.wantRepo) + assert.Contains(t, tt.wantIssueNumbers, issues[i].Number) } }) }